Vscode merge (#4582)

* Merge from vscode 37cb23d3dd4f9433d56d4ba5ea3203580719a0bd

* fix issues with merges

* bump node version in azpipe

* replace license headers

* remove duplicate launch task

* fix build errors

* fix build errors

* fix tslint issues

* working through package and linux build issues

* more work

* wip

* fix packaged builds

* working through linux build errors

* wip

* wip

* wip

* fix mac and linux file limits

* iterate linux pipeline

* disable editor typing

* revert series to parallel

* remove optimize vscode from linux

* fix linting issues

* revert testing change

* add work round for new node

* readd packaging for extensions

* fix issue with angular not resolving decorator dependencies
This commit is contained in:
Anthony Dresser
2019-03-19 17:44:35 -07:00
committed by GitHub
parent 833d197412
commit 87765e8673
1879 changed files with 54505 additions and 38058 deletions

View File

@@ -0,0 +1,138 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import * as errors from 'vs/base/common/errors';
import * as env from 'vs/base/common/platform';
import * as DOM from 'vs/base/browser/dom';
import { IAction } from 'vs/base/common/actions';
import { Button } from 'vs/base/browser/ui/button/button';
import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { OpenFolderAction, OpenFileFolderAction, AddRootFolderAction } from 'vs/workbench/browser/actions/workspaceActions';
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet';
import { ResourcesDropHandler, DragAndDropObserver } from 'vs/workbench/browser/dnd';
import { listDropBackground } from 'vs/platform/theme/common/colorRegistry';
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
export class EmptyView extends ViewletPanel {
static readonly ID: string = 'workbench.explorer.emptyView';
static readonly NAME = nls.localize('noWorkspace', "No Folder Opened");
private button: Button;
private messageElement: HTMLElement;
private titleElement: HTMLElement;
constructor(
options: IViewletViewOptions,
@IThemeService private readonly themeService: IThemeService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IKeybindingService keybindingService: IKeybindingService,
@IContextMenuService contextMenuService: IContextMenuService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IConfigurationService configurationService: IConfigurationService
) {
super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService);
this.contextService.onDidChangeWorkbenchState(() => this.setLabels());
}
renderHeader(container: HTMLElement): void {
const titleContainer = document.createElement('div');
DOM.addClass(titleContainer, 'title');
container.appendChild(titleContainer);
this.titleElement = document.createElement('span');
this.titleElement.textContent = name;
titleContainer.appendChild(this.titleElement);
}
protected renderBody(container: HTMLElement): void {
DOM.addClass(container, 'explorer-empty-view');
container.tabIndex = 0;
const messageContainer = document.createElement('div');
DOM.addClass(messageContainer, 'section');
container.appendChild(messageContainer);
this.messageElement = document.createElement('p');
messageContainer.appendChild(this.messageElement);
this.button = new Button(messageContainer);
attachButtonStyler(this.button, this.themeService);
this.disposables.push(this.button.onDidClick(() => {
if (!this.actionRunner) {
return;
}
const actionClass = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE ? AddRootFolderAction : env.isMacintosh ? OpenFileFolderAction : OpenFolderAction;
const action = this.instantiationService.createInstance<string, string, IAction>(actionClass, actionClass.ID, actionClass.LABEL);
this.actionRunner.run(action).then(() => {
action.dispose();
}, err => {
action.dispose();
errors.onUnexpectedError(err);
});
}));
this.disposables.push(new DragAndDropObserver(container, {
onDrop: e => {
const color = this.themeService.getTheme().getColor(SIDE_BAR_BACKGROUND);
container.style.backgroundColor = color ? color.toString() : '';
const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: true });
dropHandler.handleDrop(e, () => undefined, targetGroup => undefined);
},
onDragEnter: (e) => {
const color = this.themeService.getTheme().getColor(listDropBackground);
container.style.backgroundColor = color ? color.toString() : '';
},
onDragEnd: () => {
const color = this.themeService.getTheme().getColor(SIDE_BAR_BACKGROUND);
container.style.backgroundColor = color ? color.toString() : '';
},
onDragLeave: () => {
const color = this.themeService.getTheme().getColor(SIDE_BAR_BACKGROUND);
container.style.backgroundColor = color ? color.toString() : '';
},
onDragOver: e => {
e.dataTransfer!.dropEffect = 'copy';
}
}));
this.setLabels();
}
private setLabels(): void {
if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) {
this.messageElement.textContent = nls.localize('noWorkspaceHelp', "You have not yet added a folder to the workspace.");
if (this.button) {
this.button.label = nls.localize('addFolder', "Add Folder");
}
this.titleElement.textContent = EmptyView.NAME;
} else {
this.messageElement.textContent = nls.localize('noFolderHelp', "You have not yet opened a folder.");
if (this.button) {
this.button.label = nls.localize('openFolder', "Open Folder");
}
this.titleElement.textContent = this.title;
}
}
layoutBody(size: number): void {
// no-op
}
focusBody(): void {
if (this.button) {
this.button.element.focus();
}
}
}

View File

@@ -0,0 +1,65 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { localize } from 'vs/nls';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IDecorationsProvider, IDecorationData } from 'vs/workbench/services/decorations/browser/decorations';
import { listInvalidItemForeground } from 'vs/platform/theme/common/colorRegistry';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { IExplorerService } from 'vs/workbench/contrib/files/common/files';
export class ExplorerDecorationsProvider implements IDecorationsProvider {
readonly label: string = localize('label', "Explorer");
private _onDidChange = new Emitter<URI[]>();
private toDispose: IDisposable[];
constructor(
@IExplorerService private explorerService: IExplorerService,
@IWorkspaceContextService contextService: IWorkspaceContextService
) {
this.toDispose = [];
this.toDispose.push(contextService.onDidChangeWorkspaceFolders(e => {
this._onDidChange.fire(e.changed.concat(e.added).map(wf => wf.uri));
}));
this.toDispose.push(explorerService.onDidChangeItem(item => {
if (item) {
this._onDidChange.fire([item.resource]);
}
}));
}
get onDidChange(): Event<URI[]> {
return this._onDidChange.event;
}
changed(uris: URI[]): void {
this._onDidChange.fire(uris);
}
provideDecorations(resource: URI): IDecorationData | undefined {
const fileStat = this.explorerService.findClosest(resource);
if (fileStat && fileStat.isRoot && fileStat.isError) {
return {
tooltip: localize('canNotResolve', "Can not resolve workspace folder"),
letter: '!',
color: listInvalidItemForeground,
};
}
if (fileStat && fileStat.isSymbolicLink) {
return {
tooltip: localize('symbolicLlink', "Symbolic Link"),
letter: '\u2937'
};
}
return undefined;
}
dispose(): IDisposable[] {
return dispose(this.toDispose);
}
}

View File

