Path: blob/main/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as dom from '../../../../base/browser/dom.js';6import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js';7import { PixelRatio } from '../../../../base/browser/pixelRatio.js';8import { BreadcrumbsItem, BreadcrumbsWidget, IBreadcrumbsItemEvent, IBreadcrumbsWidgetStyles } from '../../../../base/browser/ui/breadcrumbs/breadcrumbsWidget.js';9import { applyDragImage } from '../../../../base/browser/ui/dnd/dnd.js';10import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js';11import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';12import { timeout } from '../../../../base/common/async.js';13import { Codicon } from '../../../../base/common/codicons.js';14import { Emitter } from '../../../../base/common/event.js';15import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';16import { combinedDisposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';17import { basename, extUri } from '../../../../base/common/resources.js';18import { URI } from '../../../../base/common/uri.js';19import { DocumentSymbol } from '../../../../editor/common/languages.js';20import { OutlineElement } from '../../../../editor/contrib/documentSymbols/browser/outlineModel.js';21import { localize, localize2 } from '../../../../nls.js';22import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';23import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';24import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';25import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';26import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js';27import { fillInSymbolsDragData, LocalSelectionTransfer } from '../../../../platform/dnd/browser/dnd.js';28import { FileKind, IFileService, IFileStat } from '../../../../platform/files/common/files.js';29import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';30import { InstantiationService } from '../../../../platform/instantiation/common/instantiationService.js';31import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';32import { ILabelService } from '../../../../platform/label/common/label.js';33import { IListService, WorkbenchAsyncDataTree, WorkbenchDataTree, WorkbenchListFocusContextKey } from '../../../../platform/list/browser/listService.js';34import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js';35import { defaultBreadcrumbsWidgetStyles } from '../../../../platform/theme/browser/defaultStyles.js';36import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js';37import { EditorResourceAccessor, IEditorPartOptions, SideBySideEditor } from '../../../common/editor.js';38import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';39import { ACTIVE_GROUP, ACTIVE_GROUP_TYPE, IEditorService, SIDE_GROUP, SIDE_GROUP_TYPE } from '../../../services/editor/common/editorService.js';40import { IOutline } from '../../../services/outline/browser/outline.js';41import { DraggedEditorIdentifier, fillEditorsDragData } from '../../dnd.js';42import { DEFAULT_LABELS_CONTAINER, ResourceLabels } from '../../labels.js';43import { BreadcrumbsConfig, IBreadcrumbsService } from './breadcrumbs.js';44import { BreadcrumbsModel, FileElement, OutlineElement2 } from './breadcrumbsModel.js';45import { BreadcrumbsFilePicker, BreadcrumbsOutlinePicker, BreadcrumbsPicker } from './breadcrumbsPicker.js';46import { IEditorGroupView } from './editor.js';47import './media/breadcrumbscontrol.css';48import { ScrollbarVisibility } from '../../../../base/common/scrollable.js';4950class OutlineItem extends BreadcrumbsItem {5152private readonly _disposables = new DisposableStore();5354constructor(55readonly model: BreadcrumbsModel,56readonly element: OutlineElement2,57readonly options: IBreadcrumbsControlOptions,58@IInstantiationService private readonly _instantiationService: InstantiationService,59) {60super();61}6263dispose(): void {64this._disposables.dispose();65}6667equals(other: BreadcrumbsItem): boolean {68if (!(other instanceof OutlineItem)) {69return false;70}71return this.element.element === other.element.element &&72this.options.showFileIcons === other.options.showFileIcons &&73this.options.showSymbolIcons === other.options.showSymbolIcons;74}7576render(container: HTMLElement): void {77const { element, outline } = this.element;7879if (element === outline) {80const element = dom.$('span', undefined, '…');81container.appendChild(element);82return;83}8485const templateId = outline.config.delegate.getTemplateId(element);86const renderer = outline.config.renderers.find(renderer => renderer.templateId === templateId);87if (!renderer) {88container.textContent = '<<NO RENDERER>>';89return;90}9192const template = renderer.renderTemplate(container);93renderer.renderElement({94element,95children: [],96depth: 0,97visibleChildrenCount: 0,98visibleChildIndex: 0,99collapsible: false,100collapsed: false,101visible: true,102filterData: undefined103}, 0, template, undefined);104105this._disposables.add(toDisposable(() => { renderer.disposeTemplate(template); }));106107if (element instanceof OutlineElement && outline.uri) {108this._disposables.add(this._instantiationService.invokeFunction(accessor => createBreadcrumbDndObserver(accessor, container, element.symbol.name, { symbol: element.symbol, uri: outline.uri! }, this.model, this.options.dragEditor)));109}110}111}112113class FileItem extends BreadcrumbsItem {114115private readonly _disposables = new DisposableStore();116117constructor(118readonly model: BreadcrumbsModel,119readonly element: FileElement,120readonly options: IBreadcrumbsControlOptions,121private readonly _labels: ResourceLabels,122private readonly _hoverDelegate: IHoverDelegate,123@IInstantiationService private readonly _instantiationService: InstantiationService,124) {125super();126}127128dispose(): void {129this._disposables.dispose();130}131132equals(other: BreadcrumbsItem): boolean {133if (!(other instanceof FileItem)) {134return false;135}136return (extUri.isEqual(this.element.uri, other.element.uri) &&137this.options.showFileIcons === other.options.showFileIcons &&138this.options.showSymbolIcons === other.options.showSymbolIcons);139140}141142render(container: HTMLElement): void {143// file/folder144const label = this._labels.create(container, { hoverDelegate: this._hoverDelegate });145label.setFile(this.element.uri, {146hidePath: true,147hideIcon: this.element.kind === FileKind.FOLDER || !this.options.showFileIcons,148fileKind: this.element.kind,149fileDecorations: { colors: this.options.showDecorationColors, badges: false },150});151container.classList.add(FileKind[this.element.kind].toLowerCase());152this._disposables.add(label);153154this._disposables.add(this._instantiationService.invokeFunction(accessor => createBreadcrumbDndObserver(accessor, container, basename(this.element.uri), this.element.uri, this.model, this.options.dragEditor)));155}156}157158159function createBreadcrumbDndObserver(accessor: ServicesAccessor, container: HTMLElement, label: string, item: URI | { symbol: DocumentSymbol; uri: URI }, model: BreadcrumbsModel, dragEditor: boolean): IDisposable {160const instantiationService = accessor.get(IInstantiationService);161162container.draggable = true;163164return new dom.DragAndDropObserver(container, {165onDragStart: event => {166if (!event.dataTransfer) {167return;168}169170// Set data transfer171event.dataTransfer.effectAllowed = 'copyMove';172173instantiationService.invokeFunction(accessor => {174if (URI.isUri(item)) {175fillEditorsDragData(accessor, [item], event);176} else { // Symbol177fillEditorsDragData(accessor, [{ resource: item.uri, selection: item.symbol.range }], event);178179fillInSymbolsDragData([{180name: item.symbol.name,181fsPath: item.uri.fsPath,182range: item.symbol.range,183kind: item.symbol.kind184}], event);185}186187if (dragEditor && model.editor && model.editor?.input) {188const editorTransfer = LocalSelectionTransfer.getInstance<DraggedEditorIdentifier>();189editorTransfer.setData([new DraggedEditorIdentifier({ editor: model.editor.input, groupId: model.editor.group.id })], DraggedEditorIdentifier.prototype);190}191});192193applyDragImage(event, container, label);194}195});196}197198export interface IBreadcrumbsControlOptions {199readonly showFileIcons: boolean;200readonly showSymbolIcons: boolean;201readonly showDecorationColors: boolean;202readonly showPlaceholder: boolean;203readonly dragEditor: boolean;204readonly widgetStyles?: IBreadcrumbsWidgetStyles;205}206207const separatorIcon = registerIcon('breadcrumb-separator', Codicon.chevronRight, localize('separatorIcon', 'Icon for the separator in the breadcrumbs.'));208209export class BreadcrumbsControl {210211static readonly HEIGHT = 22;212213private static readonly SCROLLBAR_SIZES = {214default: 3,215large: 8216};217218private static readonly SCROLLBAR_VISIBILITY = {219auto: ScrollbarVisibility.Auto,220visible: ScrollbarVisibility.Visible,221hidden: ScrollbarVisibility.Hidden222};223224static readonly Payload_Reveal = {};225static readonly Payload_RevealAside = {};226static readonly Payload_Pick = {};227228static readonly CK_BreadcrumbsPossible = new RawContextKey('breadcrumbsPossible', false, localize('breadcrumbsPossible', "Whether the editor can show breadcrumbs"));229static readonly CK_BreadcrumbsVisible = new RawContextKey('breadcrumbsVisible', false, localize('breadcrumbsVisible', "Whether breadcrumbs are currently visible"));230static readonly CK_BreadcrumbsActive = new RawContextKey('breadcrumbsActive', false, localize('breadcrumbsActive', "Whether breadcrumbs have focus"));231232private readonly _ckBreadcrumbsPossible: IContextKey<boolean>;233private readonly _ckBreadcrumbsVisible: IContextKey<boolean>;234private readonly _ckBreadcrumbsActive: IContextKey<boolean>;235236private readonly _cfUseQuickPick: BreadcrumbsConfig<boolean>;237private readonly _cfShowIcons: BreadcrumbsConfig<boolean>;238private readonly _cfTitleScrollbarSizing: BreadcrumbsConfig<IEditorPartOptions['titleScrollbarSizing']>;239private readonly _cfTitleScrollbarVisibility: BreadcrumbsConfig<IEditorPartOptions['titleScrollbarVisibility']>;240241readonly domNode: HTMLDivElement;242private readonly _widget: BreadcrumbsWidget;243244private readonly _disposables = new DisposableStore();245private readonly _breadcrumbsDisposables = new DisposableStore();246private readonly _labels: ResourceLabels;247private readonly _model = new MutableDisposable<BreadcrumbsModel>();248private _breadcrumbsPickerShowing = false;249private _breadcrumbsPickerIgnoreOnceItem: BreadcrumbsItem | undefined;250251private readonly _hoverDelegate: IHoverDelegate;252253private readonly _onDidVisibilityChange = this._disposables.add(new Emitter<void>());254get onDidVisibilityChange() { return this._onDidVisibilityChange.event; }255256constructor(257container: HTMLElement,258private readonly _options: IBreadcrumbsControlOptions,259private readonly _editorGroup: IEditorGroupView,260@IContextKeyService private readonly _contextKeyService: IContextKeyService,261@IContextViewService private readonly _contextViewService: IContextViewService,262@IInstantiationService private readonly _instantiationService: IInstantiationService,263@IQuickInputService private readonly _quickInputService: IQuickInputService,264@IFileService private readonly _fileService: IFileService,265@IEditorService private readonly _editorService: IEditorService,266@ILabelService private readonly _labelService: ILabelService,267@IConfigurationService configurationService: IConfigurationService,268@IBreadcrumbsService breadcrumbsService: IBreadcrumbsService269) {270this.domNode = document.createElement('div');271this.domNode.classList.add('breadcrumbs-control');272dom.append(container, this.domNode);273274this._cfUseQuickPick = BreadcrumbsConfig.UseQuickPick.bindTo(configurationService);275this._cfShowIcons = BreadcrumbsConfig.Icons.bindTo(configurationService);276this._cfTitleScrollbarSizing = BreadcrumbsConfig.TitleScrollbarSizing.bindTo(configurationService);277this._cfTitleScrollbarVisibility = BreadcrumbsConfig.TitleScrollbarVisibility.bindTo(configurationService);278279this._labels = this._instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER);280281const sizing = this._cfTitleScrollbarSizing.getValue() ?? 'default';282const styles = _options.widgetStyles ?? defaultBreadcrumbsWidgetStyles;283const visibility = this._cfTitleScrollbarVisibility?.getValue() ?? 'auto';284285this._widget = new BreadcrumbsWidget(286this.domNode,287BreadcrumbsControl.SCROLLBAR_SIZES[sizing],288BreadcrumbsControl.SCROLLBAR_VISIBILITY[visibility],289separatorIcon,290styles291);292this._widget.onDidSelectItem(this._onSelectEvent, this, this._disposables);293this._widget.onDidFocusItem(this._onFocusEvent, this, this._disposables);294this._widget.onDidChangeFocus(this._updateCkBreadcrumbsActive, this, this._disposables);295296this._ckBreadcrumbsPossible = BreadcrumbsControl.CK_BreadcrumbsPossible.bindTo(this._contextKeyService);297this._ckBreadcrumbsVisible = BreadcrumbsControl.CK_BreadcrumbsVisible.bindTo(this._contextKeyService);298this._ckBreadcrumbsActive = BreadcrumbsControl.CK_BreadcrumbsActive.bindTo(this._contextKeyService);299300this._hoverDelegate = getDefaultHoverDelegate('mouse');301302this._disposables.add(breadcrumbsService.register(this._editorGroup.id, this._widget));303this.hide();304}305306dispose(): void {307this._disposables.dispose();308this._breadcrumbsDisposables.dispose();309this._model.dispose();310this._ckBreadcrumbsPossible.reset();311this._ckBreadcrumbsVisible.reset();312this._ckBreadcrumbsActive.reset();313this._cfUseQuickPick.dispose();314this._cfShowIcons.dispose();315this._widget.dispose();316this._labels.dispose();317this.domNode.remove();318}319320get model(): BreadcrumbsModel | undefined {321return this._model.value;322}323324layout(dim: dom.Dimension | undefined): void {325this._widget.layout(dim);326}327328isHidden(): boolean {329return this.domNode.classList.contains('hidden');330}331332hide(): void {333const wasHidden = this.isHidden();334335this._breadcrumbsDisposables.clear();336this._ckBreadcrumbsVisible.set(false);337this.domNode.classList.toggle('hidden', true);338339if (!wasHidden) {340this._onDidVisibilityChange.fire();341}342}343344private show(): void {345const wasHidden = this.isHidden();346347this._ckBreadcrumbsVisible.set(true);348this.domNode.classList.toggle('hidden', false);349350if (wasHidden) {351this._onDidVisibilityChange.fire();352}353}354355revealLast(): void {356this._widget.revealLast();357}358359update(): boolean {360this._breadcrumbsDisposables.clear();361362// honor diff editors and such363const uri = EditorResourceAccessor.getCanonicalUri(this._editorGroup.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });364const wasHidden = this.isHidden();365366if (!uri || !this._fileService.hasProvider(uri)) {367// cleanup and return when there is no input or when368// we cannot handle this input369this._ckBreadcrumbsPossible.set(false);370if (!wasHidden) {371this.hide();372return true;373} else {374return false;375}376}377378// display uri which can be derived from certain inputs379const fileInfoUri = EditorResourceAccessor.getOriginalUri(this._editorGroup.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });380381this.show();382this._ckBreadcrumbsPossible.set(true);383384const model = this._instantiationService.createInstance(BreadcrumbsModel,385fileInfoUri ?? uri,386this._editorGroup.activeEditorPane387);388this._model.value = model;389390this.domNode.classList.toggle('backslash-path', this._labelService.getSeparator(uri.scheme, uri.authority) === '\\');391392const updateBreadcrumbs = () => {393this.domNode.classList.toggle('relative-path', model.isRelative());394const showIcons = this._cfShowIcons.getValue();395const options: IBreadcrumbsControlOptions = {396...this._options,397showFileIcons: this._options.showFileIcons && showIcons,398showSymbolIcons: this._options.showSymbolIcons && showIcons399};400const items = model.getElements().map(element => element instanceof FileElement401? this._instantiationService.createInstance(FileItem, model, element, options, this._labels, this._hoverDelegate)402: this._instantiationService.createInstance(OutlineItem, model, element, options));403if (items.length === 0) {404this._widget.setEnabled(false);405this._widget.setItems([new class extends BreadcrumbsItem {406render(container: HTMLElement): void {407container.textContent = localize('empty', "no elements");408}409equals(other: BreadcrumbsItem): boolean {410return other === this;411}412dispose(): void {413414}415}]);416} else {417this._widget.setEnabled(true);418this._widget.setItems(items);419this._widget.reveal(items[items.length - 1]);420}421};422const listener = model.onDidUpdate(updateBreadcrumbs);423const configListener = this._cfShowIcons.onDidChange(updateBreadcrumbs);424updateBreadcrumbs();425this._breadcrumbsDisposables.clear();426this._breadcrumbsDisposables.add(listener);427this._breadcrumbsDisposables.add(toDisposable(() => this._model.clear()));428this._breadcrumbsDisposables.add(configListener);429this._breadcrumbsDisposables.add(toDisposable(() => this._widget.setItems([])));430431const updateScrollbarSizing = () => {432const sizing = this._cfTitleScrollbarSizing.getValue() ?? 'default';433const visibility = this._cfTitleScrollbarVisibility?.getValue() ?? 'auto';434435this._widget.setHorizontalScrollbarSize(BreadcrumbsControl.SCROLLBAR_SIZES[sizing]);436this._widget.setHorizontalScrollbarVisibility(BreadcrumbsControl.SCROLLBAR_VISIBILITY[visibility]);437};438updateScrollbarSizing();439const updateScrollbarSizeListener = this._cfTitleScrollbarSizing.onDidChange(updateScrollbarSizing);440const updateScrollbarVisibilityListener = this._cfTitleScrollbarVisibility.onDidChange(updateScrollbarSizing);441this._breadcrumbsDisposables.add(updateScrollbarSizeListener);442this._breadcrumbsDisposables.add(updateScrollbarVisibilityListener);443444// close picker on hide/update445this._breadcrumbsDisposables.add({446dispose: () => {447if (this._breadcrumbsPickerShowing) {448this._contextViewService.hideContextView({ source: this });449}450}451});452453return wasHidden !== this.isHidden();454}455456private _onFocusEvent(event: IBreadcrumbsItemEvent): void {457if (event.item && this._breadcrumbsPickerShowing) {458this._breadcrumbsPickerIgnoreOnceItem = undefined;459this._widget.setSelection(event.item);460}461}462463private _onSelectEvent(event: IBreadcrumbsItemEvent): void {464if (!event.item) {465return;466}467468if (event.item === this._breadcrumbsPickerIgnoreOnceItem) {469this._breadcrumbsPickerIgnoreOnceItem = undefined;470this._widget.setFocused(undefined);471this._widget.setSelection(undefined);472return;473}474475const { element } = event.item as FileItem | OutlineItem;476this._editorGroup.focus();477478const group = this._getEditorGroup(event.payload);479if (group !== undefined) {480// reveal the item481this._widget.setFocused(undefined);482this._widget.setSelection(undefined);483this._revealInEditor(event, element, group);484return;485}486487if (this._cfUseQuickPick.getValue()) {488// using quick pick489this._widget.setFocused(undefined);490this._widget.setSelection(undefined);491this._quickInputService.quickAccess.show(element instanceof OutlineElement2 ? '@' : '');492return;493}494495// show picker496let picker: BreadcrumbsPicker;497let pickerAnchor: { x: number; y: number };498499interface IHideData { didPick?: boolean; source?: BreadcrumbsControl }500501this._contextViewService.showContextView({502render: (parent: HTMLElement) => {503if (event.item instanceof FileItem) {504picker = this._instantiationService.createInstance(BreadcrumbsFilePicker, parent, event.item.model.resource);505} else if (event.item instanceof OutlineItem) {506picker = this._instantiationService.createInstance(BreadcrumbsOutlinePicker, parent, event.item.model.resource);507}508509const selectListener = picker.onWillPickElement(() => this._contextViewService.hideContextView({ source: this, didPick: true }));510const zoomListener = PixelRatio.getInstance(dom.getWindow(this.domNode)).onDidChange(() => this._contextViewService.hideContextView({ source: this }));511512const focusTracker = dom.trackFocus(parent);513const blurListener = focusTracker.onDidBlur(() => {514this._breadcrumbsPickerIgnoreOnceItem = this._widget.isDOMFocused() ? event.item : undefined;515this._contextViewService.hideContextView({ source: this });516});517518this._breadcrumbsPickerShowing = true;519this._updateCkBreadcrumbsActive();520521return combinedDisposable(522picker,523selectListener,524zoomListener,525focusTracker,526blurListener527);528},529getAnchor: () => {530if (!pickerAnchor) {531const window = dom.getWindow(this.domNode);532const maxInnerWidth = window.innerWidth - 8 /*a little less the full widget*/;533let maxHeight = Math.min(window.innerHeight * 0.7, 300);534535const pickerWidth = Math.min(maxInnerWidth, Math.max(240, maxInnerWidth / 4.17));536const pickerArrowSize = 8;537let pickerArrowOffset: number;538539const data = dom.getDomNodePagePosition(event.node.firstChild as HTMLElement);540const y = data.top + data.height + pickerArrowSize;541if (y + maxHeight >= window.innerHeight) {542maxHeight = window.innerHeight - y - 30 /* room for shadow and status bar*/;543}544let x = data.left;545if (x + pickerWidth >= maxInnerWidth) {546x = maxInnerWidth - pickerWidth;547}548if (event.payload instanceof StandardMouseEvent) {549const maxPickerArrowOffset = pickerWidth - 2 * pickerArrowSize;550pickerArrowOffset = event.payload.posx - x;551if (pickerArrowOffset > maxPickerArrowOffset) {552x = Math.min(maxInnerWidth - pickerWidth, x + pickerArrowOffset - maxPickerArrowOffset);553pickerArrowOffset = maxPickerArrowOffset;554}555} else {556pickerArrowOffset = (data.left + (data.width * 0.3)) - x;557}558picker.show(element, maxHeight, pickerWidth, pickerArrowSize, Math.max(0, pickerArrowOffset));559pickerAnchor = { x, y };560}561return pickerAnchor;562},563onHide: (data?: IHideData) => {564if (!data?.didPick) {565picker.restoreViewState();566}567this._breadcrumbsPickerShowing = false;568this._updateCkBreadcrumbsActive();569if (data?.source === this) {570this._widget.setFocused(undefined);571this._widget.setSelection(undefined);572}573picker.dispose();574}575});576}577578private _updateCkBreadcrumbsActive(): void {579const value = this._widget.isDOMFocused() || this._breadcrumbsPickerShowing;580this._ckBreadcrumbsActive.set(value);581}582583private async _revealInEditor(event: IBreadcrumbsItemEvent, element: FileElement | OutlineElement2, group: SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | undefined, pinned: boolean = false): Promise<void> {584585if (element instanceof FileElement) {586if (element.kind === FileKind.FILE) {587await this._editorService.openEditor({ resource: element.uri, options: { pinned } }, group);588} else {589// show next picker590const items = this._widget.getItems();591const idx = items.indexOf(event.item);592this._widget.setFocused(items[idx + 1]);593this._widget.setSelection(items[idx + 1], BreadcrumbsControl.Payload_Pick);594}595} else {596element.outline.reveal(element, { pinned }, group === SIDE_GROUP, false);597}598}599600private _getEditorGroup(data: unknown): SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | undefined {601if (data === BreadcrumbsControl.Payload_RevealAside) {602return SIDE_GROUP;603} else if (data === BreadcrumbsControl.Payload_Reveal) {604return ACTIVE_GROUP;605} else {606return undefined;607}608}609}610611export class BreadcrumbsControlFactory {612613private readonly _disposables = new DisposableStore();614private readonly _controlDisposables = new DisposableStore();615616private _control: BreadcrumbsControl | undefined;617get control() { return this._control; }618619private readonly _onDidEnablementChange = this._disposables.add(new Emitter<void>());620get onDidEnablementChange() { return this._onDidEnablementChange.event; }621622private readonly _onDidVisibilityChange = this._disposables.add(new Emitter<void>());623get onDidVisibilityChange() { return this._onDidVisibilityChange.event; }624625constructor(626private readonly _container: HTMLElement,627private readonly _editorGroup: IEditorGroupView,628private readonly _options: IBreadcrumbsControlOptions,629@IConfigurationService configurationService: IConfigurationService,630@IInstantiationService private readonly _instantiationService: IInstantiationService,631@IFileService fileService: IFileService632) {633const config = this._disposables.add(BreadcrumbsConfig.IsEnabled.bindTo(configurationService));634this._disposables.add(config.onDidChange(() => {635const value = config.getValue();636if (!value && this._control) {637this._controlDisposables.clear();638this._control = undefined;639this._onDidEnablementChange.fire();640} else if (value && !this._control) {641this._control = this.createControl();642this._control.update();643this._onDidEnablementChange.fire();644}645}));646647if (config.getValue()) {648this._control = this.createControl();649}650651this._disposables.add(fileService.onDidChangeFileSystemProviderRegistrations(e => {652if (this._control?.model && this._control.model.resource.scheme !== e.scheme) {653// ignore if the scheme of the breadcrumbs resource is not affected654return;655}656if (this._control?.update()) {657this._onDidEnablementChange.fire();658}659}));660}661662private createControl(): BreadcrumbsControl {663const control = this._controlDisposables.add(this._instantiationService.createInstance(BreadcrumbsControl, this._container, this._options, this._editorGroup));664this._controlDisposables.add(control.onDidVisibilityChange(() => this._onDidVisibilityChange.fire()));665666return control;667}668669dispose(): void {670this._disposables.dispose();671this._controlDisposables.dispose();672}673}674675//#region commands676677// toggle command678registerAction2(class ToggleBreadcrumb extends Action2 {679680constructor() {681super({682id: 'breadcrumbs.toggle',683title: localize2('cmd.toggle', "Toggle Breadcrumbs"),684category: Categories.View,685toggled: {686condition: ContextKeyExpr.equals('config.breadcrumbs.enabled', true),687title: localize('cmd.toggle2', "Toggle Breadcrumbs"),688mnemonicTitle: localize({ key: 'miBreadcrumbs2', comment: ['&& denotes a mnemonic'] }, "&&Breadcrumbs")689},690menu: [691{ id: MenuId.CommandPalette },692{ id: MenuId.MenubarAppearanceMenu, group: '4_editor', order: 2 },693{ id: MenuId.NotebookToolbar, group: 'notebookLayout', order: 2 },694{ id: MenuId.StickyScrollContext },695{ id: MenuId.NotebookStickyScrollContext, group: 'notebookView', order: 2 },696{ id: MenuId.NotebookToolbarContext, group: 'notebookView', order: 2 }697]698});699}700701run(accessor: ServicesAccessor): void {702const config = accessor.get(IConfigurationService);703const value = BreadcrumbsConfig.IsEnabled.bindTo(config).getValue();704BreadcrumbsConfig.IsEnabled.bindTo(config).updateValue(!value);705}706707});708709// focus/focus-and-select710function focusAndSelectHandler(accessor: ServicesAccessor, select: boolean): void {711// find widget and focus/select712const groups = accessor.get(IEditorGroupsService);713const breadcrumbs = accessor.get(IBreadcrumbsService);714const widget = breadcrumbs.getWidget(groups.activeGroup.id);715if (widget) {716const item = widget.getItems().at(-1);717widget.setFocused(item);718if (select) {719widget.setSelection(item, BreadcrumbsControl.Payload_Pick);720}721}722}723registerAction2(class FocusAndSelectBreadcrumbs extends Action2 {724constructor() {725super({726id: 'breadcrumbs.focusAndSelect',727title: localize2('cmd.focusAndSelect', "Focus and Select Breadcrumbs"),728precondition: BreadcrumbsControl.CK_BreadcrumbsVisible,729keybinding: {730weight: KeybindingWeight.WorkbenchContrib,731primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Period,732when: BreadcrumbsControl.CK_BreadcrumbsPossible,733},734f1: true735});736}737run(accessor: ServicesAccessor, ...args: any[]): void {738focusAndSelectHandler(accessor, true);739}740});741742registerAction2(class FocusBreadcrumbs extends Action2 {743constructor() {744super({745id: 'breadcrumbs.focus',746title: localize2('cmd.focus', "Focus Breadcrumbs"),747precondition: BreadcrumbsControl.CK_BreadcrumbsVisible,748keybinding: {749weight: KeybindingWeight.WorkbenchContrib,750primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Semicolon,751when: BreadcrumbsControl.CK_BreadcrumbsPossible,752},753f1: true754});755}756run(accessor: ServicesAccessor, ...args: any[]): void {757focusAndSelectHandler(accessor, false);758}759});760761// this commands is only enabled when breadcrumbs are762// disabled which it then enables and focuses763KeybindingsRegistry.registerCommandAndKeybindingRule({764id: 'breadcrumbs.toggleToOn',765weight: KeybindingWeight.WorkbenchContrib,766primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Period,767when: ContextKeyExpr.not('config.breadcrumbs.enabled'),768handler: async accessor => {769const instant = accessor.get(IInstantiationService);770const config = accessor.get(IConfigurationService);771// check if enabled and iff not enable772const isEnabled = BreadcrumbsConfig.IsEnabled.bindTo(config);773if (!isEnabled.getValue()) {774await isEnabled.updateValue(true);775await timeout(50); // hacky - the widget might not be ready yet...776}777return instant.invokeFunction(focusAndSelectHandler, true);778}779});780781// navigation782KeybindingsRegistry.registerCommandAndKeybindingRule({783id: 'breadcrumbs.focusNext',784weight: KeybindingWeight.WorkbenchContrib,785primary: KeyCode.RightArrow,786secondary: [KeyMod.CtrlCmd | KeyCode.RightArrow],787mac: {788primary: KeyCode.RightArrow,789secondary: [KeyMod.Alt | KeyCode.RightArrow],790},791when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive),792handler(accessor) {793const groups = accessor.get(IEditorGroupsService);794const breadcrumbs = accessor.get(IBreadcrumbsService);795const widget = breadcrumbs.getWidget(groups.activeGroup.id);796if (!widget) {797return;798}799widget.focusNext();800}801});802KeybindingsRegistry.registerCommandAndKeybindingRule({803id: 'breadcrumbs.focusPrevious',804weight: KeybindingWeight.WorkbenchContrib,805primary: KeyCode.LeftArrow,806secondary: [KeyMod.CtrlCmd | KeyCode.LeftArrow],807mac: {808primary: KeyCode.LeftArrow,809secondary: [KeyMod.Alt | KeyCode.LeftArrow],810},811when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive),812handler(accessor) {813const groups = accessor.get(IEditorGroupsService);814const breadcrumbs = accessor.get(IBreadcrumbsService);815const widget = breadcrumbs.getWidget(groups.activeGroup.id);816if (!widget) {817return;818}819widget.focusPrev();820}821});822KeybindingsRegistry.registerCommandAndKeybindingRule({823id: 'breadcrumbs.focusNextWithPicker',824weight: KeybindingWeight.WorkbenchContrib + 1,825primary: KeyMod.CtrlCmd | KeyCode.RightArrow,826mac: {827primary: KeyMod.Alt | KeyCode.RightArrow,828},829when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive, WorkbenchListFocusContextKey),830handler(accessor) {831const groups = accessor.get(IEditorGroupsService);832const breadcrumbs = accessor.get(IBreadcrumbsService);833const widget = breadcrumbs.getWidget(groups.activeGroup.id);834if (!widget) {835return;836}837widget.focusNext();838}839});840KeybindingsRegistry.registerCommandAndKeybindingRule({841id: 'breadcrumbs.focusPreviousWithPicker',842weight: KeybindingWeight.WorkbenchContrib + 1,843primary: KeyMod.CtrlCmd | KeyCode.LeftArrow,844mac: {845primary: KeyMod.Alt | KeyCode.LeftArrow,846},847when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive, WorkbenchListFocusContextKey),848handler(accessor) {849const groups = accessor.get(IEditorGroupsService);850const breadcrumbs = accessor.get(IBreadcrumbsService);851const widget = breadcrumbs.getWidget(groups.activeGroup.id);852if (!widget) {853return;854}855widget.focusPrev();856}857});858KeybindingsRegistry.registerCommandAndKeybindingRule({859id: 'breadcrumbs.selectFocused',860weight: KeybindingWeight.WorkbenchContrib,861primary: KeyCode.Enter,862secondary: [KeyCode.DownArrow],863when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive),864handler(accessor) {865const groups = accessor.get(IEditorGroupsService);866const breadcrumbs = accessor.get(IBreadcrumbsService);867const widget = breadcrumbs.getWidget(groups.activeGroup.id);868if (!widget) {869return;870}871widget.setSelection(widget.getFocused(), BreadcrumbsControl.Payload_Pick);872}873});874KeybindingsRegistry.registerCommandAndKeybindingRule({875id: 'breadcrumbs.revealFocused',876weight: KeybindingWeight.WorkbenchContrib,877primary: KeyCode.Space,878secondary: [KeyMod.CtrlCmd | KeyCode.Enter],879when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive),880handler(accessor) {881const groups = accessor.get(IEditorGroupsService);882const breadcrumbs = accessor.get(IBreadcrumbsService);883const widget = breadcrumbs.getWidget(groups.activeGroup.id);884if (!widget) {885return;886}887widget.setSelection(widget.getFocused(), BreadcrumbsControl.Payload_Reveal);888}889});890KeybindingsRegistry.registerCommandAndKeybindingRule({891id: 'breadcrumbs.selectEditor',892weight: KeybindingWeight.WorkbenchContrib + 1,893primary: KeyCode.Escape,894when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive),895handler(accessor) {896const groups = accessor.get(IEditorGroupsService);897const breadcrumbs = accessor.get(IBreadcrumbsService);898const widget = breadcrumbs.getWidget(groups.activeGroup.id);899if (!widget) {900return;901}902widget.setFocused(undefined);903widget.setSelection(undefined);904groups.activeGroup.activeEditorPane?.focus();905}906});907KeybindingsRegistry.registerCommandAndKeybindingRule({908id: 'breadcrumbs.revealFocusedFromTreeAside',909weight: KeybindingWeight.WorkbenchContrib,910primary: KeyMod.CtrlCmd | KeyCode.Enter,911when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive, WorkbenchListFocusContextKey),912handler(accessor) {913const editors = accessor.get(IEditorService);914const lists = accessor.get(IListService);915916const tree = lists.lastFocusedList;917if (!(tree instanceof WorkbenchDataTree) && !(tree instanceof WorkbenchAsyncDataTree)) {918return;919}920921const element = <IFileStat | unknown>tree.getFocus()[0];922923if (URI.isUri((<IFileStat>element)?.resource)) {924// IFileStat: open file in editor925return editors.openEditor({926resource: (<IFileStat>element).resource,927options: { pinned: true }928}, SIDE_GROUP);929}930931// IOutline: check if this the outline and iff so reveal element932const input = tree.getInput();933if (input && typeof (<IOutline<any>>input).outlineKind === 'string') {934return (<IOutline<any>>input).reveal(element, {935pinned: true,936preserveFocus: false937}, true, false);938}939}940});941//#endregion942943944