Path: blob/main/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts
5252 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 { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';26import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';27import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js';28import { fillInSymbolsDragData, LocalSelectionTransfer } from '../../../../platform/dnd/browser/dnd.js';29import { FileKind, IFileService, IFileStat } from '../../../../platform/files/common/files.js';30import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';31import { InstantiationService } from '../../../../platform/instantiation/common/instantiationService.js';32import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';33import { ILabelService } from '../../../../platform/label/common/label.js';34import { IListService, WorkbenchAsyncDataTree, WorkbenchDataTree, WorkbenchListFocusContextKey } from '../../../../platform/list/browser/listService.js';35import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js';36import { defaultBreadcrumbsWidgetStyles } from '../../../../platform/theme/browser/defaultStyles.js';37import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js';38import { EditorResourceAccessor, IEditorPartOptions, SideBySideEditor } from '../../../common/editor.js';39import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';40import { ACTIVE_GROUP, ACTIVE_GROUP_TYPE, IEditorService, SIDE_GROUP, SIDE_GROUP_TYPE } from '../../../services/editor/common/editorService.js';41import { IOutline, IOutlineService, OutlineTarget } from '../../../services/outline/browser/outline.js';42import { DraggedEditorIdentifier, fillEditorsDragData } from '../../dnd.js';43import { DEFAULT_LABELS_CONTAINER, ResourceLabels } from '../../labels.js';44import { BreadcrumbsConfig, IBreadcrumbsService } from './breadcrumbs.js';45import { BreadcrumbsModel, FileElement, OutlineElement2 } from './breadcrumbsModel.js';46import { BreadcrumbsFilePicker, BreadcrumbsOutlinePicker } from './breadcrumbsPicker.js';47import { IEditorGroupView } from './editor.js';48import './media/breadcrumbscontrol.css';49import { ScrollbarVisibility } from '../../../../base/common/scrollable.js';50import { CancellationToken } from '../../../../base/common/cancellation.js';5152class OutlineItem extends BreadcrumbsItem {5354private readonly _disposables = new DisposableStore();5556constructor(57readonly model: BreadcrumbsModel,58readonly element: OutlineElement2,59readonly options: IBreadcrumbsControlOptions,60@IInstantiationService private readonly _instantiationService: InstantiationService,61) {62super();63}64656667dispose(): void {68this._disposables.dispose();69}7071equals(other: BreadcrumbsItem): boolean {72if (!(other instanceof OutlineItem)) {73return false;74}75return this.element.element === other.element.element &&76this.options.showFileIcons === other.options.showFileIcons &&77this.options.showSymbolIcons === other.options.showSymbolIcons;78}7980render(container: HTMLElement): void {81const { element, outline } = this.element;8283if (element === outline) {84const element = dom.$('span', undefined, '…');85container.appendChild(element);86return;87}8889const templateId = outline.config.delegate.getTemplateId(element);90const renderer = outline.config.renderers.find(renderer => renderer.templateId === templateId);91if (!renderer) {92container.textContent = '<<NO RENDERER>>';93return;94}9596const template = renderer.renderTemplate(container);97renderer.renderElement({98element,99children: [],100depth: 0,101visibleChildrenCount: 0,102visibleChildIndex: 0,103collapsible: false,104collapsed: false,105visible: true,106filterData: undefined107}, 0, template, undefined);108109if (!this.options.showSymbolIcons) {110dom.hide(template.iconClass);111}112113this._disposables.add(toDisposable(() => { renderer.disposeTemplate(template); }));114115if (element instanceof OutlineElement && outline.uri) {116this._disposables.add(this._instantiationService.invokeFunction(accessor => createBreadcrumbDndObserver(accessor, container, element.symbol.name, { symbol: element.symbol, uri: outline.uri! }, this.model, this.options.dragEditor)));117}118}119}120121class FileItem extends BreadcrumbsItem {122123private readonly _disposables = new DisposableStore();124125constructor(126readonly model: BreadcrumbsModel,127readonly element: FileElement,128readonly options: IBreadcrumbsControlOptions,129private readonly _labels: ResourceLabels,130private readonly _hoverDelegate: IHoverDelegate,131@IInstantiationService private readonly _instantiationService: InstantiationService,132) {133super();134}135136dispose(): void {137this._disposables.dispose();138}139140equals(other: BreadcrumbsItem): boolean {141if (!(other instanceof FileItem)) {142return false;143}144return (extUri.isEqual(this.element.uri, other.element.uri) &&145this.options.showFileIcons === other.options.showFileIcons &&146this.options.showSymbolIcons === other.options.showSymbolIcons);147148}149150render(container: HTMLElement): void {151// file/folder152const label = this._labels.create(container, { hoverDelegate: this._hoverDelegate });153label.setFile(this.element.uri, {154hidePath: true,155hideIcon: this.element.kind === FileKind.FOLDER || !this.options.showFileIcons,156fileKind: this.element.kind,157fileDecorations: { colors: this.options.showDecorationColors, badges: false },158});159container.classList.add(FileKind[this.element.kind].toLowerCase());160this._disposables.add(label);161162this._disposables.add(this._instantiationService.invokeFunction(accessor => createBreadcrumbDndObserver(accessor, container, basename(this.element.uri), this.element.uri, this.model, this.options.dragEditor)));163}164}165166167function createBreadcrumbDndObserver(accessor: ServicesAccessor, container: HTMLElement, label: string, item: URI | { symbol: DocumentSymbol; uri: URI }, model: BreadcrumbsModel, dragEditor: boolean): IDisposable {168const instantiationService = accessor.get(IInstantiationService);169170container.draggable = true;171172return new dom.DragAndDropObserver(container, {173onDragStart: event => {174if (!event.dataTransfer) {175return;176}177178// Set data transfer179event.dataTransfer.effectAllowed = 'copyMove';180181instantiationService.invokeFunction(accessor => {182if (URI.isUri(item)) {183fillEditorsDragData(accessor, [item], event);184} else { // Symbol185fillEditorsDragData(accessor, [{ resource: item.uri, selection: item.symbol.range }], event);186187fillInSymbolsDragData([{188name: item.symbol.name,189fsPath: item.uri.fsPath,190range: item.symbol.range,191kind: item.symbol.kind192}], event);193}194195if (dragEditor && model.editor?.input) {196const editorTransfer = LocalSelectionTransfer.getInstance<DraggedEditorIdentifier>();197editorTransfer.setData([new DraggedEditorIdentifier({ editor: model.editor.input, groupId: model.editor.group.id })], DraggedEditorIdentifier.prototype);198}199});200201applyDragImage(event, container, label);202}203});204}205206export interface IBreadcrumbsControlOptions {207readonly showFileIcons: boolean;208readonly showSymbolIcons: boolean;209readonly showDecorationColors: boolean;210readonly showPlaceholder: boolean;211readonly dragEditor: boolean;212readonly widgetStyles?: IBreadcrumbsWidgetStyles;213}214215const separatorIcon = registerIcon('breadcrumb-separator', Codicon.chevronRight, localize('separatorIcon', 'Icon for the separator in the breadcrumbs.'));216217export class BreadcrumbsControl {218219static readonly HEIGHT = 22;220221private static readonly SCROLLBAR_SIZES = {222default: 3,223large: 8224};225226private static readonly SCROLLBAR_VISIBILITY = {227auto: ScrollbarVisibility.Auto,228visible: ScrollbarVisibility.Visible,229hidden: ScrollbarVisibility.Hidden230};231232static readonly Payload_Reveal = {};233static readonly Payload_RevealAside = {};234static readonly Payload_Pick = {};235236static readonly CK_BreadcrumbsPossible = new RawContextKey('breadcrumbsPossible', false, localize('breadcrumbsPossible', "Whether the editor can show breadcrumbs"));237static readonly CK_BreadcrumbsVisible = new RawContextKey('breadcrumbsVisible', false, localize('breadcrumbsVisible', "Whether breadcrumbs are currently visible"));238static readonly CK_BreadcrumbsActive = new RawContextKey('breadcrumbsActive', false, localize('breadcrumbsActive', "Whether breadcrumbs have focus"));239240private readonly _ckBreadcrumbsPossible: IContextKey<boolean>;241private readonly _ckBreadcrumbsVisible: IContextKey<boolean>;242private readonly _ckBreadcrumbsActive: IContextKey<boolean>;243244private readonly _cfUseQuickPick: BreadcrumbsConfig<boolean>;245private readonly _cfShowIcons: BreadcrumbsConfig<boolean>;246private readonly _cfTitleScrollbarSizing: BreadcrumbsConfig<IEditorPartOptions['titleScrollbarSizing']>;247private readonly _cfTitleScrollbarVisibility: BreadcrumbsConfig<IEditorPartOptions['titleScrollbarVisibility']>;248249readonly domNode: HTMLDivElement;250private readonly _widget: BreadcrumbsWidget;251252private readonly _disposables = new DisposableStore();253private readonly _breadcrumbsDisposables = new DisposableStore();254private readonly _labels: ResourceLabels;255private readonly _model = new MutableDisposable<BreadcrumbsModel>();256private _breadcrumbsPickerShowing = false;257private _breadcrumbsPickerIgnoreOnceItem: BreadcrumbsItem | undefined;258259private readonly _hoverDelegate: IHoverDelegate;260261private readonly _onDidVisibilityChange = this._disposables.add(new Emitter<void>());262get onDidVisibilityChange() { return this._onDidVisibilityChange.event; }263264constructor(265container: HTMLElement,266private readonly _options: IBreadcrumbsControlOptions,267private readonly _editorGroup: IEditorGroupView,268@IContextKeyService private readonly _contextKeyService: IContextKeyService,269@IContextViewService private readonly _contextViewService: IContextViewService,270@IInstantiationService private readonly _instantiationService: IInstantiationService,271@IQuickInputService private readonly _quickInputService: IQuickInputService,272@IFileService private readonly _fileService: IFileService,273@IEditorService private readonly _editorService: IEditorService,274@ILabelService private readonly _labelService: ILabelService,275@IConfigurationService configurationService: IConfigurationService,276@IBreadcrumbsService breadcrumbsService: IBreadcrumbsService277) {278this.domNode = document.createElement('div');279this.domNode.classList.add('breadcrumbs-control');280dom.append(container, this.domNode);281282this._cfUseQuickPick = BreadcrumbsConfig.UseQuickPick.bindTo(configurationService);283this._cfShowIcons = BreadcrumbsConfig.Icons.bindTo(configurationService);284this._cfTitleScrollbarSizing = BreadcrumbsConfig.TitleScrollbarSizing.bindTo(configurationService);285this._cfTitleScrollbarVisibility = BreadcrumbsConfig.TitleScrollbarVisibility.bindTo(configurationService);286287this._labels = this._instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER);288289const sizing = this._cfTitleScrollbarSizing.getValue() ?? 'default';290const styles = _options.widgetStyles ?? defaultBreadcrumbsWidgetStyles;291const visibility = this._cfTitleScrollbarVisibility?.getValue() ?? 'auto';292293this._widget = new BreadcrumbsWidget(294this.domNode,295BreadcrumbsControl.SCROLLBAR_SIZES[sizing],296BreadcrumbsControl.SCROLLBAR_VISIBILITY[visibility],297separatorIcon,298styles299);300this._widget.onDidSelectItem(this._onSelectEvent, this, this._disposables);301this._widget.onDidFocusItem(this._onFocusEvent, this, this._disposables);302this._widget.onDidChangeFocus(this._updateCkBreadcrumbsActive, this, this._disposables);303304this._ckBreadcrumbsPossible = BreadcrumbsControl.CK_BreadcrumbsPossible.bindTo(this._contextKeyService);305this._ckBreadcrumbsVisible = BreadcrumbsControl.CK_BreadcrumbsVisible.bindTo(this._contextKeyService);306this._ckBreadcrumbsActive = BreadcrumbsControl.CK_BreadcrumbsActive.bindTo(this._contextKeyService);307308this._hoverDelegate = getDefaultHoverDelegate('mouse');309310this._disposables.add(breadcrumbsService.register(this._editorGroup.id, this._widget));311this.hide();312}313314dispose(): void {315this._disposables.dispose();316this._breadcrumbsDisposables.dispose();317this._model.dispose();318this._ckBreadcrumbsPossible.reset();319this._ckBreadcrumbsVisible.reset();320this._ckBreadcrumbsActive.reset();321this._cfUseQuickPick.dispose();322this._cfShowIcons.dispose();323this._cfTitleScrollbarSizing.dispose();324this._cfTitleScrollbarVisibility.dispose();325this._widget.dispose();326this._labels.dispose();327this.domNode.remove();328}329330get model(): BreadcrumbsModel | undefined {331return this._model.value;332}333334layout(dim: dom.Dimension | undefined): void {335this._widget.layout(dim);336}337338isHidden(): boolean {339return this.domNode.classList.contains('hidden');340}341342hide(): void {343const wasHidden = this.isHidden();344345this._breadcrumbsDisposables.clear();346this._ckBreadcrumbsVisible.set(false);347this.domNode.classList.toggle('hidden', true);348349if (!wasHidden) {350this._onDidVisibilityChange.fire();351}352}353354private show(): void {355const wasHidden = this.isHidden();356357this._ckBreadcrumbsVisible.set(true);358this.domNode.classList.toggle('hidden', false);359360if (wasHidden) {361this._onDidVisibilityChange.fire();362}363}364365revealLast(): void {366this._widget.revealLast();367}368369update(): boolean {370this._breadcrumbsDisposables.clear();371372// honor diff editors and such373const uri = EditorResourceAccessor.getCanonicalUri(this._editorGroup.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });374const wasHidden = this.isHidden();375376if (!uri || !this._fileService.hasProvider(uri)) {377// cleanup and return when there is no input or when378// we cannot handle this input379this._ckBreadcrumbsPossible.set(false);380if (!wasHidden) {381this.hide();382return true;383} else {384return false;385}386}387388// display uri which can be derived from certain inputs389const fileInfoUri = EditorResourceAccessor.getOriginalUri(this._editorGroup.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });390391this.show();392this._ckBreadcrumbsPossible.set(true);393394const model = this._instantiationService.createInstance(BreadcrumbsModel,395fileInfoUri ?? uri,396this._editorGroup.activeEditorPane397);398this._model.value = model;399400this.domNode.classList.toggle('backslash-path', this._labelService.getSeparator(uri.scheme, uri.authority) === '\\');401402const updateBreadcrumbs = () => {403this.domNode.classList.toggle('relative-path', model.isRelative());404const showIcons = this._cfShowIcons.getValue();405const options: IBreadcrumbsControlOptions = {406...this._options,407showFileIcons: this._options.showFileIcons && showIcons,408showSymbolIcons: this._options.showSymbolIcons && showIcons409};410const items = model.getElements().map(element => element instanceof FileElement411? this._instantiationService.createInstance(FileItem, model, element, options, this._labels, this._hoverDelegate)412: this._instantiationService.createInstance(OutlineItem, model, element, options));413if (items.length === 0) {414this._widget.setEnabled(false);415this._widget.setItems([new class extends BreadcrumbsItem {416render(container: HTMLElement): void {417container.textContent = localize('empty', "no elements");418}419equals(other: BreadcrumbsItem): boolean {420return other === this;421}422dispose(): void {423424}425}]);426} else {427this._widget.setEnabled(true);428this._widget.setItems(items);429this._widget.reveal(items[items.length - 1]);430}431};432const listener = model.onDidUpdate(updateBreadcrumbs);433const configListener = this._cfShowIcons.onDidChange(updateBreadcrumbs);434updateBreadcrumbs();435this._breadcrumbsDisposables.clear();436this._breadcrumbsDisposables.add(listener);437this._breadcrumbsDisposables.add(toDisposable(() => this._model.clear()));438this._breadcrumbsDisposables.add(configListener);439this._breadcrumbsDisposables.add(toDisposable(() => this._widget.setItems([])));440441const updateScrollbarSizing = () => {442const sizing = this._cfTitleScrollbarSizing.getValue() ?? 'default';443const visibility = this._cfTitleScrollbarVisibility?.getValue() ?? 'auto';444445this._widget.setHorizontalScrollbarSize(BreadcrumbsControl.SCROLLBAR_SIZES[sizing]);446this._widget.setHorizontalScrollbarVisibility(BreadcrumbsControl.SCROLLBAR_VISIBILITY[visibility]);447};448updateScrollbarSizing();449const updateScrollbarSizeListener = this._cfTitleScrollbarSizing.onDidChange(updateScrollbarSizing);450const updateScrollbarVisibilityListener = this._cfTitleScrollbarVisibility.onDidChange(updateScrollbarSizing);451this._breadcrumbsDisposables.add(updateScrollbarSizeListener);452this._breadcrumbsDisposables.add(updateScrollbarVisibilityListener);453454// close picker on hide/update455this._breadcrumbsDisposables.add({456dispose: () => {457if (this._breadcrumbsPickerShowing) {458this._contextViewService.hideContextView({ source: this });459}460}461});462463return wasHidden !== this.isHidden();464}465466private _onFocusEvent(event: IBreadcrumbsItemEvent): void {467if (event.item && this._breadcrumbsPickerShowing) {468this._breadcrumbsPickerIgnoreOnceItem = undefined;469this._widget.setSelection(event.item);470}471}472473private _onSelectEvent(event: IBreadcrumbsItemEvent): void {474if (!event.item) {475return;476}477478if (event.item === this._breadcrumbsPickerIgnoreOnceItem) {479this._breadcrumbsPickerIgnoreOnceItem = undefined;480this._widget.setFocused(undefined);481this._widget.setSelection(undefined);482return;483}484485const { element } = event.item as FileItem | OutlineItem;486this._editorGroup.focus();487488const group = this._getEditorGroup(event.payload);489if (group !== undefined) {490// reveal the item491this._widget.setFocused(undefined);492this._widget.setSelection(undefined);493this._revealInEditor(event, element, group);494return;495}496497if (this._cfUseQuickPick.getValue()) {498// using quick pick499this._widget.setFocused(undefined);500this._widget.setSelection(undefined);501this._quickInputService.quickAccess.show(element instanceof OutlineElement2 ? '@' : '');502return;503}504505// show picker506let picker: BreadcrumbsFilePicker | BreadcrumbsOutlinePicker;507let pickerAnchor: { x: number; y: number };508509interface IHideData { didPick?: boolean; source?: BreadcrumbsControl }510511this._contextViewService.showContextView({512render: (parent: HTMLElement) => {513if (event.item instanceof FileItem) {514picker = this._instantiationService.createInstance(BreadcrumbsFilePicker, parent, event.item.model.resource);515} else if (event.item instanceof OutlineItem) {516picker = this._instantiationService.createInstance(BreadcrumbsOutlinePicker, parent, event.item.model.resource);517}518519const selectListener = picker.onWillPickElement(() => this._contextViewService.hideContextView({ source: this, didPick: true }));520const zoomListener = PixelRatio.getInstance(dom.getWindow(this.domNode)).onDidChange(() => this._contextViewService.hideContextView({ source: this }));521522const focusTracker = dom.trackFocus(parent);523const blurListener = focusTracker.onDidBlur(() => {524this._breadcrumbsPickerIgnoreOnceItem = this._widget.isDOMFocused() ? event.item : undefined;525this._contextViewService.hideContextView({ source: this });526});527528this._breadcrumbsPickerShowing = true;529this._updateCkBreadcrumbsActive();530531return combinedDisposable(532picker,533selectListener,534zoomListener,535focusTracker,536blurListener537);538},539getAnchor: () => {540if (!pickerAnchor) {541const window = dom.getWindow(this.domNode);542const maxInnerWidth = window.innerWidth - 8 /*a little less the full widget*/;543let maxHeight = Math.min(window.innerHeight * 0.7, 300);544545const pickerWidth = Math.min(maxInnerWidth, Math.max(240, maxInnerWidth / 4.17));546const pickerArrowSize = 8;547let pickerArrowOffset: number;548549const data = dom.getDomNodePagePosition(event.node);550const y = data.top + data.height + pickerArrowSize;551if (y + maxHeight >= window.innerHeight) {552maxHeight = window.innerHeight - y - 30 /* room for shadow and status bar*/;553}554let x = data.left;555if (x + pickerWidth >= maxInnerWidth) {556x = maxInnerWidth - pickerWidth;557}558if (event.payload instanceof StandardMouseEvent) {559const maxPickerArrowOffset = pickerWidth - 2 * pickerArrowSize;560pickerArrowOffset = event.payload.posx - x;561if (pickerArrowOffset > maxPickerArrowOffset) {562x = Math.min(maxInnerWidth - pickerWidth, x + pickerArrowOffset - maxPickerArrowOffset);563pickerArrowOffset = maxPickerArrowOffset;564}565} else {566pickerArrowOffset = (data.left + (data.width * 0.3)) - x;567}568picker.show(element, maxHeight, pickerWidth, pickerArrowSize, Math.max(0, pickerArrowOffset));569pickerAnchor = { x, y };570}571return pickerAnchor;572},573onHide: (data?: IHideData) => {574if (!data?.didPick) {575picker.restoreViewState();576}577this._breadcrumbsPickerShowing = false;578this._updateCkBreadcrumbsActive();579if (data?.source === this) {580this._widget.setFocused(undefined);581this._widget.setSelection(undefined);582}583picker.dispose();584}585});586}587588private _updateCkBreadcrumbsActive(): void {589const value = this._widget.isDOMFocused() || this._breadcrumbsPickerShowing;590this._ckBreadcrumbsActive.set(value);591}592593private async _revealInEditor(event: IBreadcrumbsItemEvent, element: FileElement | OutlineElement2, group: SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | undefined, pinned: boolean = false): Promise<void> {594595if (element instanceof FileElement) {596if (element.kind === FileKind.FILE) {597await this._editorService.openEditor({ resource: element.uri, options: { pinned } }, group);598} else {599// show next picker600const items = this._widget.getItems();601const idx = items.indexOf(event.item);602this._widget.setFocused(items[idx + 1]);603this._widget.setSelection(items[idx + 1], BreadcrumbsControl.Payload_Pick);604}605} else {606element.outline.reveal(element, { pinned }, group === SIDE_GROUP, false);607}608}609610private _getEditorGroup(data: unknown): SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | undefined {611if (data === BreadcrumbsControl.Payload_RevealAside) {612return SIDE_GROUP;613} else if (data === BreadcrumbsControl.Payload_Reveal) {614return ACTIVE_GROUP;615} else {616return undefined;617}618}619}620621export class BreadcrumbsControlFactory {622623private readonly _disposables = new DisposableStore();624private readonly _controlDisposables = new DisposableStore();625626private _control: BreadcrumbsControl | undefined;627get control() { return this._control; }628629private readonly _onDidEnablementChange = this._disposables.add(new Emitter<void>());630get onDidEnablementChange() { return this._onDidEnablementChange.event; }631632private readonly _onDidVisibilityChange = this._disposables.add(new Emitter<void>());633get onDidVisibilityChange() { return this._onDidVisibilityChange.event; }634635constructor(636private readonly _container: HTMLElement,637private readonly _editorGroup: IEditorGroupView,638private readonly _options: IBreadcrumbsControlOptions,639@IConfigurationService configurationService: IConfigurationService,640@IInstantiationService private readonly _instantiationService: IInstantiationService,641@IFileService fileService: IFileService642) {643const config = this._disposables.add(BreadcrumbsConfig.IsEnabled.bindTo(configurationService));644this._disposables.add(config.onDidChange(() => {645const value = config.getValue();646if (!value && this._control) {647this._controlDisposables.clear();648this._control = undefined;649this._onDidEnablementChange.fire();650} else if (value && !this._control) {651this._control = this.createControl();652this._control.update();653this._onDidEnablementChange.fire();654}655}));656657if (config.getValue()) {658this._control = this.createControl();659}660661this._disposables.add(fileService.onDidChangeFileSystemProviderRegistrations(e => {662if (this._control?.model && this._control.model.resource.scheme !== e.scheme) {663// ignore if the scheme of the breadcrumbs resource is not affected664return;665}666if (this._control?.update()) {667this._onDidEnablementChange.fire();668}669}));670}671672private createControl(): BreadcrumbsControl {673const control = this._controlDisposables.add(this._instantiationService.createInstance(BreadcrumbsControl, this._container, this._options, this._editorGroup));674this._controlDisposables.add(control.onDidVisibilityChange(() => this._onDidVisibilityChange.fire()));675676return control;677}678679dispose(): void {680this._disposables.dispose();681this._controlDisposables.dispose();682}683}684685//#region commands686687// toggle command688registerAction2(class ToggleBreadcrumb extends Action2 {689690constructor() {691super({692id: 'breadcrumbs.toggle',693title: localize2('cmd.toggle', "Toggle Breadcrumbs"),694shortTitle: localize2('cmd.toggle.short', "Breadcrumbs"),695category: Categories.View,696toggled: {697condition: ContextKeyExpr.equals('config.breadcrumbs.enabled', true),698title: localize('cmd.toggle2', "Breadcrumbs"),699mnemonicTitle: localize({ key: 'miBreadcrumbs2', comment: ['&& denotes a mnemonic'] }, "&&Breadcrumbs")700},701menu: [702{ id: MenuId.CommandPalette },703{ id: MenuId.MenubarAppearanceMenu, group: '4_editor', order: 2 },704{ id: MenuId.NotebookToolbar, group: 'notebookLayout', order: 2 },705{ id: MenuId.StickyScrollContext },706{ id: MenuId.NotebookStickyScrollContext, group: 'notebookView', order: 2 },707{ id: MenuId.NotebookToolbarContext, group: 'notebookView', order: 2 }708]709});710}711712run(accessor: ServicesAccessor): void {713const config = accessor.get(IConfigurationService);714const breadCrumbsConfig = BreadcrumbsConfig.IsEnabled.bindTo(config);715const value = breadCrumbsConfig.getValue();716breadCrumbsConfig.updateValue(!value);717breadCrumbsConfig.dispose();718}719720});721722// focus/focus-and-select723function focusAndSelectHandler(accessor: ServicesAccessor, select: boolean): void {724// find widget and focus/select725const groups = accessor.get(IEditorGroupsService);726const breadcrumbs = accessor.get(IBreadcrumbsService);727const widget = breadcrumbs.getWidget(groups.activeGroup.id);728if (widget) {729const item = widget.getItems().at(-1);730widget.setFocused(item);731if (select) {732widget.setSelection(item, BreadcrumbsControl.Payload_Pick);733}734}735}736registerAction2(class FocusAndSelectBreadcrumbs extends Action2 {737constructor() {738super({739id: 'breadcrumbs.focusAndSelect',740title: localize2('cmd.focusAndSelect', "Focus and Select Breadcrumbs"),741precondition: BreadcrumbsControl.CK_BreadcrumbsVisible,742keybinding: {743weight: KeybindingWeight.WorkbenchContrib,744primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Period,745when: BreadcrumbsControl.CK_BreadcrumbsPossible,746},747f1: true748});749}750run(accessor: ServicesAccessor, ...args: unknown[]): void {751focusAndSelectHandler(accessor, true);752}753});754755registerAction2(class FocusBreadcrumbs extends Action2 {756constructor() {757super({758id: 'breadcrumbs.focus',759title: localize2('cmd.focus', "Focus Breadcrumbs"),760precondition: BreadcrumbsControl.CK_BreadcrumbsVisible,761keybinding: {762weight: KeybindingWeight.WorkbenchContrib,763primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Semicolon,764when: BreadcrumbsControl.CK_BreadcrumbsPossible,765},766f1: true767});768}769run(accessor: ServicesAccessor, ...args: unknown[]): void {770focusAndSelectHandler(accessor, false);771}772});773774// this commands is only enabled when breadcrumbs are775// disabled which it then enables and focuses776KeybindingsRegistry.registerCommandAndKeybindingRule({777id: 'breadcrumbs.toggleToOn',778weight: KeybindingWeight.WorkbenchContrib,779primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Period,780when: ContextKeyExpr.not('config.breadcrumbs.enabled'),781handler: async accessor => {782const instant = accessor.get(IInstantiationService);783const config = accessor.get(IConfigurationService);784// check if enabled and iff not enable785const isEnabled = BreadcrumbsConfig.IsEnabled.bindTo(config);786if (!isEnabled.getValue()) {787await isEnabled.updateValue(true);788await timeout(50); // hacky - the widget might not be ready yet...789}790isEnabled.dispose();791return instant.invokeFunction(focusAndSelectHandler, true);792}793});794795// navigation796KeybindingsRegistry.registerCommandAndKeybindingRule({797id: 'breadcrumbs.focusNext',798weight: KeybindingWeight.WorkbenchContrib,799primary: KeyCode.RightArrow,800secondary: [KeyMod.CtrlCmd | KeyCode.RightArrow],801mac: {802primary: KeyCode.RightArrow,803secondary: [KeyMod.Alt | KeyCode.RightArrow],804},805when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive),806handler(accessor) {807const groups = accessor.get(IEditorGroupsService);808const breadcrumbs = accessor.get(IBreadcrumbsService);809const widget = breadcrumbs.getWidget(groups.activeGroup.id);810if (!widget) {811return;812}813widget.focusNext();814}815});816KeybindingsRegistry.registerCommandAndKeybindingRule({817id: 'breadcrumbs.focusPrevious',818weight: KeybindingWeight.WorkbenchContrib,819primary: KeyCode.LeftArrow,820secondary: [KeyMod.CtrlCmd | KeyCode.LeftArrow],821mac: {822primary: KeyCode.LeftArrow,823secondary: [KeyMod.Alt | KeyCode.LeftArrow],824},825when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive),826handler(accessor) {827const groups = accessor.get(IEditorGroupsService);828const breadcrumbs = accessor.get(IBreadcrumbsService);829const widget = breadcrumbs.getWidget(groups.activeGroup.id);830if (!widget) {831return;832}833widget.focusPrev();834}835});836KeybindingsRegistry.registerCommandAndKeybindingRule({837id: 'breadcrumbs.focusNextWithPicker',838weight: KeybindingWeight.WorkbenchContrib + 1,839primary: KeyMod.CtrlCmd | KeyCode.RightArrow,840mac: {841primary: KeyMod.Alt | KeyCode.RightArrow,842},843when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive, WorkbenchListFocusContextKey),844handler(accessor) {845const groups = accessor.get(IEditorGroupsService);846const breadcrumbs = accessor.get(IBreadcrumbsService);847const widget = breadcrumbs.getWidget(groups.activeGroup.id);848if (!widget) {849return;850}851widget.focusNext();852}853});854KeybindingsRegistry.registerCommandAndKeybindingRule({855id: 'breadcrumbs.focusPreviousWithPicker',856weight: KeybindingWeight.WorkbenchContrib + 1,857primary: KeyMod.CtrlCmd | KeyCode.LeftArrow,858mac: {859primary: KeyMod.Alt | KeyCode.LeftArrow,860},861when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive, WorkbenchListFocusContextKey),862handler(accessor) {863const groups = accessor.get(IEditorGroupsService);864const breadcrumbs = accessor.get(IBreadcrumbsService);865const widget = breadcrumbs.getWidget(groups.activeGroup.id);866if (!widget) {867return;868}869widget.focusPrev();870}871});872KeybindingsRegistry.registerCommandAndKeybindingRule({873id: 'breadcrumbs.selectFocused',874weight: KeybindingWeight.WorkbenchContrib,875primary: KeyCode.Enter,876secondary: [KeyCode.DownArrow],877when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive),878handler(accessor) {879const groups = accessor.get(IEditorGroupsService);880const breadcrumbs = accessor.get(IBreadcrumbsService);881const widget = breadcrumbs.getWidget(groups.activeGroup.id);882if (!widget) {883return;884}885widget.setSelection(widget.getFocused(), BreadcrumbsControl.Payload_Pick);886}887});888KeybindingsRegistry.registerCommandAndKeybindingRule({889id: 'breadcrumbs.revealFocused',890weight: KeybindingWeight.WorkbenchContrib,891primary: KeyCode.Space,892secondary: [KeyMod.CtrlCmd | KeyCode.Enter],893when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive),894handler(accessor) {895const groups = accessor.get(IEditorGroupsService);896const breadcrumbs = accessor.get(IBreadcrumbsService);897const widget = breadcrumbs.getWidget(groups.activeGroup.id);898if (!widget) {899return;900}901widget.setSelection(widget.getFocused(), BreadcrumbsControl.Payload_Reveal);902}903});904KeybindingsRegistry.registerCommandAndKeybindingRule({905id: 'breadcrumbs.selectEditor',906weight: KeybindingWeight.WorkbenchContrib + 1,907primary: KeyCode.Escape,908when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive),909handler(accessor) {910const groups = accessor.get(IEditorGroupsService);911const breadcrumbs = accessor.get(IBreadcrumbsService);912const widget = breadcrumbs.getWidget(groups.activeGroup.id);913if (!widget) {914return;915}916widget.setFocused(undefined);917widget.setSelection(undefined);918groups.activeGroup.activeEditorPane?.focus();919}920});921KeybindingsRegistry.registerCommandAndKeybindingRule({922id: 'breadcrumbs.revealFocusedFromTreeAside',923weight: KeybindingWeight.WorkbenchContrib,924primary: KeyMod.CtrlCmd | KeyCode.Enter,925when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive, WorkbenchListFocusContextKey),926handler(accessor) {927const editors = accessor.get(IEditorService);928const lists = accessor.get(IListService);929930const tree = lists.lastFocusedList;931if (!(tree instanceof WorkbenchDataTree) && !(tree instanceof WorkbenchAsyncDataTree)) {932return;933}934935const element = <IFileStat | unknown>tree.getFocus()[0];936937if (URI.isUri((<IFileStat>element)?.resource)) {938// IFileStat: open file in editor939return editors.openEditor({940resource: (<IFileStat>element).resource,941options: { pinned: true }942}, SIDE_GROUP);943}944945// IOutline: check if this the outline and iff so reveal element946const input = tree.getInput();947if (input && typeof (<IOutline<unknown>>input).outlineKind === 'string') {948return (<IOutline<unknown>>input).reveal(element, {949pinned: true,950preserveFocus: false951}, true, false);952}953}954});955//#endregion956957registerAction2(class CopyBreadcrumbPath extends Action2 {958constructor() {959super({960id: 'breadcrumbs.copyPath',961title: localize2('cmd.copyPath', "Copy Breadcrumbs Path"),962category: Categories.View,963precondition: BreadcrumbsControl.CK_BreadcrumbsVisible,964f1: true,965menu: [{966id: MenuId.EditorTitleContext,967group: '1_cutcopypaste',968order: 100,969when: BreadcrumbsControl.CK_BreadcrumbsPossible970}]971});972}973async run(accessor: ServicesAccessor): Promise<void> {974const groups = accessor.get(IEditorGroupsService);975const clipboardService = accessor.get(IClipboardService);976const configurationService = accessor.get(IConfigurationService);977const outlineService = accessor.get(IOutlineService);978979if (!groups.activeGroup.activeEditorPane) {980return;981}982983const outline = await outlineService.createOutline(groups.activeGroup.activeEditorPane, OutlineTarget.Breadcrumbs, CancellationToken.None);984if (!outline) {985return;986}987988const elements = outline.config.breadcrumbsDataSource.getBreadcrumbElements();989const labels = elements.map(item => item.label).filter(Boolean);990991outline.dispose();992993if (labels.length === 0) {994return;995}996997// Get separator with language override support998const resource = groups.activeGroup.activeEditorPane.input.resource;999const config = BreadcrumbsConfig.SymbolPathSeparator.bindTo(configurationService);1000const separator = config.getValue(resource && { resource }) ?? '.';1001config.dispose();10021003const path = labels.join(separator);1004await clipboardService.writeText(path);1005}1006});100710081009