@@ -0,0 +1,554 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { URI } from 'vs/base/common/uri';
import * as perf from 'vs/base/common/performance';
import { sequence } from 'vs/base/common/async';
import { Action, IAction } from 'vs/base/common/actions';
import { memoize } from 'vs/base/common/decorators';
import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, IExplorerService, ExplorerResourceCut } from 'vs/workbench/contrib/files/common/files';
import { NewFolderAction, NewFileAction, FileCopiedContext, RefreshExplorerView } from 'vs/workbench/contrib/files/browser/fileActions';
import { toResource } from 'vs/workbench/common/editor';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import * as DOM from 'vs/base/browser/dom';
import { CollapseAction } from 'vs/workbench/browser/viewlet';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { ExplorerDecorationsProvider } from 'vs/workbench/contrib/files/browser/views/explorerDecorationsProvider';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IProgressService } from 'vs/platform/progress/common/progress';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { ResourceContextKey } from 'vs/workbench/common/resources';
import { IDecorationsService } from 'vs/workbench/services/decorations/browser/decorations';
import { WorkbenchAsyncDataTree, TreeResourceNavigator2 } from 'vs/platform/list/browser/listService';
import { DelayedDragHandler } from 'vs/base/browser/dnd';
import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { IViewletPanelOptions, ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet';
import { ILabelService } from 'vs/platform/label/common/label';
import { ExplorerDelegate, ExplorerAccessibilityProvider, ExplorerDataSource, FilesRenderer, FilesFilter, FileSorter, FileDragAndDrop } from 'vs/workbench/contrib/files/browser/views/explorerViewer';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree';
import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions';
import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel';
import { onUnexpectedError } from 'vs/base/common/errors';
import { ResourceLabels, IResourceLabelsContainer } from 'vs/workbench/browser/labels';
import { createFileIconThemableTreeContainerScope } from 'vs/workbench/browser/parts/views/views';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree';
import { FuzzyScore } from 'vs/base/common/filters';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { isMacintosh } from 'vs/base/common/platform';
import { KeyCode } from 'vs/base/common/keyCodes';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { withNullAsUndefined } from 'vs/base/common/types';
export class ExplorerView extends ViewletPanel {
static readonly ID: string = 'workbench.explorer.fileView';
static readonly TREE_VIEW_STATE_STORAGE_KEY: string = 'workbench.explorer.treeViewState';
private tree: WorkbenchAsyncDataTree<ExplorerItem | ExplorerItem[], ExplorerItem, FuzzyScore>;
private filter: FilesFilter;
private resourceContext: ResourceContextKey;
private folderContext: IContextKey<boolean>;
private readonlyContext: IContextKey<boolean>;
private rootContext: IContextKey<boolean>;
// Refresh is needed on the initial explorer open
private shouldRefresh = true;
private dragHandler: DelayedDragHandler;
private decorationProvider: ExplorerDecorationsProvider;
private autoReveal = false;
constructor(
options: IViewletPanelOptions,
@IContextMenuService contextMenuService: IContextMenuService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IProgressService private readonly progressService: IProgressService,
@IEditorService private readonly editorService: IEditorService,
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
@IKeybindingService keybindingService: IKeybindingService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IConfigurationService configurationService: IConfigurationService,
@IDecorationsService decorationService: IDecorationsService,
@ILabelService private readonly labelService: ILabelService,
@IThemeService private readonly themeService: IWorkbenchThemeService,
@IMenuService private readonly menuService: IMenuService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IExplorerService private readonly explorerService: IExplorerService,
@IStorageService private readonly storageService: IStorageService,
@IClipboardService private clipboardService: IClipboardService
) {
super({ ...(options as IViewletPanelOptions), id: ExplorerView.ID, ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService);
this.resourceContext = instantiationService.createInstance(ResourceContextKey);
this.disposables.push(this.resourceContext);
this.folderContext = ExplorerFolderContext.bindTo(contextKeyService);
this.readonlyContext = ExplorerResourceReadonlyContext.bindTo(contextKeyService);
this.rootContext = ExplorerRootContext.bindTo(contextKeyService);
this.decorationProvider = new ExplorerDecorationsProvider(this.explorerService, contextService);
decorationService.registerDecorationsProvider(this.decorationProvider);
this.disposables.push(this.decorationProvider);
this.disposables.push(this.resourceContext);
}
get name(): string {
return this.labelService.getWorkspaceLabel(this.contextService.getWorkspace());
}
get title(): string {
return this.name;
}
set title(value: string) {
// noop
}
// Memoized locals
@memoize private get contributedContextMenu(): IMenu {
const contributedContextMenu = this.menuService.createMenu(MenuId.ExplorerContext, this.tree.contextKeyService);
this.disposables.push(contributedContextMenu);
return contributedContextMenu;
}
@memoize private get fileCopiedContextKey(): IContextKey<boolean> {
return FileCopiedContext.bindTo(this.contextKeyService);
}
@memoize private get resourceCutContextKey(): IContextKey<boolean> {
return ExplorerResourceCut.bindTo(this.contextKeyService);
}
// Split view methods
protected renderHeader(container: HTMLElement): void {
super.renderHeader(container);
// Expand on drag over
this.dragHandler = new DelayedDragHandler(container, () => this.setExpanded(true));
const titleElement = container.querySelector('.title') as HTMLElement;
const setHeader = () => {
const workspace = this.contextService.getWorkspace();
const title = workspace.folders.map(folder => folder.name).join();
titleElement.textContent = this.name;
titleElement.title = title;
};
this.disposables.push(this.contextService.onDidChangeWorkspaceName(setHeader));
this.disposables.push(this.labelService.onDidChangeFormatters(setHeader));
setHeader();
}
protected layoutBody(height: number, width: number): void {
this.tree.layout(height, width);
}
renderBody(container: HTMLElement): void {
const treeContainer = DOM.append(container, DOM.$('.explorer-folders-view'));
this.createTree(treeContainer);
if (this.toolbar) {
this.toolbar.setActions(this.getActions(), this.getSecondaryActions())();
}
this.disposables.push(this.labelService.onDidChangeFormatters(() => {
this._onDidChangeTitleArea.fire();
this.refresh();
}));
this.disposables.push(this.explorerService.onDidChangeRoots(() => this.setTreeInput()));
this.disposables.push(this.explorerService.onDidChangeItem(e => this.refresh(e)));
this.disposables.push(this.explorerService.onDidChangeEditable(async e => {
const isEditing = !!this.explorerService.getEditableData(e);
if (isEditing) {
await this.tree.expand(e.parent!);
} else {
DOM.removeClass(treeContainer, 'highlight');
}
await this.refresh(e.parent);
if (isEditing) {
DOM.addClass(treeContainer, 'highlight');
this.tree.reveal(e);
} else {
this.tree.domFocus();
}
}));
this.disposables.push(this.explorerService.onDidSelectItem(e => this.onSelectItem(e.item, e.reveal)));
this.disposables.push(this.explorerService.onDidCopyItems(e => this.onCopyItems(e.items, e.cut, e.previouslyCutItems)));
// Update configuration
const configuration = this.configurationService.getValue<IFilesConfiguration>();
this.onConfigurationUpdated(configuration);
// When the explorer viewer is loaded, listen to changes to the editor input
this.disposables.push(this.editorService.onDidActiveEditorChange(() => {
this.selectActiveFile(true);
}));
// Also handle configuration updates
this.disposables.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue<IFilesConfiguration>(), e)));
this.disposables.push(this.onDidChangeBodyVisibility(async visible => {
if (visible) {
// If a refresh was requested and we are now visible, run it
if (this.shouldRefresh) {
this.shouldRefresh = false;
await this.setTreeInput();
}
// Find resource to focus from active editor input if set
this.selectActiveFile(false, true);
}
}));
}
getActions(): IAction[] {
const actions: Action[] = [];
const getFocus = () => {
const focus = this.tree.getFocus();
return focus.length > 0 ? focus[0] : undefined;
};
actions.push(this.instantiationService.createInstance(NewFileAction, getFocus));
actions.push(this.instantiationService.createInstance(NewFolderAction, getFocus));
actions.push(this.instantiationService.createInstance(RefreshExplorerView, RefreshExplorerView.ID, RefreshExplorerView.LABEL));
actions.push(this.instantiationService.createInstance(CollapseAction, this.tree, true, 'explorer-action collapse-explorer'));
return actions;
}
focus(): void {
this.tree.domFocus();
const focused = this.tree.getFocus();
if (focused.length === 1) {
if (this.autoReveal) {
this.tree.reveal(focused[0], 0.5);
}
const activeFile = this.getActiveFile();
if (!activeFile && !focused[0].isDirectory) {
// Open the focused element in the editor if there is currently no file opened #67708
this.editorService.openEditor({ resource: focused[0].resource, options: { preserveFocus: true, revealIfVisible: true } })
.then(undefined, onUnexpectedError);
}
}
}
private selectActiveFile(deselect?: boolean, reveal = this.autoReveal): void {
if (this.autoReveal) {
const activeFile = this.getActiveFile();
if (activeFile) {
const focus = this.tree.getFocus();
if (focus.length === 1 && focus[0].resource.toString() === activeFile.toString()) {
// No action needed, active file is already focused
return;
}
this.explorerService.select(activeFile, reveal);
} else if (deselect) {
this.tree.setSelection([]);
this.tree.setFocus([]);
}
}
}
private createTree(container: HTMLElement): void {
this.filter = this.instantiationService.createInstance(FilesFilter);
this.disposables.push(this.filter);
const explorerLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility } as IResourceLabelsContainer);
this.disposables.push(explorerLabels);
const updateWidth = (stat: ExplorerItem) => this.tree.updateWidth(stat);
const filesRenderer = this.instantiationService.createInstance(FilesRenderer, explorerLabels, updateWidth);
this.disposables.push(filesRenderer);
this.disposables.push(createFileIconThemableTreeContainerScope(container, this.themeService));
this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree, container, new ExplorerDelegate(), [filesRenderer],
this.instantiationService.createInstance(ExplorerDataSource), {
accessibilityProvider: new ExplorerAccessibilityProvider(),
ariaLabel: nls.localize('treeAriaLabel', "Files Explorer"),
identityProvider: {
getId: stat => (<ExplorerItem>stat).resource
},
keyboardNavigationLabelProvider: {
getKeyboardNavigationLabel: stat => {
const item = <ExplorerItem>stat;
if (this.explorerService.isEditable(item)) {
return undefined;
}
return item.name;
}
},
multipleSelectionSupport: true,
filter: this.filter,
sorter: this.instantiationService.createInstance(FileSorter),
dnd: this.instantiationService.createInstance(FileDragAndDrop),
autoExpandSingleChildren: true
}) as WorkbenchAsyncDataTree<ExplorerItem | ExplorerItem[], ExplorerItem, FuzzyScore>;
this.disposables.push(this.tree);
// Bind context keys
FilesExplorerFocusedContext.bindTo(this.tree.contextKeyService);
ExplorerFocusedContext.bindTo(this.tree.contextKeyService);
// Update resource context based on focused element
this.disposables.push(this.tree.onDidChangeFocus(e => this.onFocusChanged(e.elements)));
this.onFocusChanged([]);
const explorerNavigator = new TreeResourceNavigator2(this.tree);
this.disposables.push(explorerNavigator);
// Open when selecting via keyboard
this.disposables.push(explorerNavigator.onDidOpenResource(e => {
const selection = this.tree.getSelection();
// Do not react if the user is expanding selection via keyboard.
// Check if the item was previously also selected, if yes the user is simply expanding / collapsing current selection #66589.
const shiftDown = e.browserEvent instanceof KeyboardEvent && e.browserEvent.shiftKey;
if (selection.length === 1 && !shiftDown) {
if (selection[0].isDirectory || this.explorerService.isEditable(undefined)) {
// Do not react if user is clicking on explorer items while some are being edited #70276
// Do not react if clicking on directories
return;
}
/* __GDPR__
"workbenchActionExecuted" : {
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}*/
this.telemetryService.publicLog('workbenchActionExecuted', { id: 'workbench.files.openFile', from: 'explorer' });
this.editorService.openEditor({ resource: selection[0].resource, options: { preserveFocus: e.editorOptions.preserveFocus, pinned: e.editorOptions.pinned } }, e.sideBySide ? SIDE_GROUP : ACTIVE_GROUP)
.then(undefined, onUnexpectedError);
}
}));
this.disposables.push(this.tree.onContextMenu(e => this.onContextMenu(e)));
this.disposables.push(this.tree.onKeyDown(e => {
const event = new StandardKeyboardEvent(e);
const toggleCollapsed = isMacintosh ? (event.keyCode === KeyCode.DownArrow && event.metaKey) : event.keyCode === KeyCode.Enter;
if (toggleCollapsed && !this.explorerService.isEditable(undefined)) {
const focus = this.tree.getFocus();
if (focus.length === 1 && focus[0].isDirectory) {
this.tree.toggleCollapsed(focus[0]);
}
}
}));
// save view state on shutdown
this.storageService.onWillSaveState(() => {
this.storageService.store(ExplorerView.TREE_VIEW_STATE_STORAGE_KEY, JSON.stringify(this.tree.getViewState()), StorageScope.WORKSPACE);
}, null, this.disposables);
}
// React on events
private onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): void {
this.autoReveal = configuration && configuration.explorer && configuration.explorer.autoReveal;
// Push down config updates to components of viewer
let needsRefresh = false;
if (this.filter) {
needsRefresh = this.filter.updateConfiguration();
}
if (event && !needsRefresh) {
needsRefresh = event.affectsConfiguration('explorer.decorations.colors')
|| event.affectsConfiguration('explorer.decorations.badges');
}
// Refresh viewer as needed if this originates from a config event
if (event && needsRefresh) {
this.refresh();
}
}
private onContextMenu(e: ITreeContextMenuEvent<ExplorerItem>): void {
const stat = e.element;
// update dynamic contexts
this.fileCopiedContextKey.set(this.clipboardService.hasResources());
const selection = this.tree.getSelection();
this.contextMenuService.showContextMenu({
getAnchor: () => e.anchor,
getActions: () => {
const actions: IAction[] = [];
// If the click is outside of the elements pass the root resource if there is only one root. If there are multiple roots pass empty object.
const roots = this.explorerService.roots;
const arg = stat instanceof ExplorerItem ? stat.resource : roots.length === 1 ? roots[0].resource : {};
fillInContextMenuActions(this.contributedContextMenu, { arg, shouldForwardArgs: true }, actions, this.contextMenuService);
return actions;
},
onHide: (wasCancelled?: boolean) => {
if (wasCancelled) {
this.tree.domFocus();
}
},
getActionsContext: () => stat && selection && selection.indexOf(stat) >= 0
? selection.map((fs: ExplorerItem) => fs.resource)
: stat instanceof ExplorerItem ? [stat.resource] : []
});
}
private onFocusChanged(elements: ExplorerItem[]): void {
const stat = elements && elements.length ? elements[0] : undefined;
const isSingleFolder = this.contextService.getWorkbenchState() === WorkbenchState.FOLDER;
const resource = stat ? stat.resource : isSingleFolder ? this.contextService.getWorkspace().folders[0].uri : null;
this.resourceContext.set(resource);
this.folderContext.set((isSingleFolder && !stat) || !!stat && stat.isDirectory);
this.readonlyContext.set(!!stat && stat.isReadonly);
this.rootContext.set(!stat || (stat && stat.isRoot));
}
// General methods
/**
* Refresh the contents of the explorer to get up to date data from the disk about the file structure.
* If the item is passed we refresh only that level of the tree, otherwise we do a full refresh.
*/
private refresh(item?: ExplorerItem): Promise<void> {
if (!this.tree || !this.isBodyVisible()) {
this.shouldRefresh = true;
return Promise.resolve(undefined);
}
// Tree node doesn't exist yet
if (item && !this.tree.hasNode(item)) {
return Promise.resolve(undefined);
}
const recursive = !item;
const toRefresh = item || this.tree.getInput();
return this.tree.updateChildren(toRefresh, recursive);
}
getOptimalWidth(): number {
const parentNode = this.tree.getHTMLElement();
const childNodes = ([] as HTMLElement[]).slice.call(parentNode.querySelectorAll('.explorer-item .label-name')); // select all file labels
return DOM.getLargestChildWidth(parentNode, childNodes);
}
// private didLoad = false;
private setTreeInput(): Promise<void> {
if (!this.isBodyVisible()) {
this.shouldRefresh = true;
return Promise.resolve(undefined);
}
const initialInputSetup = !this.tree.getInput();
if (initialInputSetup) {
perf.mark('willResolveExplorer');
}
const roots = this.explorerService.roots;
let input: ExplorerItem | ExplorerItem[] = roots[0];
if (this.contextService.getWorkbenchState() !== WorkbenchState.FOLDER || roots[0].isError) {
// Display roots only when multi folder workspace
input = roots;
}
let viewState: IAsyncDataTreeViewState | undefined;
if (this.tree && this.tree.getInput()) {
viewState = this.tree.getViewState();
} else {
const rawViewState = this.storageService.get(ExplorerView.TREE_VIEW_STATE_STORAGE_KEY, StorageScope.WORKSPACE);
if (rawViewState) {
viewState = JSON.parse(rawViewState) as IAsyncDataTreeViewState;
}
}
const previousInput = this.tree.getInput();
const promise = this.tree.setInput(input, viewState).then(() => {
if (Array.isArray(input)) {
if (!viewState || previousInput instanceof ExplorerItem) {
// There is no view state for this workspace, expand all roots. Or we transitioned from a folder workspace.
input.forEach(item => this.tree.expand(item).then(undefined, onUnexpectedError));
}
if (Array.isArray(previousInput) && previousInput.length < input.length) {
// Roots added to the explorer -> expand them.
input.slice(previousInput.length).forEach(item => this.tree.expand(item).then(undefined, onUnexpectedError));
}
}
if (initialInputSetup) {
perf.mark('didResolveExplorer');
}
});
this.progressService.showWhile(promise, this.layoutService.isRestored() ? 800 : 1200 /* less ugly initial startup */);
return promise;
}
private getActiveFile(): URI | undefined {
const input = this.editorService.activeEditor;
// ignore diff editor inputs (helps to get out of diffing when returning to explorer)
if (input instanceof DiffEditorInput) {
return undefined;
}
// check for files
return withNullAsUndefined(toResource(input, { supportSideBySide: true }));
}
private onSelectItem(fileStat: ExplorerItem | undefined, reveal = this.autoReveal): Promise<void> {
if (!fileStat || !this.isBodyVisible() || this.tree.getInput() === fileStat) {
return Promise.resolve(undefined);
}
// Expand all stats in the parent chain
const toExpand: ExplorerItem[] = [];
let parent = fileStat.parent;
while (parent) {
toExpand.push(parent);
parent = parent.parent;
}
return sequence(toExpand.reverse().map(s => () => this.tree.expand(s))).then(() => {
if (reveal) {
this.tree.reveal(fileStat, 0.5);
}
this.tree.setFocus([fileStat]);
this.tree.setSelection([fileStat]);
});
}
private onCopyItems(stats: ExplorerItem[], cut: boolean, previousCut: ExplorerItem[] | undefined): void {
this.fileCopiedContextKey.set(stats.length > 0);
this.resourceCutContextKey.set(cut && stats.length > 0);
if (previousCut) {
previousCut.forEach(item => this.tree.rerender(item));
}
if (cut) {
stats.forEach(s => this.tree.rerender(s));
}
}
collapseAll(): void {
this.tree.collapseAll();
}
dispose(): void {
if (this.dragHandler) {
this.dragHandler.dispose();
}
super.dispose();
}
}

