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