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
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 { 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
this.cts = new CancellationTokenSource();
171
this._register(toDisposable(() => this.cts!.dispose(true)));
172
173
this.list.splice(0, this.list.length, this.mapFrames(frames));
174
}
175
176
public layout(height?: number, width?: number): void {
177
this.list.layout(height, width);
178
this.layoutEmitter.fire();
179
}
180
181
public collapseAll() {
182
transaction(tx => {
183
for (let i = 0; i < this.list.length; i++) {
184
const frame = this.list.element(i);
185
if (isFrameLike(frame)) {
186
frame.collapsed.set(true, tx);
187
}
188
}
189
});
190
}
191
192
private async loadFrame(replacing: SkippedCallFrames): Promise<void> {
193
if (!this.cts) {
194
return;
195
}
196
197
const frames = await replacing.load(this.cts.token);
198
if (this.cts.token.isCancellationRequested) {
199
return;
200
}
201
202
const index = this.list.indexOf(replacing);
203
this.list.splice(index, 1, this.mapFrames(frames));
204
}
205
206
private mapFrames(frames: AnyStackFrame[]): ListItem[] {
207
const result: ListItem[] = [];
208
for (const frame of frames) {
209
if (frame instanceof SkippedCallFrames) {
210
result.push(frame);
211
continue;
212
}
213
214
const wrapped = frame instanceof CustomStackFrame
215
? new WrappedCustomStackFrame(frame) : new WrappedCallStackFrame(frame);
216
result.push(wrapped);
217
218
this.currentFramesDs.add(autorun(reader => {
219
const height = wrapped.height.read(reader);
220
const idx = this.list.indexOf(wrapped);
221
if (idx !== -1) {
222
this.list.updateElementHeight(idx, height);
223
}
224
}));
225
}
226
227
return result;
228
}
229
}
230
231
class StackAccessibilityProvider implements IListAccessibilityProvider<ListItem> {
232
constructor(@ILabelService private readonly labelService: ILabelService) { }
233
234
getAriaLabel(e: ListItem): string | IObservable<string> | null {
235
if (e instanceof SkippedCallFrames) {
236
return e.label;
237
}
238
239
if (e instanceof WrappedCustomStackFrame) {
240
return e.original.label;
241
}
242
243
if (e instanceof CallStackFrame) {
244
if (e.source && e.line) {
245
return localize({
246
comment: ['{0} is an extension-defined label, then line number and filename'],
247
key: 'stackTraceLabel',
248
}, '{0}, line {1} in {2}', e.name, e.line, this.labelService.getUriLabel(e.source, { relative: true }));
249
}
250
251
return e.name;
252
}
253
254
assertNever(e);
255
}
256
getWidgetAriaLabel(): string {
257
return localize('stackTrace', 'Stack Trace');
258
}
259
}
260
261
class StackDelegate implements IListVirtualDelegate<ListItem> {
262
getHeight(element: ListItem): number {
263
if (element instanceof CallStackFrame || element instanceof WrappedCustomStackFrame) {
264
return element.height.get();
265
}
266
if (element instanceof SkippedCallFrames) {
267
return CALL_STACK_WIDGET_HEADER_HEIGHT;
268
}
269
270
assertNever(element);
271
}
272
273
getTemplateId(element: ListItem): string {
274
if (element instanceof CallStackFrame) {
275
return element.source ? FrameCodeRenderer.templateId : MissingCodeRenderer.templateId;
276
}
277
if (element instanceof SkippedCallFrames) {
278
return SkippedRenderer.templateId;
279
}
280
if (element instanceof WrappedCustomStackFrame) {
281
return CustomRenderer.templateId;
282
}
283
284
assertNever(element);
285
}
286
}
287
288
interface IStackTemplateData extends IAbstractFrameRendererTemplateData {
289
editor: CodeEditorWidget;
290
toolbar: MenuWorkbenchToolBar;
291
}
292
293
const editorOptions: IEditorOptions = {
294
scrollBeyondLastLine: false,
295
scrollbar: {
296
vertical: 'hidden',
297
horizontal: 'hidden',
298
handleMouseWheel: false,
299
useShadows: false,
300
},
301
overviewRulerLanes: 0,
302
fixedOverflowWidgets: true,
303
overviewRulerBorder: false,
304
stickyScroll: { enabled: false },
305
minimap: { enabled: false },
306
readOnly: true,
307
automaticLayout: false,
308
};
309
310
const makeFrameElements = () => dom.h('div.multiCallStackFrame', [
311
dom.h('div.header@header', [
312
dom.h('div.collapse-button@collapseButton'),
313
dom.h('div.title.show-file-icons@title'),
314
dom.h('div.actions@actions'),
315
]),
316
317
dom.h('div.editorParent', [
318
dom.h('div.editorContainer@editor'),
319
])
320
]);
321
322
export const CALL_STACK_WIDGET_HEADER_HEIGHT = 24;
323
324
interface IAbstractFrameRendererTemplateData {
325
container: HTMLElement;
326
label: ResourceLabel;
327
elements: ReturnType<typeof makeFrameElements>;
328
decorations: string[];
329
collapse: Button;
330
elementStore: DisposableStore;
331
templateStore: DisposableStore;
332
}
333
334
abstract class AbstractFrameRenderer<T extends IAbstractFrameRendererTemplateData> implements IListRenderer<ListItem, T> {
335
public abstract templateId: string;
336
337
constructor(
338
@IInstantiationService protected readonly instantiationService: IInstantiationService,
339
) { }
340
341
renderTemplate(container: HTMLElement): T {
342
const elements = makeFrameElements();
343
container.appendChild(elements.root);
344
345
346
const templateStore = new DisposableStore();
347
container.classList.add('multiCallStackFrameContainer');
348
templateStore.add(toDisposable(() => {
349
container.classList.remove('multiCallStackFrameContainer');
350
elements.root.remove();
351
}));
352
353
const label = templateStore.add(this.instantiationService.createInstance(ResourceLabel, elements.title, {}));
354
355
const collapse = templateStore.add(new Button(elements.collapseButton, {}));
356
357
const contentId = generateUuid();
358
elements.editor.id = contentId;
359
elements.editor.role = 'region';
360
elements.collapseButton.setAttribute('aria-controls', contentId);
361
362
return this.finishRenderTemplate({
363
container,
364
decorations: [],
365
elements,
366
label,
367
collapse,
368
elementStore: templateStore.add(new DisposableStore()),
369
templateStore,
370
});
371
}
372
373
protected abstract finishRenderTemplate(data: IAbstractFrameRendererTemplateData): T;
374
375
renderElement(element: ListItem, index: number, template: T): void {
376
const { elementStore } = template;
377
elementStore.clear();
378
const item = element as IFrameLikeItem;
379
380
this.setupCollapseButton(item, template);
381
}
382
383
private setupCollapseButton(item: IFrameLikeItem, { elementStore, elements, collapse }: T) {
384
elementStore.add(autorun(reader => {
385
collapse.element.className = '';
386
const collapsed = item.collapsed.read(reader);
387
collapse.icon = collapsed ? Codicon.chevronRight : Codicon.chevronDown;
388
collapse.element.ariaExpanded = String(!collapsed);
389
elements.root.classList.toggle('collapsed', collapsed);
390
}));
391
const toggleCollapse = () => item.collapsed.set(!item.collapsed.get(), undefined);
392
elementStore.add(collapse.onDidClick(toggleCollapse));
393
elementStore.add(dom.addDisposableListener(elements.title, 'click', toggleCollapse));
394
}
395
396
disposeElement(element: ListItem, index: number, templateData: T): void {
397
templateData.elementStore.clear();
398
}
399
400
disposeTemplate(templateData: T): void {
401
templateData.templateStore.dispose();
402
}
403
}
404
405
const CONTEXT_LINES = 2;
406
407
/** Renderer for a normal stack frame where code is available. */
408
class FrameCodeRenderer extends AbstractFrameRenderer<IStackTemplateData> {
409
public static readonly templateId = 'f';
410
411
public readonly templateId = FrameCodeRenderer.templateId;
412
413
constructor(
414
private readonly containingEditor: ICodeEditor | undefined,
415
private readonly onLayout: Event<void>,
416
@ITextModelService private readonly modelService: ITextModelService,
417
@IInstantiationService instantiationService: IInstantiationService,
418
) {
419
super(instantiationService);
420
}
421
422
protected override finishRenderTemplate(data: IAbstractFrameRendererTemplateData): IStackTemplateData {
423
// override default e.g. language contributions, only allow users to click
424
// on code in the call stack to go to its source location
425
const contributions: IEditorContributionDescription[] = [{
426
id: ClickToLocationContribution.ID,
427
instantiation: EditorContributionInstantiation.BeforeFirstInteraction,
428
ctor: ClickToLocationContribution as EditorContributionCtor,
429
}];
430
431
const editor = this.containingEditor
432
? this.instantiationService.createInstance(
433
EmbeddedCodeEditorWidget,
434
data.elements.editor,
435
editorOptions,
436
{ isSimpleWidget: true, contributions },
437
this.containingEditor,
438
)
439
: this.instantiationService.createInstance(
440
CodeEditorWidget,
441
data.elements.editor,
442
editorOptions,
443
{ isSimpleWidget: true, contributions },
444
);
445
446
data.templateStore.add(editor);
447
448
const toolbar = data.templateStore.add(this.instantiationService.createInstance(MenuWorkbenchToolBar, data.elements.actions, MenuId.DebugCallStackToolbar, {
449
menuOptions: { shouldForwardArgs: true },
450
actionViewItemProvider: (action, options) => createActionViewItem(this.instantiationService, action, options),
451
}));
452
453
return { ...data, editor, toolbar };
454
}
455
456
override renderElement(element: ListItem, index: number, template: IStackTemplateData): void {
457
super.renderElement(element, index, template);
458
459
const { elementStore, editor } = template;
460
461
const item = element as WrappedCallStackFrame;
462
const uri = item.source!;
463
464
template.label.element.setFile(uri);
465
const cts = new CancellationTokenSource();
466
elementStore.add(toDisposable(() => cts.dispose(true)));
467
this.modelService.createModelReference(uri).then(reference => {
468
if (cts.token.isCancellationRequested) {
469
return reference.dispose();
470
}
471
472
elementStore.add(reference);
473
editor.setModel(reference.object.textEditorModel);
474
this.setupEditorAfterModel(item, template);
475
this.setupEditorLayout(item, template);
476
});
477
}
478
479
private setupEditorLayout(item: WrappedCallStackFrame, { elementStore, container, editor }: IStackTemplateData) {
480
const layout = () => {
481
const prev = editor.getContentHeight();
482
editor.layout({ width: container.clientWidth, height: prev });
483
484
const next = editor.getContentHeight();
485
if (next !== prev) {
486
editor.layout({ width: container.clientWidth, height: next });
487
}
488
489
item.editorHeight.set(next, undefined);
490
};
491
elementStore.add(editor.onDidChangeModelDecorations(layout));
492
elementStore.add(editor.onDidChangeModelContent(layout));
493
elementStore.add(editor.onDidChangeModelOptions(layout));
494
elementStore.add(this.onLayout(layout));
495
layout();
496
}
497
498
private setupEditorAfterModel(item: WrappedCallStackFrame, template: IStackTemplateData): void {
499
const range = Range.fromPositions({
500
column: item.column ?? 1,
501
lineNumber: item.line ?? 1,
502
});
503
504
template.toolbar.context = { uri: item.source, range };
505
506
template.editor.setHiddenAreas([
507
Range.fromPositions(
508
{ column: 1, lineNumber: 1 },
509
{ column: 1, lineNumber: Math.max(1, item.line - CONTEXT_LINES - 1) },
510
),
511
Range.fromPositions(
512
{ column: 1, lineNumber: item.line + CONTEXT_LINES + 1 },
513
{ column: 1, lineNumber: Constants.MAX_SAFE_SMALL_INTEGER },
514
),
515
]);
516
517
template.editor.changeDecorations(accessor => {
518
for (const d of template.decorations) {
519
accessor.removeDecoration(d);
520
}
521
template.decorations.length = 0;
522
523
const beforeRange = range.setStartPosition(range.startLineNumber, 1);
524
const hasCharactersBefore = !!template.editor.getModel()?.getValueInRange(beforeRange).trim();
525
const decoRange = range.setEndPosition(range.startLineNumber, Constants.MAX_SAFE_SMALL_INTEGER);
526
527
template.decorations.push(accessor.addDecoration(
528
decoRange,
529
makeStackFrameColumnDecoration(!hasCharactersBefore),
530
));
531
template.decorations.push(accessor.addDecoration(
532
decoRange,
533
TOP_STACK_FRAME_DECORATION,
534
));
535
});
536
537
item.editorHeight.set(template.editor.getContentHeight(), undefined);
538
}
539
}
540
541
interface IMissingTemplateData {
542
elements: ReturnType<typeof makeFrameElements>;
543
label: ResourceLabel;
544
}
545
546
/** Renderer for a call frame that's missing a URI */
547
class MissingCodeRenderer implements IListRenderer<ListItem, IMissingTemplateData> {
548
public static readonly templateId = 'm';
549
public readonly templateId = MissingCodeRenderer.templateId;
550
551
constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) { }
552
553
renderTemplate(container: HTMLElement): IMissingTemplateData {
554
const elements = makeFrameElements();
555
elements.root.classList.add('missing');
556
container.appendChild(elements.root);
557
const label = this.instantiationService.createInstance(ResourceLabel, elements.title, {});
558
return { elements, label };
559
}
560
561
renderElement(element: ListItem, _index: number, templateData: IMissingTemplateData): void {
562
const cast = element as CallStackFrame;
563
templateData.label.element.setResource({
564
name: cast.name,
565
description: localize('stackFrameLocation', 'Line {0} column {1}', cast.line, cast.column),
566
range: { startLineNumber: cast.line, startColumn: cast.column, endColumn: cast.column, endLineNumber: cast.line },
567
}, {
568
icon: Codicon.fileBinary,
569
});
570
}
571
572
disposeTemplate(templateData: IMissingTemplateData): void {
573
templateData.label.dispose();
574
templateData.elements.root.remove();
575
}
576
}
577
578
/** Renderer for a call frame that's missing a URI */
579
class CustomRenderer extends AbstractFrameRenderer<IAbstractFrameRendererTemplateData> {
580
public static readonly templateId = 'c';
581
public readonly templateId = CustomRenderer.templateId;
582
583
protected override finishRenderTemplate(data: IAbstractFrameRendererTemplateData): IAbstractFrameRendererTemplateData {
584
return data;
585
}
586
587
override renderElement(element: ListItem, index: number, template: IAbstractFrameRendererTemplateData): void {
588
super.renderElement(element, index, template);
589
590
const item = element as WrappedCustomStackFrame;
591
const { elementStore, container, label } = template;
592
593
label.element.setResource({ name: item.original.label }, { icon: item.original.icon });
594
595
elementStore.add(autorun(reader => {
596
template.elements.header.style.display = item.original.showHeader.read(reader) ? '' : 'none';
597
}));
598
599
elementStore.add(autorunWithStore((reader, store) => {
600
if (!item.collapsed.read(reader)) {
601
store.add(item.original.render(container));
602
}
603
}));
604
605
const actions = item.original.renderActions?.(template.elements.actions);
606
if (actions) {
607
elementStore.add(actions);
608
}
609
}
610
}
611
612
interface ISkippedTemplateData {
613
button: Button;
614
current?: SkippedCallFrames;
615
store: DisposableStore;
616
}
617
618
/** Renderer for a button to load more call frames */
619
class SkippedRenderer implements IListRenderer<ListItem, ISkippedTemplateData> {
620
public static readonly templateId = 's';
621
public readonly templateId = SkippedRenderer.templateId;
622
623
constructor(
624
private readonly loadFrames: (fromItem: SkippedCallFrames) => Promise<void>,
625
@INotificationService private readonly notificationService: INotificationService,
626
) { }
627
628
renderTemplate(container: HTMLElement): ISkippedTemplateData {
629
const store = new DisposableStore();
630
const button = new Button(container, { title: '', ...defaultButtonStyles });
631
const data: ISkippedTemplateData = { button, store };
632
633
store.add(button);
634
store.add(button.onDidClick(() => {
635
if (!data.current || !button.enabled) {
636
return;
637
}
638
639
button.enabled = false;
640
this.loadFrames(data.current).catch(e => {
641
this.notificationService.error(localize('failedToLoadFrames', 'Failed to load stack frames: {0}', e.message));
642
});
643
}));
644
645
return data;
646
}
647
648
renderElement(element: ListItem, index: number, templateData: ISkippedTemplateData): void {
649
const cast = element as SkippedCallFrames;
650
templateData.button.enabled = true;
651
templateData.button.label = cast.label;
652
templateData.current = cast;
653
}
654
655
disposeTemplate(templateData: ISkippedTemplateData): void {
656
templateData.store.dispose();
657
}
658
}
659
660
/** A simple contribution that makes all data in the editor clickable to go to the location */
661
class ClickToLocationContribution extends Disposable implements IEditorContribution {
662
public static readonly ID = 'clickToLocation';
663
private readonly linkDecorations: IEditorDecorationsCollection;
664
private current: { line: number; word: IWordAtPosition } | undefined;
665
666
constructor(
667
private readonly editor: ICodeEditor,
668
@IEditorService editorService: IEditorService,
669
) {
670
super();
671
this.linkDecorations = editor.createDecorationsCollection();
672
this._register(toDisposable(() => this.linkDecorations.clear()));
673
674
const clickLinkGesture = this._register(new ClickLinkGesture(editor));
675
676
this._register(clickLinkGesture.onMouseMoveOrRelevantKeyDown(([mouseEvent, keyboardEvent]) => {
677
this.onMove(mouseEvent);
678
}));
679
this._register(clickLinkGesture.onExecute((e) => {
680
const model = this.editor.getModel();
681
if (!this.current || !model) {
682
return;
683
}
684
685
editorService.openEditor({
686
resource: model.uri,
687
options: {
688
selection: Range.fromPositions(new Position(this.current.line, this.current.word.startColumn)),
689
selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport,
690
},
691
}, e.hasSideBySideModifier ? SIDE_GROUP : undefined);
692
}));
693
}
694
695
private onMove(mouseEvent: ClickLinkMouseEvent) {
696
if (!mouseEvent.hasTriggerModifier) {
697
return this.clear();
698
}
699
700
const position = mouseEvent.target.position;
701
const word = position && this.editor.getModel()?.getWordAtPosition(position);
702
if (!word) {
703
return this.clear();
704
}
705
706
const prev = this.current?.word;
707
if (prev && prev.startColumn === word.startColumn && prev.endColumn === word.endColumn && prev.word === word.word) {
708
return;
709
}
710
711
this.current = { word, line: position.lineNumber };
712
this.linkDecorations.set([{
713
range: new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn),
714
options: {
715
description: 'call-stack-go-to-file-link',
716
inlineClassName: 'call-stack-go-to-file-link',
717
},
718
}]);
719
}
720
721
private clear() {
722
this.linkDecorations.clear();
723
this.current = undefined;
724
}
725
}
726
727
registerAction2(class extends Action2 {
728
constructor() {
729
super({
730
id: 'callStackWidget.goToFile',
731
title: localize2('goToFile', 'Open File'),
732
icon: Codicon.goToFile,
733
menu: {
734
id: MenuId.DebugCallStackToolbar,
735
order: 22,
736
group: 'navigation',
737
},
738
});
739
}
740
741
async run(accessor: ServicesAccessor, { uri, range }: Location): Promise<void> {
742
const editorService = accessor.get(IEditorService);
743
await editorService.openEditor({
744
resource: uri,
745
options: {
746
selection: range,
747
selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport,
748
},
749
});
750
}
751
});
752
753