未验证 提交 82309adf 编写于 作者: B Benjamin Pasero 提交者: GitHub

Leverage native clipboard for file copy/paste (#43029)

* wip

* wire copy action into clipboard

* add tests

* fix compile

* macOS: leverage NSFilenamesPboardType
上级 83c35ca9
...@@ -217,6 +217,9 @@ suite('Snippet Variables Resolver', function () { ...@@ -217,6 +217,9 @@ suite('Snippet Variables Resolver', function () {
writeText = this._throw; writeText = this._throw;
readFindText = this._throw; readFindText = this._throw;
writeFindText = this._throw; writeFindText = this._throw;
writeFiles = this._throw;
readFiles = this._throw;
hasFiles = this._throw;
}; };
let resolver = new ClipboardBasedVariableResolver(clipboardService, 1, 0); let resolver = new ClipboardBasedVariableResolver(clipboardService, 1, 0);
...@@ -247,6 +250,9 @@ suite('Snippet Variables Resolver', function () { ...@@ -247,6 +250,9 @@ suite('Snippet Variables Resolver', function () {
writeText = this._throw; writeText = this._throw;
readFindText = this._throw; readFindText = this._throw;
writeFindText = this._throw; writeFindText = this._throw;
writeFiles = this._throw;
readFiles = this._throw;
hasFiles = this._throw;
}; };
resolver = new ClipboardBasedVariableResolver(clipboardService, 1, 2); resolver = new ClipboardBasedVariableResolver(clipboardService, 1, 2);
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
'use strict'; 'use strict';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import URI from 'vs/base/common/uri';
export const IClipboardService = createDecorator<IClipboardService>('clipboardService'); export const IClipboardService = createDecorator<IClipboardService>('clipboardService');
...@@ -32,4 +33,19 @@ export interface IClipboardService { ...@@ -32,4 +33,19 @@ export interface IClipboardService {
* Writes text to the system find pasteboard. * Writes text to the system find pasteboard.
*/ */
writeFindText(text: string): void; writeFindText(text: string): void;
/**
* Writes files to the system clipboard.
*/
writeFiles(files: URI[]): void;
/**
* Reads files from the system clipboard.
*/
readFiles(): URI[];
/**
* Find out if files are copied to the clipboard.
*/
hasFiles(): boolean;
} }
...@@ -7,10 +7,17 @@ ...@@ -7,10 +7,17 @@
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { clipboard } from 'electron'; import { clipboard } from 'electron';
import * as platform from 'vs/base/common/platform'; import URI from 'vs/base/common/uri';
import { isMacintosh } from 'vs/base/common/platform';
import { parse } from 'fast-plist';
export class ClipboardService implements IClipboardService { export class ClipboardService implements IClipboardService {
// Clipboard format for files
// Windows/Linux: custom
// macOS: native, see https://developer.apple.com/documentation/appkit/nsfilenamespboardtype
private static FILE_FORMAT = isMacintosh ? 'NSFilenamesPboardType' : 'code/file-list';
_serviceBrand: any; _serviceBrand: any;
public writeText(text: string): void { public writeText(text: string): void {
...@@ -22,15 +29,82 @@ export class ClipboardService implements IClipboardService { ...@@ -22,15 +29,82 @@ export class ClipboardService implements IClipboardService {
} }
public readFindText(): string { public readFindText(): string {
if (platform.isMacintosh) { if (isMacintosh) {
return clipboard.readFindText(); return clipboard.readFindText();
} }
return ''; return '';
} }
public writeFindText(text: string): void { public writeFindText(text: string): void {
if (platform.isMacintosh) { if (isMacintosh) {
clipboard.writeFindText(text); clipboard.writeFindText(text);
} }
} }
}
public writeFiles(resources: URI[]): void {
const files = resources.filter(f => f.scheme === 'file');
if (files.length) {
clipboard.writeBuffer(ClipboardService.FILE_FORMAT, this.filesToBuffer(files));
}
}
public readFiles(): URI[] {
return this.bufferToFiles(clipboard.readBuffer(ClipboardService.FILE_FORMAT));
}
public hasFiles(): boolean {
return clipboard.has(ClipboardService.FILE_FORMAT);
}
private filesToBuffer(resources: URI[]): Buffer {
if (isMacintosh) {
return this.macOSFilesToBuffer(resources);
}
return new Buffer(resources.map(r => r.fsPath).join('\n'));
}
private bufferToFiles(buffer: Buffer): URI[] {
if (!buffer) {
return [];
}
const bufferValue = buffer.toString();
if (!bufferValue) {
return [];
}
try {
if (isMacintosh) {
return this.macOSBufferToFiles(bufferValue);
}
return bufferValue.split('\n').map(f => URI.file(f));
} catch (error) {
return []; // do not trust clipboard data
}
}
private macOSFilesToBuffer(resources: URI[]): Buffer {
return new Buffer(`
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
${resources.map(r => `<string>${r.fsPath}</string>`).join('\n')}
</array>
</plist>
`);
}
private macOSBufferToFiles(buffer: string): URI[] {
const result = parse(buffer) as string[];
if (Array.isArray(result)) {
return result.map(f => URI.file(f));
}
return [];
}
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* 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 * as assert from 'assert';
import { ClipboardService } from 'vs/platform/clipboard/electron-browser/clipboardService';
import URI from 'vs/base/common/uri';
suite('ClipboardService', () => {
test('writeFiles, hasFiles, readFiles', function () {
const clipboardService = new ClipboardService();
clipboardService.writeText('test');
assert.ok(!clipboardService.hasFiles());
const files: URI[] = [];
files.push(URI.file('/test/file.txt'));
files.push(URI.file('/test/otherfile.txt'));
clipboardService.writeFiles(files);
assert.ok(clipboardService.hasFiles());
const clipboardFiles = clipboardService.readFiles();
assert.equal(clipboardFiles.length, 2);
assert.equal(clipboardFiles[0].fsPath, files[0].fsPath);
assert.equal(clipboardFiles[1].fsPath, files[1].fsPath);
clipboardService.writeText('test');
assert.ok(!clipboardService.hasFiles());
});
});
\ No newline at end of file
...@@ -95,7 +95,7 @@ const PASTE_FILE_ID = 'filesExplorer.paste'; ...@@ -95,7 +95,7 @@ const PASTE_FILE_ID = 'filesExplorer.paste';
KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({
id: PASTE_FILE_ID, id: PASTE_FILE_ID,
weight: KeybindingsRegistry.WEIGHT.workbenchContrib(explorerCommandsWeightBonus), weight: KeybindingsRegistry.WEIGHT.workbenchContrib(explorerCommandsWeightBonus),
when: ContextKeyExpr.and(FilesExplorerFocusCondition, FileCopiedContext), when: ContextKeyExpr.and(FilesExplorerFocusCondition),
primary: KeyMod.CtrlCmd | KeyCode.KEY_V, primary: KeyMod.CtrlCmd | KeyCode.KEY_V,
handler: pasteFileHandler handler: pasteFileHandler
}); });
......
...@@ -50,8 +50,8 @@ import { IModeService } from 'vs/editor/common/services/modeService'; ...@@ -50,8 +50,8 @@ import { IModeService } from 'vs/editor/common/services/modeService';
import { IModelService } from 'vs/editor/common/services/modelService'; import { IModelService } from 'vs/editor/common/services/modelService';
import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IListService, ListWidget } from 'vs/platform/list/browser/listService'; import { IListService, ListWidget } from 'vs/platform/list/browser/listService';
import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { distinctParents } from 'vs/base/common/resources'; import { distinctParents, basenameOrAuthority } from 'vs/base/common/resources';
export interface IEditableData { export interface IEditableData {
action: IAction; action: IAction;
...@@ -873,9 +873,6 @@ export class ImportFileAction extends BaseFileAction { ...@@ -873,9 +873,6 @@ export class ImportFileAction extends BaseFileAction {
} }
// Copy File/Folder // Copy File/Folder
let filesToCopy: FileStat[];
let fileCopiedContextKey: IContextKey<boolean>;
class CopyFileAction extends BaseFileAction { class CopyFileAction extends BaseFileAction {
private tree: ITree; private tree: ITree;
...@@ -885,22 +882,19 @@ class CopyFileAction extends BaseFileAction { ...@@ -885,22 +882,19 @@ class CopyFileAction extends BaseFileAction {
@IFileService fileService: IFileService, @IFileService fileService: IFileService,
@IMessageService messageService: IMessageService, @IMessageService messageService: IMessageService,
@ITextFileService textFileService: ITextFileService, @ITextFileService textFileService: ITextFileService,
@IContextKeyService contextKeyService: IContextKeyService @IContextKeyService contextKeyService: IContextKeyService,
@IClipboardService private clipboardService: IClipboardService
) { ) {
super('filesExplorer.copy', COPY_FILE_LABEL, fileService, messageService, textFileService); super('filesExplorer.copy', COPY_FILE_LABEL, fileService, messageService, textFileService);
this.tree = tree; this.tree = tree;
if (!fileCopiedContextKey) {
fileCopiedContextKey = FileCopiedContext.bindTo(contextKeyService);
}
this._updateEnablement(); this._updateEnablement();
} }
public run(): TPromise<any> { public run(): TPromise<any> {
// Remember as file/folder to copy // Write to clipboard as file/folder to copy
filesToCopy = this.elements; this.clipboardService.writeFiles(this.elements.map(e => e.resource));
fileCopiedContextKey.set(!!filesToCopy.length);
// Remove highlight // Remove highlight
if (this.tree) { if (this.tree) {
...@@ -926,7 +920,7 @@ class PasteFileAction extends BaseFileAction { ...@@ -926,7 +920,7 @@ class PasteFileAction extends BaseFileAction {
@IFileService fileService: IFileService, @IFileService fileService: IFileService,
@IMessageService messageService: IMessageService, @IMessageService messageService: IMessageService,
@ITextFileService textFileService: ITextFileService, @ITextFileService textFileService: ITextFileService,
@IInstantiationService private instantiationService: IInstantiationService @IWorkbenchEditorService private editorService: IWorkbenchEditorService
) { ) {
super(PasteFileAction.ID, PASTE_FILE_LABEL, fileService, messageService, textFileService); super(PasteFileAction.ID, PASTE_FILE_LABEL, fileService, messageService, textFileService);
...@@ -939,33 +933,42 @@ class PasteFileAction extends BaseFileAction { ...@@ -939,33 +933,42 @@ class PasteFileAction extends BaseFileAction {
this._updateEnablement(); this._updateEnablement();
} }
public run(fileToCopy: FileStat): TPromise<any> { public run(fileToPaste: URI): TPromise<any> {
const exists = fileToCopy.root.find(fileToCopy.resource);
if (!exists) {
fileToCopy = null;
fileCopiedContextKey.set(false);
throw new Error(nls.localize('fileDeleted', "File was deleted or moved meanwhile"));
}
// Check if target is ancestor of pasted folder // Check if target is ancestor of pasted folder
if (this.element.resource.toString() !== fileToCopy.resource.toString() && resources.isEqualOrParent(this.element.resource, fileToCopy.resource, !isLinux /* ignorecase */)) { if (this.element.resource.toString() !== fileToPaste.toString() && resources.isEqualOrParent(this.element.resource, fileToPaste, !isLinux /* ignorecase */)) {
throw new Error(nls.localize('fileIsAncestor', "File to copy is an ancestor of the desitnation folder")); throw new Error(nls.localize('fileIsAncestor', "File to paste is an ancestor of the destination folder"));
} }
// Find target return this.fileService.resolveFile(fileToPaste).then(fileToPasteStat => {
let target: FileStat;
if (this.element.resource.toString() === fileToCopy.resource.toString()) {
target = this.element.parent;
} else {
target = this.element.isDirectory ? this.element : this.element.parent;
}
// Reuse duplicate action // Remove highlight
const pasteAction = this.instantiationService.createInstance(DuplicateFileAction, this.tree, fileToCopy, target); if (this.tree) {
this.tree.clearHighlight();
}
// Find target
let target: FileStat;
if (this.element.resource.toString() === fileToPaste.toString()) {
target = this.element.parent;
} else {
target = this.element.isDirectory ? this.element : this.element.parent;
}
const targetFile = findValidPasteFileTarget(target, { resource: fileToPaste, isDirectory: fileToPasteStat.isDirectory });
// Copy File
return this.fileService.copyFile(fileToPaste, targetFile).then(stat => {
if (!stat.isDirectory) {
return this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } });
}
return pasteAction.run().then(() => { return void 0;
this.tree.DOMFocus(); }, error => this.onError(error)).then(() => {
this.tree.DOMFocus();
});
}, error => {
this.onError(new Error(nls.localize('fileDeleted', "File to paste was deleted or moved meanwhile")));
}); });
} }
} }
...@@ -973,11 +976,11 @@ class PasteFileAction extends BaseFileAction { ...@@ -973,11 +976,11 @@ class PasteFileAction extends BaseFileAction {
// Duplicate File/Folder // Duplicate File/Folder
export class DuplicateFileAction extends BaseFileAction { export class DuplicateFileAction extends BaseFileAction {
private tree: ITree; private tree: ITree;
private target: IFileStat; private target: FileStat;
constructor( constructor(
tree: ITree, tree: ITree,
element: FileStat, fileToDuplicate: FileStat,
target: FileStat, target: FileStat,
@IFileService fileService: IFileService, @IFileService fileService: IFileService,
@IWorkbenchEditorService private editorService: IWorkbenchEditorService, @IWorkbenchEditorService private editorService: IWorkbenchEditorService,
...@@ -987,8 +990,8 @@ export class DuplicateFileAction extends BaseFileAction { ...@@ -987,8 +990,8 @@ export class DuplicateFileAction extends BaseFileAction {
super('workbench.files.action.duplicateFile', nls.localize('duplicateFile', "Duplicate"), fileService, messageService, textFileService); super('workbench.files.action.duplicateFile', nls.localize('duplicateFile', "Duplicate"), fileService, messageService, textFileService);
this.tree = tree; this.tree = tree;
this.element = element; this.element = fileToDuplicate;
this.target = (target && target.isDirectory) ? target : element.parent; this.target = (target && target.isDirectory) ? target : fileToDuplicate.parent;
this._updateEnablement(); this._updateEnablement();
} }
...@@ -1000,7 +1003,7 @@ export class DuplicateFileAction extends BaseFileAction { ...@@ -1000,7 +1003,7 @@ export class DuplicateFileAction extends BaseFileAction {
} }
// Copy File // Copy File
const result = this.fileService.copyFile(this.element.resource, this.findTarget()).then(stat => { const result = this.fileService.copyFile(this.element.resource, findValidPasteFileTarget(this.target, { resource: this.element.resource, isDirectory: this.element.isDirectory })).then(stat => {
if (!stat.isDirectory) { if (!stat.isDirectory) {
return this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }); return this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } });
} }
...@@ -1010,44 +1013,44 @@ export class DuplicateFileAction extends BaseFileAction { ...@@ -1010,44 +1013,44 @@ export class DuplicateFileAction extends BaseFileAction {
return result; return result;
} }
}
private findTarget(): URI { function findValidPasteFileTarget(targetFolder: FileStat, fileToPaste: { resource: URI, isDirectory?: boolean }): URI {
let name = this.element.name; let name = basenameOrAuthority(fileToPaste.resource);
let candidate = this.target.resource.with({ path: paths.join(this.target.resource.path, name) });
while (true) {
if (!this.element.root.find(candidate)) {
break;
}
name = this.toCopyName(name, this.element.isDirectory); let candidate = targetFolder.resource.with({ path: paths.join(targetFolder.resource.path, name) });
candidate = this.target.resource.with({ path: paths.join(this.target.resource.path, name) }); while (true) {
if (!targetFolder.root.find(candidate)) {
break;
} }
return candidate; name = incrementFileName(name, fileToPaste.isDirectory);
candidate = targetFolder.resource.with({ path: paths.join(targetFolder.resource.path, name) });
} }
private toCopyName(name: string, isFolder: boolean): string { return candidate;
}
// file.1.txt=>file.2.txt function incrementFileName(name: string, isFolder: boolean): string {
if (!isFolder && name.match(/(.*\.)(\d+)(\..*)$/)) {
return name.replace(/(.*\.)(\d+)(\..*)$/, (match, g1?, g2?, g3?) => { return g1 + (parseInt(g2) + 1) + g3; });
}
// file.txt=>file.1.txt // file.1.txt=>file.2.txt
const lastIndexOfDot = name.lastIndexOf('.'); if (!isFolder && name.match(/(.*\.)(\d+)(\..*)$/)) {
if (!isFolder && lastIndexOfDot >= 0) { return name.replace(/(.*\.)(\d+)(\..*)$/, (match, g1?, g2?, g3?) => { return g1 + (parseInt(g2) + 1) + g3; });
return strings.format('{0}.1{1}', name.substr(0, lastIndexOfDot), name.substr(lastIndexOfDot)); }
}
// folder.1=>folder.2 // file.txt=>file.1.txt
if (isFolder && name.match(/(\d+)$/)) { const lastIndexOfDot = name.lastIndexOf('.');
return name.replace(/(\d+)$/, (match: string, ...groups: any[]) => { return String(parseInt(groups[0]) + 1); }); if (!isFolder && lastIndexOfDot >= 0) {
} return strings.format('{0}.1{1}', name.substr(0, lastIndexOfDot), name.substr(lastIndexOfDot));
}
// file/folder=>file.1/folder.1 // folder.1=>folder.2
return strings.format('{0}.1', name); if (isFolder && name.match(/(\d+)$/)) {
return name.replace(/(\d+)$/, (match: string, ...groups: any[]) => { return String(parseInt(groups[0]) + 1); });
} }
// file/folder=>file.1/folder.1
return strings.format('{0}.1', name);
} }
// Global Compare with // Global Compare with
...@@ -1587,9 +1590,10 @@ export const copyFileHandler = (accessor: ServicesAccessor) => { ...@@ -1587,9 +1590,10 @@ export const copyFileHandler = (accessor: ServicesAccessor) => {
export const pasteFileHandler = (accessor: ServicesAccessor) => { export const pasteFileHandler = (accessor: ServicesAccessor) => {
const instantationService = accessor.get(IInstantiationService); const instantationService = accessor.get(IInstantiationService);
const listService = accessor.get(IListService); const listService = accessor.get(IListService);
const clipboardService = accessor.get(IClipboardService);
const explorerContext = getContext(listService.lastFocusedList, accessor.get(IViewletService)); const explorerContext = getContext(listService.lastFocusedList, accessor.get(IViewletService));
return TPromise.join(distinctParents(filesToCopy, s => s.resource).map(toCopy => { return TPromise.join(distinctParents(clipboardService.readFiles(), r => r).map(toCopy => {
const pasteFileAction = instantationService.createInstance(PasteFileAction, listService.lastFocusedList, explorerContext.stat); const pasteFileAction = instantationService.createInstance(PasteFileAction, listService.lastFocusedList, explorerContext.stat);
return pasteFileAction.run(toCopy); return pasteFileAction.run(toCopy);
})); }));
......
...@@ -26,7 +26,7 @@ import { IFilesConfiguration, SortOrder } from 'vs/workbench/parts/files/common/ ...@@ -26,7 +26,7 @@ import { IFilesConfiguration, SortOrder } from 'vs/workbench/parts/files/common/
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { FileOperationError, FileOperationResult, IFileService, FileKind } from 'vs/platform/files/common/files'; import { FileOperationError, FileOperationResult, IFileService, FileKind } from 'vs/platform/files/common/files';
import { ResourceMap } from 'vs/base/common/map'; import { ResourceMap } from 'vs/base/common/map';
import { DuplicateFileAction, ImportFileAction, IEditableData, IFileViewletState } from 'vs/workbench/parts/files/electron-browser/fileActions'; import { DuplicateFileAction, ImportFileAction, IEditableData, IFileViewletState, FileCopiedContext } from 'vs/workbench/parts/files/electron-browser/fileActions';
import { IDataSource, ITree, IAccessibilityProvider, IRenderer, ContextMenuEvent, ISorter, IFilter, IDragAndDropData, IDragOverReaction, DRAG_OVER_ACCEPT_BUBBLE_DOWN, DRAG_OVER_ACCEPT_BUBBLE_DOWN_COPY, DRAG_OVER_ACCEPT_BUBBLE_UP, DRAG_OVER_ACCEPT_BUBBLE_UP_COPY, DRAG_OVER_REJECT } from 'vs/base/parts/tree/browser/tree'; import { IDataSource, ITree, IAccessibilityProvider, IRenderer, ContextMenuEvent, ISorter, IFilter, IDragAndDropData, IDragOverReaction, DRAG_OVER_ACCEPT_BUBBLE_DOWN, DRAG_OVER_ACCEPT_BUBBLE_DOWN_COPY, DRAG_OVER_ACCEPT_BUBBLE_UP, DRAG_OVER_ACCEPT_BUBBLE_UP_COPY, DRAG_OVER_REJECT } from 'vs/base/parts/tree/browser/tree';
import { DesktopDragAndDropData, ExternalElementsDragAndDropData, SimpleFileResourceDragAndDrop } from 'vs/base/parts/tree/browser/treeDnd'; import { DesktopDragAndDropData, ExternalElementsDragAndDropData, SimpleFileResourceDragAndDrop } from 'vs/base/parts/tree/browser/treeDnd';
import { ClickBehavior } from 'vs/base/parts/tree/browser/treeDefaults'; import { ClickBehavior } from 'vs/base/parts/tree/browser/treeDefaults';
...@@ -36,7 +36,7 @@ import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/edi ...@@ -36,7 +36,7 @@ import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/edi
import { IPartService } from 'vs/workbench/services/part/common/partService'; import { IPartService } from 'vs/workbench/services/part/common/partService';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IMessageService, IConfirmation, Severity, IConfirmationResult, getConfirmMessage } from 'vs/platform/message/common/message'; import { IMessageService, IConfirmation, Severity, IConfirmationResult, getConfirmMessage } from 'vs/platform/message/common/message';
...@@ -57,6 +57,7 @@ import { relative } from 'path'; ...@@ -57,6 +57,7 @@ import { relative } from 'path';
import { DataTransfers } from 'vs/base/browser/dnd'; import { DataTransfers } from 'vs/base/browser/dnd';
import { distinctParents } from 'vs/base/common/resources'; import { distinctParents } from 'vs/base/common/resources';
import { WorkbenchTree, WorkbenchTreeController } from 'vs/platform/list/browser/listService'; import { WorkbenchTree, WorkbenchTreeController } from 'vs/platform/list/browser/listService';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
export class FileDataSource implements IDataSource { export class FileDataSource implements IDataSource {
constructor( constructor(
...@@ -326,7 +327,7 @@ export class FileAccessibilityProvider implements IAccessibilityProvider { ...@@ -326,7 +327,7 @@ export class FileAccessibilityProvider implements IAccessibilityProvider {
// Explorer Controller // Explorer Controller
export class FileController extends WorkbenchTreeController implements IDisposable { export class FileController extends WorkbenchTreeController implements IDisposable {
private fileCopiedContextKey: IContextKey<boolean>;
private contributedContextMenu: IMenu; private contributedContextMenu: IMenu;
private toDispose: IDisposable[]; private toDispose: IDisposable[];
private previousSelectionRangeStop: FileStat; private previousSelectionRangeStop: FileStat;
...@@ -337,10 +338,12 @@ export class FileController extends WorkbenchTreeController implements IDisposab ...@@ -337,10 +338,12 @@ export class FileController extends WorkbenchTreeController implements IDisposab
@ITelemetryService private telemetryService: ITelemetryService, @ITelemetryService private telemetryService: ITelemetryService,
@IMenuService private menuService: IMenuService, @IMenuService private menuService: IMenuService,
@IContextKeyService contextKeyService: IContextKeyService, @IContextKeyService contextKeyService: IContextKeyService,
@IClipboardService private clipboardService: IClipboardService,
@IConfigurationService configurationService: IConfigurationService @IConfigurationService configurationService: IConfigurationService
) { ) {
super({ clickBehavior: ClickBehavior.ON_MOUSE_UP /* do not change to not break DND */ }, configurationService); super({ clickBehavior: ClickBehavior.ON_MOUSE_UP /* do not change to not break DND */ }, configurationService);
this.fileCopiedContextKey = FileCopiedContext.bindTo(contextKeyService);
this.toDispose = []; this.toDispose = [];
} }
...@@ -446,6 +449,9 @@ export class FileController extends WorkbenchTreeController implements IDisposab ...@@ -446,6 +449,9 @@ export class FileController extends WorkbenchTreeController implements IDisposab
tree.setFocus(stat); tree.setFocus(stat);
// update dynamic contexts
this.fileCopiedContextKey.set(this.clipboardService.hasFiles());
if (!this.contributedContextMenu) { if (!this.contributedContextMenu) {
this.contributedContextMenu = this.menuService.createMenu(MenuId.ExplorerContext, tree.contextKeyService); this.contributedContextMenu = this.menuService.createMenu(MenuId.ExplorerContext, tree.contextKeyService);
this.toDispose.push(this.contributedContextMenu); this.toDispose.push(this.contributedContextMenu);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册