mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-04-01 01:20:31 -04:00
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:
138
src/vs/workbench/contrib/files/browser/views/emptyView.ts
Normal file
138
src/vs/workbench/contrib/files/browser/views/emptyView.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
554
src/vs/workbench/contrib/files/browser/views/explorerView.ts
Normal file
554
src/vs/workbench/contrib/files/browser/views/explorerView.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
825
src/vs/workbench/contrib/files/browser/views/explorerViewer.ts
Normal file
825
src/vs/workbench/contrib/files/browser/views/explorerViewer.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
670
src/vs/workbench/contrib/files/browser/views/openEditorsView.ts
Normal file
670
src/vs/workbench/contrib/files/browser/views/openEditorsView.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user