Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/debug/browser/disassemblyView.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 { PixelRatio } from '../../../../base/browser/pixelRatio.js';
7
import { $, Dimension, addStandardDisposableListener, append } from '../../../../base/browser/dom.js';
8
import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js';
9
import { ITableContextMenuEvent, ITableRenderer, ITableVirtualDelegate } from '../../../../base/browser/ui/table/table.js';
10
import { binarySearch2 } from '../../../../base/common/arrays.js';
11
import { Color } from '../../../../base/common/color.js';
12
import { Emitter } from '../../../../base/common/event.js';
13
import { Disposable, IDisposable, dispose } from '../../../../base/common/lifecycle.js';
14
import { isAbsolute } from '../../../../base/common/path.js';
15
import { Constants } from '../../../../base/common/uint.js';
16
import { URI } from '../../../../base/common/uri.js';
17
import { applyFontInfo } from '../../../../editor/browser/config/domFontInfo.js';
18
import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
19
import { BareFontInfo } from '../../../../editor/common/config/fontInfo.js';
20
import { IRange, Range } from '../../../../editor/common/core/range.js';
21
import { StringBuilder } from '../../../../editor/common/core/stringBuilder.js';
22
import { ITextModel } from '../../../../editor/common/model.js';
23
import { ITextModelService } from '../../../../editor/common/services/resolverService.js';
24
import { localize } from '../../../../nls.js';
25
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
26
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
27
import { TextEditorSelectionRevealType } from '../../../../platform/editor/common/editor.js';
28
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
29
import { WorkbenchTable } from '../../../../platform/list/browser/listService.js';
30
import { ILogService } from '../../../../platform/log/common/log.js';
31
import { IStorageService } from '../../../../platform/storage/common/storage.js';
32
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
33
import { editorBackground } from '../../../../platform/theme/common/colorRegistry.js';
34
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
35
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
36
import { EditorPane } from '../../../browser/parts/editor/editorPane.js';
37
import { IWorkbenchContribution } from '../../../common/contributions.js';
38
import { focusedStackFrameColor, topStackFrameColor } from './callStackEditorContribution.js';
39
import * as icons from './debugIcons.js';
40
import { CONTEXT_LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST, DISASSEMBLY_VIEW_ID, IDebugConfiguration, IDebugService, IDebugSession, IInstructionBreakpoint, State } from '../common/debug.js';
41
import { InstructionBreakpoint } from '../common/debugModel.js';
42
import { getUriFromSource } from '../common/debugSource.js';
43
import { isUri, sourcesEqual } from '../common/debugUtils.js';
44
import { IEditorService } from '../../../services/editor/common/editorService.js';
45
import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js';
46
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
47
import { IMenu, IMenuService, MenuId } from '../../../../platform/actions/common/actions.js';
48
import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';
49
import { COPY_ADDRESS_ID, COPY_ADDRESS_LABEL } from '../../../../workbench/contrib/debug/browser/debugCommands.js';
50
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
51
import { getFlatContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
52
53
export interface IDisassembledInstructionEntry {
54
allowBreakpoint: boolean;
55
isBreakpointSet: boolean;
56
isBreakpointEnabled: boolean;
57
/** Instruction reference from the DA */
58
instructionReference: string;
59
/** Offset from the instructionReference that's the basis for the `instructionOffset` */
60
instructionReferenceOffset: number;
61
/** The number of instructions (+/-) away from the instructionReference and instructionReferenceOffset this instruction lies */
62
instructionOffset: number;
63
/** Whether this is the first instruction on the target line. */
64
showSourceLocation?: boolean;
65
/** Original instruction from the debugger */
66
instruction: DebugProtocol.DisassembledInstruction;
67
/** Parsed instruction address */
68
address: bigint;
69
}
70
71
// Special entry as a placeholer when disassembly is not available
72
const disassemblyNotAvailable: IDisassembledInstructionEntry = {
73
allowBreakpoint: false,
74
isBreakpointSet: false,
75
isBreakpointEnabled: false,
76
instructionReference: '',
77
instructionOffset: 0,
78
instructionReferenceOffset: 0,
79
address: 0n,
80
instruction: {
81
address: '-1',
82
instruction: localize('instructionNotAvailable', "Disassembly not available.")
83
},
84
};
85
86
export class DisassemblyView extends EditorPane {
87
88
private static readonly NUM_INSTRUCTIONS_TO_LOAD = 50;
89
90
// Used in instruction renderer
91
private _fontInfo: BareFontInfo | undefined;
92
private _disassembledInstructions: WorkbenchTable<IDisassembledInstructionEntry> | undefined;
93
private _onDidChangeStackFrame: Emitter<void>;
94
private _previousDebuggingState: State;
95
private _instructionBpList: readonly IInstructionBreakpoint[] = [];
96
private _enableSourceCodeRender: boolean = true;
97
private _loadingLock: boolean = false;
98
private readonly _referenceToMemoryAddress = new Map<string, bigint>();
99
private menu: IMenu;
100
101
constructor(
102
group: IEditorGroup,
103
@ITelemetryService telemetryService: ITelemetryService,
104
@IThemeService themeService: IThemeService,
105
@IStorageService storageService: IStorageService,
106
@IConfigurationService private readonly _configurationService: IConfigurationService,
107
@IInstantiationService private readonly _instantiationService: IInstantiationService,
108
@IDebugService private readonly _debugService: IDebugService,
109
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
110
@IMenuService menuService: IMenuService,
111
@IContextKeyService contextKeyService: IContextKeyService,
112
) {
113
super(DISASSEMBLY_VIEW_ID, group, telemetryService, themeService, storageService);
114
115
this.menu = menuService.createMenu(MenuId.DebugDisassemblyContext, contextKeyService);
116
this._register(this.menu);
117
this._disassembledInstructions = undefined;
118
this._onDidChangeStackFrame = this._register(new Emitter<void>({ leakWarningThreshold: 1000 }));
119
this._previousDebuggingState = _debugService.state;
120
this._register(_configurationService.onDidChangeConfiguration(e => {
121
if (e.affectsConfiguration('debug')) {
122
// show/hide source code requires changing height which WorkbenchTable doesn't support dynamic height, thus force a total reload.
123
const newValue = this._configurationService.getValue<IDebugConfiguration>('debug').disassemblyView.showSourceCode;
124
if (this._enableSourceCodeRender !== newValue) {
125
this._enableSourceCodeRender = newValue;
126
// todo: trigger rerender
127
} else {
128
this._disassembledInstructions?.rerender();
129
}
130
}
131
}));
132
}
133
134
get fontInfo() {
135
if (!this._fontInfo) {
136
this._fontInfo = this.createFontInfo();
137
138
this._register(this._configurationService.onDidChangeConfiguration(e => {
139
if (e.affectsConfiguration('editor')) {
140
this._fontInfo = this.createFontInfo();
141
}
142
}));
143
}
144
145
return this._fontInfo;
146
}
147
148
private createFontInfo() {
149
return BareFontInfo.createFromRawSettings(this._configurationService.getValue('editor'), PixelRatio.getInstance(this.window).value);
150
}
151
152
get currentInstructionAddresses() {
153
return this._debugService.getModel().getSessions(false).
154
map(session => session.getAllThreads()).
155
reduce((prev, curr) => prev.concat(curr), []).
156
map(thread => thread.getTopStackFrame()).
157
map(frame => frame?.instructionPointerReference).
158
map(ref => ref ? this.getReferenceAddress(ref) : undefined);
159
}
160
161
// Instruction reference of the top stack frame of the focused stack
162
get focusedCurrentInstructionReference() {
163
return this._debugService.getViewModel().focusedStackFrame?.thread.getTopStackFrame()?.instructionPointerReference;
164
}
165
166
get focusedCurrentInstructionAddress() {
167
const ref = this.focusedCurrentInstructionReference;
168
return ref ? this.getReferenceAddress(ref) : undefined;
169
}
170
171
get focusedInstructionReference() {
172
return this._debugService.getViewModel().focusedStackFrame?.instructionPointerReference;
173
}
174
175
get focusedInstructionAddress() {
176
const ref = this.focusedInstructionReference;
177
return ref ? this.getReferenceAddress(ref) : undefined;
178
}
179
180
get isSourceCodeRender() { return this._enableSourceCodeRender; }
181
182
get debugSession(): IDebugSession | undefined {
183
return this._debugService.getViewModel().focusedSession;
184
}
185
186
get onDidChangeStackFrame() { return this._onDidChangeStackFrame.event; }
187
188
get focusedAddressAndOffset() {
189
const element = this._disassembledInstructions?.getFocusedElements()[0];
190
if (!element) {
191
return undefined;
192
}
193
194
return this.getAddressAndOffset(element);
195
}
196
197
getAddressAndOffset(element: IDisassembledInstructionEntry) {
198
const reference = element.instructionReference;
199
const offset = Number(element.address - this.getReferenceAddress(reference)!);
200
return { reference, offset, address: element.address };
201
}
202
203
protected createEditor(parent: HTMLElement): void {
204
this._enableSourceCodeRender = this._configurationService.getValue<IDebugConfiguration>('debug').disassemblyView.showSourceCode;
205
const lineHeight = this.fontInfo.lineHeight;
206
const thisOM = this;
207
const delegate = new class implements ITableVirtualDelegate<IDisassembledInstructionEntry> {
208
headerRowHeight: number = 0; // No header
209
getHeight(row: IDisassembledInstructionEntry): number {
210
if (thisOM.isSourceCodeRender && row.showSourceLocation && row.instruction.location?.path && row.instruction.line) {
211
// instruction line + source lines
212
if (row.instruction.endLine) {
213
return lineHeight * Math.max(2, (row.instruction.endLine - row.instruction.line + 2));
214
} else {
215
// source is only a single line.
216
return lineHeight * 2;
217
}
218
}
219
220
// just instruction line
221
return lineHeight;
222
}
223
};
224
225
const instructionRenderer = this._register(this._instantiationService.createInstance(InstructionRenderer, this));
226
227
this._disassembledInstructions = this._register(this._instantiationService.createInstance(WorkbenchTable,
228
'DisassemblyView', parent, delegate,
229
[
230
{
231
label: '',
232
tooltip: '',
233
weight: 0,
234
minimumWidth: this.fontInfo.lineHeight,
235
maximumWidth: this.fontInfo.lineHeight,
236
templateId: BreakpointRenderer.TEMPLATE_ID,
237
project(row: IDisassembledInstructionEntry): IDisassembledInstructionEntry { return row; }
238
},
239
{
240
label: localize('disassemblyTableColumnLabel', "instructions"),
241
tooltip: '',
242
weight: 0.3,
243
templateId: InstructionRenderer.TEMPLATE_ID,
244
project(row: IDisassembledInstructionEntry): IDisassembledInstructionEntry { return row; }
245
},
246
],
247
[
248
this._instantiationService.createInstance(BreakpointRenderer, this),
249
instructionRenderer,
250
],
251
{
252
identityProvider: { getId: (e: IDisassembledInstructionEntry) => e.instruction.address },
253
horizontalScrolling: false,
254
overrideStyles: {
255
listBackground: editorBackground
256
},
257
multipleSelectionSupport: false,
258
setRowLineHeight: false,
259
openOnSingleClick: false,
260
accessibilityProvider: new AccessibilityProvider(),
261
mouseSupport: false
262
}
263
)) as WorkbenchTable<IDisassembledInstructionEntry>;
264
265
this._disassembledInstructions.domNode.classList.add('disassembly-view');
266
267
if (this.focusedInstructionReference) {
268
this.reloadDisassembly(this.focusedInstructionReference, 0);
269
}
270
271
this._register(this._disassembledInstructions.onDidScroll(e => {
272
if (this._loadingLock) {
273
return;
274
}
275
276
if (e.oldScrollTop > e.scrollTop && e.scrollTop < e.height) {
277
this._loadingLock = true;
278
const prevTop = Math.floor(e.scrollTop / this.fontInfo.lineHeight);
279
this.scrollUp_LoadDisassembledInstructions(DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD).then((loaded) => {
280
if (loaded > 0) {
281
this._disassembledInstructions!.reveal(prevTop + loaded, 0);
282
}
283
this._loadingLock = false;
284
});
285
} else if (e.oldScrollTop < e.scrollTop && e.scrollTop + e.height > e.scrollHeight - e.height) {
286
this._loadingLock = true;
287
this.scrollDown_LoadDisassembledInstructions(DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD).then(() => { this._loadingLock = false; });
288
}
289
}));
290
291
this._register(this._disassembledInstructions.onContextMenu(e => this.onContextMenu(e)));
292
293
this._register(this._debugService.getViewModel().onDidFocusStackFrame(({ stackFrame }) => {
294
if (this._disassembledInstructions && stackFrame?.instructionPointerReference) {
295
this.goToInstructionAndOffset(stackFrame.instructionPointerReference, 0);
296
}
297
this._onDidChangeStackFrame.fire();
298
}));
299
300
// refresh breakpoints view
301
this._register(this._debugService.getModel().onDidChangeBreakpoints(bpEvent => {
302
if (bpEvent && this._disassembledInstructions) {
303
// draw viewable BP
304
let changed = false;
305
bpEvent.added?.forEach((bp) => {
306
if (bp instanceof InstructionBreakpoint) {
307
const index = this.getIndexFromReferenceAndOffset(bp.instructionReference, bp.offset);
308
if (index >= 0) {
309
this._disassembledInstructions!.row(index).isBreakpointSet = true;
310
this._disassembledInstructions!.row(index).isBreakpointEnabled = bp.enabled;
311
changed = true;
312
}
313
}
314
});
315
316
bpEvent.removed?.forEach((bp) => {
317
if (bp instanceof InstructionBreakpoint) {
318
const index = this.getIndexFromReferenceAndOffset(bp.instructionReference, bp.offset);
319
if (index >= 0) {
320
this._disassembledInstructions!.row(index).isBreakpointSet = false;
321
changed = true;
322
}
323
}
324
});
325
326
bpEvent.changed?.forEach((bp) => {
327
if (bp instanceof InstructionBreakpoint) {
328
const index = this.getIndexFromReferenceAndOffset(bp.instructionReference, bp.offset);
329
if (index >= 0) {
330
if (this._disassembledInstructions!.row(index).isBreakpointEnabled !== bp.enabled) {
331
this._disassembledInstructions!.row(index).isBreakpointEnabled = bp.enabled;
332
changed = true;
333
}
334
}
335
}
336
});
337
338
// get an updated list so that items beyond the current range would render when reached.
339
this._instructionBpList = this._debugService.getModel().getInstructionBreakpoints();
340
341
// breakpoints restored from a previous session can be based on memory
342
// references that may no longer exist in the current session. Request
343
// those instructions to be loaded so the BP can be displayed.
344
for (const bp of this._instructionBpList) {
345
this.primeMemoryReference(bp.instructionReference);
346
}
347
348
if (changed) {
349
this._onDidChangeStackFrame.fire();
350
}
351
}
352
}));
353
354
this._register(this._debugService.onDidChangeState(e => {
355
if ((e === State.Running || e === State.Stopped) &&
356
(this._previousDebuggingState !== State.Running && this._previousDebuggingState !== State.Stopped)) {
357
// Just started debugging, clear the view
358
this.clear();
359
this._enableSourceCodeRender = this._configurationService.getValue<IDebugConfiguration>('debug').disassemblyView.showSourceCode;
360
}
361
362
this._previousDebuggingState = e;
363
this._onDidChangeStackFrame.fire();
364
}));
365
}
366
367
layout(dimension: Dimension): void {
368
this._disassembledInstructions?.layout(dimension.height);
369
}
370
371
async goToInstructionAndOffset(instructionReference: string, offset: number, focus?: boolean) {
372
let addr = this._referenceToMemoryAddress.get(instructionReference);
373
if (addr === undefined) {
374
await this.loadDisassembledInstructions(instructionReference, 0, -DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD, DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD * 2);
375
addr = this._referenceToMemoryAddress.get(instructionReference);
376
}
377
378
if (addr) {
379
this.goToAddress(addr + BigInt(offset), focus);
380
}
381
}
382
383
/** Gets the address associated with the instruction reference. */
384
getReferenceAddress(instructionReference: string) {
385
return this._referenceToMemoryAddress.get(instructionReference);
386
}
387
388
/**
389
* Go to the address provided. If no address is provided, reveal the address of the currently focused stack frame. Returns false if that address is not available.
390
*/
391
private goToAddress(address: bigint, focus?: boolean): boolean {
392
if (!this._disassembledInstructions) {
393
return false;
394
}
395
396
if (!address) {
397
return false;
398
}
399
400
const index = this.getIndexFromAddress(address);
401
if (index >= 0) {
402
this._disassembledInstructions.reveal(index);
403
404
if (focus) {
405
this._disassembledInstructions.domFocus();
406
this._disassembledInstructions.setFocus([index]);
407
}
408
return true;
409
}
410
411
return false;
412
}
413
414
private async scrollUp_LoadDisassembledInstructions(instructionCount: number): Promise<number> {
415
const first = this._disassembledInstructions?.row(0);
416
if (first) {
417
return this.loadDisassembledInstructions(
418
first.instructionReference,
419
first.instructionReferenceOffset,
420
first.instructionOffset - instructionCount,
421
instructionCount,
422
);
423
}
424
425
return 0;
426
}
427
428
private async scrollDown_LoadDisassembledInstructions(instructionCount: number): Promise<number> {
429
const last = this._disassembledInstructions?.row(this._disassembledInstructions?.length - 1);
430
if (last) {
431
return this.loadDisassembledInstructions(
432
last.instructionReference,
433
last.instructionReferenceOffset,
434
last.instructionOffset + 1,
435
instructionCount,
436
);
437
}
438
439
return 0;
440
}
441
442
/**
443
* Sets the memory reference address. We don't just loadDisassembledInstructions
444
* for this, since we can't really deal with discontiguous ranges (we can't
445
* detect _if_ a range is discontiguous since we don't know how much memory
446
* comes between instructions.)
447
*/
448
private async primeMemoryReference(instructionReference: string) {
449
if (this._referenceToMemoryAddress.has(instructionReference)) {
450
return true;
451
}
452
453
const s = await this.debugSession?.disassemble(instructionReference, 0, 0, 1);
454
if (s && s.length > 0) {
455
try {
456
this._referenceToMemoryAddress.set(instructionReference, BigInt(s[0].address));
457
return true;
458
} catch {
459
return false;
460
}
461
}
462
463
return false;
464
}
465
466
/** Loads disasembled instructions. Returns the number of instructions that were loaded. */
467
private async loadDisassembledInstructions(instructionReference: string, offset: number, instructionOffset: number, instructionCount: number): Promise<number> {
468
const session = this.debugSession;
469
const resultEntries = await session?.disassemble(instructionReference, offset, instructionOffset, instructionCount);
470
471
// Ensure we always load the baseline instructions so we know what address the instructionReference refers to.
472
if (!this._referenceToMemoryAddress.has(instructionReference) && instructionOffset !== 0) {
473
await this.loadDisassembledInstructions(instructionReference, 0, 0, DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD);
474
}
475
476
if (session && resultEntries && this._disassembledInstructions) {
477
const newEntries: IDisassembledInstructionEntry[] = [];
478
479
let lastLocation: DebugProtocol.Source | undefined;
480
let lastLine: IRange | undefined;
481
for (let i = 0; i < resultEntries.length; i++) {
482
const instruction = resultEntries[i];
483
const thisInstructionOffset = instructionOffset + i;
484
485
// Forward fill the missing location as detailed in the DAP spec.
486
if (instruction.location) {
487
lastLocation = instruction.location;
488
lastLine = undefined;
489
}
490
491
if (instruction.line) {
492
const currentLine: IRange = {
493
startLineNumber: instruction.line,
494
startColumn: instruction.column ?? 0,
495
endLineNumber: instruction.endLine ?? instruction.line,
496
endColumn: instruction.endColumn ?? 0,
497
};
498
499
// Add location only to the first unique range. This will give the appearance of grouping of instructions.
500
if (!Range.equalsRange(currentLine, lastLine ?? null)) {
501
lastLine = currentLine;
502
instruction.location = lastLocation;
503
}
504
}
505
506
let address: bigint;
507
try {
508
address = BigInt(instruction.address);
509
} catch {
510
console.error(`Could not parse disassembly address ${instruction.address} (in ${JSON.stringify(instruction)})`);
511
continue;
512
}
513
514
if (address === -1n) {
515
// Ignore invalid instructions returned by the adapter.
516
continue;
517
}
518
519
const entry: IDisassembledInstructionEntry = {
520
allowBreakpoint: true,
521
isBreakpointSet: false,
522
isBreakpointEnabled: false,
523
instructionReference,
524
instructionReferenceOffset: offset,
525
instructionOffset: thisInstructionOffset,
526
instruction,
527
address,
528
};
529
530
newEntries.push(entry);
531
532
// if we just loaded the first instruction for this reference, mark its address.
533
if (offset === 0 && thisInstructionOffset === 0) {
534
this._referenceToMemoryAddress.set(instructionReference, address);
535
}
536
}
537
538
if (newEntries.length === 0) {
539
return 0;
540
}
541
542
const refBaseAddress = this._referenceToMemoryAddress.get(instructionReference);
543
const bps = this._instructionBpList.map(p => {
544
const base = this._referenceToMemoryAddress.get(p.instructionReference);
545
if (!base) {
546
return undefined;
547
}
548
return {
549
enabled: p.enabled,
550
address: base + BigInt(p.offset || 0),
551
};
552
});
553
554
if (refBaseAddress !== undefined) {
555
for (const entry of newEntries) {
556
const bp = bps.find(p => p?.address === entry.address);
557
if (bp) {
558
entry.isBreakpointSet = true;
559
entry.isBreakpointEnabled = bp.enabled;
560
}
561
}
562
}
563
564
const da = this._disassembledInstructions;
565
if (da.length === 1 && this._disassembledInstructions.row(0) === disassemblyNotAvailable) {
566
da.splice(0, 1);
567
}
568
569
const firstAddr = newEntries[0].address;
570
const lastAddr = newEntries[newEntries.length - 1].address;
571
572
const startN = binarySearch2(da.length, i => Number(da.row(i).address - firstAddr));
573
const start = startN < 0 ? ~startN : startN;
574
const endN = binarySearch2(da.length, i => Number(da.row(i).address - lastAddr));
575
const end = endN < 0 ? ~endN : endN + 1;
576
const toDelete = end - start;
577
578
// Go through everything we're about to add, and only show the source
579
// location if it's different from the previous one, "grouping" instructions by line
580
let lastLocated: undefined | DebugProtocol.DisassembledInstruction;
581
for (let i = start - 1; i >= 0; i--) {
582
const { instruction } = da.row(i);
583
if (instruction.location && instruction.line !== undefined) {
584
lastLocated = instruction;
585
break;
586
}
587
}
588
589
const shouldShowLocation = (instruction: DebugProtocol.DisassembledInstruction) =>
590
instruction.line !== undefined && instruction.location !== undefined &&
591
(!lastLocated || !sourcesEqual(instruction.location, lastLocated.location) || instruction.line !== lastLocated.line);
592
593
for (const entry of newEntries) {
594
if (shouldShowLocation(entry.instruction)) {
595
entry.showSourceLocation = true;
596
lastLocated = entry.instruction;
597
}
598
}
599
600
da.splice(start, toDelete, newEntries);
601
602
return newEntries.length - toDelete;
603
}
604
605
return 0;
606
}
607
608
private getIndexFromReferenceAndOffset(instructionReference: string, offset: number): number {
609
const addr = this._referenceToMemoryAddress.get(instructionReference);
610
if (addr === undefined) {
611
return -1;
612
}
613
614
return this.getIndexFromAddress(addr + BigInt(offset));
615
}
616
617
private getIndexFromAddress(address: bigint): number {
618
const disassembledInstructions = this._disassembledInstructions;
619
if (disassembledInstructions && disassembledInstructions.length > 0) {
620
return binarySearch2(disassembledInstructions.length, index => {
621
const row = disassembledInstructions.row(index);
622
return Number(row.address - address);
623
});
624
}
625
626
return -1;
627
}
628
629
/**
630
* Clears the table and reload instructions near the target address
631
*/
632
private reloadDisassembly(instructionReference: string, offset: number) {
633
if (!this._disassembledInstructions) {
634
return;
635
}
636
637
this._loadingLock = true; // stop scrolling during the load.
638
this.clear();
639
this._instructionBpList = this._debugService.getModel().getInstructionBreakpoints();
640
this.loadDisassembledInstructions(instructionReference, offset, -DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD * 4, DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD * 8).then(() => {
641
// on load, set the target instruction in the middle of the page.
642
if (this._disassembledInstructions!.length > 0) {
643
const targetIndex = Math.floor(this._disassembledInstructions!.length / 2);
644
this._disassembledInstructions!.reveal(targetIndex, 0.5);
645
646
// Always focus the target address on reload, or arrow key navigation would look terrible
647
this._disassembledInstructions!.domFocus();
648
this._disassembledInstructions!.setFocus([targetIndex]);
649
}
650
this._loadingLock = false;
651
});
652
}
653
654
private clear() {
655
this._referenceToMemoryAddress.clear();
656
this._disassembledInstructions?.splice(0, this._disassembledInstructions.length, [disassemblyNotAvailable]);
657
}
658
659
private onContextMenu(e: ITableContextMenuEvent<IDisassembledInstructionEntry>): void {
660
const actions = getFlatContextMenuActions(this.menu.getActions({ shouldForwardArgs: true }));
661
this._contextMenuService.showContextMenu({
662
getAnchor: () => e.anchor,
663
getActions: () => actions,
664
getActionsContext: () => e.element
665
});
666
}
667
}
668
669
interface IBreakpointColumnTemplateData {
670
currentElement: { element?: IDisassembledInstructionEntry };
671
icon: HTMLElement;
672
disposables: IDisposable[];
673
}
674
675
class BreakpointRenderer implements ITableRenderer<IDisassembledInstructionEntry, IBreakpointColumnTemplateData> {
676
677
static readonly TEMPLATE_ID = 'breakpoint';
678
679
templateId: string = BreakpointRenderer.TEMPLATE_ID;
680
681
private readonly _breakpointIcon = 'codicon-' + icons.breakpoint.regular.id;
682
private readonly _breakpointDisabledIcon = 'codicon-' + icons.breakpoint.disabled.id;
683
private readonly _breakpointHintIcon = 'codicon-' + icons.debugBreakpointHint.id;
684
private readonly _debugStackframe = 'codicon-' + icons.debugStackframe.id;
685
private readonly _debugStackframeFocused = 'codicon-' + icons.debugStackframeFocused.id;
686
687
constructor(
688
private readonly _disassemblyView: DisassemblyView,
689
@IDebugService private readonly _debugService: IDebugService
690
) {
691
}
692
693
renderTemplate(container: HTMLElement): IBreakpointColumnTemplateData {
694
// align from the bottom so that it lines up with instruction when source code is present.
695
container.style.alignSelf = 'flex-end';
696
697
const icon = append(container, $('.codicon'));
698
icon.style.display = 'flex';
699
icon.style.alignItems = 'center';
700
icon.style.justifyContent = 'center';
701
icon.style.height = this._disassemblyView.fontInfo.lineHeight + 'px';
702
703
const currentElement: { element?: IDisassembledInstructionEntry } = { element: undefined };
704
705
const disposables = [
706
this._disassemblyView.onDidChangeStackFrame(() => this.rerenderDebugStackframe(icon, currentElement.element)),
707
addStandardDisposableListener(container, 'mouseover', () => {
708
if (currentElement.element?.allowBreakpoint) {
709
icon.classList.add(this._breakpointHintIcon);
710
}
711
}),
712
addStandardDisposableListener(container, 'mouseout', () => {
713
if (currentElement.element?.allowBreakpoint) {
714
icon.classList.remove(this._breakpointHintIcon);
715
}
716
}),
717
addStandardDisposableListener(container, 'click', () => {
718
if (currentElement.element?.allowBreakpoint) {
719
// click show hint while waiting for BP to resolve.
720
icon.classList.add(this._breakpointHintIcon);
721
const reference = currentElement.element.instructionReference;
722
const offset = Number(currentElement.element.address - this._disassemblyView.getReferenceAddress(reference)!);
723
if (currentElement.element.isBreakpointSet) {
724
this._debugService.removeInstructionBreakpoints(reference, offset);
725
} else if (currentElement.element.allowBreakpoint && !currentElement.element.isBreakpointSet) {
726
this._debugService.addInstructionBreakpoint({ instructionReference: reference, offset, address: currentElement.element.address, canPersist: false });
727
}
728
}
729
})
730
];
731
732
return { currentElement, icon, disposables };
733
}
734
735
renderElement(element: IDisassembledInstructionEntry, index: number, templateData: IBreakpointColumnTemplateData): void {
736
templateData.currentElement.element = element;
737
this.rerenderDebugStackframe(templateData.icon, element);
738
}
739
740
disposeTemplate(templateData: IBreakpointColumnTemplateData): void {
741
dispose(templateData.disposables);
742
templateData.disposables = [];
743
}
744
745
private rerenderDebugStackframe(icon: HTMLElement, element?: IDisassembledInstructionEntry) {
746
if (element?.address === this._disassemblyView.focusedCurrentInstructionAddress) {
747
icon.classList.add(this._debugStackframe);
748
} else if (element?.address === this._disassemblyView.focusedInstructionAddress) {
749
icon.classList.add(this._debugStackframeFocused);
750
} else {
751
icon.classList.remove(this._debugStackframe);
752
icon.classList.remove(this._debugStackframeFocused);
753
}
754
755
icon.classList.remove(this._breakpointHintIcon);
756
757
if (element?.isBreakpointSet) {
758
if (element.isBreakpointEnabled) {
759
icon.classList.add(this._breakpointIcon);
760
icon.classList.remove(this._breakpointDisabledIcon);
761
} else {
762
icon.classList.remove(this._breakpointIcon);
763
icon.classList.add(this._breakpointDisabledIcon);
764
}
765
} else {
766
icon.classList.remove(this._breakpointIcon);
767
icon.classList.remove(this._breakpointDisabledIcon);
768
}
769
}
770
}
771
772
interface IInstructionColumnTemplateData {
773
currentElement: { element?: IDisassembledInstructionEntry };
774
// TODO: hover widget?
775
instruction: HTMLElement;
776
sourcecode: HTMLElement;
777
// disposed when cell is closed.
778
cellDisposable: IDisposable[];
779
// disposed when template is closed.
780
disposables: IDisposable[];
781
}
782
783
class InstructionRenderer extends Disposable implements ITableRenderer<IDisassembledInstructionEntry, IInstructionColumnTemplateData> {
784
785
static readonly TEMPLATE_ID = 'instruction';
786
787
private static readonly INSTRUCTION_ADDR_MIN_LENGTH = 25;
788
private static readonly INSTRUCTION_BYTES_MIN_LENGTH = 30;
789
790
templateId: string = InstructionRenderer.TEMPLATE_ID;
791
792
private _topStackFrameColor: Color | undefined;
793
private _focusedStackFrameColor: Color | undefined;
794
795
constructor(
796
private readonly _disassemblyView: DisassemblyView,
797
@IThemeService themeService: IThemeService,
798
@IEditorService private readonly editorService: IEditorService,
799
@ITextModelService private readonly textModelService: ITextModelService,
800
@IUriIdentityService private readonly uriService: IUriIdentityService,
801
@ILogService private readonly logService: ILogService,
802
) {
803
super();
804
805
this._topStackFrameColor = themeService.getColorTheme().getColor(topStackFrameColor);
806
this._focusedStackFrameColor = themeService.getColorTheme().getColor(focusedStackFrameColor);
807
808
this._register(themeService.onDidColorThemeChange(e => {
809
this._topStackFrameColor = e.getColor(topStackFrameColor);
810
this._focusedStackFrameColor = e.getColor(focusedStackFrameColor);
811
}));
812
}
813
814
renderTemplate(container: HTMLElement): IInstructionColumnTemplateData {
815
const sourcecode = append(container, $('.sourcecode'));
816
const instruction = append(container, $('.instruction'));
817
this.applyFontInfo(sourcecode);
818
this.applyFontInfo(instruction);
819
const currentElement: { element?: IDisassembledInstructionEntry } = { element: undefined };
820
const cellDisposable: IDisposable[] = [];
821
822
const disposables = [
823
this._disassemblyView.onDidChangeStackFrame(() => this.rerenderBackground(instruction, sourcecode, currentElement.element)),
824
addStandardDisposableListener(sourcecode, 'dblclick', () => this.openSourceCode(currentElement.element?.instruction)),
825
];
826
827
return { currentElement, instruction, sourcecode, cellDisposable, disposables };
828
}
829
830
renderElement(element: IDisassembledInstructionEntry, index: number, templateData: IInstructionColumnTemplateData): void {
831
this.renderElementInner(element, index, templateData);
832
}
833
834
private async renderElementInner(element: IDisassembledInstructionEntry, index: number, templateData: IInstructionColumnTemplateData): Promise<void> {
835
templateData.currentElement.element = element;
836
const instruction = element.instruction;
837
templateData.sourcecode.innerText = '';
838
const sb = new StringBuilder(1000);
839
840
if (this._disassemblyView.isSourceCodeRender && element.showSourceLocation && instruction.location?.path && instruction.line !== undefined) {
841
const sourceURI = this.getUriFromSource(instruction);
842
843
if (sourceURI) {
844
let textModel: ITextModel | undefined = undefined;
845
const sourceSB = new StringBuilder(10000);
846
const ref = await this.textModelService.createModelReference(sourceURI);
847
if (templateData.currentElement.element !== element) {
848
return; // avoid a race, #192831
849
}
850
textModel = ref.object.textEditorModel;
851
templateData.cellDisposable.push(ref);
852
853
// templateData could have moved on during async. Double check if it is still the same source.
854
if (textModel && templateData.currentElement.element === element) {
855
let lineNumber = instruction.line;
856
857
while (lineNumber && lineNumber >= 1 && lineNumber <= textModel.getLineCount()) {
858
const lineContent = textModel.getLineContent(lineNumber);
859
sourceSB.appendString(` ${lineNumber}: `);
860
sourceSB.appendString(lineContent + '\n');
861
862
if (instruction.endLine && lineNumber < instruction.endLine) {
863
lineNumber++;
864
continue;
865
}
866
867
break;
868
}
869
870
templateData.sourcecode.innerText = sourceSB.build();
871
}
872
}
873
}
874
875
let spacesToAppend = 10;
876
877
if (instruction.address !== '-1') {
878
sb.appendString(instruction.address);
879
if (instruction.address.length < InstructionRenderer.INSTRUCTION_ADDR_MIN_LENGTH) {
880
spacesToAppend = InstructionRenderer.INSTRUCTION_ADDR_MIN_LENGTH - instruction.address.length;
881
}
882
for (let i = 0; i < spacesToAppend; i++) {
883
sb.appendString(' ');
884
}
885
}
886
887
if (instruction.instructionBytes) {
888
sb.appendString(instruction.instructionBytes);
889
spacesToAppend = 10;
890
if (instruction.instructionBytes.length < InstructionRenderer.INSTRUCTION_BYTES_MIN_LENGTH) {
891
spacesToAppend = InstructionRenderer.INSTRUCTION_BYTES_MIN_LENGTH - instruction.instructionBytes.length;
892
}
893
for (let i = 0; i < spacesToAppend; i++) {
894
sb.appendString(' ');
895
}
896
}
897
898
sb.appendString(instruction.instruction);
899
templateData.instruction.innerText = sb.build();
900
901
this.rerenderBackground(templateData.instruction, templateData.sourcecode, element);
902
}
903
904
disposeElement(element: IDisassembledInstructionEntry, index: number, templateData: IInstructionColumnTemplateData): void {
905
dispose(templateData.cellDisposable);
906
templateData.cellDisposable = [];
907
}
908
909
disposeTemplate(templateData: IInstructionColumnTemplateData): void {
910
dispose(templateData.disposables);
911
templateData.disposables = [];
912
}
913
914
private rerenderBackground(instruction: HTMLElement, sourceCode: HTMLElement, element?: IDisassembledInstructionEntry) {
915
if (element && this._disassemblyView.currentInstructionAddresses.includes(element.address)) {
916
instruction.style.background = this._topStackFrameColor?.toString() || 'transparent';
917
} else if (element?.address === this._disassemblyView.focusedInstructionAddress) {
918
instruction.style.background = this._focusedStackFrameColor?.toString() || 'transparent';
919
} else {
920
instruction.style.background = 'transparent';
921
}
922
}
923
924
private openSourceCode(instruction: DebugProtocol.DisassembledInstruction | undefined) {
925
if (instruction) {
926
const sourceURI = this.getUriFromSource(instruction);
927
const selection = instruction.endLine ? {
928
startLineNumber: instruction.line!,
929
endLineNumber: instruction.endLine,
930
startColumn: instruction.column || 1,
931
endColumn: instruction.endColumn || Constants.MAX_SAFE_SMALL_INTEGER,
932
} : {
933
startLineNumber: instruction.line!,
934
endLineNumber: instruction.line!,
935
startColumn: instruction.column || 1,
936
endColumn: instruction.endColumn || Constants.MAX_SAFE_SMALL_INTEGER,
937
};
938
939
this.editorService.openEditor({
940
resource: sourceURI,
941
description: localize('editorOpenedFromDisassemblyDescription', "from disassembly"),
942
options: {
943
preserveFocus: false,
944
selection: selection,
945
revealIfOpened: true,
946
selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport,
947
pinned: false,
948
}
949
});
950
}
951
}
952
953
private getUriFromSource(instruction: DebugProtocol.DisassembledInstruction): URI {
954
// Try to resolve path before consulting the debugSession.
955
const path = instruction.location!.path;
956
if (path && isUri(path)) { // path looks like a uri
957
return this.uriService.asCanonicalUri(URI.parse(path));
958
}
959
// assume a filesystem path
960
if (path && isAbsolute(path)) {
961
return this.uriService.asCanonicalUri(URI.file(path));
962
}
963
964
return getUriFromSource(instruction.location!, instruction.location!.path, this._disassemblyView.debugSession!.getId(), this.uriService, this.logService);
965
}
966
967
private applyFontInfo(element: HTMLElement) {
968
applyFontInfo(element, this._disassemblyView.fontInfo);
969
element.style.whiteSpace = 'pre';
970
}
971
}
972
973
class AccessibilityProvider implements IListAccessibilityProvider<IDisassembledInstructionEntry> {
974
975
getWidgetAriaLabel(): string {
976
return localize('disassemblyView', "Disassembly View");
977
}
978
979
getAriaLabel(element: IDisassembledInstructionEntry): string | null {
980
let label = '';
981
982
const instruction = element.instruction;
983
if (instruction.address !== '-1') {
984
label += `${localize('instructionAddress', "Address")}: ${instruction.address}`;
985
}
986
if (instruction.instructionBytes) {
987
label += `, ${localize('instructionBytes', "Bytes")}: ${instruction.instructionBytes}`;
988
}
989
label += `, ${localize(`instructionText`, "Instruction")}: ${instruction.instruction}`;
990
991
return label;
992
}
993
}
994
995
export class DisassemblyViewContribution implements IWorkbenchContribution {
996
997
private readonly _onDidActiveEditorChangeListener: IDisposable;
998
private _onDidChangeModelLanguage: IDisposable | undefined;
999
private _languageSupportsDisassembleRequest: IContextKey<boolean> | undefined;
1000
1001
constructor(
1002
@IEditorService editorService: IEditorService,
1003
@IDebugService debugService: IDebugService,
1004
@IContextKeyService contextKeyService: IContextKeyService
1005
) {
1006
contextKeyService.bufferChangeEvents(() => {
1007
this._languageSupportsDisassembleRequest = CONTEXT_LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST.bindTo(contextKeyService);
1008
});
1009
1010
const onDidActiveEditorChangeListener = () => {
1011
if (this._onDidChangeModelLanguage) {
1012
this._onDidChangeModelLanguage.dispose();
1013
this._onDidChangeModelLanguage = undefined;
1014
}
1015
1016
const activeTextEditorControl = editorService.activeTextEditorControl;
1017
if (isCodeEditor(activeTextEditorControl)) {
1018
const language = activeTextEditorControl.getModel()?.getLanguageId();
1019
// TODO: instead of using idDebuggerInterestedInLanguage, have a specific ext point for languages
1020
// support disassembly
1021
this._languageSupportsDisassembleRequest?.set(!!language && debugService.getAdapterManager().someDebuggerInterestedInLanguage(language));
1022
1023
this._onDidChangeModelLanguage = activeTextEditorControl.onDidChangeModelLanguage(e => {
1024
this._languageSupportsDisassembleRequest?.set(debugService.getAdapterManager().someDebuggerInterestedInLanguage(e.newLanguage));
1025
});
1026
} else {
1027
this._languageSupportsDisassembleRequest?.set(false);
1028
}
1029
};
1030
1031
onDidActiveEditorChangeListener();
1032
this._onDidActiveEditorChangeListener = editorService.onDidActiveEditorChange(onDidActiveEditorChangeListener);
1033
}
1034
1035
dispose(): void {
1036
this._onDidActiveEditorChangeListener.dispose();
1037
this._onDidChangeModelLanguage?.dispose();
1038
}
1039
}
1040
1041
CommandsRegistry.registerCommand({
1042
metadata: {
1043
description: COPY_ADDRESS_LABEL,
1044
},
1045
id: COPY_ADDRESS_ID,
1046
handler: async (accessor: ServicesAccessor, entry?: IDisassembledInstructionEntry) => {
1047
if (entry?.instruction?.address) {
1048
const clipboardService = accessor.get(IClipboardService);
1049
clipboardService.writeText(entry.instruction.address);
1050
}
1051
}
1052
});
1053
1054