vscode/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts
2021-11-24 13:58:32 +01:00

818 lines
32 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/openeditors';
import * as nls from 'vs/nls';
import { RunOnceScheduler } from 'vs/base/common/async';
import { IAction, ActionRunner, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions';
import * as dom from 'vs/base/browser/dom';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupOrientation } 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 { Verbosity, EditorResourceAccessor, SideBySideEditor, EditorInputCapabilities, IEditorIdentifier, GroupChangeKind } from 'vs/workbench/common/editor';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { SaveAllInGroupAction, CloseGroupAction } from 'vs/workbench/contrib/files/browser/fileActions';
import { OpenEditorsFocusedContext, ExplorerFocusedContext, IFilesConfiguration, OpenEditor } from 'vs/workbench/contrib/files/common/files';
import { CloseAllEditorsAction, CloseEditorAction, UnpinEditorAction } from 'vs/workbench/browser/parts/editor/editorActions';
import { IContextKeyService, IContextKey, ContextKeyExpr } 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 } from 'vs/workbench/browser/labels';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IMenuService, MenuId, IMenu, Action2, registerAction2, MenuRegistry } from 'vs/platform/actions/common/actions';
import { OpenEditorsDirtyEditorContext, OpenEditorsGroupContext, OpenEditorsReadonlyEditorContext, SAVE_ALL_LABEL, SAVE_ALL_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands';
import { ResourceContextKey } from 'vs/workbench/common/resources';
import { ResourcesDropHandler, fillEditorsDragData, CodeDataTransfers, containsDragType } from 'vs/workbench/browser/dnd';
import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane';
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, NativeDragAndDropData } from 'vs/base/browser/ui/list/listView';
import { withUndefinedAsNull } from 'vs/base/common/types';
import { isWeb } from 'vs/base/common/platform';
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy';
import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { IViewDescriptorService } from 'vs/workbench/common/views';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { Orientation } from 'vs/base/browser/ui/splitview/splitview';
import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
import { compareFileNamesDefault } from 'vs/base/common/comparers';
import { Codicon } from 'vs/base/common/codicons';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ICommandService } from 'vs/platform/commands/common/commands';
const $ = dom.$;
export class OpenEditorsView extends ViewPane {
private static readonly DEFAULT_VISIBLE_OPEN_EDITORS = 9;
static readonly ID = 'workbench.explorer.openEditorsView';
static readonly 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 | undefined;
private contributedContextMenu!: IMenu;
private needsRefresh = false;
private elements: (OpenEditor | IEditorGroup)[] = [];
private sortOrder: 'editorOrder' | 'alphabetical';
private resourceContext!: ResourceContextKey;
private groupFocusedContext!: IContextKey<boolean>;
private dirtyEditorFocusedContext!: IContextKey<boolean>;
private readonlyEditorFocusedContext!: IContextKey<boolean>;
constructor(
options: IViewletViewOptions,
@IInstantiationService instantiationService: IInstantiationService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@IContextMenuService contextMenuService: IContextMenuService,
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
@IConfigurationService configurationService: IConfigurationService,
@IKeybindingService keybindingService: IKeybindingService,
@IContextKeyService contextKeyService: IContextKeyService,
@IThemeService themeService: IThemeService,
@ITelemetryService telemetryService: ITelemetryService,
@IMenuService private readonly menuService: IMenuService,
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService,
@IOpenerService openerService: IOpenerService,
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
this.structuralRefreshDelay = 0;
let labelChangeListeners: IDisposable[] = [];
this.listRefreshScheduler = new RunOnceScheduler(() => {
labelChangeListeners = dispose(labelChangeListeners);
const previousLength = this.list.length;
const elements = this.getElements();
this.list.splice(0, this.list.length, elements);
this.focusActiveEditor();
if (previousLength !== this.list.length) {
this.updateSize();
}
this.needsRefresh = false;
if (this.sortOrder === 'alphabetical') {
// We need to resort the list if the editor label changed
elements.forEach(e => {
if (e instanceof OpenEditor) {
labelChangeListeners.push(e.editor.onDidChangeLabel(() => this.listRefreshScheduler.schedule()));
}
});
}
}, this.structuralRefreshDelay);
this.sortOrder = configurationService.getValue('explorer.openEditors.sortOrder');
this.registerUpdateEvents();
// Also handle configuration updates
this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChange(e)));
// Handle dirty counter
this._register(this.workingCopyService.onDidChangeDirty(workingCopy => this.updateDirtyIndicator(workingCopy)));
}
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_INDEX: {
if (index >= 0) {
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_CAPABILITIES:
case GroupChangeKind.EDITOR_STICKY:
case GroupChangeKind.EDITOR_PIN: {
this.list.splice(index, 1, [new OpenEditor(e.editor!, group)]);
this.focusActiveEditor();
break;
}
case GroupChangeKind.EDITOR_OPEN:
case GroupChangeKind.EDITOR_CLOSE:
case GroupChangeKind.EDITOR_MOVE: {
updateWholeList();
break;
}
}
}));
this._register(groupDisposables.get(group.id)!);
};
this.editorGroupService.groups.forEach(g => addGroupListener(g));
this._register(this.editorGroupService.onDidAddGroup(group => {
addGroupListener(group);
updateWholeList();
}));
this._register(this.editorGroupService.onDidMoveGroup(() => updateWholeList()));
this._register(this.editorGroupService.onDidRemoveGroup(group => {
dispose(groupDisposables.get(group.id));
updateWholeList();
}));
}
protected override renderHeaderTitle(container: HTMLElement): void {
super.renderHeaderTitle(container, this.title);
const count = dom.append(container, $('.count'));
this.dirtyCountElement = dom.append(count, $('.dirty-count.monaco-count-badge.long'));
this._register((attachStylerCallback(this.themeService, { badgeBackground, badgeForeground, contrastBorder }, colors => {
const background = colors.badgeBackground ? colors.badgeBackground.toString() : '';
const foreground = colors.badgeForeground ? colors.badgeForeground.toString() : '';
const border = colors.contrastBorder ? colors.contrastBorder.toString() : '';
this.dirtyCountElement.style.backgroundColor = background;
this.dirtyCountElement.style.color = foreground;
this.dirtyCountElement.style.borderWidth = border ? '1px' : '';
this.dirtyCountElement.style.borderStyle = border ? 'solid' : '';
this.dirtyCountElement.style.borderColor = border;
})));
this.updateDirtyIndicator();
}
override renderBody(container: HTMLElement): void {
super.renderBody(container);
container.classList.add('open-editors');
container.classList.add('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 });
this.list = this.instantiationService.createInstance(WorkbenchList, 'OpenEditors', 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),
overrideStyles: {
listBackground: this.getBackgroundColor()
},
accessibilityProvider: new OpenEditorsAccessibilityProvider()
}) as WorkbenchList<OpenEditor | IEditorGroup>;
this._register(this.list);
this._register(this.listLabels);
this.contributedContextMenu = this.menuService.createMenu(MenuId.OpenEditorsContext, this.list.contextKeyService);
this._register(this.contributedContextMenu);
this.updateSize();
// Bind context keys
OpenEditorsFocusedContext.bindTo(this.list.contextKeyService);
ExplorerFocusedContext.bindTo(this.list.contextKeyService);
this.resourceContext = this.instantiationService.createInstance(ResourceContextKey);
this._register(this.resourceContext);
this.groupFocusedContext = OpenEditorsGroupContext.bindTo(this.contextKeyService);
this.dirtyEditorFocusedContext = OpenEditorsDirtyEditorContext.bindTo(this.contextKeyService);
this.readonlyEditorFocusedContext = OpenEditorsReadonlyEditorContext.bindTo(this.contextKeyService);
this._register(this.list.onContextMenu(e => this.onListContextMenu(e)));
this.list.onDidChangeFocus(e => {
this.resourceContext.reset();
this.groupFocusedContext.reset();
this.dirtyEditorFocusedContext.reset();
this.readonlyEditorFocusedContext.reset();
const element = e.elements.length ? e.elements[0] : undefined;
if (element instanceof OpenEditor) {
const resource = element.getResource();
this.dirtyEditorFocusedContext.set(element.editor.isDirty() && !element.editor.isSaving());
this.readonlyEditorFocusedContext.set(element.editor.hasCapability(EditorInputCapabilities.Readonly));
this.resourceContext.set(withUndefinedAsNull(resource));
} else if (!!element) {
this.groupFocusedContext.set(true);
}
});
// Open when selecting via keyboard
this._register(this.list.onMouseMiddleClick(e => {
if (e && e.element instanceof OpenEditor) {
e.element.group.closeEditor(e.element.editor, { preserveFocus: true });
}
}));
this._register(this.list.onDidOpen(e => {
if (!e.element) {
return;
} else if (e.element instanceof OpenEditor) {
if (e.browserEvent instanceof MouseEvent && e.browserEvent.button === 1) {
return; // middle click already handled above: closes the editor
}
this.openEditor(e.element, { preserveFocus: e.editorOptions.preserveFocus, pinned: e.editorOptions.pinned, sideBySide: e.sideBySide });
} else {
this.editorGroupService.activateGroup(e.element);
}
}));
this.listRefreshScheduler.schedule(0);
this._register(this.onDidChangeBodyVisibility(visible => {
if (visible && this.needsRefresh) {
this.listRefreshScheduler.schedule(0);
}
}));
const containerModel = this.viewDescriptorService.getViewContainerModel(this.viewDescriptorService.getViewContainerByViewId(this.id)!)!;
this._register(containerModel.onDidChangeAllViewDescriptors(() => {
this.updateSize();
}));
}
override focus(): void {
super.focus();
this.list.domFocus();
}
getList(): WorkbenchList<OpenEditor | IEditorGroup> {
return this.list;
}
protected override layoutBody(height: number, width: number): void {
super.layoutBody(height, width);
if (this.list) {
this.list.layout(height, width);
}
}
private get showGroups(): boolean {
return this.editorGroupService.groups.length > 1;
}
private getElements(): Array<IEditorGroup | OpenEditor> {
this.elements = [];
this.editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE).forEach(g => {
if (this.showGroups) {
this.elements.push(g);
}
let editors = g.editors.map(ei => new OpenEditor(ei, g));
if (this.sortOrder === 'alphabetical') {
editors = editors.sort((first, second) => compareFileNamesDefault(first.editor.getName(), second.editor.getName()));
}
this.elements.push(...editors);
});
return this.elements;
}
private getIndex(group: IEditorGroup, editor: EditorInput | undefined | null): number {
if (!editor) {
return this.elements.findIndex(e => !(e instanceof OpenEditor) && e.id === group.id);
}
return this.elements.findIndex(e => e instanceof OpenEditor && e.editor === editor && e.group.id === group.id);
}
private openEditor(element: OpenEditor, options: { preserveFocus?: boolean; pinned?: boolean; sideBySide?: boolean; }): void {
if (element) {
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('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.group); // needed for https://github.com/microsoft/vscode/issues/6672
}
const targetGroup = options.sideBySide ? this.editorGroupService.sideGroup : this.editorGroupService.activeGroup;
targetGroup.openEditor(element.editor, options);
}
}
private onListContextMenu(e: IListContextMenuEvent<OpenEditor | IEditorGroup>): void {
if (!e.element) {
return;
}
const element = e.element;
const actions: IAction[] = [];
const actionsDisposable = createAndFillInContextMenuActions(this.contributedContextMenu, { shouldForwardArgs: true, arg: element instanceof OpenEditor ? EditorResourceAccessor.getOriginalUri(element.editor) : {} }, actions);
this.contextMenuService.showContextMenu({
getAnchor: () => e.anchor,
getActions: () => actions,
getActionsContext: () => element instanceof OpenEditor ? { groupId: element.groupId, editorIndex: element.group.getIndexOfEditor(element.editor) } : { groupId: element.id },
onHide: () => dispose(actionsDisposable)
});
}
private focusActiveEditor(): void {
if (this.list.length && this.editorGroupService.activeGroup) {
const index = this.getIndex(this.editorGroupService.activeGroup, this.editorGroupService.activeGroup.activeEditor);
if (index >= 0) {
try {
this.list.setFocus([index]);
this.list.setSelection([index]);
this.list.reveal(index);
} catch (e) {
// noop list updated in the meantime
}
return;
}
}
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 or the sort order changed
if (event.affectsConfiguration('explorer.decorations') || event.affectsConfiguration('explorer.openEditors.sortOrder')) {
this.sortOrder = this.configurationService.getValue('explorer.openEditors.sortOrder');
this.listRefreshScheduler.schedule();
}
}
private updateSize(): void {
// Adjust expanded body size
this.minimumBodySize = this.orientation === Orientation.VERTICAL ? this.getMinExpandedBodySize() : 170;
this.maximumBodySize = this.orientation === Orientation.VERTICAL ? this.getMaxExpandedBodySize() : Number.POSITIVE_INFINITY;
}
private updateDirtyIndicator(workingCopy?: IWorkingCopy): void {
if (workingCopy) {
const gotDirty = workingCopy.isDirty();
if (gotDirty && !(workingCopy.capabilities & WorkingCopyCapabilities.Untitled) && this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) {
return; // do not indicate dirty of working copies that are auto saved after short delay
}
}
let dirty = this.workingCopyService.dirtyCount;
if (dirty === 0) {
this.dirtyCountElement.classList.add('hidden');
} else {
this.dirtyCountElement.textContent = nls.localize('dirtyCounter', "{0} unsaved", dirty);
this.dirtyCountElement.classList.remove('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 {
const containerModel = this.viewDescriptorService.getViewContainerModel(this.viewDescriptorService.getViewContainerByViewId(this.id)!)!;
if (containerModel.visibleViewDescriptors.length <= 1) {
return Number.POSITIVE_INFINITY;
}
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;
}
setStructuralRefreshDelay(delay: number): void {
this.structuralRefreshDelay = delay;
}
override 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 | undefined;
override async run(action: IAction): Promise<void> {
if (!this.editor) {
return;
}
return super.run(action, { groupId: this.editor.groupId, editorIndex: this.editor.group.getIndexOfEditor(this.editor.editor) });
}
}
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';
private readonly closeEditorAction = this.instantiationService.createInstance(CloseEditorAction, CloseEditorAction.ID, CloseEditorAction.LABEL);
private readonly unpinEditorAction = this.instantiationService.createInstance(UnpinEditorAction, UnpinEditorAction.ID, UnpinEditorAction.LABEL);
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 });
editorTemplate.root = this.labels.create(container);
return editorTemplate;
}
renderElement(openedEditor: OpenEditor, _index: number, templateData: IOpenEditorTemplateData): void {
const editor = openedEditor.editor;
templateData.actionRunner.editor = openedEditor;
templateData.container.classList.toggle('dirty', editor.isDirty() && !editor.isSaving());
templateData.container.classList.toggle('sticky', openedEditor.isSticky());
templateData.root.setResource({
resource: EditorResourceAccessor.getOriginalUri(editor, { supportSideBySide: SideBySideEditor.BOTH }),
name: editor.getName(),
description: editor.getDescription(Verbosity.MEDIUM)
}, {
italic: openedEditor.isPreview(),
extraClasses: ['open-editor'].concat(openedEditor.editor.getLabelExtraClasses()),
fileDecorations: this.configurationService.getValue<IFilesConfiguration>().explorer.decorations,
title: editor.getTitle(Verbosity.LONG)
});
const editorAction = openedEditor.isSticky() ? this.unpinEditorAction : this.closeEditorAction;
if (!templateData.actionBar.hasAction(editorAction)) {
if (!templateData.actionBar.isEmpty()) {
templateData.actionBar.clear();
}
templateData.actionBar.push(editorAction, { icon: true, label: false, keybinding: this.keybindingService.lookupKeybinding(editorAction.id)?.getLabel() });
}
}
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 {
if (elements.length > 1) {
return String(elements.length);
}
const element = elements[0];
return element instanceof OpenEditor ? element.editor.getName() : element.label;
}
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
const items = (data as ElementsDragAndDropData<OpenEditor | IEditorGroup>).elements;
const editors: IEditorIdentifier[] = [];
if (items) {
for (const item of items) {
if (item instanceof OpenEditor) {
editors.push(item);
}
}
}
if (editors.length) {
// Apply some datatransfer types to allow for dragging the element outside of the application
this.instantiationService.invokeFunction(fillEditorsDragData, editors, originalEvent);
}
}
onDragOver(data: IDragAndDropData, _targetElement: OpenEditor | IEditorGroup, _targetIndex: number, originalEvent: DragEvent): boolean | IListDragOverReaction {
if (data instanceof NativeDragAndDropData) {
if (isWeb) {
return false; // dropping files into editor is unsupported on web
}
return containsDragType(originalEvent, DataTransfers.FILES, CodeDataTransfers.FILES);
}
return true;
}
drop(data: IDragAndDropData, targetElement: OpenEditor | IEditorGroup | undefined, _targetIndex: number, originalEvent: DragEvent): void {
const group = targetElement instanceof OpenEditor ? targetElement.group : targetElement || this.editorGroupService.groups[this.editorGroupService.count - 1];
const index = targetElement instanceof OpenEditor ? targetElement.group.getIndexOfEditor(targetElement.editor) : 0;
if (data instanceof ElementsDragAndDropData) {
const elementsData = data.elements;
elementsData.forEach((oe: OpenEditor, 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);
}
}
}
class OpenEditorsAccessibilityProvider implements IListAccessibilityProvider<OpenEditor | IEditorGroup> {
getWidgetAriaLabel(): string {
return nls.localize('openEditors', "Open Editors");
}
getAriaLabel(element: OpenEditor | IEditorGroup): string | null {
if (element instanceof OpenEditor) {
return `${element.editor.getName()}, ${element.editor.getDescription()}`;
}
return element.ariaLabel;
}
}
const toggleEditorGroupLayoutId = 'workbench.action.toggleEditorGroupLayout';
registerAction2(class extends Action2 {
constructor() {
super({
id: 'workbench.action.toggleEditorGroupLayout',
title: { value: nls.localize('flipLayout', "Toggle Vertical/Horizontal Editor Layout"), original: 'Toggle Vertical/Horizontal Editor Layout' },
f1: true,
keybinding: {
primary: KeyMod.Shift | KeyMod.Alt | KeyCode.Digit0,
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Digit0 },
weight: KeybindingWeight.WorkbenchContrib
},
icon: Codicon.editorLayout,
menu: {
id: MenuId.ViewTitle,
group: 'navigation',
when: ContextKeyExpr.equals('view', OpenEditorsView.ID),
order: 10
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const editorGroupService = accessor.get(IEditorGroupsService);
const newOrientation = (editorGroupService.orientation === GroupOrientation.VERTICAL) ? GroupOrientation.HORIZONTAL : GroupOrientation.VERTICAL;
editorGroupService.setGroupOrientation(newOrientation);
}
});
MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, {
group: '4_flip',
command: {
id: toggleEditorGroupLayoutId,
title: nls.localize({ key: 'miToggleEditorLayout', comment: ['&& denotes a mnemonic'] }, "Flip &&Layout")
},
order: 1
});
registerAction2(class extends Action2 {
constructor() {
super({
id: 'workbench.action.files.saveAll',
title: { value: SAVE_ALL_LABEL, original: 'Save All' },
f1: true,
icon: Codicon.saveAll,
menu: {
id: MenuId.ViewTitle,
group: 'navigation',
when: ContextKeyExpr.equals('view', OpenEditorsView.ID),
order: 20
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandService = accessor.get(ICommandService);
await commandService.executeCommand(SAVE_ALL_COMMAND_ID);
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: 'openEditors.closeAll',
title: CloseAllEditorsAction.LABEL,
f1: false,
icon: Codicon.closeAll,
menu: {
id: MenuId.ViewTitle,
group: 'navigation',
when: ContextKeyExpr.equals('view', OpenEditorsView.ID),
order: 30
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const instantiationService = accessor.get(IInstantiationService);
const closeAll = instantiationService.createInstance(CloseAllEditorsAction, CloseAllEditorsAction.ID, CloseAllEditorsAction.LABEL);
await closeAll.run();
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: 'openEditors.newUntitledFile',
title: { value: nls.localize('newUntitledFile', "New Untitled File"), original: 'New Untitled File' },
f1: false,
icon: Codicon.newFile,
menu: {
id: MenuId.ViewTitle,
group: 'navigation',
when: ContextKeyExpr.equals('view', OpenEditorsView.ID),
order: 5
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandService = accessor.get(ICommandService);
await commandService.executeCommand(NEW_UNTITLED_FILE_COMMAND_ID);
}
});