View File

@@ -0,0 +1,825 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
import * as DOM from 'vs/base/browser/dom';
import * as glob from 'vs/base/common/glob';
import { IListVirtualDelegate, ListDragOverEffect } from 'vs/base/browser/ui/list/list';
import { IProgressService } from 'vs/platform/progress/common/progress';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IFileService, FileKind, IFileStat, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IDisposable, Disposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
import { KeyCode } from 'vs/base/common/keyCodes';
import { IFileLabelOptions, IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels';
import { ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IAsyncDataSource, ITreeSorter, ITreeDragAndDrop, ITreeDragOverReaction, TreeDragOverBubble } from 'vs/base/browser/ui/tree/tree';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { IFilesConfiguration, IExplorerService, IEditableData } from 'vs/workbench/contrib/files/common/files';
import { dirname, joinPath, isEqualOrParent, basename, hasToIgnoreCase, distinctParents } from 'vs/base/common/resources';
import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { localize } from 'vs/nls';
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { once } from 'vs/base/common/functional';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { equals, deepClone } from 'vs/base/common/objects';
import * as path from 'vs/base/common/path';
import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel';
import { compareFileExtensions, compareFileNames } from 'vs/base/common/comparers';
import { fillResourceDataTransfers, CodeDataTransfers, extractResources } from 'vs/workbench/browser/dnd';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd';
import { Schemas } from 'vs/base/common/network';
import { DesktopDragAndDropData, ExternalElementsDragAndDropData, ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
import { isMacintosh, isLinux } from 'vs/base/common/platform';
import { IDialogService, IConfirmationResult, IConfirmation, getConfirmMessage } from 'vs/platform/dialogs/common/dialogs';
import { ITextFileService, ITextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing';
import { URI } from 'vs/base/common/uri';
import { ITask, sequence } from 'vs/base/common/async';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces';
import { findValidPasteFileTarget } from 'vs/workbench/contrib/files/browser/fileActions';
import { FuzzyScore, createMatches } from 'vs/base/common/filters';
export class ExplorerDelegate implements IListVirtualDelegate<ExplorerItem> {
private static readonly ITEM_HEIGHT = 22;
getHeight(element: ExplorerItem): number {
return ExplorerDelegate.ITEM_HEIGHT;
}
getTemplateId(element: ExplorerItem): string {
return FilesRenderer.ID;
}
}
export class ExplorerDataSource implements IAsyncDataSource<ExplorerItem | ExplorerItem[], ExplorerItem> {
constructor(
@IProgressService private progressService: IProgressService,
@INotificationService private notificationService: INotificationService,
@IWorkbenchLayoutService private layoutService: IWorkbenchLayoutService,
@IFileService private fileService: IFileService
) { }
hasChildren(element: ExplorerItem | ExplorerItem[]): boolean {
return Array.isArray(element) || element.isDirectory;
}
getChildren(element: ExplorerItem | ExplorerItem[]): Promise<ExplorerItem[]> {
if (Array.isArray(element)) {
return Promise.resolve(element);
}
const promise = element.fetchChildren(this.fileService).then(undefined, e => {
// Do not show error for roots since we already use an explorer decoration to notify user
if (!(element instanceof ExplorerItem && element.isRoot)) {
this.notificationService.error(e);
}
return []; // we could not resolve any children because of an error
});
this.progressService.showWhile(promise, this.layoutService.isRestored() ? 800 : 3200 /* less ugly initial startup */);
return promise;
}
}
export interface IFileTemplateData {
elementDisposable: IDisposable;
label: IResourceLabel;
container: HTMLElement;
}
export class FilesRenderer implements ITreeRenderer<ExplorerItem, FuzzyScore, IFileTemplateData>, IDisposable {
static readonly ID = 'file';
private config: IFilesConfiguration;
private configListener: IDisposable;
constructor(
private labels: ResourceLabels,
private updateWidth: (stat: ExplorerItem) => void,
@IContextViewService private readonly contextViewService: IContextViewService,
@IThemeService private readonly themeService: IThemeService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IExplorerService private readonly explorerService: IExplorerService
) {
this.config = this.configurationService.getValue<IFilesConfiguration>();
this.configListener = this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('explorer')) {
this.config = this.configurationService.getValue();
}
});
}
get templateId(): string {
return FilesRenderer.ID;
}
renderTemplate(container: HTMLElement): IFileTemplateData {
const elementDisposable = Disposable.None;
const label = this.labels.create(container, { supportHighlights: true });
return { elementDisposable, label, container };
}
renderElement(node: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
templateData.elementDisposable.dispose();
const stat = node.element;
const editableData = this.explorerService.getEditableData(stat);
// File Label
if (!editableData) {
templateData.label.element.style.display = 'flex';
const extraClasses = ['explorer-item'];
if (this.explorerService.isCut(stat)) {
extraClasses.push('cut');
}
templateData.label.setFile(stat.resource, {
hidePath: true,
fileKind: stat.isRoot ? FileKind.ROOT_FOLDER : stat.isDirectory ? FileKind.FOLDER : FileKind.FILE,
extraClasses,
fileDecorations: this.config.explorer.decorations,
matches: createMatches(node.filterData)
});
templateData.elementDisposable = templateData.label.onDidRender(() => {
this.updateWidth(stat);
});
}
// Input Box
else {
templateData.label.element.style.display = 'none';
templateData.elementDisposable = this.renderInputBox(templateData.container, stat, editableData);
}
}
private renderInputBox(container: HTMLElement, stat: ExplorerItem, editableData: IEditableData): IDisposable {
// Use a file label only for the icon next to the input box
const label = this.labels.create(container);
const extraClasses = ['explorer-item', 'explorer-item-edited'];
const fileKind = stat.isRoot ? FileKind.ROOT_FOLDER : stat.isDirectory ? FileKind.FOLDER : FileKind.FILE;
const labelOptions: IFileLabelOptions = { hidePath: true, hideLabel: true, fileKind, extraClasses };
const parent = stat.name ? dirname(stat.resource) : stat.resource;
const value = stat.name || '';
label.setFile(joinPath(parent, value || ' '), labelOptions); // Use icon for ' ' if name is empty.
// Input field for name
const inputBox = new InputBox(label.element, this.contextViewService, {
validationOptions: {
validation: (value) => {
const content = editableData.validationMessage(value);
if (!content) {
return null;
}
return {
content,
formatContent: true,
type: MessageType.ERROR
};
}
},
ariaLabel: localize('fileInputAriaLabel', "Type file name. Press Enter to confirm or Escape to cancel.")
});
const styler = attachInputBoxStyler(inputBox, this.themeService);
inputBox.onDidChange(value => {
label.setFile(joinPath(parent, value || ' '), labelOptions); // update label icon while typing!
});
const lastDot = value.lastIndexOf('.');
inputBox.value = value;
inputBox.focus();
inputBox.select({ start: 0, end: lastDot > 0 && !stat.isDirectory ? lastDot : value.length });
const done = once(async (success: boolean) => {
label.element.style.display = 'none';
const value = inputBox.value;
dispose(toDispose);
container.removeChild(label.element);
editableData.onFinish(value, success);
});
let ignoreDisposeAndBlur = true;
setTimeout(() => ignoreDisposeAndBlur = false, 100);
const blurDisposable = DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, () => {
if (!ignoreDisposeAndBlur) {
done(inputBox.isInputValid());
}
});
const toDispose = [
inputBox,
DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, (e: IKeyboardEvent) => {
if (e.equals(KeyCode.Enter)) {
if (inputBox.validate()) {
done(true);
}
} else if (e.equals(KeyCode.Escape)) {
done(false);
}
}),
blurDisposable,
label,
styler
];
return toDisposable(() => {
if (!ignoreDisposeAndBlur) {
blurDisposable.dispose();
done(inputBox.isInputValid());
}
});
}
disposeElement?(element: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
templateData.elementDisposable.dispose();
}
disposeTemplate(templateData: IFileTemplateData): void {
templateData.elementDisposable.dispose();
templateData.label.dispose();
}
dispose(): void {
this.configListener.dispose();
}
}
export class ExplorerAccessibilityProvider implements IAccessibilityProvider<ExplorerItem> {
getAriaLabel(element: ExplorerItem): string {
return element.name;
}
}
interface CachedParsedExpression {
original: glob.IExpression;
parsed: glob.ParsedExpression;
}
export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> {
private hiddenExpressionPerRoot: Map<string, CachedParsedExpression>;
private workspaceFolderChangeListener: IDisposable;
constructor(
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IExplorerService private readonly explorerService: IExplorerService
) {
this.hiddenExpressionPerRoot = new Map<string, CachedParsedExpression>();
this.workspaceFolderChangeListener = this.contextService.onDidChangeWorkspaceFolders(() => this.updateConfiguration());
}
updateConfiguration(): boolean {
let needsRefresh = false;
this.contextService.getWorkspace().folders.forEach(folder => {
const configuration = this.configurationService.getValue<IFilesConfiguration>({ resource: folder.uri });
const excludesConfig: glob.IExpression = (configuration && configuration.files && configuration.files.exclude) || Object.create(null);
if (!needsRefresh) {
const cached = this.hiddenExpressionPerRoot.get(folder.uri.toString());
needsRefresh = !cached || !equals(cached.original, excludesConfig);
}
const excludesConfigCopy = deepClone(excludesConfig); // do not keep the config, as it gets mutated under our hoods
this.hiddenExpressionPerRoot.set(folder.uri.toString(), { original: excludesConfigCopy, parsed: glob.parse(excludesConfigCopy) } as CachedParsedExpression);
});
return needsRefresh;
}
filter(stat: ExplorerItem, parentVisibility: TreeVisibility): TreeFilterResult<FuzzyScore> {
if (parentVisibility === TreeVisibility.Hidden) {
return false;
}
if (this.explorerService.getEditableData(stat) || stat.isRoot) {
return true; // always visible
}
// Hide those that match Hidden Patterns
const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString());
if (cached && cached.parsed(path.normalize(path.relative(stat.root.resource.path, stat.resource.path)), stat.name, name => !!(stat.parent && stat.parent.getChild(name)))) {
// review (isidor): is path.normalize necessary? path.relative already returns an os path
return false; // hidden through pattern
}
return true;
}
public dispose(): void {
this.workspaceFolderChangeListener = dispose(this.workspaceFolderChangeListener);
}
}
// // Explorer Sorter
export class FileSorter implements ITreeSorter<ExplorerItem> {
constructor(
@IExplorerService private readonly explorerService: IExplorerService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
) { }
public compare(statA: ExplorerItem, statB: ExplorerItem): number {
// Do not sort roots
if (statA.isRoot) {
if (statB.isRoot) {
const workspaceA = this.contextService.getWorkspaceFolder(statA.resource);
const workspaceB = this.contextService.getWorkspaceFolder(statB.resource);
return workspaceA && workspaceB ? (workspaceA.index - workspaceB.index) : -1;
}
return -1;
}
if (statB.isRoot) {
return 1;
}
const sortOrder = this.explorerService.sortOrder;
// Sort Directories
switch (sortOrder) {
case 'type':
if (statA.isDirectory && !statB.isDirectory) {
return -1;
}
if (statB.isDirectory && !statA.isDirectory) {
return 1;
}
if (statA.isDirectory && statB.isDirectory) {
return compareFileNames(statA.name, statB.name);
}
break;
case 'filesFirst':
if (statA.isDirectory && !statB.isDirectory) {
return 1;
}
if (statB.isDirectory && !statA.isDirectory) {
return -1;
}
break;
case 'mixed':
break; // not sorting when "mixed" is on
default: /* 'default', 'modified' */
if (statA.isDirectory && !statB.isDirectory) {
return -1;
}
if (statB.isDirectory && !statA.isDirectory) {
return 1;
}
break;
}
// Sort Files
switch (sortOrder) {
case 'type':
return compareFileExtensions(statA.name, statB.name);
case 'modified':
if (statA.mtime !== statB.mtime) {
return (statA.mtime && statB.mtime && statA.mtime < statB.mtime) ? 1 : -1;
}
return compareFileNames(statA.name, statB.name);
default: /* 'default', 'mixed', 'filesFirst' */
return compareFileNames(statA.name, statB.name);
}
}
}
export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
private static readonly CONFIRM_DND_SETTING_KEY = 'explorer.confirmDragAndDrop';
private toDispose: IDisposable[];
private dropEnabled: boolean;
constructor(
@INotificationService private notificationService: INotificationService,
@IExplorerService private explorerService: IExplorerService,
@IEditorService private editorService: IEditorService,
@IDialogService private dialogService: IDialogService,
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@IFileService private fileService: IFileService,
@IConfigurationService private configurationService: IConfigurationService,
@IInstantiationService private instantiationService: IInstantiationService,
@ITextFileService private textFileService: ITextFileService,
@IWindowService private windowService: IWindowService,
@IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService
) {
this.toDispose = [];
const updateDropEnablement = () => {
this.dropEnabled = this.configurationService.getValue('explorer.enableDragAndDrop');
};
updateDropEnablement();
this.toDispose.push(this.configurationService.onDidChangeConfiguration((e) => updateDropEnablement()));
}
onDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
if (!this.dropEnabled) {
return false;
}
const isCopy = originalEvent && ((originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh));
const fromDesktop = data instanceof DesktopDragAndDropData;
const effect = (fromDesktop || isCopy) ? ListDragOverEffect.Copy : ListDragOverEffect.Move;
// Desktop DND
if (fromDesktop && originalEvent.dataTransfer) {
const types = originalEvent.dataTransfer.types;
const typesArray: string[] = [];
for (let i = 0; i < types.length; i++) {
typesArray.push(types[i].toLowerCase()); // somehow the types are lowercase
}
if (typesArray.indexOf(DataTransfers.FILES.toLowerCase()) === -1 && typesArray.indexOf(CodeDataTransfers.FILES.toLowerCase()) === -1) {
return false;
}
}
// Other-Tree DND
else if (data instanceof ExternalElementsDragAndDropData) {
return false;
}
// In-Explorer DND
else {
const items = (data as ElementsDragAndDropData<ExplorerItem>).elements;
if (!target) {
// Droping onto the empty area. Do not accept if items dragged are already
// children of the root unless we are copying the file
if (!isCopy && items.every(i => !!i.parent && i.parent.isRoot)) {
return false;
}
return { accept: true, bubble: TreeDragOverBubble.Down, effect, autoExpand: false };
}
if (!Array.isArray(items)) {
return false;
}
if (items.some((source) => {
if (source.isRoot && target instanceof ExplorerItem && !target.isRoot) {
return true; // Root folder can not be moved to a non root file stat.
}
if (source.resource.toString() === target.resource.toString()) {
return true; // Can not move anything onto itself
}
if (source.isRoot && target instanceof ExplorerItem && target.isRoot) {
// Disable moving workspace roots in one another
return false;
}
if (!isCopy && dirname(source.resource).toString() === target.resource.toString()) {
return true; // Can not move a file to the same parent unless we copy
}
if (isEqualOrParent(target.resource, source.resource, !isLinux /* ignorecase */)) {
return true; // Can not move a parent folder into one of its children
}
return false;
})) {
return false;
}
}
// All (target = model)
if (!target) {
return { accept: true, bubble: TreeDragOverBubble.Down, effect };
}
// All (target = file/folder)
else {
if (target.isDirectory) {
if (target.isReadonly) {
return false;
}
return { accept: true, bubble: TreeDragOverBubble.Down, effect, autoExpand: true };
}
if (this.contextService.getWorkspace().folders.every(folder => folder.uri.toString() !== target.resource.toString())) {
return { accept: true, bubble: TreeDragOverBubble.Up, effect };
}
}
return false;
}
getDragURI(element: ExplorerItem): string | null {
if (this.explorerService.isEditable(element)) {
return null;
}
return element.resource.toString();
}
getDragLabel(elements: ExplorerItem[]): string | undefined {
if (elements.length > 1) {
return String(elements.length);
}
return elements[0].name;
}
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
const items = (data as ElementsDragAndDropData<ExplorerItem>).elements;
if (items && items.length && originalEvent.dataTransfer) {
// Apply some datatransfer types to allow for dragging the element outside of the application
this.instantiationService.invokeFunction(fillResourceDataTransfers, items, originalEvent);
// The only custom data transfer we set from the explorer is a file transfer
// to be able to DND between multiple code file explorers across windows
const fileResources = items.filter(s => !s.isDirectory && s.resource.scheme === Schemas.file).map(r => r.resource.fsPath);
if (fileResources.length) {
originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources));
}
}
}
drop(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void {
// Find parent to add to
if (!target) {
target = this.explorerService.roots[this.explorerService.roots.length - 1];
}
if (!target.isDirectory && target.parent) {
target = target.parent;
}
if (target.isReadonly) {
return;
}
// Desktop DND (Import file)
if (data instanceof DesktopDragAndDropData) {
this.handleExternalDrop(data, target, originalEvent);
}
// In-Explorer DND (Move/Copy file)
else {
this.handleExplorerDrop(data, target, originalEvent);
}
}
private handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
const droppedResources = extractResources(originalEvent, true);
// Check for dropped external files to be folders
return this.fileService.resolveFiles(droppedResources).then(result => {
// Pass focus to window
this.windowService.focusWindow();
// Handle folders by adding to workspace if we are in workspace context
const folders = result.filter(r => r.success && r.stat && r.stat.isDirectory).map(result => ({ uri: result.stat!.resource }));
if (folders.length > 0) {
// If we are in no-workspace context, ask for confirmation to create a workspace
let confirmedPromise: Promise<IConfirmationResult> = Promise.resolve({ confirmed: true });
if (this.contextService.getWorkbenchState() !== WorkbenchState.WORKSPACE) {
confirmedPromise = this.dialogService.confirm({
message: folders.length > 1 ? localize('dropFolders', "Do you want to add the folders to the workspace?") : localize('dropFolder', "Do you want to add the folder to the workspace?"),
type: 'question',
primaryButton: folders.length > 1 ? localize('addFolders', "&&Add Folders") : localize('addFolder', "&&Add Folder")
});
}
return confirmedPromise.then(res => {
if (res.confirmed) {
return this.workspaceEditingService.addFolders(folders);
}
return undefined;
});
}
// Handle dropped files (only support FileStat as target)
else if (target instanceof ExplorerItem) {
return this.addResources(target, droppedResources.map(res => res.resource));
}
return undefined;
});
}
private addResources(target: ExplorerItem, resources: URI[]): Promise<any> {
if (resources && resources.length > 0) {
// Resolve target to check for name collisions and ask user
return this.fileService.resolveFile(target.resource).then((targetStat: IFileStat) => {
// Check for name collisions
const targetNames = new Set<string>();
if (targetStat.children) {
targetStat.children.forEach((child) => {
targetNames.add(isLinux ? child.name : child.name.toLowerCase());
});
}
let overwritePromise: Promise<IConfirmationResult> = Promise.resolve({ confirmed: true });
if (resources.some(resource => {
return targetNames.has(!hasToIgnoreCase(resource) ? basename(resource) : basename(resource).toLowerCase());
})) {
const confirm: IConfirmation = {
message: localize('confirmOverwrite', "A file or folder with the same name already exists in the destination folder. Do you want to replace it?"),
detail: localize('irreversible', "This action is irreversible!"),
primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
type: 'warning'
};
overwritePromise = this.dialogService.confirm(confirm);
}
return overwritePromise.then(res => {
if (!res.confirmed) {
return [];
}
// Run add in sequence
const addPromisesFactory: ITask<Promise<void>>[] = [];
resources.forEach(resource => {
addPromisesFactory.push(() => {
const sourceFile = resource;
const targetFile = joinPath(target.resource, basename(sourceFile));
// if the target exists and is dirty, make sure to revert it. otherwise the dirty contents
// of the target file would replace the contents of the added file. since we already
// confirmed the overwrite before, this is OK.
let revertPromise: Promise<ITextFileOperationResult | null> = Promise.resolve(null);
if (this.textFileService.isDirty(targetFile)) {
revertPromise = this.textFileService.revertAll([targetFile], { soft: true });
}
return revertPromise.then(() => {
const copyTarget = joinPath(target.resource, basename(sourceFile));
return this.fileService.copyFile(sourceFile, copyTarget, true).then(stat => {
// if we only add one file, just open it directly
if (resources.length === 1) {
this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } });
}
});
});
});
});
return sequence(addPromisesFactory);
});
});
}
return Promise.resolve(undefined);
}
private handleExplorerDrop(data: IDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
const elementsData = (data as ElementsDragAndDropData<ExplorerItem>).elements;
const items = distinctParents(elementsData, s => s.resource);
const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh);
let confirmPromise: Promise<IConfirmationResult>;
// Handle confirm setting
const confirmDragAndDrop = !isCopy && this.configurationService.getValue<boolean>(FileDragAndDrop.CONFIRM_DND_SETTING_KEY);
if (confirmDragAndDrop) {
confirmPromise = this.dialogService.confirm({
message: items.length > 1 && items.every(s => s.isRoot) ? localize('confirmRootsMove', "Are you sure you want to change the order of multiple root folders in your workspace?")
: items.length > 1 ? getConfirmMessage(localize('confirmMultiMove', "Are you sure you want to move the following {0} files?", items.length), items.map(s => s.resource))
: items[0].isRoot ? localize('confirmRootMove', "Are you sure you want to change the order of root folder '{0}' in your workspace?", items[0].name)
: localize('confirmMove', "Are you sure you want to move '{0}'?", items[0].name),
checkbox: {
label: localize('doNotAskAgain', "Do not ask me again")
},
type: 'question',
primaryButton: localize({ key: 'moveButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Move")
});
} else {
confirmPromise = Promise.resolve({ confirmed: true } as IConfirmationResult);
}
return confirmPromise.then(res => {
// Check for confirmation checkbox
let updateConfirmSettingsPromise: Promise<void> = Promise.resolve(undefined);
if (res.confirmed && res.checkboxChecked === true) {
updateConfirmSettingsPromise = this.configurationService.updateValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY, false, ConfigurationTarget.USER);
}
return updateConfirmSettingsPromise.then(() => {
if (res.confirmed) {
const rootDropPromise = this.doHandleRootDrop(items.filter(s => s.isRoot), target);
return Promise.all(items.filter(s => !s.isRoot).map(source => this.doHandleExplorerDrop(source, target, isCopy)).concat(rootDropPromise)).then(() => undefined);
}
return Promise.resolve(undefined);
});
});
}
private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem): Promise<void> {
if (roots.length === 0) {
return Promise.resolve(undefined);
}
const folders = this.contextService.getWorkspace().folders;
let targetIndex: number | undefined;
const workspaceCreationData: IWorkspaceFolderCreationData[] = [];
const rootsToMove: IWorkspaceFolderCreationData[] = [];
for (let index = 0; index < folders.length; index++) {
const data = {
uri: folders[index].uri,
name: folders[index].name
};
if (target instanceof ExplorerItem && folders[index].uri.toString() === target.resource.toString()) {
targetIndex = index;
}
if (roots.every(r => r.resource.toString() !== folders[index].uri.toString())) {
workspaceCreationData.push(data);
} else {
rootsToMove.push(data);
}
}
if (!targetIndex) {
targetIndex = workspaceCreationData.length;
}
workspaceCreationData.splice(targetIndex, 0, ...rootsToMove);
return this.workspaceEditingService.updateFolders(0, workspaceCreationData.length, workspaceCreationData);
}
private doHandleExplorerDrop(source: ExplorerItem, target: ExplorerItem, isCopy: boolean): Promise<void> {
// Reuse duplicate action if user copies
if (isCopy) {
return this.fileService.copyFile(source.resource, findValidPasteFileTarget(target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwirte: false })).then(stat => {
if (!stat.isDirectory) {
return this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }).then(() => undefined);
}
return undefined;
});
}
// Otherwise move
const targetResource = joinPath(target.resource, source.name);
return this.textFileService.move(source.resource, targetResource).then(undefined, error => {
// Conflict
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_MOVE_CONFLICT) {
const confirm: IConfirmation = {
message: localize('confirmOverwriteMessage', "'{0}' already exists in the destination folder. Do you want to replace it?", source.name),
detail: localize('irreversible', "This action is irreversible!"),
primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
type: 'warning'
};
// Move with overwrite if the user confirms
return this.dialogService.confirm(confirm).then(res => {
if (res.confirmed) {
return this.textFileService.move(source.resource, targetResource, true /* overwrite */).then(undefined, error => this.notificationService.error(error));
}
return undefined;
});
}
// Any other error
else {
this.notificationService.error(error);
}
return undefined;
});
}
}

