Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explorer multiple selection #41461

Merged
merged 16 commits into from
Jan 11, 2018
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/vs/base/browser/dnd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ export const DataTransfers = {
*/
URL: 'URL',

/**
* Application specific resource transfer type when multiple resources are being dragged.
*/
URLS: 'URLS',

/**
* Browser specific transfer type to download.
*/
Expand All @@ -58,4 +63,4 @@ export const DataTransfers = {
* Typicaly transfer type for copy/paste transfers.
*/
TEXT: 'text/plain'
};
};
20 changes: 20 additions & 0 deletions src/vs/platform/message/common/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
'use strict';

import nls = require('vs/nls');
import uri from 'vs/base/common/uri';
import paths = require('vs/base/common/paths');
import { TPromise } from 'vs/base/common/winjs.base';
import Severity from 'vs/base/common/severity';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
Expand Down Expand Up @@ -35,6 +37,24 @@ export const CancelAction = new Action('cancel.message', nls.localize('cancel',

export const IMessageService = createDecorator<IMessageService>('messageService');

const MAX_CONFIRM_FILES = 10;
export function getConfirmMessage(start: string, resourcesToConfirm: uri[]): string {
const message = [start];
message.push('');
message.push(...resourcesToConfirm.slice(0, MAX_CONFIRM_FILES).map(r => paths.basename(r.fsPath)));

if (resourcesToConfirm.length > MAX_CONFIRM_FILES) {
if (resourcesToConfirm.length - MAX_CONFIRM_FILES === 1) {
message.push(nls.localize('moreFile', "...1 additional file not shown"));
} else {
message.push(nls.localize('moreFiles', "...{0} additional files not shown", resourcesToConfirm.length - MAX_CONFIRM_FILES));
}
}

message.push('');
return message.join('\n');
}

export interface IConfirmationResult {
confirmed: boolean;
checkboxChecked?: boolean;
Expand Down
20 changes: 13 additions & 7 deletions src/vs/workbench/browser/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,13 +241,19 @@ export function extractResources(e: DragEvent, externalOnly?: boolean): (IDragge

// Data Transfer: URL
else {
const rawURLData = e.dataTransfer.getData(DataTransfers.URL);
if (rawURLData) {
try {
resources.push({ resource: URI.parse(rawURLData), isExternal: false });
} catch (error) {
// Invalid URI
try {
const rawURLsData = e.dataTransfer.getData(DataTransfers.URLS);
if (rawURLsData) {
const uriStrArray: string[] = JSON.parse(rawURLsData);
resources.push(...uriStrArray.map(uriStr => ({ resource: URI.parse(uriStr), isExternal: false })));
} else {
const rawURLData = e.dataTransfer.getData(DataTransfers.URL);
if (rawURLData) {
resources.push({ resource: URI.parse(rawURLData), isExternal: false });
}
}
} catch (error) {
// Invalid URI
}
}
}
Expand All @@ -268,4 +274,4 @@ export function extractResources(e: DragEvent, externalOnly?: boolean): (IDragge
}

return resources;
}
}
100 changes: 58 additions & 42 deletions src/vs/workbench/parts/files/electron-browser/fileActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IUntitledResourceInput } from 'vs/platform/editor/common/editor';
import { IInstantiationService, IConstructorSignature2, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IMessageService, IMessageWithAction, IConfirmation, Severity, CancelAction, IConfirmationResult } from 'vs/platform/message/common/message';
import { IMessageService, IMessageWithAction, IConfirmation, Severity, CancelAction, IConfirmationResult, getConfirmMessage } from 'vs/platform/message/common/message';
import { ITextModel } from 'vs/editor/common/model';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { IWindowsService } from 'vs/platform/windows/common/windows';
Expand Down Expand Up @@ -626,14 +626,12 @@ class BaseDeleteFileAction extends BaseFileAction {

private static readonly CONFIRM_DELETE_SETTING_KEY = 'explorer.confirmDelete';

private tree: ITree;
private useTrash: boolean;
private skipConfirm: boolean;

constructor(
tree: ITree,
element: FileStat,
useTrash: boolean,
private tree: ITree,
private elements: FileStat[],
private useTrash: boolean,
@IFileService fileService: IFileService,
@IMessageService messageService: IMessageService,
@ITextFileService textFileService: ITextFileService,
Expand All @@ -642,13 +640,12 @@ class BaseDeleteFileAction extends BaseFileAction {
super('moveFileToTrash', MOVE_FILE_TO_TRASH_LABEL, fileService, messageService, textFileService);

this.tree = tree;
this.element = element;
this.useTrash = useTrash && !paths.isUNC(element.resource.fsPath); // on UNC shares there is no trash
this.useTrash = useTrash && elements.every(e => !paths.isUNC(e.resource.fsPath)); // on UNC shares there is no trash

this._updateEnablement();
}

public run(context?: any): TPromise<any> {
public run(): TPromise<any> {

// Remove highlight
if (this.tree) {
Expand All @@ -664,10 +661,12 @@ class BaseDeleteFileAction extends BaseFileAction {

// Handle dirty
let confirmDirtyPromise: TPromise<boolean> = TPromise.as(true);
const dirty = this.textFileService.getDirty().filter(d => resources.isEqualOrParent(d, this.element.resource, !isLinux /* ignorecase */));
const dirty = this.textFileService.getDirty().filter(d => this.elements.some(e => resources.isEqualOrParent(d, e.resource, !isLinux /* ignorecase */)));
if (dirty.length) {
let message: string;
if (this.element.isDirectory) {
if (this.elements.length > 1) {
message = nls.localize('dirtyMessageFilesDelete', "You are deleting files with unsaved changes. Do you want to continue?");
} else if (this.elements[0].isDirectory) {
if (dirty.length === 1) {
message = nls.localize('dirtyMessageFolderOneDelete', "You are deleting a folder with unsaved changes in 1 file. Do you want to continue?");
} else {
Expand Down Expand Up @@ -707,8 +706,11 @@ class BaseDeleteFileAction extends BaseFileAction {

// Confirm for moving to trash
else if (this.useTrash) {
const message = this.elements.length > 1 ? getConfirmMessage(nls.localize('confirmMoveTrashMessageMultiple', "Are you sure you want to delete the following {0} files?", this.elements.length), this.elements.map(e => e.resource))
: this.elements[0].isDirectory ? nls.localize('confirmMoveTrashMessageFolder', "Are you sure you want to delete '{0}' and its contents?", this.elements[0].name)
: nls.localize('confirmMoveTrashMessageFile', "Are you sure you want to delete '{0}'?", this.elements[0].name);
confirmDeletePromise = this.messageService.confirmWithCheckbox({
message: this.element.isDirectory ? nls.localize('confirmMoveTrashMessageFolder', "Are you sure you want to delete '{0}' and its contents?", this.element.name) : nls.localize('confirmMoveTrashMessageFile', "Are you sure you want to delete '{0}'?", this.element.name),
message,
detail: isWindows ? nls.localize('undoBin', "You can restore from the recycle bin.") : nls.localize('undoTrash', "You can restore from the trash."),
primaryButton,
checkbox: {
Expand All @@ -720,8 +722,11 @@ class BaseDeleteFileAction extends BaseFileAction {

// Confirm for deleting permanently
else {
const message = this.elements.length > 1 ? getConfirmMessage(nls.localize('confirmDeleteMessageMultiple', "Are you sure you want to permanently delete the following {0} files?", this.elements.length), this.elements.map(e => e.resource))
: this.elements[0].isDirectory ? nls.localize('confirmDeleteMessageFolder', "Are you sure you want to permanently delete '{0}' and its contents?", this.elements[0].name)
: nls.localize('confirmDeleteMessageFile', "Are you sure you want to permanently delete '{0}'?", this.elements[0].name);
confirmDeletePromise = this.messageService.confirmWithCheckbox({
message: this.element.isDirectory ? nls.localize('confirmDeleteMessageFolder', "Are you sure you want to permanently delete '{0}' and its contents?", this.element.name) : nls.localize('confirmDeleteMessageFile', "Are you sure you want to permanently delete '{0}'?", this.element.name),
message,
detail: nls.localize('irreversible', "This action is irreversible!"),
primaryButton,
type: 'warning'
Expand All @@ -744,20 +749,21 @@ class BaseDeleteFileAction extends BaseFileAction {
}

// Call function
const servicePromise = this.fileService.del(this.element.resource, this.useTrash).then(() => {
if (this.element.parent) {
this.tree.setFocus(this.element.parent); // move focus to parent
const servicePromise = TPromise.join(this.elements.map(e => this.fileService.del(e.resource, this.useTrash))).then(() => {
if (this.elements[0].parent) {
this.tree.setFocus(this.elements[0].parent); // move focus to parent
}
}, (error: any) => {

// Allow to retry
let extraAction: Action;
if (this.useTrash) {
extraAction = new Action('permanentDelete', nls.localize('permDelete', "Delete Permanently"), null, true, () => { this.useTrash = false; this.skipConfirm = true; return this.run(); });
if (this.elements.length === 1) {
// Allow to retry
let extraAction: Action;
if (this.useTrash) {
extraAction = new Action('permanentDelete', nls.localize('permDelete', "Delete Permanently"), null, true, () => { this.useTrash = false; this.skipConfirm = true; return this.run(); });
}

this.onErrorWithRetry(error, () => this.run(), extraAction);
}

this.onErrorWithRetry(error, () => this.run(), extraAction);

// Focus back to tree
this.tree.DOMFocus();
});
Expand Down Expand Up @@ -885,15 +891,15 @@ export class ImportFileAction extends BaseFileAction {
}

// Copy File/Folder
let fileToCopy: FileStat;
let filesToCopy: FileStat[];
let fileCopiedContextKey: IContextKey<boolean>;

class CopyFileAction extends BaseFileAction {

private tree: ITree;
constructor(
tree: ITree,
element: FileStat,
private elements: FileStat[],
@IFileService fileService: IFileService,
@IMessageService messageService: IMessageService,
@ITextFileService textFileService: ITextFileService,
Expand All @@ -902,7 +908,6 @@ class CopyFileAction extends BaseFileAction {
super('filesExplorer.copy', COPY_FILE_LABEL, fileService, messageService, textFileService);

this.tree = tree;
this.element = element;
if (!fileCopiedContextKey) {
fileCopiedContextKey = FileCopiedContext.bindTo(contextKeyService);
}
Expand All @@ -912,8 +917,8 @@ class CopyFileAction extends BaseFileAction {
public run(): TPromise<any> {

// Remember as file/folder to copy
fileToCopy = this.element;
fileCopiedContextKey.set(!!this.element);
filesToCopy = this.elements;
fileCopiedContextKey.set(!!filesToCopy.length);

// Remove highlight
if (this.tree) {
Expand Down Expand Up @@ -952,7 +957,7 @@ class PasteFileAction extends BaseFileAction {
this._updateEnablement();
}

public run(): TPromise<any> {
public run(fileToCopy: FileStat): TPromise<any> {

const exists = fileToCopy.root.find(fileToCopy.resource);
if (!exists) {
Expand Down Expand Up @@ -1533,11 +1538,17 @@ if (!diag) {
interface IExplorerContext {
viewletState: IFileViewletState;
stat: FileStat;
selection: FileStat[];
}

function getContext(tree: ListWidget, viewletService: IViewletService): IExplorerContext {
function getContext(listWidget: ListWidget, viewletService: IViewletService): IExplorerContext {
// These commands can only be triggered when explorer viewlet is visible so get it using the active viewlet
return { stat: tree.getFocus(), viewletState: (<ExplorerViewlet>viewletService.getActiveViewlet()).getViewletState() };
const tree = <ITree>listWidget;
const stat = tree.getFocus();
const selection = tree.getSelection();

// Only respect the selection if user clicked inside it (focus belongs to it)
return { stat, selection: selection && selection.indexOf(stat) >= 0 ? selection : [], viewletState: (<ExplorerViewlet>viewletService.getActiveViewlet()).getViewletState() };
}

// TODO@isidor these commands are calling into actions due to the complex inheritance action structure.
Expand Down Expand Up @@ -1575,38 +1586,43 @@ export const renameHandler = (accessor: ServicesAccessor) => {
return renameAction.run(explorerContext);
};

export const moveFileToTrashHandler = (accessor) => {
export const moveFileToTrashHandler = (accessor: ServicesAccessor) => {
const instantationService = accessor.get(IInstantiationService);
const listService = accessor.get(IListService);
const explorerContext = getContext(listService.lastFocusedList, accessor.get(IViewletService));
const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat];

const moveFileToTrashAction = instantationService.createInstance(BaseDeleteFileAction, listService.lastFocusedList, explorerContext.stat, true);
return moveFileToTrashAction.run(explorerContext);
const moveFileToTrashAction = instantationService.createInstance(BaseDeleteFileAction, listService.lastFocusedList, stats, true);
return moveFileToTrashAction.run();
};

export const deleteFileHandler = (accessor) => {
export const deleteFileHandler = (accessor: ServicesAccessor) => {
const instantationService = accessor.get(IInstantiationService);
const listService = accessor.get(IListService);
const explorerContext = getContext(listService.lastFocusedList, accessor.get(IViewletService));
const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat];

const deleteFileAction = instantationService.createInstance(BaseDeleteFileAction, listService.lastFocusedList, explorerContext.stat, false);
return deleteFileAction.run(explorerContext);
const deleteFileAction = instantationService.createInstance(BaseDeleteFileAction, listService.lastFocusedList, stats, false);
return deleteFileAction.run();
};

export const copyFileHandler = (accessor) => {
export const copyFileHandler = (accessor: ServicesAccessor) => {
const instantationService = accessor.get(IInstantiationService);
const listService = accessor.get(IListService);
const explorerContext = getContext(listService.lastFocusedList, accessor.get(IViewletService));
const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat];

const copyFileAction = instantationService.createInstance(CopyFileAction, listService.lastFocusedList, explorerContext.stat);
const copyFileAction = instantationService.createInstance(CopyFileAction, listService.lastFocusedList, stats);
return copyFileAction.run();
};

export const pasteFileHandler = (accessor) => {
export const pasteFileHandler = (accessor: ServicesAccessor) => {
const instantationService = accessor.get(IInstantiationService);
const listService = accessor.get(IListService);
const explorerContext = getContext(listService.lastFocusedList, accessor.get(IViewletService));

const pasteFileAction = instantationService.createInstance(PasteFileAction, listService.lastFocusedList, explorerContext.stat);
return pasteFileAction.run();
return TPromise.join(filesToCopy.map(toCopy => {
const pasteFileAction = instantationService.createInstance(PasteFileAction, listService.lastFocusedList, explorerContext.stat);
return pasteFileAction.run(toCopy);
}));
};
Loading