Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/debug/browser/callStackWidget.ts
5240 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 { Button } from '../../../../base/browser/ui/button/button.js';
8
import { IListRenderer, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';
9
import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js';
10
import { assertNever } from '../../../../base/common/assert.js';
11
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
12
import { Codicon } from '../../../../base/common/codicons.js';
13
import { Emitter, Event } from '../../../../base/common/event.js';
14
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
15
import { autorun, autorunWithStore, derived, IObservable, ISettableObservable, observableValue, transaction } from '../../../../base/common/observable.js';
16
import { ThemeIcon } from '../../../../base/common/themables.js';
17
import { Constants } from '../../../../base/common/uint.js';
18
import { URI } from '../../../../base/common/uri.js';
19
import { generateUuid } from '../../../../base/common/uuid.js';
20
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
21
import { EditorContributionCtor, EditorContributionInstantiation, IEditorContributionDescription } from '../../../../editor/browser/editorExtensions.js';
22
import { CodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';
23
import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js';
24
import { IEditorOptions } from '../../../../editor/common/config/editorOptions.js';
25
import { Position } from '../../../../editor/common/core/position.js';
26
import { Range } from '../../../../editor/common/core/range.js';
27
import { IWordAtPosition } from '../../../../editor/common/core/wordHelper.js';
28
import { IEditorContribution, IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js';
29
import { Location } from '../../../../editor/common/languages.js';
30
import { ITextModelService } from '../../../../editor/common/services/resolverService.js';
31
import { ClickLinkGesture, ClickLinkMouseEvent } from '../../../../editor/contrib/gotoSymbol/browser/link/clickLinkGesture.js';
32
import { localize, localize2 } from '../../../../nls.js';
33
import { createActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
34
import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';
35
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
36
import { TextEditorSelectionRevealType } from '../../../../platform/editor/common/editor.js';
37
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
38
import { ILabelService } from '../../../../platform/label/common/label.js';
39
import { WorkbenchList } from '../../../../platform/list/browser/listService.js';
40
import { INotificationService } from '../../../../platform/notification/common/notification.js';
41
import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js';
42
import { ResourceLabel } from '../../../browser/labels.js';
43
import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js';
44
import { makeStackFrameColumnDecoration, TOP_STACK_FRAME_DECORATION } from './callStackEditorContribution.js';
45
import './media/callStackWidget.css';
46
47
48
export class CallStackFrame {
49
constructor(
50
public readonly name: string,
51
public readonly source?: URI,
52
public readonly line = 1,
53
public readonly column = 1,
54
) { }
55
}
56
57
export class SkippedCallFrames {
58
constructor(
59
public readonly label: string,
60
public readonly load: (token: CancellationToken) => Promise<AnyStackFrame[]>,
61
) { }
62
}
63
64
export abstract class CustomStackFrame {
65
public readonly showHeader = observableValue('CustomStackFrame.showHeader', true);
66
public abstract readonly height: IObservable<number>;
67
public abstract readonly label: string;
68
public icon?: ThemeIcon;
69
public abstract render(container: HTMLElement): IDisposable;
70
public renderActions?(container: HTMLElement): IDisposable;
71
}
72
73
export type AnyStackFrame = SkippedCallFrames | CallStackFrame | CustomStackFrame;
74
75
interface IFrameLikeItem {
76
readonly collapsed: ISettableObservable<boolean>;
77
readonly height: IObservable<number>;
78
}
79
80
class WrappedCallStackFrame extends CallStackFrame implements IFrameLikeItem {
81
public readonly editorHeight = observableValue('WrappedCallStackFrame.height', this.source ? 100 : 0);
82
public readonly collapsed = observableValue('WrappedCallStackFrame.collapsed', false);
83
84
public readonly height = derived(reader => {
85
return this.collapsed.read(reader) ? CALL_STACK_WIDGET_HEADER_HEIGHT : CALL_STACK_WIDGET_HEADER_HEIGHT + this.editorHeight.read(reader);
86
});
87
88
constructor(original: CallStackFrame) {
89
super(original.name, original.source, original.line, original.column);
90
}
91
}
92
93
class WrappedCustomStackFrame implements IFrameLikeItem {
94
public readonly collapsed = observableValue('WrappedCallStackFrame.collapsed', false);
95
96
public readonly height = derived(reader => {
97
const headerHeight = this.original.showHeader.read(reader) ? CALL_STACK_WIDGET_HEADER_HEIGHT : 0;
98
return this.collapsed.read(reader) ? headerHeight : headerHeight + this.original.height.read(reader);
99
});
100
101
constructor(public readonly original: CustomStackFrame) { }
102
}
103
104
const isFrameLike = (item: unknown): item is IFrameLikeItem =>
105
item instanceof WrappedCallStackFrame || item instanceof WrappedCustomStackFrame;
106
107
type ListItem = WrappedCallStackFrame | SkippedCallFrames | WrappedCustomStackFrame;
108
109
const WIDGET_CLASS_NAME = 'multiCallStackWidget';
110
111
/**
112
* A reusable widget that displays a call stack as a series of editors. Note
113
* that this both used in debug's exception widget as well as in the testing
114
* call stack view.
115
*/
116
export class CallStackWidget extends Disposable {
117
private readonly list: WorkbenchList<ListItem>;
118
private readonly layoutEmitter = this._register(new Emitter<void>());
119
private readonly currentFramesDs = this._register(new DisposableStore());
120
private cts?: CancellationTokenSource;
121
122
public get onDidChangeContentHeight() {
123
return this.list.onDidChangeContentHeight;
124
}
125
126
public get onDidScroll() {
127
return this.list.onDidScroll;
128
}
129
130
public get contentHeight() {
131
return this.list.contentHeight;
132
}
133
134
constructor(
135
container: HTMLElement,
136
containingEditor: ICodeEditor | undefined,
137
@IInstantiationService instantiationService: IInstantiationService,
138
) {
139
super();
140
141
container.classList.add(WIDGET_CLASS_NAME);
142
this._register(toDisposable(() => container.classList.remove(WIDGET_CLASS_NAME)));
143
144
this.list = this._register(instantiationService.createInstance(
145
WorkbenchList,
146
'TestResultStackWidget',
147
container,
148
new StackDelegate(),
149
[
150
instantiationService.createInstance(FrameCodeRenderer, containingEditor, this.layoutEmitter.event),
151
instantiationService.createInstance(MissingCodeRenderer),
152
instantiationService.createInstance(CustomRenderer),
153
instantiationService.createInstance(SkippedRenderer, (i) => this.loadFrame(i)),
154
],
155
{
156
multipleSelectionSupport: false,
157
mouseSupport: false,
158
keyboardSupport: false,
159
setRowLineHeight: false,
160
alwaysConsumeMouseWheel: false,
161
accessibilityProvider: instantiationService.createInstance(StackAccessibilityProvider),
162
}
163
) as WorkbenchList<ListItem>);
164
}
165
166
/** Replaces the call frames display in the view. */
167
public setFrames(frames: AnyStackFrame[]): void {
168
// cancel any existing load
169
this.currentFramesDs.clear();
170
const cts = new CancellationTokenSource();
171
this.currentFramesDs.add(toDisposable(() => cts.dispose(true)));
172
this.cts = cts;
173
174
this.list.splice(0, this.list.length, this.mapFrames(frames));
175
}
176
177
public layout(height?: number, width?: number): void {
178
this.list.layout(height, width);
179
this.layoutEmitter.fire();
180
}
181
182
public collapseAll() {
183
transaction(tx => {
184
for (let i = 0; i < this.list.length; i++) {
185
const frame = this.list.element(i);
186
if (isFrameLike(frame)) {
187
frame.collapsed.set(true, tx);
188
}
189
}
190
});
191
}
192
193
private async loadFrame(replacing: SkippedCallFrames): Promise<void> {
194
if (!this.cts) {
195
return;
196
}
197
198
const frames = await replacing.load(this.cts.token);
199
if (this.cts.token.isCancellationRequested) {
200
return;
201
}
202
203
const index = this.list.indexOf(replacing);
204
this.list.splice(index, 1, this.mapFrames(frames));
205
}
206
207
private mapFrames(frames: AnyStackFrame[]): ListItem[] {
208
const result: ListItem[] = [];
209
for (const frame of frames) {
210
if (frame instanceof SkippedCallFrames) {
211
result.push(frame);
212
continue;
213
}
214
215
const wrapped = frame instanceof CustomStackFrame
216
? new WrappedCustomStackFrame(frame) : new WrappedCallStackFrame(frame);
217
result.push(wrapped);
218
219
this.currentFramesDs.add(autorun(reader => {
220
const height = wrapped.height.read(reader);
221
const idx = this.list.indexOf(wrapped);
222
if (idx !== -1) {
223
this.list.updateElementHeight(idx, height);
224
}
225
}));
226
}
227
228
return result;
229
}
230
}
231
232
class StackAccessibilityProvider implements IListAccessibilityProvider<ListItem> {
233
constructor(@ILabelService private readonly labelService: ILabelService) { }
234
235
getAriaLabel(e: ListItem): string | IObservable<string> | null {
236
if (e instanceof SkippedCallFrames) {
237
return e.label;
238
}
239
240
if (e instanceof WrappedCustomStackFrame) {
241
return e.original.label;
242
}
243
244
if (e instanceof CallStackFrame) {
245
if (e.source && e.line) {
246
return localize({
247
comment: ['{0} is an extension-defined label, then line number and filename'],
248
key: 'stackTraceLabel',
249
}, '{0}, line {1} in {2}', e.name, e.line, this.labelService.getUriLabel(e.source, { relative: true }));
250
}
251
252
return e.name;
253
}
254
255
assertNever(e);
256
}
257
getWidgetAriaLabel(): string {
258
return localize('stackTrace', 'Stack Trace');
259
}
260
}
261
262
class StackDelegate implements IListVirtualDelegate<ListItem> {
263
getHeight(element: ListItem): number {
264
if (element instanceof CallStackFrame || element instanceof WrappedCustomStackFrame) {
265
return element.height.get();
266
}
267
if (element instanceof SkippedCallFrames) {
268
return CALL_STACK_WIDGET_HEADER_HEIGHT;
269
}
270
271
assertNever(element);
272
}
273
274
getTemplateId(element: ListItem): string {
275
if (element instanceof CallStackFrame) {
276
return element.source ? FrameCodeRenderer.templateId : MissingCodeRenderer.templateId;
277
}
278
if (element instanceof SkippedCallFrames) {
279
return SkippedRenderer.templateId;
280
}
281
if (element instanceof WrappedCustomStackFrame) {
282
return CustomRenderer.templateId;
283
}
284
285
assertNever(element);
286
}
287
}
288
289
interface IStackTemplateData extends IAbstractFrameRendererTemplateData {
290
editor: CodeEditorWidget;
291
toolbar: MenuWorkbenchToolBar;
292
}
293
294
const editorOptions: IEditorOptions = {
295
scrollBeyondLastLine: false,
296
scrollbar: {
297
vertical: 'hidden',
298
horizontal: 'hidden',
299
handleMouseWheel: false,
300
useShadows: false,
301
},
302
overviewRulerLanes: 0,
303
fixedOverflowWidgets: true,
304
overviewRulerBorder: false,
305
stickyScroll: { enabled: false },
306
minimap: { enabled: false },
307
readOnly: true,
308
automaticLayout: false,
309
};
310
311
const makeFrameElements = () => dom.h('div.multiCallStackFrame', [
312
dom.h('div.header@header', [
313
dom.h('div.collapse-button@collapseButton'),
314
dom.h('div.title.show-file-icons@title'),
315
dom.h('div.actions@actions'),
316
]),
317
318
dom.h('div.editorParent', [
319
dom.h('div.editorContainer@editor'),
320
])
321
]);
322
323
export const CALL_STACK_WIDGET_HEADER_HEIGHT = 24;
324
325
interface IAbstractFrameRendererTemplateData {
326
container: HTMLElement;
327
label: ResourceLabel;
328
elements: ReturnType<typeof makeFrameElements>;
329
decorations: string[];
330
collapse: Button;
331
elementStore: DisposableStore;
332
templateStore: DisposableStore;
333
}
334
335
abstract class AbstractFrameRenderer<T extends IAbstractFrameRendererTemplateData> implements IListRenderer<ListItem, T> {
336
public abstract templateId: string;
337
338
constructor(
339
@IInstantiationService protected readonly instantiationService: IInstantiationService,
340
) { }
341
342
renderTemplate(container: HTMLElement): T {
343
const elements = makeFrameElements();
344
container.appendChild(elements.root);
345
346
347
const templateStore = new DisposableStore();
348
container.classList.add('multiCallStackFrameContainer');
349
templateStore.add(toDisposable(() => {
350
container.classList.remove('multiCallStackFrameContainer');
351
elements.root.remove();
352
}));
353
354
const label = templateStore.add(this.instantiationService.createInstance(ResourceLabel, elements.title, {}));
355
356
const collapse = templateStore.add(new Button(elements.collapseButton, {}));
357
358
const contentId = generateUuid();
359
elements.editor.id = contentId;
360
elements.editor.role = 'region';
361
elements.collapseButton.setAttribute('aria-controls', contentId);
362
363
return this.finishRenderTemplate({
364
container,
365
decorations: [],
366
elements,
367
label,
368
collapse,
369
elementStore: templateStore.add(new DisposableStore()),
370
templateStore,
371
});
372
}
373
374
protected abstract finishRenderTemplate(data: IAbstractFrameRendererTemplateData): T;
375
376
renderElement(element: ListItem, index: number, template: T): void {
377
const { elementStore } = template;
378
elementStore.clear();
379
const item = element as IFrameLikeItem;
380
381
this.setupCollapseButton(item, template);
382
}
383
384
private setupCollapseButton(item: IFrameLikeItem, { elementStore, elements, collapse }: T) {
385
elementStore.add(autorun(reader => {
386
collapse.element.className = '';
387
const collapsed = item.collapsed.read(reader);
388
collapse.icon = collapsed ? Codicon.chevronRight : Codicon.chevronDown;
389
collapse.element.ariaExpanded = String(!collapsed);
390
elements.root.classList.toggle('collapsed', collapsed);
391
}));
392
const toggleCollapse = () => item.collapsed.set(!item.collapsed.get(), undefined);
393
elementStore.add(collapse.onDidClick(toggleCollapse));
394
elementStore.add(dom.addDisposableListener(elements.title, 'click', toggleCollapse));
395
}
396
397
disposeElement(element: ListItem, index: number, templateData: T): void {
398
templateData.elementStore.clear();
399
}
400
401
disposeTemplate(templateData: T): void {
402
templateData.templateStore.dispose();
403
}
404
}
405
406
const CONTEXT_LINES = 2;
407
408
/** Renderer for a normal stack frame where code is available. */
409
class FrameCodeRenderer extends AbstractFrameRenderer<IStackTemplateData> {
410
public static readonly templateId = 'f';
411
412
public readonly templateId = FrameCodeRenderer.templateId;
413
414
constructor(
415
private readonly containingEditor: ICodeEditor | undefined,
416
private readonly onLayout: Event<void>,
417
@ITextModelService private readonly modelService: ITextModelService,
418
@IInstantiationService instantiationService: IInstantiationService,
419
) {
420
super(instantiationService);
421
}
422
423
protected override finishRenderTemplate(data: IAbstractFrameRendererTemplateData): IStackTemplateData {
424
// override default e.g. language contributions, only allow users to click
425
// on code in the call stack to go to its source location
426
const contributions: IEditorContributionDescription[] = [{
427
id: ClickToLocationContribution.ID,
428
instantiation: EditorContributionInstantiation.BeforeFirstInteraction,
429
ctor: ClickToLocationContribution as EditorContributionCtor,
430
}];
431
432
const editor = this.containingEditor
433
? this.instantiationService.createInstance(
434
EmbeddedCodeEditorWidget,
435
data.elements.editor,
436
editorOptions,
437
{ isSimpleWidget: true, contributions },
438
this.containingEditor,
439
)
440
: this.instantiationService.createInstance(
441
CodeEditorWidget,
442
data.elements.editor,
443
editorOptions,
444
{ isSimpleWidget: true, contributions },
445
);
446
447
data.templateStore.add(editor);
448
449
const toolbar = data.templateStore.add(this.instantiationService.createInstance(MenuWorkbenchToolBar, data.elements.actions, MenuId.DebugCallStackToolbar, {
450
menuOptions: { shouldForwardArgs: true },
451
actionViewItemProvider: (action, options) => createActionViewItem(this.instantiationService, action, options),
452
}));
453
454
return { ...data, editor, toolbar };
455
}
456
457
override renderElement(element: ListItem, index: number, template: IStackTemplateData): void {
458
super.renderElement(element, index, template);
459
460
const { elementStore, editor } = template;
461
462
const item = element as WrappedCallStackFrame;
463
const uri = item.source!;
464
465
template.label.element.setFile(uri);
466
const cts = new CancellationTokenSource();
467
elementStore.add(toDisposable(() => cts.dispose(true)));
468
this.modelService.createModelReference(uri).then(reference => {
469
if (cts.token.isCancellationRequested) {
470
return reference.dispose();
471
}
472
473
elementStore.add(reference);
474
editor.setModel(reference.object.textEditorModel);
475
this.setupEditorAfterModel(item, template);
476
this.setupEditorLayout(item, template);
477
});
478
}
479
480
private setupEditorLayout(item: WrappedCallStackFrame, { elementStore, container, editor }: IStackTemplateData) {
481
const layout = () => {
482
const prev = editor.getContentHeight();
483
editor.layout({ width: container.clientWidth, height: prev });
484
485
const next = editor.getContentHeight();
486
if (next !== prev) {
487
editor.layout({ width: container.clientWidth, height: next });
488
}
489
490
item.editorHeight.set(next, undefined);
491
};
492
elementStore.add(editor.onDidChangeModelDecorations(layout));
493
elementStore.add(editor.onDidChangeModelContent(layout));
494
elementStore.add(editor.onDidChangeModelOptions(layout));
495
elementStore.add(this.onLayout(layout));
496
layout();
497
}
498
499
private setupEditorAfterModel(item: WrappedCallStackFrame, template: IStackTemplateData): void {
500
const range = Range.fromPositions({
501
column: item.column ?? 1,
502
lineNumber: item.line ?? 1,
503
});
504
505
template.toolbar.context = { uri: item.source, range };
506
507
template.editor.setHiddenAreas([
508
Range.fromPositions(
509
{ column: 1, lineNumber: 1 },
510
{ column: 1, lineNumber: Math.max(1, item.line - CONTEXT_LINES - 1) },
511
),
512
Range.fromPositions(
513
{ column: 1, lineNumber: item.line + CONTEXT_LINES + 1 },
514
{ column: 1, lineNumber: Constants.MAX_SAFE_SMALL_INTEGER },
515
),
516
]);
517
518
template.editor.changeDecorations(accessor => {
519
for (const d of template.decorations) {
520
accessor.removeDecoration(d);
521
}
522
template.decorations.length = 0;
523
524
const beforeRange = range.setStartPosition(range.startLineNumber, 1);
525
const hasCharactersBefore = !!template.editor.getModel()?.getValueInRange(beforeRange).trim();
526
const decoRange = range.setEndPosition(range.startLineNumber, Constants.MAX_SAFE_SMALL_INTEGER);
527
528
template.decorations.push(accessor.addDecoration(
529
decoRange,
530
makeStackFrameColumnDecoration(!hasCharactersBefore),
531
));
532
template.decorations.push(accessor.addDecoration(
533
decoRange,
534
TOP_STACK_FRAME_DECORATION,
535
));
536
});
537
538
item.editorHeight.set(template.editor.getContentHeight(), undefined);
539
}
540
}
541
542
interface IMissingTemplateData {
543
elements: ReturnType<typeof makeFrameElements>;
544
label: ResourceLabel;
545
}
546
547
/** Renderer for a call frame that's missing a URI */
548
class MissingCodeRenderer implements IListRenderer<ListItem, IMissingTemplateData> {
549
public static readonly templateId = 'm';
550
public readonly templateId = MissingCodeRenderer.templateId;
551
552
constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) { }
553
554
renderTemplate(container: HTMLElement): IMissingTemplateData {
555
const elements = makeFrameElements();
556
elements.root.classList.add('missing');
557
container.appendChild(elements.root);
558
const label = this.instantiationService.createInstance(ResourceLabel, elements.title, {});
559
return { elements, label };
560
}
561
562
renderElement(element: ListItem, _index: number, templateData: IMissingTemplateData): void {
563
const cast = element as CallStackFrame;
564
templateData.label.element.setResource({
565
name: cast.name,
566
description: localize('stackFrameLocation', 'Line {0} column {1}', cast.line, cast.column),
567
range: { startLineNumber: cast.line, startColumn: cast.column, endColumn: cast.column, endLineNumber: cast.line },
568
}, {
569
icon: Codicon.fileBinary,
570
});
571
}
572
573
disposeTemplate(templateData: IMissingTemplateData): void {
574
templateData.label.dispose();
575
templateData.elements.root.remove();
576
}
577
}
578
579
/** Renderer for a call frame that's missing a URI */
580
class CustomRenderer extends AbstractFrameRenderer<IAbstractFrameRendererTemplateData> {
581
public static readonly templateId = 'c';
582
public readonly templateId = CustomRenderer.templateId;
583
584
protected override finishRenderTemplate(data: IAbstractFrameRendererTemplateData): IAbstractFrameRendererTemplateData {
585
return data;
586
}
587
588
override renderElement(element: ListItem, index: number, template: IAbstractFrameRendererTemplateData): void {
589
super.renderElement(element, index, template);
590
591
const item = element as WrappedCustomStackFrame;
592
const { elementStore, container, label } = template;
593
594
label.element.setResource({ name: item.original.label }, { icon: item.original.icon });
595
596
elementStore.add(autorun(reader => {
597
template.elements.header.style.display = item.original.showHeader.read(reader) ? '' : 'none';
598
}));
599
600
elementStore.add(autorunWithStore((reader, store) => {
601
if (!item.collapsed.read(reader)) {
602
store.add(item.original.render(container));
603
}
604
}));
605
606
const actions = item.original.renderActions?.(template.elements.actions);
607
if (actions) {
608
elementStore.add(actions);
609
}
610
}
611
}
612
613
interface ISkippedTemplateData {
614
button: Button;
615
current?: SkippedCallFrames;
616
store: DisposableStore;
617
}
618
619
/** Renderer for a button to load more call frames */
620
class SkippedRenderer implements IListRenderer<ListItem, ISkippedTemplateData> {
621
public static readonly templateId = 's';
622
public readonly templateId = SkippedRenderer.templateId;
623
624
constructor(
625
private readonly loadFrames: (fromItem: SkippedCallFrames) => Promise<void>,
626
@INotificationService private readonly notificationService: INotificationService,
627
) { }
628
629
renderTemplate(container: HTMLElement): ISkippedTemplateData {
630
const store = new DisposableStore();
631
const button = new Button(container, { title: '', ...defaultButtonStyles });
632
const data: ISkippedTemplateData = { button, store };
633
634
store.add(button);
635
store.add(button.onDidClick(() => {
636
if (!data.current || !button.enabled) {
637
return;
638
}
639
640
button.enabled = false;
641
this.loadFrames(data.current).catch(e => {
642
this.notificationService.error(localize('failedToLoadFrames', 'Failed to load stack frames: {0}', e.message));
643
});
644
}));
645
646
return data;
647
}
648
649
renderElement(element: ListItem, index: number, templateData: ISkippedTemplateData): void {
650
const cast = element as SkippedCallFrames;
651
templateData.button.enabled = true;
652
templateData.button.label = cast.label;
653
templateData.current = cast;
654
}
655
656
disposeTemplate(templateData: ISkippedTemplateData): void {
657
templateData.store.dispose();
658
}
659
}
660
661
/** A simple contribution that makes all data in the editor clickable to go to the location */
662
class ClickToLocationContribution extends Disposable implements IEditorContribution {
663
public static readonly ID = 'clickToLocation';
664
private readonly linkDecorations: IEditorDecorationsCollection;
665
private current: { line: number; word: IWordAtPosition } | undefined;
666
667
constructor(
668
private readonly editor: ICodeEditor,
669
@IEditorService editorService: IEditorService,
670
) {
671
super();
672
this.linkDecorations = editor.createDecorationsCollection();
673
this._register(toDisposable(() => this.linkDecorations.clear()));
674
675
const clickLinkGesture = this._register(new ClickLinkGesture(editor));
676
677
this._register(clickLinkGesture.onMouseMoveOrRelevantKeyDown(([mouseEvent, keyboardEvent]) => {
678
this.onMove(mouseEvent);
679
}));
680
this._register(clickLinkGesture.onExecute((e) => {
681
const model = this.editor.getModel();
682
if (!this.current || !model) {
683
return;
684
}
685
686
editorService.openEditor({
687
resource: model.uri,
688
options: {
689
selection: Range.fromPositions(new Position(this.current.line, this.current.word.startColumn)),
690
selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport,
691
},
692
}, e.hasSideBySideModifier ? SIDE_GROUP : undefined);
693
}));
694
}
695
696
private onMove(mouseEvent: ClickLinkMouseEvent) {
697
if (!mouseEvent.hasTriggerModifier) {
698
return this.clear();
699
}
700
701
const position = mouseEvent.target.position;
702
const word = position && this.editor.getModel()?.getWordAtPosition(position);
703
if (!word) {
704
return this.clear();
705
}
706
707
const prev = this.current?.word;
708
if (prev && prev.startColumn === word.startColumn && prev.endColumn === word.endColumn && prev.word === word.word) {
709
return;
710
}
711
712
this.current = { word, line: position.lineNumber };
713
this.linkDecorations.set([{
714
range: new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn),
715
options: {
716
description: 'call-stack-go-to-file-link',
717
inlineClassName: 'call-stack-go-to-file-link',
718
},
719
}]);
720
}
721
722
private clear() {
723
this.linkDecorations.clear();
724
this.current = undefined;
725
}
726
}
727
728
registerAction2(class extends Action2 {
729
constructor() {
730
super({
731
id: 'callStackWidget.goToFile',
732
title: localize2('goToFile', 'Open File'),
733
icon: Codicon.goToFile,
734
menu: {
735
id: MenuId.DebugCallStackToolbar,
736
order: 22,
737
group: 'navigation',
738
},
739
});
740
}
741
742
async run(accessor: ServicesAccessor, { uri, range }: Location): Promise<void> {
743
const editorService = accessor.get(IEditorService);
744
await editorService.openEditor({
745
resource: uri,
746
options: {
747
selection: range,
748
selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport,
749
},
750
});
751
}
752
});
753
754