Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as dom from '../../../../base/browser/dom.js';
7
import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js';
8
import { PixelRatio } from '../../../../base/browser/pixelRatio.js';
9
import { BreadcrumbsItem, BreadcrumbsWidget, IBreadcrumbsItemEvent, IBreadcrumbsWidgetStyles } from '../../../../base/browser/ui/breadcrumbs/breadcrumbsWidget.js';
10
import { applyDragImage } from '../../../../base/browser/ui/dnd/dnd.js';
11
import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js';
12
import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
13
import { timeout } from '../../../../base/common/async.js';
14
import { Codicon } from '../../../../base/common/codicons.js';
15
import { Emitter } from '../../../../base/common/event.js';
16
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
17
import { combinedDisposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
18
import { basename, extUri } from '../../../../base/common/resources.js';
19
import { URI } from '../../../../base/common/uri.js';
20
import { DocumentSymbol } from '../../../../editor/common/languages.js';
21
import { OutlineElement } from '../../../../editor/contrib/documentSymbols/browser/outlineModel.js';
22
import { localize, localize2 } from '../../../../nls.js';
23
import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';
24
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
25
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
26
import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
27
import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js';
28
import { fillInSymbolsDragData, LocalSelectionTransfer } from '../../../../platform/dnd/browser/dnd.js';
29
import { FileKind, IFileService, IFileStat } from '../../../../platform/files/common/files.js';
30
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
31
import { InstantiationService } from '../../../../platform/instantiation/common/instantiationService.js';
32
import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
33
import { ILabelService } from '../../../../platform/label/common/label.js';
34
import { IListService, WorkbenchAsyncDataTree, WorkbenchDataTree, WorkbenchListFocusContextKey } from '../../../../platform/list/browser/listService.js';
35
import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js';
36
import { defaultBreadcrumbsWidgetStyles } from '../../../../platform/theme/browser/defaultStyles.js';
37
import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js';
38
import { EditorResourceAccessor, IEditorPartOptions, SideBySideEditor } from '../../../common/editor.js';
39
import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';
40
import { ACTIVE_GROUP, ACTIVE_GROUP_TYPE, IEditorService, SIDE_GROUP, SIDE_GROUP_TYPE } from '../../../services/editor/common/editorService.js';
41
import { IOutline } from '../../../services/outline/browser/outline.js';
42
import { DraggedEditorIdentifier, fillEditorsDragData } from '../../dnd.js';
43
import { DEFAULT_LABELS_CONTAINER, ResourceLabels } from '../../labels.js';
44
import { BreadcrumbsConfig, IBreadcrumbsService } from './breadcrumbs.js';
45
import { BreadcrumbsModel, FileElement, OutlineElement2 } from './breadcrumbsModel.js';
46
import { BreadcrumbsFilePicker, BreadcrumbsOutlinePicker, BreadcrumbsPicker } from './breadcrumbsPicker.js';
47
import { IEditorGroupView } from './editor.js';
48
import './media/breadcrumbscontrol.css';
49
import { ScrollbarVisibility } from '../../../../base/common/scrollable.js';
50
51
class OutlineItem extends BreadcrumbsItem {
52
53
private readonly _disposables = new DisposableStore();
54
55
constructor(
56
readonly model: BreadcrumbsModel,
57
readonly element: OutlineElement2,
58
readonly options: IBreadcrumbsControlOptions,
59
@IInstantiationService private readonly _instantiationService: InstantiationService,
60
) {
61
super();
62
}
63
64
dispose(): void {
65
this._disposables.dispose();
66
}
67
68
equals(other: BreadcrumbsItem): boolean {
69
if (!(other instanceof OutlineItem)) {
70
return false;
71
}
72
return this.element.element === other.element.element &&
73
this.options.showFileIcons === other.options.showFileIcons &&
74
this.options.showSymbolIcons === other.options.showSymbolIcons;
75
}
76
77
render(container: HTMLElement): void {
78
const { element, outline } = this.element;
79
80
if (element === outline) {
81
const element = dom.$('span', undefined, '…');
82
container.appendChild(element);
83
return;
84
}
85
86
const templateId = outline.config.delegate.getTemplateId(element);
87
const renderer = outline.config.renderers.find(renderer => renderer.templateId === templateId);
88
if (!renderer) {
89
container.textContent = '<<NO RENDERER>>';
90
return;
91
}
92
93
const template = renderer.renderTemplate(container);
94
renderer.renderElement({
95
element,
96
children: [],
97
depth: 0,
98
visibleChildrenCount: 0,
99
visibleChildIndex: 0,
100
collapsible: false,
101
collapsed: false,
102
visible: true,
103
filterData: undefined
104
}, 0, template, undefined);
105
106
this._disposables.add(toDisposable(() => { renderer.disposeTemplate(template); }));
107
108
if (element instanceof OutlineElement && outline.uri) {
109
this._disposables.add(this._instantiationService.invokeFunction(accessor => createBreadcrumbDndObserver(accessor, container, element.symbol.name, { symbol: element.symbol, uri: outline.uri! }, this.model, this.options.dragEditor)));
110
}
111
}
112
}
113
114
class FileItem extends BreadcrumbsItem {
115
116
private readonly _disposables = new DisposableStore();
117
118
constructor(
119
readonly model: BreadcrumbsModel,
120
readonly element: FileElement,
121
readonly options: IBreadcrumbsControlOptions,
122
private readonly _labels: ResourceLabels,
123
private readonly _hoverDelegate: IHoverDelegate,
124
@IInstantiationService private readonly _instantiationService: InstantiationService,
125
) {
126
super();
127
}
128
129
dispose(): void {
130
this._disposables.dispose();
131
}
132
133
equals(other: BreadcrumbsItem): boolean {
134
if (!(other instanceof FileItem)) {
135
return false;
136
}
137
return (extUri.isEqual(this.element.uri, other.element.uri) &&
138
this.options.showFileIcons === other.options.showFileIcons &&
139
this.options.showSymbolIcons === other.options.showSymbolIcons);
140
141
}
142
143
render(container: HTMLElement): void {
144
// file/folder
145
const label = this._labels.create(container, { hoverDelegate: this._hoverDelegate });
146
label.setFile(this.element.uri, {
147
hidePath: true,
148
hideIcon: this.element.kind === FileKind.FOLDER || !this.options.showFileIcons,
149
fileKind: this.element.kind,
150
fileDecorations: { colors: this.options.showDecorationColors, badges: false },
151
});
152
container.classList.add(FileKind[this.element.kind].toLowerCase());
153
this._disposables.add(label);
154
155
this._disposables.add(this._instantiationService.invokeFunction(accessor => createBreadcrumbDndObserver(accessor, container, basename(this.element.uri), this.element.uri, this.model, this.options.dragEditor)));
156
}
157
}
158
159
160
function createBreadcrumbDndObserver(accessor: ServicesAccessor, container: HTMLElement, label: string, item: URI | { symbol: DocumentSymbol; uri: URI }, model: BreadcrumbsModel, dragEditor: boolean): IDisposable {
161
const instantiationService = accessor.get(IInstantiationService);
162
163
container.draggable = true;
164
165
return new dom.DragAndDropObserver(container, {
166
onDragStart: event => {
167
if (!event.dataTransfer) {
168
return;
169
}
170
171
// Set data transfer
172
event.dataTransfer.effectAllowed = 'copyMove';
173
174
instantiationService.invokeFunction(accessor => {
175
if (URI.isUri(item)) {
176
fillEditorsDragData(accessor, [item], event);
177
} else { // Symbol
178
fillEditorsDragData(accessor, [{ resource: item.uri, selection: item.symbol.range }], event);
179
180
fillInSymbolsDragData([{
181
name: item.symbol.name,
182
fsPath: item.uri.fsPath,
183
range: item.symbol.range,
184
kind: item.symbol.kind
185
}], event);
186
}
187
188
if (dragEditor && model.editor && model.editor?.input) {
189
const editorTransfer = LocalSelectionTransfer.getInstance<DraggedEditorIdentifier>();
190
editorTransfer.setData([new DraggedEditorIdentifier({ editor: model.editor.input, groupId: model.editor.group.id })], DraggedEditorIdentifier.prototype);
191
}
192
});
193
194
applyDragImage(event, container, label);
195
}
196
});
197
}
198
199
export interface IBreadcrumbsControlOptions {
200
readonly showFileIcons: boolean;
201
readonly showSymbolIcons: boolean;
202
readonly showDecorationColors: boolean;
203
readonly showPlaceholder: boolean;
204
readonly dragEditor: boolean;
205
readonly widgetStyles?: IBreadcrumbsWidgetStyles;
206
}
207
208
const separatorIcon = registerIcon('breadcrumb-separator', Codicon.chevronRight, localize('separatorIcon', 'Icon for the separator in the breadcrumbs.'));
209
210
export class BreadcrumbsControl {
211
212
static readonly HEIGHT = 22;
213
214
private static readonly SCROLLBAR_SIZES = {
215
default: 3,
216
large: 8
217
};
218
219
private static readonly SCROLLBAR_VISIBILITY = {
220
auto: ScrollbarVisibility.Auto,
221
visible: ScrollbarVisibility.Visible,
222
hidden: ScrollbarVisibility.Hidden
223
};
224
225
static readonly Payload_Reveal = {};
226
static readonly Payload_RevealAside = {};
227
static readonly Payload_Pick = {};
228
229
static readonly CK_BreadcrumbsPossible = new RawContextKey('breadcrumbsPossible', false, localize('breadcrumbsPossible', "Whether the editor can show breadcrumbs"));
230
static readonly CK_BreadcrumbsVisible = new RawContextKey('breadcrumbsVisible', false, localize('breadcrumbsVisible', "Whether breadcrumbs are currently visible"));
231
static readonly CK_BreadcrumbsActive = new RawContextKey('breadcrumbsActive', false, localize('breadcrumbsActive', "Whether breadcrumbs have focus"));
232
233
private readonly _ckBreadcrumbsPossible: IContextKey<boolean>;
234
private readonly _ckBreadcrumbsVisible: IContextKey<boolean>;
235
private readonly _ckBreadcrumbsActive: IContextKey<boolean>;
236
237
private readonly _cfUseQuickPick: BreadcrumbsConfig<boolean>;
238
private readonly _cfShowIcons: BreadcrumbsConfig<boolean>;
239
private readonly _cfTitleScrollbarSizing: BreadcrumbsConfig<IEditorPartOptions['titleScrollbarSizing']>;
240
private readonly _cfTitleScrollbarVisibility: BreadcrumbsConfig<IEditorPartOptions['titleScrollbarVisibility']>;
241
242
readonly domNode: HTMLDivElement;
243
private readonly _widget: BreadcrumbsWidget;
244
245
private readonly _disposables = new DisposableStore();
246
private readonly _breadcrumbsDisposables = new DisposableStore();
247
private readonly _labels: ResourceLabels;
248
private readonly _model = new MutableDisposable<BreadcrumbsModel>();
249
private _breadcrumbsPickerShowing = false;
250
private _breadcrumbsPickerIgnoreOnceItem: BreadcrumbsItem | undefined;
251
252
private readonly _hoverDelegate: IHoverDelegate;
253
254
private readonly _onDidVisibilityChange = this._disposables.add(new Emitter<void>());
255
get onDidVisibilityChange() { return this._onDidVisibilityChange.event; }
256
257
constructor(
258
container: HTMLElement,
259
private readonly _options: IBreadcrumbsControlOptions,
260
private readonly _editorGroup: IEditorGroupView,
261
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
262
@IContextViewService private readonly _contextViewService: IContextViewService,
263
@IInstantiationService private readonly _instantiationService: IInstantiationService,
264
@IQuickInputService private readonly _quickInputService: IQuickInputService,
265
@IFileService private readonly _fileService: IFileService,
266
@IEditorService private readonly _editorService: IEditorService,
267
@ILabelService private readonly _labelService: ILabelService,
268
@IConfigurationService configurationService: IConfigurationService,
269
@IBreadcrumbsService breadcrumbsService: IBreadcrumbsService
270
) {
271
this.domNode = document.createElement('div');
272
this.domNode.classList.add('breadcrumbs-control');
273
dom.append(container, this.domNode);
274
275
this._cfUseQuickPick = BreadcrumbsConfig.UseQuickPick.bindTo(configurationService);
276
this._cfShowIcons = BreadcrumbsConfig.Icons.bindTo(configurationService);
277
this._cfTitleScrollbarSizing = BreadcrumbsConfig.TitleScrollbarSizing.bindTo(configurationService);
278
this._cfTitleScrollbarVisibility = BreadcrumbsConfig.TitleScrollbarVisibility.bindTo(configurationService);
279
280
this._labels = this._instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER);
281
282
const sizing = this._cfTitleScrollbarSizing.getValue() ?? 'default';
283
const styles = _options.widgetStyles ?? defaultBreadcrumbsWidgetStyles;
284
const visibility = this._cfTitleScrollbarVisibility?.getValue() ?? 'auto';
285
286
this._widget = new BreadcrumbsWidget(
287
this.domNode,
288
BreadcrumbsControl.SCROLLBAR_SIZES[sizing],
289
BreadcrumbsControl.SCROLLBAR_VISIBILITY[visibility],
290
separatorIcon,
291
styles
292
);
293
this._widget.onDidSelectItem(this._onSelectEvent, this, this._disposables);
294
this._widget.onDidFocusItem(this._onFocusEvent, this, this._disposables);
295
this._widget.onDidChangeFocus(this._updateCkBreadcrumbsActive, this, this._disposables);
296
297
this._ckBreadcrumbsPossible = BreadcrumbsControl.CK_BreadcrumbsPossible.bindTo(this._contextKeyService);
298
this._ckBreadcrumbsVisible = BreadcrumbsControl.CK_BreadcrumbsVisible.bindTo(this._contextKeyService);
299
this._ckBreadcrumbsActive = BreadcrumbsControl.CK_BreadcrumbsActive.bindTo(this._contextKeyService);
300
301
this._hoverDelegate = getDefaultHoverDelegate('mouse');
302
303
this._disposables.add(breadcrumbsService.register(this._editorGroup.id, this._widget));
304
this.hide();
305
}
306
307
dispose(): void {
308
this._disposables.dispose();
309
this._breadcrumbsDisposables.dispose();
310
this._model.dispose();
311
this._ckBreadcrumbsPossible.reset();
312
this._ckBreadcrumbsVisible.reset();
313
this._ckBreadcrumbsActive.reset();
314
this._cfUseQuickPick.dispose();
315
this._cfShowIcons.dispose();
316
this._widget.dispose();
317
this._labels.dispose();
318
this.domNode.remove();
319
}
320
321
get model(): BreadcrumbsModel | undefined {
322
return this._model.value;
323
}
324
325
layout(dim: dom.Dimension | undefined): void {
326
this._widget.layout(dim);
327
}
328
329
isHidden(): boolean {
330
return this.domNode.classList.contains('hidden');
331
}
332
333
hide(): void {
334
const wasHidden = this.isHidden();
335
336
this._breadcrumbsDisposables.clear();
337
this._ckBreadcrumbsVisible.set(false);
338
this.domNode.classList.toggle('hidden', true);
339
340
if (!wasHidden) {
341
this._onDidVisibilityChange.fire();
342
}
343
}
344
345
private show(): void {
346
const wasHidden = this.isHidden();
347
348
this._ckBreadcrumbsVisible.set(true);
349
this.domNode.classList.toggle('hidden', false);
350
351
if (wasHidden) {
352
this._onDidVisibilityChange.fire();
353
}
354
}
355
356
revealLast(): void {
357
this._widget.revealLast();
358
}
359
360
update(): boolean {
361
this._breadcrumbsDisposables.clear();
362
363
// honor diff editors and such
364
const uri = EditorResourceAccessor.getCanonicalUri(this._editorGroup.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });
365
const wasHidden = this.isHidden();
366
367
if (!uri || !this._fileService.hasProvider(uri)) {
368
// cleanup and return when there is no input or when
369
// we cannot handle this input
370
this._ckBreadcrumbsPossible.set(false);
371
if (!wasHidden) {
372
this.hide();
373
return true;
374
} else {
375
return false;
376
}
377
}
378
379
// display uri which can be derived from certain inputs
380
const fileInfoUri = EditorResourceAccessor.getOriginalUri(this._editorGroup.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });
381
382
this.show();
383
this._ckBreadcrumbsPossible.set(true);
384
385
const model = this._instantiationService.createInstance(BreadcrumbsModel,
386
fileInfoUri ?? uri,
387
this._editorGroup.activeEditorPane
388
);
389
this._model.value = model;
390
391
this.domNode.classList.toggle('backslash-path', this._labelService.getSeparator(uri.scheme, uri.authority) === '\\');
392
393
const updateBreadcrumbs = () => {
394
this.domNode.classList.toggle('relative-path', model.isRelative());
395
const showIcons = this._cfShowIcons.getValue();
396
const options: IBreadcrumbsControlOptions = {
397
...this._options,
398
showFileIcons: this._options.showFileIcons && showIcons,
399
showSymbolIcons: this._options.showSymbolIcons && showIcons
400
};
401
const items = model.getElements().map(element => element instanceof FileElement
402
? this._instantiationService.createInstance(FileItem, model, element, options, this._labels, this._hoverDelegate)
403
: this._instantiationService.createInstance(OutlineItem, model, element, options));
404
if (items.length === 0) {
405
this._widget.setEnabled(false);
406
this._widget.setItems([new class extends BreadcrumbsItem {
407
render(container: HTMLElement): void {
408
container.textContent = localize('empty', "no elements");
409
}
410
equals(other: BreadcrumbsItem): boolean {
411
return other === this;
412
}
413
dispose(): void {
414
415
}
416
}]);
417
} else {
418
this._widget.setEnabled(true);
419
this._widget.setItems(items);
420
this._widget.reveal(items[items.length - 1]);
421
}
422
};
423
const listener = model.onDidUpdate(updateBreadcrumbs);
424
const configListener = this._cfShowIcons.onDidChange(updateBreadcrumbs);
425
updateBreadcrumbs();
426
this._breadcrumbsDisposables.clear();
427
this._breadcrumbsDisposables.add(listener);
428
this._breadcrumbsDisposables.add(toDisposable(() => this._model.clear()));
429
this._breadcrumbsDisposables.add(configListener);
430
this._breadcrumbsDisposables.add(toDisposable(() => this._widget.setItems([])));
431
432
const updateScrollbarSizing = () => {
433
const sizing = this._cfTitleScrollbarSizing.getValue() ?? 'default';
434
const visibility = this._cfTitleScrollbarVisibility?.getValue() ?? 'auto';
435
436
this._widget.setHorizontalScrollbarSize(BreadcrumbsControl.SCROLLBAR_SIZES[sizing]);
437
this._widget.setHorizontalScrollbarVisibility(BreadcrumbsControl.SCROLLBAR_VISIBILITY[visibility]);
438
};
439
updateScrollbarSizing();
440
const updateScrollbarSizeListener = this._cfTitleScrollbarSizing.onDidChange(updateScrollbarSizing);
441
const updateScrollbarVisibilityListener = this._cfTitleScrollbarVisibility.onDidChange(updateScrollbarSizing);
442
this._breadcrumbsDisposables.add(updateScrollbarSizeListener);
443
this._breadcrumbsDisposables.add(updateScrollbarVisibilityListener);
444
445
// close picker on hide/update
446
this._breadcrumbsDisposables.add({
447
dispose: () => {
448
if (this._breadcrumbsPickerShowing) {
449
this._contextViewService.hideContextView({ source: this });
450
}
451
}
452
});
453
454
return wasHidden !== this.isHidden();
455
}
456
457
private _onFocusEvent(event: IBreadcrumbsItemEvent): void {
458
if (event.item && this._breadcrumbsPickerShowing) {
459
this._breadcrumbsPickerIgnoreOnceItem = undefined;
460
this._widget.setSelection(event.item);
461
}
462
}
463
464
private _onSelectEvent(event: IBreadcrumbsItemEvent): void {
465
if (!event.item) {
466
return;
467
}
468
469
if (event.item === this._breadcrumbsPickerIgnoreOnceItem) {
470
this._breadcrumbsPickerIgnoreOnceItem = undefined;
471
this._widget.setFocused(undefined);
472
this._widget.setSelection(undefined);
473
return;
474
}
475
476
const { element } = event.item as FileItem | OutlineItem;
477
this._editorGroup.focus();
478
479
const group = this._getEditorGroup(event.payload);
480
if (group !== undefined) {
481
// reveal the item
482
this._widget.setFocused(undefined);
483
this._widget.setSelection(undefined);
484
this._revealInEditor(event, element, group);
485
return;
486
}
487
488
if (this._cfUseQuickPick.getValue()) {
489
// using quick pick
490
this._widget.setFocused(undefined);
491
this._widget.setSelection(undefined);
492
this._quickInputService.quickAccess.show(element instanceof OutlineElement2 ? '@' : '');
493
return;
494
}
495
496
// show picker
497
let picker: BreadcrumbsPicker;
498
let pickerAnchor: { x: number; y: number };
499
500
interface IHideData { didPick?: boolean; source?: BreadcrumbsControl }
501
502
this._contextViewService.showContextView({
503
render: (parent: HTMLElement) => {
504
if (event.item instanceof FileItem) {
505
picker = this._instantiationService.createInstance(BreadcrumbsFilePicker, parent, event.item.model.resource);
506
} else if (event.item instanceof OutlineItem) {
507
picker = this._instantiationService.createInstance(BreadcrumbsOutlinePicker, parent, event.item.model.resource);
508
}
509
510
const selectListener = picker.onWillPickElement(() => this._contextViewService.hideContextView({ source: this, didPick: true }));
511
const zoomListener = PixelRatio.getInstance(dom.getWindow(this.domNode)).onDidChange(() => this._contextViewService.hideContextView({ source: this }));
512
513
const focusTracker = dom.trackFocus(parent);
514
const blurListener = focusTracker.onDidBlur(() => {
515
this._breadcrumbsPickerIgnoreOnceItem = this._widget.isDOMFocused() ? event.item : undefined;
516
this._contextViewService.hideContextView({ source: this });
517
});
518
519
this._breadcrumbsPickerShowing = true;
520
this._updateCkBreadcrumbsActive();
521
522
return combinedDisposable(
523
picker,
524
selectListener,
525
zoomListener,
526
focusTracker,
527
blurListener
528
);
529
},
530
getAnchor: () => {
531
if (!pickerAnchor) {
532
const window = dom.getWindow(this.domNode);
533
const maxInnerWidth = window.innerWidth - 8 /*a little less the full widget*/;
534
let maxHeight = Math.min(window.innerHeight * 0.7, 300);
535
536
const pickerWidth = Math.min(maxInnerWidth, Math.max(240, maxInnerWidth / 4.17));
537
const pickerArrowSize = 8;
538
let pickerArrowOffset: number;
539
540
const data = dom.getDomNodePagePosition(event.node.firstChild as HTMLElement);
541
const y = data.top + data.height + pickerArrowSize;
542
if (y + maxHeight >= window.innerHeight) {
543
maxHeight = window.innerHeight - y - 30 /* room for shadow and status bar*/;
544
}
545
let x = data.left;
546
if (x + pickerWidth >= maxInnerWidth) {
547
x = maxInnerWidth - pickerWidth;
548
}
549
if (event.payload instanceof StandardMouseEvent) {
550
const maxPickerArrowOffset = pickerWidth - 2 * pickerArrowSize;
551
pickerArrowOffset = event.payload.posx - x;
552
if (pickerArrowOffset > maxPickerArrowOffset) {
553
x = Math.min(maxInnerWidth - pickerWidth, x + pickerArrowOffset - maxPickerArrowOffset);
554
pickerArrowOffset = maxPickerArrowOffset;
555
}
556
} else {
557
pickerArrowOffset = (data.left + (data.width * 0.3)) - x;
558
}
559
picker.show(element, maxHeight, pickerWidth, pickerArrowSize, Math.max(0, pickerArrowOffset));
560
pickerAnchor = { x, y };
561
}
562
return pickerAnchor;
563
},
564
onHide: (data?: IHideData) => {
565
if (!data?.didPick) {
566
picker.restoreViewState();
567
}
568
this._breadcrumbsPickerShowing = false;
569
this._updateCkBreadcrumbsActive();
570
if (data?.source === this) {
571
this._widget.setFocused(undefined);
572
this._widget.setSelection(undefined);
573
}
574
picker.dispose();
575
}
576
});
577
}
578
579
private _updateCkBreadcrumbsActive(): void {
580
const value = this._widget.isDOMFocused() || this._breadcrumbsPickerShowing;
581
this._ckBreadcrumbsActive.set(value);
582
}
583
584
private async _revealInEditor(event: IBreadcrumbsItemEvent, element: FileElement | OutlineElement2, group: SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | undefined, pinned: boolean = false): Promise<void> {
585
586
if (element instanceof FileElement) {
587
if (element.kind === FileKind.FILE) {
588
await this._editorService.openEditor({ resource: element.uri, options: { pinned } }, group);
589
} else {
590
// show next picker
591
const items = this._widget.getItems();
592
const idx = items.indexOf(event.item);
593
this._widget.setFocused(items[idx + 1]);
594
this._widget.setSelection(items[idx + 1], BreadcrumbsControl.Payload_Pick);
595
}
596
} else {
597
element.outline.reveal(element, { pinned }, group === SIDE_GROUP, false);
598
}
599
}
600
601
private _getEditorGroup(data: unknown): SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | undefined {
602
if (data === BreadcrumbsControl.Payload_RevealAside) {
603
return SIDE_GROUP;
604
} else if (data === BreadcrumbsControl.Payload_Reveal) {
605
return ACTIVE_GROUP;
606
} else {
607
return undefined;
608
}
609
}
610
}
611
612
export class BreadcrumbsControlFactory {
613
614
private readonly _disposables = new DisposableStore();
615
private readonly _controlDisposables = new DisposableStore();
616
617
private _control: BreadcrumbsControl | undefined;
618
get control() { return this._control; }
619
620
private readonly _onDidEnablementChange = this._disposables.add(new Emitter<void>());
621
get onDidEnablementChange() { return this._onDidEnablementChange.event; }
622
623
private readonly _onDidVisibilityChange = this._disposables.add(new Emitter<void>());
624
get onDidVisibilityChange() { return this._onDidVisibilityChange.event; }
625
626
constructor(
627
private readonly _container: HTMLElement,
628
private readonly _editorGroup: IEditorGroupView,
629
private readonly _options: IBreadcrumbsControlOptions,
630
@IConfigurationService configurationService: IConfigurationService,
631
@IInstantiationService private readonly _instantiationService: IInstantiationService,
632
@IFileService fileService: IFileService
633
) {
634
const config = this._disposables.add(BreadcrumbsConfig.IsEnabled.bindTo(configurationService));
635
this._disposables.add(config.onDidChange(() => {
636
const value = config.getValue();
637
if (!value && this._control) {
638
this._controlDisposables.clear();
639
this._control = undefined;
640
this._onDidEnablementChange.fire();
641
} else if (value && !this._control) {
642
this._control = this.createControl();
643
this._control.update();
644
this._onDidEnablementChange.fire();
645
}
646
}));
647
648
if (config.getValue()) {
649
this._control = this.createControl();
650
}
651
652
this._disposables.add(fileService.onDidChangeFileSystemProviderRegistrations(e => {
653
if (this._control?.model && this._control.model.resource.scheme !== e.scheme) {
654
// ignore if the scheme of the breadcrumbs resource is not affected
655
return;
656
}
657
if (this._control?.update()) {
658
this._onDidEnablementChange.fire();
659
}
660
}));
661
}
662
663
private createControl(): BreadcrumbsControl {
664
const control = this._controlDisposables.add(this._instantiationService.createInstance(BreadcrumbsControl, this._container, this._options, this._editorGroup));
665
this._controlDisposables.add(control.onDidVisibilityChange(() => this._onDidVisibilityChange.fire()));
666
667
return control;
668
}
669
670
dispose(): void {
671
this._disposables.dispose();
672
this._controlDisposables.dispose();
673
}
674
}
675
676
//#region commands
677
678
// toggle command
679
registerAction2(class ToggleBreadcrumb extends Action2 {
680
681
constructor() {
682
super({
683
id: 'breadcrumbs.toggle',
684
title: localize2('cmd.toggle', "Toggle Breadcrumbs"),
685
category: Categories.View,
686
toggled: {
687
condition: ContextKeyExpr.equals('config.breadcrumbs.enabled', true),
688
title: localize('cmd.toggle2', "Toggle Breadcrumbs"),
689
mnemonicTitle: localize({ key: 'miBreadcrumbs2', comment: ['&& denotes a mnemonic'] }, "&&Breadcrumbs")
690
},
691
menu: [
692
{ id: MenuId.CommandPalette },
693
{ id: MenuId.MenubarAppearanceMenu, group: '4_editor', order: 2 },
694
{ id: MenuId.NotebookToolbar, group: 'notebookLayout', order: 2 },
695
{ id: MenuId.StickyScrollContext },
696
{ id: MenuId.NotebookStickyScrollContext, group: 'notebookView', order: 2 },
697
{ id: MenuId.NotebookToolbarContext, group: 'notebookView', order: 2 }
698
]
699
});
700
}
701
702
run(accessor: ServicesAccessor): void {
703
const config = accessor.get(IConfigurationService);
704
const value = BreadcrumbsConfig.IsEnabled.bindTo(config).getValue();
705
BreadcrumbsConfig.IsEnabled.bindTo(config).updateValue(!value);
706
}
707
708
});
709
710
// focus/focus-and-select
711
function focusAndSelectHandler(accessor: ServicesAccessor, select: boolean): void {
712
// find widget and focus/select
713
const groups = accessor.get(IEditorGroupsService);
714
const breadcrumbs = accessor.get(IBreadcrumbsService);
715
const widget = breadcrumbs.getWidget(groups.activeGroup.id);
716
if (widget) {
717
const item = widget.getItems().at(-1);
718
widget.setFocused(item);
719
if (select) {
720
widget.setSelection(item, BreadcrumbsControl.Payload_Pick);
721
}
722
}
723
}
724
registerAction2(class FocusAndSelectBreadcrumbs extends Action2 {
725
constructor() {
726
super({
727
id: 'breadcrumbs.focusAndSelect',
728
title: localize2('cmd.focusAndSelect', "Focus and Select Breadcrumbs"),
729
precondition: BreadcrumbsControl.CK_BreadcrumbsVisible,
730
keybinding: {
731
weight: KeybindingWeight.WorkbenchContrib,
732
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Period,
733
when: BreadcrumbsControl.CK_BreadcrumbsPossible,
734
},
735
f1: true
736
});
737
}
738
run(accessor: ServicesAccessor, ...args: any[]): void {
739
focusAndSelectHandler(accessor, true);
740
}
741
});
742
743
registerAction2(class FocusBreadcrumbs extends Action2 {
744
constructor() {
745
super({
746
id: 'breadcrumbs.focus',
747
title: localize2('cmd.focus', "Focus Breadcrumbs"),
748
precondition: BreadcrumbsControl.CK_BreadcrumbsVisible,
749
keybinding: {
750
weight: KeybindingWeight.WorkbenchContrib,
751
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Semicolon,
752
when: BreadcrumbsControl.CK_BreadcrumbsPossible,
753
},
754
f1: true
755
});
756
}
757
run(accessor: ServicesAccessor, ...args: any[]): void {
758
focusAndSelectHandler(accessor, false);
759
}
760
});
761
762
// this commands is only enabled when breadcrumbs are
763
// disabled which it then enables and focuses
764
KeybindingsRegistry.registerCommandAndKeybindingRule({
765
id: 'breadcrumbs.toggleToOn',
766
weight: KeybindingWeight.WorkbenchContrib,
767
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Period,
768
when: ContextKeyExpr.not('config.breadcrumbs.enabled'),
769
handler: async accessor => {
770
const instant = accessor.get(IInstantiationService);
771
const config = accessor.get(IConfigurationService);
772
// check if enabled and iff not enable
773
const isEnabled = BreadcrumbsConfig.IsEnabled.bindTo(config);
774
if (!isEnabled.getValue()) {
775
await isEnabled.updateValue(true);
776
await timeout(50); // hacky - the widget might not be ready yet...
777
}
778
return instant.invokeFunction(focusAndSelectHandler, true);
779
}
780
});
781
782
// navigation
783
KeybindingsRegistry.registerCommandAndKeybindingRule({
784
id: 'breadcrumbs.focusNext',
785
weight: KeybindingWeight.WorkbenchContrib,
786
primary: KeyCode.RightArrow,
787
secondary: [KeyMod.CtrlCmd | KeyCode.RightArrow],
788
mac: {
789
primary: KeyCode.RightArrow,
790
secondary: [KeyMod.Alt | KeyCode.RightArrow],
791
},
792
when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive),
793
handler(accessor) {
794
const groups = accessor.get(IEditorGroupsService);
795
const breadcrumbs = accessor.get(IBreadcrumbsService);
796
const widget = breadcrumbs.getWidget(groups.activeGroup.id);
797
if (!widget) {
798
return;
799
}
800
widget.focusNext();
801
}
802
});
803
KeybindingsRegistry.registerCommandAndKeybindingRule({
804
id: 'breadcrumbs.focusPrevious',
805
weight: KeybindingWeight.WorkbenchContrib,
806
primary: KeyCode.LeftArrow,
807
secondary: [KeyMod.CtrlCmd | KeyCode.LeftArrow],
808
mac: {
809
primary: KeyCode.LeftArrow,
810
secondary: [KeyMod.Alt | KeyCode.LeftArrow],
811
},
812
when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive),
813
handler(accessor) {
814
const groups = accessor.get(IEditorGroupsService);
815
const breadcrumbs = accessor.get(IBreadcrumbsService);
816
const widget = breadcrumbs.getWidget(groups.activeGroup.id);
817
if (!widget) {
818
return;
819
}
820
widget.focusPrev();
821
}
822
});
823
KeybindingsRegistry.registerCommandAndKeybindingRule({
824
id: 'breadcrumbs.focusNextWithPicker',
825
weight: KeybindingWeight.WorkbenchContrib + 1,
826
primary: KeyMod.CtrlCmd | KeyCode.RightArrow,
827
mac: {
828
primary: KeyMod.Alt | KeyCode.RightArrow,
829
},
830
when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive, WorkbenchListFocusContextKey),
831
handler(accessor) {
832
const groups = accessor.get(IEditorGroupsService);
833
const breadcrumbs = accessor.get(IBreadcrumbsService);
834
const widget = breadcrumbs.getWidget(groups.activeGroup.id);
835
if (!widget) {
836
return;
837
}
838
widget.focusNext();
839
}
840
});
841
KeybindingsRegistry.registerCommandAndKeybindingRule({
842
id: 'breadcrumbs.focusPreviousWithPicker',
843
weight: KeybindingWeight.WorkbenchContrib + 1,
844
primary: KeyMod.CtrlCmd | KeyCode.LeftArrow,
845
mac: {
846
primary: KeyMod.Alt | KeyCode.LeftArrow,
847
},
848
when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive, WorkbenchListFocusContextKey),
849
handler(accessor) {
850
const groups = accessor.get(IEditorGroupsService);
851
const breadcrumbs = accessor.get(IBreadcrumbsService);
852
const widget = breadcrumbs.getWidget(groups.activeGroup.id);
853
if (!widget) {
854
return;
855
}
856
widget.focusPrev();
857
}
858
});
859
KeybindingsRegistry.registerCommandAndKeybindingRule({
860
id: 'breadcrumbs.selectFocused',
861
weight: KeybindingWeight.WorkbenchContrib,
862
primary: KeyCode.Enter,
863
secondary: [KeyCode.DownArrow],
864
when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive),
865
handler(accessor) {
866
const groups = accessor.get(IEditorGroupsService);
867
const breadcrumbs = accessor.get(IBreadcrumbsService);
868
const widget = breadcrumbs.getWidget(groups.activeGroup.id);
869
if (!widget) {
870
return;
871
}
872
widget.setSelection(widget.getFocused(), BreadcrumbsControl.Payload_Pick);
873
}
874
});
875
KeybindingsRegistry.registerCommandAndKeybindingRule({
876
id: 'breadcrumbs.revealFocused',
877
weight: KeybindingWeight.WorkbenchContrib,
878
primary: KeyCode.Space,
879
secondary: [KeyMod.CtrlCmd | KeyCode.Enter],
880
when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive),
881
handler(accessor) {
882
const groups = accessor.get(IEditorGroupsService);
883
const breadcrumbs = accessor.get(IBreadcrumbsService);
884
const widget = breadcrumbs.getWidget(groups.activeGroup.id);
885
if (!widget) {
886
return;
887
}
888
widget.setSelection(widget.getFocused(), BreadcrumbsControl.Payload_Reveal);
889
}
890
});
891
KeybindingsRegistry.registerCommandAndKeybindingRule({
892
id: 'breadcrumbs.selectEditor',
893
weight: KeybindingWeight.WorkbenchContrib + 1,
894
primary: KeyCode.Escape,
895
when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive),
896
handler(accessor) {
897
const groups = accessor.get(IEditorGroupsService);
898
const breadcrumbs = accessor.get(IBreadcrumbsService);
899
const widget = breadcrumbs.getWidget(groups.activeGroup.id);
900
if (!widget) {
901
return;
902
}
903
widget.setFocused(undefined);
904
widget.setSelection(undefined);
905
groups.activeGroup.activeEditorPane?.focus();
906
}
907
});
908
KeybindingsRegistry.registerCommandAndKeybindingRule({
909
id: 'breadcrumbs.revealFocusedFromTreeAside',
910
weight: KeybindingWeight.WorkbenchContrib,
911
primary: KeyMod.CtrlCmd | KeyCode.Enter,
912
when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive, WorkbenchListFocusContextKey),
913
handler(accessor) {
914
const editors = accessor.get(IEditorService);
915
const lists = accessor.get(IListService);
916
917
const tree = lists.lastFocusedList;
918
if (!(tree instanceof WorkbenchDataTree) && !(tree instanceof WorkbenchAsyncDataTree)) {
919
return;
920
}
921
922
const element = <IFileStat | unknown>tree.getFocus()[0];
923
924
if (URI.isUri((<IFileStat>element)?.resource)) {
925
// IFileStat: open file in editor
926
return editors.openEditor({
927
resource: (<IFileStat>element).resource,
928
options: { pinned: true }
929
}, SIDE_GROUP);
930
}
931
932
// IOutline: check if this the outline and iff so reveal element
933
const input = tree.getInput();
934
if (input && typeof (<IOutline<any>>input).outlineKind === 'string') {
935
return (<IOutline<any>>input).reveal(element, {
936
pinned: true,
937
preserveFocus: false
938
}, true, false);
939
}
940
}
941
});
942
//#endregion
943
944