View File

@@ -0,0 +1,670 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { RunOnceScheduler } from 'vs/base/common/async';
import { IAction, ActionRunner } from 'vs/base/common/actions';
import * as dom from 'vs/base/browser/dom';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IEditorGroupsService, IEditorGroup, GroupChangeKind, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IEditorInput } from 'vs/workbench/common/editor';
import { SaveAllAction, SaveAllInGroupAction, CloseGroupAction } from 'vs/workbench/contrib/files/browser/fileActions';
import { OpenEditorsFocusedContext, ExplorerFocusedContext, IFilesConfiguration, OpenEditor } from 'vs/workbench/contrib/files/common/files';
import { ITextFileService, AutoSaveMode } from 'vs/workbench/services/textfile/common/textfiles';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { CloseAllEditorsAction, CloseEditorAction } from 'vs/workbench/browser/parts/editor/editorActions';
import { ToggleEditorLayoutAction } from 'vs/workbench/browser/actions/layoutActions';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { attachStylerCallback } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { badgeBackground, badgeForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { WorkbenchList } from 'vs/platform/list/browser/listService';
import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction } from 'vs/base/browser/ui/list/list';
import { ResourceLabels, IResourceLabel, IResourceLabelsContainer } from 'vs/workbench/browser/labels';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem';
import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions';
import { DirtyEditorContext, OpenEditorsGroupContext } from 'vs/workbench/contrib/files/browser/fileCommands';
import { ResourceContextKey } from 'vs/workbench/common/resources';
import { ResourcesDropHandler, fillResourceDataTransfers, CodeDataTransfers } from 'vs/workbench/browser/dnd';
import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet';
import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd';
import { memoize } from 'vs/base/common/decorators';
import { ElementsDragAndDropData, DesktopDragAndDropData } from 'vs/base/browser/ui/list/listView';
import { URI } from 'vs/base/common/uri';
import { withNullAsUndefined } from 'vs/base/common/types';
const $ = dom.$;
export class OpenEditorsView extends ViewletPanel {
private static readonly DEFAULT_VISIBLE_OPEN_EDITORS = 9;
static readonly ID = 'workbench.explorer.openEditorsView';
static NAME = nls.localize({ key: 'openEditors', comment: ['Open is an adjective'] }, "Open Editors");
private dirtyCountElement: HTMLElement;
private listRefreshScheduler: RunOnceScheduler;
private structuralRefreshDelay: number;
private list: WorkbenchList<OpenEditor | IEditorGroup>;
private listLabels: ResourceLabels;
private contributedContextMenu: IMenu;
private needsRefresh: boolean;
private resourceContext: ResourceContextKey;
private groupFocusedContext: IContextKey<boolean>;
private dirtyEditorFocusedContext: IContextKey<boolean>;
constructor(
options: IViewletViewOptions,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextMenuService contextMenuService: IContextMenuService,
@ITextFileService private readonly textFileService: ITextFileService,
@IEditorService private readonly editorService: IEditorService,
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
@IConfigurationService configurationService: IConfigurationService,
@IKeybindingService keybindingService: IKeybindingService,
@IUntitledEditorService private readonly untitledEditorService: IUntitledEditorService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IThemeService private readonly themeService: IThemeService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IMenuService private readonly menuService: IMenuService
) {
super({
...(options as IViewletPanelOptions),
ariaHeaderLabel: nls.localize({ key: 'openEditosrSection', comment: ['Open is an adjective'] }, "Open Editors Section"),
}, keybindingService, contextMenuService, configurationService);
this.structuralRefreshDelay = 0;
this.listRefreshScheduler = new RunOnceScheduler(() => {
const previousLength = this.list.length;
this.list.splice(0, this.list.length, this.elements);
this.focusActiveEditor();
if (previousLength !== this.list.length) {
this.updateSize();
}
this.needsRefresh = false;
}, this.structuralRefreshDelay);
this.registerUpdateEvents();
// Also handle configuration updates
this.disposables.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChange(e)));
// Handle dirty counter
this.disposables.push(this.untitledEditorService.onDidChangeDirty(() => this.updateDirtyIndicator()));
this.disposables.push(this.textFileService.models.onModelsDirty(() => this.updateDirtyIndicator()));
this.disposables.push(this.textFileService.models.onModelsSaved(() => this.updateDirtyIndicator()));
this.disposables.push(this.textFileService.models.onModelsSaveError(() => this.updateDirtyIndicator()));
this.disposables.push(this.textFileService.models.onModelsReverted(() => this.updateDirtyIndicator()));
}
private registerUpdateEvents(): void {
const updateWholeList = () => {
if (!this.isBodyVisible() || !this.list) {
this.needsRefresh = true;
return;
}
this.listRefreshScheduler.schedule(this.structuralRefreshDelay);
};
const groupDisposables = new Map<number, IDisposable>();
const addGroupListener = (group: IEditorGroup) => {
groupDisposables.set(group.id, group.onDidGroupChange(e => {
if (this.listRefreshScheduler.isScheduled()) {
return;
}
if (!this.isBodyVisible() || !this.list) {
this.needsRefresh = true;
return;
}
const index = this.getIndex(group, e.editor);
switch (e.kind) {
case GroupChangeKind.GROUP_LABEL: {
if (this.showGroups) {
this.list.splice(index, 1, [group]);
}
break;
}
case GroupChangeKind.GROUP_ACTIVE:
case GroupChangeKind.EDITOR_ACTIVE: {
this.focusActiveEditor();
break;
}
case GroupChangeKind.EDITOR_DIRTY:
case GroupChangeKind.EDITOR_LABEL:
case GroupChangeKind.EDITOR_PIN: {
this.list.splice(index, 1, [new OpenEditor(e.editor!, group)]);
break;
}
case GroupChangeKind.EDITOR_OPEN: {
this.list.splice(index, 0, [new OpenEditor(e.editor!, group)]);
setTimeout(() => this.updateSize(), this.structuralRefreshDelay);
break;
}
case GroupChangeKind.EDITOR_CLOSE: {
const previousIndex = this.getIndex(group, undefined) + (e.editorIndex || 0) + (this.showGroups ? 1 : 0);
this.list.splice(previousIndex, 1);
this.updateSize();
break;
}
case GroupChangeKind.EDITOR_MOVE: {
this.listRefreshScheduler.schedule();
break;
}
}
}));
this.disposables.push(groupDisposables.get(group.id)!);
};
this.editorGroupService.groups.forEach(g => addGroupListener(g));
this.disposables.push(this.editorGroupService.onDidAddGroup(group => {
addGroupListener(group);
updateWholeList();
}));
this.disposables.push(this.editorGroupService.onDidMoveGroup(() => updateWholeList()));
this.disposables.push(this.editorGroupService.onDidRemoveGroup(group => {
dispose(groupDisposables.get(group.id));
updateWholeList();
}));
}
protected renderHeaderTitle(container: HTMLElement): void {
super.renderHeaderTitle(container, this.title);
const count = dom.append(container, $('.count'));
this.dirtyCountElement = dom.append(count, $('.monaco-count-badge'));
this.disposables.push((attachStylerCallback(this.themeService, { badgeBackground, badgeForeground, contrastBorder }, colors => {
const background = colors.badgeBackground ? colors.badgeBackground.toString() : null;
const foreground = colors.badgeForeground ? colors.badgeForeground.toString() : null;
const border = colors.contrastBorder ? colors.contrastBorder.toString() : null;
this.dirtyCountElement.style.backgroundColor = background;
this.dirtyCountElement.style.color = foreground;
this.dirtyCountElement.style.borderWidth = border ? '1px' : null;
this.dirtyCountElement.style.borderStyle = border ? 'solid' : null;
this.dirtyCountElement.style.borderColor = border;
})));
this.updateDirtyIndicator();
}
public renderBody(container: HTMLElement): void {
dom.addClass(container, 'explorer-open-editors');
dom.addClass(container, 'show-file-icons');
const delegate = new OpenEditorsDelegate();
if (this.list) {
this.list.dispose();
}
if (this.listLabels) {
this.listLabels.clear();
}
this.listLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility } as IResourceLabelsContainer);
this.list = this.instantiationService.createInstance(WorkbenchList, container, delegate, [
new EditorGroupRenderer(this.keybindingService, this.instantiationService),
new OpenEditorRenderer(this.listLabels, this.instantiationService, this.keybindingService, this.configurationService)
], {
identityProvider: { getId: (element: OpenEditor | IEditorGroup) => element instanceof OpenEditor ? element.getId() : element.id.toString() },
dnd: new OpenEditorsDragAndDrop(this.instantiationService, this.editorGroupService)
}) as WorkbenchList<OpenEditor | IEditorGroup>;
this.disposables.push(this.list);
this.disposables.push(this.listLabels);
this.contributedContextMenu = this.menuService.createMenu(MenuId.OpenEditorsContext, this.list.contextKeyService);
this.disposables.push(this.contributedContextMenu);
this.updateSize();
// Bind context keys
OpenEditorsFocusedContext.bindTo(this.list.contextKeyService);
ExplorerFocusedContext.bindTo(this.list.contextKeyService);
this.resourceContext = this.instantiationService.createInstance(ResourceContextKey);
this.disposables.push(this.resourceContext);
this.groupFocusedContext = OpenEditorsGroupContext.bindTo(this.contextKeyService);
this.dirtyEditorFocusedContext = DirtyEditorContext.bindTo(this.contextKeyService);
this.disposables.push(this.list.onContextMenu(e => this.onListContextMenu(e)));
this.list.onFocusChange(e => {
this.resourceContext.reset();
this.groupFocusedContext.reset();
this.dirtyEditorFocusedContext.reset();
const element = e.elements.length ? e.elements[0] : undefined;
if (element instanceof OpenEditor) {
this.dirtyEditorFocusedContext.set(this.textFileService.isDirty(withNullAsUndefined(element.getResource())));
this.resourceContext.set(element.getResource());
} else if (!!element) {
this.groupFocusedContext.set(true);
}
});
// Open when selecting via keyboard
this.disposables.push(this.list.onMouseMiddleClick(e => {
if (e && e.element instanceof OpenEditor) {
e.element.group.closeEditor(e.element.editor, { preserveFocus: true });
}
}));
this.disposables.push(this.list.onDidOpen(e => {
const browserEvent = e.browserEvent;
let openToSide = false;
let isSingleClick = false;
let isDoubleClick = false;
if (browserEvent instanceof MouseEvent) {
isSingleClick = browserEvent.detail === 1;
isDoubleClick = browserEvent.detail === 2;
openToSide = this.list.useAltAsMultipleSelectionModifier ? (browserEvent.ctrlKey || browserEvent.metaKey) : browserEvent.altKey;
}
const focused = this.list.getFocusedElements();
const element = focused.length ? focused[0] : undefined;
if (element instanceof OpenEditor) {
this.openEditor(element, { preserveFocus: isSingleClick, pinned: isDoubleClick, sideBySide: openToSide });
} else if (element) {
this.editorGroupService.activateGroup(element);
}
}));
this.listRefreshScheduler.schedule(0);
this.disposables.push(this.onDidChangeBodyVisibility(visible => {
if (visible && this.needsRefresh) {
this.listRefreshScheduler.schedule(0);
}
}));
}
public getActions(): IAction[] {
return [
this.instantiationService.createInstance(ToggleEditorLayoutAction, ToggleEditorLayoutAction.ID, ToggleEditorLayoutAction.LABEL),
this.instantiationService.createInstance(SaveAllAction, SaveAllAction.ID, SaveAllAction.LABEL),
this.instantiationService.createInstance(CloseAllEditorsAction, CloseAllEditorsAction.ID, CloseAllEditorsAction.LABEL)
];
}
public focus(): void {
super.focus();
this.list.domFocus();
}
public getList(): WorkbenchList<OpenEditor | IEditorGroup> {
return this.list;
}
protected layoutBody(height: number, width: number): void {
if (this.list) {
this.list.layout(height, width);
}
}
private get showGroups(): boolean {
return this.editorGroupService.groups.length > 1;
}
private get elements(): Array<IEditorGroup | OpenEditor> {
const result: Array<IEditorGroup | OpenEditor> = [];
this.editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE).forEach(g => {
if (this.showGroups) {
result.push(g);
}
result.push(...g.editors.map(ei => new OpenEditor(ei, g)));
});
return result;
}
private getIndex(group: IEditorGroup, editor: IEditorInput | undefined | null): number {
let index = editor ? group.getIndexOfEditor(editor) : 0;
if (!this.showGroups) {
return index;
}
for (let g of this.editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE)) {
if (g.id === group.id) {
return index + (!!editor ? 1 : 0);
} else {
index += g.count + 1;
}
}
return -1;
}
private openEditor(element: OpenEditor, options: { preserveFocus: boolean; pinned: boolean; sideBySide: boolean; }): void {
if (element) {
/* __GDPR__
"workbenchActionExecuted" : {
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('workbenchActionExecuted', { id: 'workbench.files.openFile', from: 'openEditors' });
const preserveActivateGroup = options.sideBySide && options.preserveFocus; // needed for https://github.com/Microsoft/vscode/issues/42399
if (!preserveActivateGroup) {
this.editorGroupService.activateGroup(element.groupId); // needed for https://github.com/Microsoft/vscode/issues/6672
}
this.editorService.openEditor(element.editor, options, options.sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => {
if (editor && !preserveActivateGroup && editor.group) {
this.editorGroupService.activateGroup(editor.group);
}
});
}
}
private onListContextMenu(e: IListContextMenuEvent<OpenEditor | IEditorGroup>): void {
if (!e.element) {
return;
}
const element = e.element;
this.contextMenuService.showContextMenu({
getAnchor: () => e.anchor,
getActions: () => {
const actions: IAction[] = [];
fillInContextMenuActions(this.contributedContextMenu, { shouldForwardArgs: true, arg: element instanceof OpenEditor ? element.editor.getResource() : {} }, actions, this.contextMenuService);
return actions;
},
getActionsContext: () => element instanceof OpenEditor ? { groupId: element.groupId, editorIndex: element.editorIndex } : { groupId: element.id }
});
}
private focusActiveEditor(): void {
if (this.list.length && this.editorGroupService.activeGroup) {
const index = this.getIndex(this.editorGroupService.activeGroup, this.editorGroupService.activeGroup.activeEditor);
this.list.setFocus([index]);
this.list.setSelection([index]);
this.list.reveal(index);
} else {
this.list.setFocus([]);
this.list.setSelection([]);
}
}
private onConfigurationChange(event: IConfigurationChangeEvent): void {
if (event.affectsConfiguration('explorer.openEditors')) {
this.updateSize();
}
// Trigger a 'repaint' when decoration settings change
if (event.affectsConfiguration('explorer.decorations')) {
this.listRefreshScheduler.schedule();
}
}
private updateSize(): void {
// Adjust expanded body size
this.minimumBodySize = this.getMinExpandedBodySize();
this.maximumBodySize = this.getMaxExpandedBodySize();
}
private updateDirtyIndicator(): void {
let dirty = this.textFileService.getAutoSaveMode() !== AutoSaveMode.AFTER_SHORT_DELAY ? this.textFileService.getDirty().length
: this.untitledEditorService.getDirty().length;
if (dirty === 0) {
dom.addClass(this.dirtyCountElement, 'hidden');
} else {
this.dirtyCountElement.textContent = nls.localize('dirtyCounter', "{0} unsaved", dirty);
dom.removeClass(this.dirtyCountElement, 'hidden');
}
}
private get elementCount(): number {
return this.editorGroupService.groups.map(g => g.count)
.reduce((first, second) => first + second, this.showGroups ? this.editorGroupService.groups.length : 0);
}
private getMaxExpandedBodySize(): number {
return this.elementCount * OpenEditorsDelegate.ITEM_HEIGHT;
}
private getMinExpandedBodySize(): number {
let visibleOpenEditors = this.configurationService.getValue<number>('explorer.openEditors.visible');
if (typeof visibleOpenEditors !== 'number') {
visibleOpenEditors = OpenEditorsView.DEFAULT_VISIBLE_OPEN_EDITORS;
}
return this.computeMinExpandedBodySize(visibleOpenEditors);
}
private computeMinExpandedBodySize(visibleOpenEditors = OpenEditorsView.DEFAULT_VISIBLE_OPEN_EDITORS): number {
const itemsToShow = Math.min(Math.max(visibleOpenEditors, 1), this.elementCount);
return itemsToShow * OpenEditorsDelegate.ITEM_HEIGHT;
}
public setStructuralRefreshDelay(delay: number): void {
this.structuralRefreshDelay = delay;
}
public getOptimalWidth(): number {
let parentNode = this.list.getHTMLElement();
let childNodes: HTMLElement[] = [].slice.call(parentNode.querySelectorAll('.open-editor > a'));
return dom.getLargestChildWidth(parentNode, childNodes);
}
}
interface IOpenEditorTemplateData {
container: HTMLElement;
root: IResourceLabel;
actionBar: ActionBar;
actionRunner: OpenEditorActionRunner;
}
interface IEditorGroupTemplateData {
root: HTMLElement;
name: HTMLSpanElement;
actionBar: ActionBar;
editorGroup: IEditorGroup;
}
class OpenEditorActionRunner extends ActionRunner {
public editor: OpenEditor;
run(action: IAction, context?: any): Promise<void> {
return super.run(action, { groupId: this.editor.groupId, editorIndex: this.editor.editorIndex });
}
}
class OpenEditorsDelegate implements IListVirtualDelegate<OpenEditor | IEditorGroup> {
public static readonly ITEM_HEIGHT = 22;
getHeight(element: OpenEditor | IEditorGroup): number {
return OpenEditorsDelegate.ITEM_HEIGHT;
}
getTemplateId(element: OpenEditor | IEditorGroup): string {
if (element instanceof OpenEditor) {
return OpenEditorRenderer.ID;
}
return EditorGroupRenderer.ID;
}
}
class EditorGroupRenderer implements IListRenderer<IEditorGroup, IEditorGroupTemplateData> {
static readonly ID = 'editorgroup';
constructor(
private keybindingService: IKeybindingService,
private instantiationService: IInstantiationService,
) {
// noop
}
get templateId() {
return EditorGroupRenderer.ID;
}
renderTemplate(container: HTMLElement): IEditorGroupTemplateData {
const editorGroupTemplate: IEditorGroupTemplateData = Object.create(null);
editorGroupTemplate.root = dom.append(container, $('.editor-group'));
editorGroupTemplate.name = dom.append(editorGroupTemplate.root, $('span.name'));
editorGroupTemplate.actionBar = new ActionBar(container);
const saveAllInGroupAction = this.instantiationService.createInstance(SaveAllInGroupAction, SaveAllInGroupAction.ID, SaveAllInGroupAction.LABEL);
const saveAllInGroupKey = this.keybindingService.lookupKeybinding(saveAllInGroupAction.id);
editorGroupTemplate.actionBar.push(saveAllInGroupAction, { icon: true, label: false, keybinding: saveAllInGroupKey ? saveAllInGroupKey.getLabel() : undefined });
const closeGroupAction = this.instantiationService.createInstance(CloseGroupAction, CloseGroupAction.ID, CloseGroupAction.LABEL);
const closeGroupActionKey = this.keybindingService.lookupKeybinding(closeGroupAction.id);
editorGroupTemplate.actionBar.push(closeGroupAction, { icon: true, label: false, keybinding: closeGroupActionKey ? closeGroupActionKey.getLabel() : undefined });
return editorGroupTemplate;
}
renderElement(editorGroup: IEditorGroup, index: number, templateData: IEditorGroupTemplateData): void {
templateData.editorGroup = editorGroup;
templateData.name.textContent = editorGroup.label;
templateData.actionBar.context = { groupId: editorGroup.id };
}
disposeTemplate(templateData: IEditorGroupTemplateData): void {
templateData.actionBar.dispose();
}
}
class OpenEditorRenderer implements IListRenderer<OpenEditor, IOpenEditorTemplateData> {
static readonly ID = 'openeditor';
constructor(
private labels: ResourceLabels,
private instantiationService: IInstantiationService,
private keybindingService: IKeybindingService,
private configurationService: IConfigurationService
) {
// noop
}
get templateId() {
return OpenEditorRenderer.ID;
}
renderTemplate(container: HTMLElement): IOpenEditorTemplateData {
const editorTemplate: IOpenEditorTemplateData = Object.create(null);
editorTemplate.container = container;
editorTemplate.actionRunner = new OpenEditorActionRunner();
editorTemplate.actionBar = new ActionBar(container, { actionRunner: editorTemplate.actionRunner });
container.draggable = true;
const closeEditorAction = this.instantiationService.createInstance(CloseEditorAction, CloseEditorAction.ID, CloseEditorAction.LABEL);
const key = this.keybindingService.lookupKeybinding(closeEditorAction.id);
editorTemplate.actionBar.push(closeEditorAction, { icon: true, label: false, keybinding: key ? key.getLabel() : undefined });
editorTemplate.root = this.labels.create(container);
return editorTemplate;
}
renderElement(editor: OpenEditor, index: number, templateData: IOpenEditorTemplateData): void {
templateData.actionRunner.editor = editor;
editor.isDirty() ? dom.addClass(templateData.container, 'dirty') : dom.removeClass(templateData.container, 'dirty');
templateData.root.setEditor(editor.editor, {
italic: editor.isPreview(),
extraClasses: ['open-editor'],
fileDecorations: this.configurationService.getValue<IFilesConfiguration>().explorer.decorations
});
}
disposeTemplate(templateData: IOpenEditorTemplateData): void {
templateData.actionBar.dispose();
templateData.root.dispose();
templateData.actionRunner.dispose();
}
}
class OpenEditorsDragAndDrop implements IListDragAndDrop<OpenEditor | IEditorGroup> {
constructor(
private instantiationService: IInstantiationService,
private editorGroupService: IEditorGroupsService
) { }
@memoize private get dropHandler(): ResourcesDropHandler {
return this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: false });
}
getDragURI(element: OpenEditor | IEditorGroup): string | null {
if (element instanceof OpenEditor) {
const resource = element.getResource();
if (resource) {
return resource.toString();
}
}
return null;
}
getDragLabel?(elements: (OpenEditor | IEditorGroup)[]): string | undefined {
if (elements.length > 1) {
return String(elements.length);
}
const element = elements[0];
return element instanceof OpenEditor ? withNullAsUndefined(element.editor.getName()) : element.label;
}
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
const items = (data as ElementsDragAndDropData<OpenEditor | IEditorGroup>).elements;
const resources: URI[] = [];
if (items) {
items.forEach(i => {
if (i instanceof OpenEditor) {
const resource = i.getResource();
if (resource) {
resources.push(resource);
}
}
});
}
if (resources.length) {
// Apply some datatransfer types to allow for dragging the element outside of the application
this.instantiationService.invokeFunction(fillResourceDataTransfers, resources, originalEvent);
}
}
onDragOver(data: IDragAndDropData, targetElement: OpenEditor | IEditorGroup, targetIndex: number, originalEvent: DragEvent): boolean | IListDragOverReaction {
if (data instanceof DesktopDragAndDropData && originalEvent.dataTransfer) {
const types = originalEvent.dataTransfer.types;
const typesArray: string[] = [];
for (let i = 0; i < types.length; i++) {
typesArray.push(types[i].toLowerCase()); // somehow the types are lowercase
}
if (typesArray.indexOf(DataTransfers.FILES.toLowerCase()) === -1 && typesArray.indexOf(CodeDataTransfers.FILES.toLowerCase()) === -1) {
return false;
}
}
return true;
}
drop(data: IDragAndDropData, targetElement: OpenEditor | IEditorGroup, targetIndex: number, originalEvent: DragEvent): void {
const group = targetElement instanceof OpenEditor ? targetElement.group : targetElement;
const index = targetElement instanceof OpenEditor ? targetElement.group.getIndexOfEditor(targetElement.editor) : 0;
if (data instanceof ElementsDragAndDropData) {
const elementsData = data.elements;
elementsData.forEach((oe, offset) => {
oe.group.moveEditor(oe.editor, group, { index: index + offset, preserveFocus: true });
});
this.editorGroupService.activateGroup(group);
} else {
this.dropHandler.handleDrop(originalEvent, () => group, () => group.focus(), index);
}
}
}