Path: blob/main/src/vs/workbench/contrib/debug/browser/disassemblyView.ts
5252 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { PixelRatio } from '../../../../base/browser/pixelRatio.js';6import { $, Dimension, addStandardDisposableListener, append } from '../../../../base/browser/dom.js';7import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js';8import { ITableContextMenuEvent, ITableRenderer, ITableVirtualDelegate } from '../../../../base/browser/ui/table/table.js';9import { binarySearch2 } from '../../../../base/common/arrays.js';10import { Color } from '../../../../base/common/color.js';11import { Emitter } from '../../../../base/common/event.js';12import { Disposable, IDisposable, dispose } from '../../../../base/common/lifecycle.js';13import { isAbsolute } from '../../../../base/common/path.js';14import { Constants } from '../../../../base/common/uint.js';15import { URI } from '../../../../base/common/uri.js';16import { applyFontInfo } from '../../../../editor/browser/config/domFontInfo.js';17import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';18import { BareFontInfo } from '../../../../editor/common/config/fontInfo.js';19import { createBareFontInfoFromRawSettings } from '../../../../editor/common/config/fontInfoFromSettings.js';20import { IRange, Range } from '../../../../editor/common/core/range.js';21import { StringBuilder } from '../../../../editor/common/core/stringBuilder.js';22import { ITextModel } from '../../../../editor/common/model.js';23import { ITextModelService } from '../../../../editor/common/services/resolverService.js';24import { localize } from '../../../../nls.js';25import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';26import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';27import { TextEditorSelectionRevealType } from '../../../../platform/editor/common/editor.js';28import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';29import { WorkbenchTable } from '../../../../platform/list/browser/listService.js';30import { ILogService } from '../../../../platform/log/common/log.js';31import { IStorageService } from '../../../../platform/storage/common/storage.js';32import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';33import { editorBackground } from '../../../../platform/theme/common/colorRegistry.js';34import { IThemeService } from '../../../../platform/theme/common/themeService.js';35import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';36import { EditorPane } from '../../../browser/parts/editor/editorPane.js';37import { IWorkbenchContribution } from '../../../common/contributions.js';38import { focusedStackFrameColor, topStackFrameColor } from './callStackEditorContribution.js';39import * as icons from './debugIcons.js';40import { CONTEXT_LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST, DISASSEMBLY_VIEW_ID, IDebugConfiguration, IDebugService, IDebugSession, IInstructionBreakpoint, State } from '../common/debug.js';41import { InstructionBreakpoint } from '../common/debugModel.js';42import { getUriFromSource } from '../common/debugSource.js';43import { isUriString, sourcesEqual } from '../common/debugUtils.js';44import { IEditorService } from '../../../services/editor/common/editorService.js';45import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js';46import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';47import { IMenu, IMenuService, MenuId } from '../../../../platform/actions/common/actions.js';48import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';49import { COPY_ADDRESS_ID, COPY_ADDRESS_LABEL } from '../../../../workbench/contrib/debug/browser/debugCommands.js';50import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';51import { getFlatContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';5253export interface IDisassembledInstructionEntry {54allowBreakpoint: boolean;55isBreakpointSet: boolean;56isBreakpointEnabled: boolean;57/** Instruction reference from the DA */58instructionReference: string;59/** Offset from the instructionReference that's the basis for the `instructionOffset` */60instructionReferenceOffset: number;61/** The number of instructions (+/-) away from the instructionReference and instructionReferenceOffset this instruction lies */62instructionOffset: number;63/** Whether this is the first instruction on the target line. */64showSourceLocation?: boolean;65/** Original instruction from the debugger */66instruction: DebugProtocol.DisassembledInstruction;67/** Parsed instruction address */68address: bigint;69}7071// Special entry as a placeholer when disassembly is not available72const disassemblyNotAvailable: IDisassembledInstructionEntry = {73allowBreakpoint: false,74isBreakpointSet: false,75isBreakpointEnabled: false,76instructionReference: '',77instructionOffset: 0,78instructionReferenceOffset: 0,79address: 0n,80instruction: {81address: '-1',82instruction: localize('instructionNotAvailable', "Disassembly not available.")83},84};8586export class DisassemblyView extends EditorPane {8788private static readonly NUM_INSTRUCTIONS_TO_LOAD = 50;8990// Used in instruction renderer91private _fontInfo: BareFontInfo | undefined;92private _disassembledInstructions: WorkbenchTable<IDisassembledInstructionEntry> | undefined;93private _onDidChangeStackFrame: Emitter<void>;94private _previousDebuggingState: State;95private _instructionBpList: readonly IInstructionBreakpoint[] = [];96private _enableSourceCodeRender: boolean = true;97private _loadingLock: boolean = false;98private readonly _referenceToMemoryAddress = new Map<string, bigint>();99private menu: IMenu;100101constructor(102group: 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) {113super(DISASSEMBLY_VIEW_ID, group, telemetryService, themeService, storageService);114115this.menu = menuService.createMenu(MenuId.DebugDisassemblyContext, contextKeyService);116this._register(this.menu);117this._disassembledInstructions = undefined;118this._onDidChangeStackFrame = this._register(new Emitter<void>({ leakWarningThreshold: 1000 }));119this._previousDebuggingState = _debugService.state;120this._register(_configurationService.onDidChangeConfiguration(e => {121if (e.affectsConfiguration('debug')) {122// show/hide source code requires changing height which WorkbenchTable doesn't support dynamic height, thus force a total reload.123const newValue = this._configurationService.getValue<IDebugConfiguration>('debug').disassemblyView.showSourceCode;124if (this._enableSourceCodeRender !== newValue) {125this._enableSourceCodeRender = newValue;126// todo: trigger rerender127} else {128this._disassembledInstructions?.rerender();129}130}131}));132}133134get fontInfo() {135if (!this._fontInfo) {136this._fontInfo = this.createFontInfo();137138this._register(this._configurationService.onDidChangeConfiguration(e => {139if (e.affectsConfiguration('editor')) {140this._fontInfo = this.createFontInfo();141}142}));143}144145return this._fontInfo;146}147148private createFontInfo() {149return createBareFontInfoFromRawSettings(this._configurationService.getValue('editor'), PixelRatio.getInstance(this.window).value);150}151152get currentInstructionAddresses() {153return this._debugService.getModel().getSessions(false).154map(session => session.getAllThreads()).155reduce((prev, curr) => prev.concat(curr), []).156map(thread => thread.getTopStackFrame()).157map(frame => frame?.instructionPointerReference).158map(ref => ref ? this.getReferenceAddress(ref) : undefined);159}160161// Instruction reference of the top stack frame of the focused stack162get focusedCurrentInstructionReference() {163return this._debugService.getViewModel().focusedStackFrame?.thread.getTopStackFrame()?.instructionPointerReference;164}165166get focusedCurrentInstructionAddress() {167const ref = this.focusedCurrentInstructionReference;168return ref ? this.getReferenceAddress(ref) : undefined;169}170171get focusedInstructionReference() {172return this._debugService.getViewModel().focusedStackFrame?.instructionPointerReference;173}174175get focusedInstructionAddress() {176const ref = this.focusedInstructionReference;177return ref ? this.getReferenceAddress(ref) : undefined;178}179180get isSourceCodeRender() { return this._enableSourceCodeRender; }181182get debugSession(): IDebugSession | undefined {183return this._debugService.getViewModel().focusedSession;184}185186get onDidChangeStackFrame() { return this._onDidChangeStackFrame.event; }187188get focusedAddressAndOffset() {189const element = this._disassembledInstructions?.getFocusedElements()[0];190if (!element) {191return undefined;192}193194return this.getAddressAndOffset(element);195}196197getAddressAndOffset(element: IDisassembledInstructionEntry) {198const reference = element.instructionReference;199const offset = Number(element.address - this.getReferenceAddress(reference)!);200return { reference, offset, address: element.address };201}202203protected createEditor(parent: HTMLElement): void {204this._enableSourceCodeRender = this._configurationService.getValue<IDebugConfiguration>('debug').disassemblyView.showSourceCode;205const lineHeight = this.fontInfo.lineHeight;206const thisOM = this;207const delegate = new class implements ITableVirtualDelegate<IDisassembledInstructionEntry> {208headerRowHeight: number = 0; // No header209getHeight(row: IDisassembledInstructionEntry): number {210if (thisOM.isSourceCodeRender && row.showSourceLocation && row.instruction.location?.path && row.instruction.line) {211// instruction line + source lines212if (row.instruction.endLine) {213return lineHeight * Math.max(2, (row.instruction.endLine - row.instruction.line + 2));214} else {215// source is only a single line.216return lineHeight * 2;217}218}219220// just instruction line221return lineHeight;222}223};224225const instructionRenderer = this._register(this._instantiationService.createInstance(InstructionRenderer, this));226227this._disassembledInstructions = this._register(this._instantiationService.createInstance(WorkbenchTable,228'DisassemblyView', parent, delegate,229[230{231label: '',232tooltip: '',233weight: 0,234minimumWidth: this.fontInfo.lineHeight,235maximumWidth: this.fontInfo.lineHeight,236templateId: BreakpointRenderer.TEMPLATE_ID,237project(row: IDisassembledInstructionEntry): IDisassembledInstructionEntry { return row; }238},239{240label: localize('disassemblyTableColumnLabel', "instructions"),241tooltip: '',242weight: 0.3,243templateId: InstructionRenderer.TEMPLATE_ID,244project(row: IDisassembledInstructionEntry): IDisassembledInstructionEntry { return row; }245},246],247[248this._instantiationService.createInstance(BreakpointRenderer, this),249instructionRenderer,250],251{252identityProvider: { getId: (e: IDisassembledInstructionEntry) => e.instruction.address },253horizontalScrolling: false,254overrideStyles: {255listBackground: editorBackground256},257multipleSelectionSupport: false,258setRowLineHeight: false,259openOnSingleClick: false,260accessibilityProvider: new AccessibilityProvider(),261mouseSupport: false262}263)) as WorkbenchTable<IDisassembledInstructionEntry>;264265this._disassembledInstructions.domNode.classList.add('disassembly-view');266267if (this.focusedInstructionReference) {268this.reloadDisassembly(this.focusedInstructionReference, 0);269}270271this._register(this._disassembledInstructions.onDidScroll(e => {272if (this._disassembledInstructions?.row(0) === disassemblyNotAvailable) {273return;274}275if (this._loadingLock) {276return;277}278279if (e.oldScrollTop > e.scrollTop && e.scrollTop < e.height) {280this._loadingLock = true;281const prevTop = Math.floor(e.scrollTop / this.fontInfo.lineHeight);282this.scrollUp_LoadDisassembledInstructions(DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD).then((loaded) => {283if (loaded > 0) {284this._disassembledInstructions!.reveal(prevTop + loaded, 0);285}286}).finally(() => { this._loadingLock = false; });287} else if (e.oldScrollTop < e.scrollTop && e.scrollTop + e.height > e.scrollHeight - e.height) {288this._loadingLock = true;289this.scrollDown_LoadDisassembledInstructions(DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD).finally(() => { this._loadingLock = false; });290}291}));292293this._register(this._disassembledInstructions.onContextMenu(e => this.onContextMenu(e)));294295this._register(this._debugService.getViewModel().onDidFocusStackFrame(({ stackFrame }) => {296if (this._disassembledInstructions && stackFrame?.instructionPointerReference) {297this.goToInstructionAndOffset(stackFrame.instructionPointerReference, 0);298}299this._onDidChangeStackFrame.fire();300}));301302// refresh breakpoints view303this._register(this._debugService.getModel().onDidChangeBreakpoints(bpEvent => {304if (bpEvent && this._disassembledInstructions) {305// draw viewable BP306let changed = false;307bpEvent.added?.forEach((bp) => {308if (bp instanceof InstructionBreakpoint) {309const index = this.getIndexFromReferenceAndOffset(bp.instructionReference, bp.offset);310if (index >= 0) {311this._disassembledInstructions!.row(index).isBreakpointSet = true;312this._disassembledInstructions!.row(index).isBreakpointEnabled = bp.enabled;313changed = true;314}315}316});317318bpEvent.removed?.forEach((bp) => {319if (bp instanceof InstructionBreakpoint) {320const index = this.getIndexFromReferenceAndOffset(bp.instructionReference, bp.offset);321if (index >= 0) {322this._disassembledInstructions!.row(index).isBreakpointSet = false;323changed = true;324}325}326});327328bpEvent.changed?.forEach((bp) => {329if (bp instanceof InstructionBreakpoint) {330const index = this.getIndexFromReferenceAndOffset(bp.instructionReference, bp.offset);331if (index >= 0) {332if (this._disassembledInstructions!.row(index).isBreakpointEnabled !== bp.enabled) {333this._disassembledInstructions!.row(index).isBreakpointEnabled = bp.enabled;334changed = true;335}336}337}338});339340// get an updated list so that items beyond the current range would render when reached.341this._instructionBpList = this._debugService.getModel().getInstructionBreakpoints();342343// breakpoints restored from a previous session can be based on memory344// references that may no longer exist in the current session. Request345// those instructions to be loaded so the BP can be displayed.346for (const bp of this._instructionBpList) {347this.primeMemoryReference(bp.instructionReference);348}349350if (changed) {351this._onDidChangeStackFrame.fire();352}353}354}));355356this._register(this._debugService.onDidChangeState(e => {357if ((e === State.Running || e === State.Stopped) &&358(this._previousDebuggingState !== State.Running && this._previousDebuggingState !== State.Stopped)) {359// Just started debugging, clear the view360this.clear();361this._enableSourceCodeRender = this._configurationService.getValue<IDebugConfiguration>('debug').disassemblyView.showSourceCode;362}363364this._previousDebuggingState = e;365this._onDidChangeStackFrame.fire();366}));367}368369layout(dimension: Dimension): void {370this._disassembledInstructions?.layout(dimension.height);371}372373async goToInstructionAndOffset(instructionReference: string, offset: number, focus?: boolean) {374let addr = this._referenceToMemoryAddress.get(instructionReference);375if (addr === undefined) {376await this.loadDisassembledInstructions(instructionReference, 0, -DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD, DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD * 2);377addr = this._referenceToMemoryAddress.get(instructionReference);378}379380if (addr) {381this.goToAddress(addr + BigInt(offset), focus);382}383}384385/** Gets the address associated with the instruction reference. */386getReferenceAddress(instructionReference: string) {387return this._referenceToMemoryAddress.get(instructionReference);388}389390/**391* 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.392*/393private goToAddress(address: bigint, focus?: boolean): boolean {394if (!this._disassembledInstructions) {395return false;396}397398if (!address) {399return false;400}401402const index = this.getIndexFromAddress(address);403if (index >= 0) {404this._disassembledInstructions.reveal(index);405406if (focus) {407this._disassembledInstructions.domFocus();408this._disassembledInstructions.setFocus([index]);409}410return true;411}412413return false;414}415416private async scrollUp_LoadDisassembledInstructions(instructionCount: number): Promise<number> {417const first = this._disassembledInstructions?.row(0);418if (first) {419return this.loadDisassembledInstructions(420first.instructionReference,421first.instructionReferenceOffset,422first.instructionOffset - instructionCount,423instructionCount,424);425}426427return 0;428}429430private async scrollDown_LoadDisassembledInstructions(instructionCount: number): Promise<number> {431const last = this._disassembledInstructions?.row(this._disassembledInstructions?.length - 1);432if (last) {433return this.loadDisassembledInstructions(434last.instructionReference,435last.instructionReferenceOffset,436last.instructionOffset + 1,437instructionCount,438);439}440441return 0;442}443444/**445* Sets the memory reference address. We don't just loadDisassembledInstructions446* for this, since we can't really deal with discontiguous ranges (we can't447* detect _if_ a range is discontiguous since we don't know how much memory448* comes between instructions.)449*/450private async primeMemoryReference(instructionReference: string) {451if (this._referenceToMemoryAddress.has(instructionReference)) {452return true;453}454455const s = await this.debugSession?.disassemble(instructionReference, 0, 0, 1);456if (s && s.length > 0) {457try {458this._referenceToMemoryAddress.set(instructionReference, BigInt(s[0].address));459return true;460} catch {461return false;462}463}464465return false;466}467468/** Loads disasembled instructions. Returns the number of instructions that were loaded. */469private async loadDisassembledInstructions(instructionReference: string, offset: number, instructionOffset: number, instructionCount: number): Promise<number> {470const session = this.debugSession;471const resultEntries = await session?.disassemble(instructionReference, offset, instructionOffset, instructionCount);472473// Ensure we always load the baseline instructions so we know what address the instructionReference refers to.474if (!this._referenceToMemoryAddress.has(instructionReference) && instructionOffset !== 0) {475await this.loadDisassembledInstructions(instructionReference, 0, 0, DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD);476}477478if (session && resultEntries && this._disassembledInstructions) {479const newEntries: IDisassembledInstructionEntry[] = [];480481let lastLocation: DebugProtocol.Source | undefined;482let lastLine: IRange | undefined;483for (let i = 0; i < resultEntries.length; i++) {484const instruction = resultEntries[i];485const thisInstructionOffset = instructionOffset + i;486487// Forward fill the missing location as detailed in the DAP spec.488if (instruction.location) {489lastLocation = instruction.location;490lastLine = undefined;491}492493if (instruction.line) {494const currentLine: IRange = {495startLineNumber: instruction.line,496startColumn: instruction.column ?? 0,497endLineNumber: instruction.endLine ?? instruction.line,498endColumn: instruction.endColumn ?? 0,499};500501// Add location only to the first unique range. This will give the appearance of grouping of instructions.502if (!Range.equalsRange(currentLine, lastLine ?? null)) {503lastLine = currentLine;504instruction.location = lastLocation;505}506}507508let address: bigint;509try {510address = BigInt(instruction.address);511} catch {512console.error(`Could not parse disassembly address ${instruction.address} (in ${JSON.stringify(instruction)})`);513continue;514}515516if (address === -1n) {517// Ignore invalid instructions returned by the adapter.518continue;519}520521const entry: IDisassembledInstructionEntry = {522allowBreakpoint: true,523isBreakpointSet: false,524isBreakpointEnabled: false,525instructionReference,526instructionReferenceOffset: offset,527instructionOffset: thisInstructionOffset,528instruction,529address,530};531532newEntries.push(entry);533534// if we just loaded the first instruction for this reference, mark its address.535if (offset === 0 && thisInstructionOffset === 0) {536this._referenceToMemoryAddress.set(instructionReference, address);537}538}539540if (newEntries.length === 0) {541return 0;542}543544const refBaseAddress = this._referenceToMemoryAddress.get(instructionReference);545const bps = this._instructionBpList.map(p => {546const base = this._referenceToMemoryAddress.get(p.instructionReference);547if (!base) {548return undefined;549}550return {551enabled: p.enabled,552address: base + BigInt(p.offset || 0),553};554});555556if (refBaseAddress !== undefined) {557for (const entry of newEntries) {558const bp = bps.find(p => p?.address === entry.address);559if (bp) {560entry.isBreakpointSet = true;561entry.isBreakpointEnabled = bp.enabled;562}563}564}565566const da = this._disassembledInstructions;567if (da.length === 1 && this._disassembledInstructions.row(0) === disassemblyNotAvailable) {568da.splice(0, 1);569}570571const firstAddr = newEntries[0].address;572const lastAddr = newEntries[newEntries.length - 1].address;573574const startN = binarySearch2(da.length, i => Number(da.row(i).address - firstAddr));575const start = startN < 0 ? ~startN : startN;576const endN = binarySearch2(da.length, i => Number(da.row(i).address - lastAddr));577const end = endN < 0 ? ~endN : endN + 1;578const toDelete = end - start;579580// Go through everything we're about to add, and only show the source581// location if it's different from the previous one, "grouping" instructions by line582let lastLocated: undefined | DebugProtocol.DisassembledInstruction;583for (let i = start - 1; i >= 0; i--) {584const { instruction } = da.row(i);585if (instruction.location && instruction.line !== undefined) {586lastLocated = instruction;587break;588}589}590591const shouldShowLocation = (instruction: DebugProtocol.DisassembledInstruction) =>592instruction.line !== undefined && instruction.location !== undefined &&593(!lastLocated || !sourcesEqual(instruction.location, lastLocated.location) || instruction.line !== lastLocated.line);594595for (const entry of newEntries) {596if (shouldShowLocation(entry.instruction)) {597entry.showSourceLocation = true;598lastLocated = entry.instruction;599}600}601602da.splice(start, toDelete, newEntries);603604return newEntries.length - toDelete;605}606607return 0;608}609610private getIndexFromReferenceAndOffset(instructionReference: string, offset: number): number {611const addr = this._referenceToMemoryAddress.get(instructionReference);612if (addr === undefined) {613return -1;614}615616return this.getIndexFromAddress(addr + BigInt(offset));617}618619private getIndexFromAddress(address: bigint): number {620const disassembledInstructions = this._disassembledInstructions;621if (disassembledInstructions && disassembledInstructions.length > 0) {622return binarySearch2(disassembledInstructions.length, index => {623const row = disassembledInstructions.row(index);624return Number(row.address - address);625});626}627628return -1;629}630631/**632* Clears the table and reload instructions near the target address633*/634private reloadDisassembly(instructionReference: string, offset: number) {635if (!this._disassembledInstructions) {636return;637}638639this._loadingLock = true; // stop scrolling during the load.640this.clear();641this._instructionBpList = this._debugService.getModel().getInstructionBreakpoints();642this.loadDisassembledInstructions(instructionReference, offset, -DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD * 4, DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD * 8).then(() => {643// on load, set the target instruction as the current instructionReference.644if (this._disassembledInstructions!.length > 0) {645let targetIndex: number | undefined = undefined;646const refBaseAddress = this._referenceToMemoryAddress.get(instructionReference);647if (refBaseAddress !== undefined) {648const da = this._disassembledInstructions!;649targetIndex = binarySearch2(da.length, i => Number(da.row(i).address - refBaseAddress));650if (targetIndex < 0) {651targetIndex = ~targetIndex; // shouldn't happen, but fail gracefully if it does652}653}654655// If didn't find the instructonReference, set the target instruction in the middle of the page.656if (targetIndex === undefined) {657targetIndex = Math.floor(this._disassembledInstructions!.length / 2);658}659660this._disassembledInstructions!.reveal(targetIndex, 0.5);661662// Always focus the target address on reload, or arrow key navigation would look terrible663this._disassembledInstructions!.domFocus();664this._disassembledInstructions!.setFocus([targetIndex]);665}666this._loadingLock = false;667});668}669670private clear() {671this._referenceToMemoryAddress.clear();672this._disassembledInstructions?.splice(0, this._disassembledInstructions.length, [disassemblyNotAvailable]);673}674675private onContextMenu(e: ITableContextMenuEvent<IDisassembledInstructionEntry>): void {676const actions = getFlatContextMenuActions(this.menu.getActions({ shouldForwardArgs: true }));677this._contextMenuService.showContextMenu({678getAnchor: () => e.anchor,679getActions: () => actions,680getActionsContext: () => e.element681});682}683}684685interface IBreakpointColumnTemplateData {686currentElement: { element?: IDisassembledInstructionEntry };687icon: HTMLElement;688disposables: IDisposable[];689}690691class BreakpointRenderer implements ITableRenderer<IDisassembledInstructionEntry, IBreakpointColumnTemplateData> {692693static readonly TEMPLATE_ID = 'breakpoint';694695templateId: string = BreakpointRenderer.TEMPLATE_ID;696697private readonly _breakpointIcon = 'codicon-' + icons.breakpoint.regular.id;698private readonly _breakpointDisabledIcon = 'codicon-' + icons.breakpoint.disabled.id;699private readonly _breakpointHintIcon = 'codicon-' + icons.debugBreakpointHint.id;700private readonly _debugStackframe = 'codicon-' + icons.debugStackframe.id;701private readonly _debugStackframeFocused = 'codicon-' + icons.debugStackframeFocused.id;702703constructor(704private readonly _disassemblyView: DisassemblyView,705@IDebugService private readonly _debugService: IDebugService706) {707}708709renderTemplate(container: HTMLElement): IBreakpointColumnTemplateData {710// align from the bottom so that it lines up with instruction when source code is present.711container.style.alignSelf = 'flex-end';712713const icon = append(container, $('.codicon'));714icon.style.display = 'flex';715icon.style.alignItems = 'center';716icon.style.justifyContent = 'center';717icon.style.height = this._disassemblyView.fontInfo.lineHeight + 'px';718719const currentElement: { element?: IDisassembledInstructionEntry } = { element: undefined };720721const disposables = [722this._disassemblyView.onDidChangeStackFrame(() => this.rerenderDebugStackframe(icon, currentElement.element)),723addStandardDisposableListener(container, 'mouseover', () => {724if (currentElement.element?.allowBreakpoint) {725icon.classList.add(this._breakpointHintIcon);726}727}),728addStandardDisposableListener(container, 'mouseout', () => {729if (currentElement.element?.allowBreakpoint) {730icon.classList.remove(this._breakpointHintIcon);731}732}),733addStandardDisposableListener(container, 'click', () => {734if (currentElement.element?.allowBreakpoint) {735// click show hint while waiting for BP to resolve.736icon.classList.add(this._breakpointHintIcon);737const reference = currentElement.element.instructionReference;738const offset = Number(currentElement.element.address - this._disassemblyView.getReferenceAddress(reference)!);739if (currentElement.element.isBreakpointSet) {740this._debugService.removeInstructionBreakpoints(reference, offset);741} else if (currentElement.element.allowBreakpoint && !currentElement.element.isBreakpointSet) {742this._debugService.addInstructionBreakpoint({ instructionReference: reference, offset, address: currentElement.element.address, canPersist: false });743}744}745})746];747748return { currentElement, icon, disposables };749}750751renderElement(element: IDisassembledInstructionEntry, index: number, templateData: IBreakpointColumnTemplateData): void {752templateData.currentElement.element = element;753this.rerenderDebugStackframe(templateData.icon, element);754}755756disposeTemplate(templateData: IBreakpointColumnTemplateData): void {757dispose(templateData.disposables);758templateData.disposables = [];759}760761private rerenderDebugStackframe(icon: HTMLElement, element?: IDisassembledInstructionEntry) {762if (element?.address === this._disassemblyView.focusedCurrentInstructionAddress) {763icon.classList.add(this._debugStackframe);764} else if (element?.address === this._disassemblyView.focusedInstructionAddress) {765icon.classList.add(this._debugStackframeFocused);766} else {767icon.classList.remove(this._debugStackframe);768icon.classList.remove(this._debugStackframeFocused);769}770771icon.classList.remove(this._breakpointHintIcon);772773if (element?.isBreakpointSet) {774if (element.isBreakpointEnabled) {775icon.classList.add(this._breakpointIcon);776icon.classList.remove(this._breakpointDisabledIcon);777} else {778icon.classList.remove(this._breakpointIcon);779icon.classList.add(this._breakpointDisabledIcon);780}781} else {782icon.classList.remove(this._breakpointIcon);783icon.classList.remove(this._breakpointDisabledIcon);784}785}786}787788interface IInstructionColumnTemplateData {789currentElement: { element?: IDisassembledInstructionEntry };790// TODO: hover widget?791instruction: HTMLElement;792sourcecode: HTMLElement;793// disposed when cell is closed.794cellDisposable: IDisposable[];795// disposed when template is closed.796disposables: IDisposable[];797}798799class InstructionRenderer extends Disposable implements ITableRenderer<IDisassembledInstructionEntry, IInstructionColumnTemplateData> {800801static readonly TEMPLATE_ID = 'instruction';802803private static readonly INSTRUCTION_ADDR_MIN_LENGTH = 25;804private static readonly INSTRUCTION_BYTES_MIN_LENGTH = 30;805806templateId: string = InstructionRenderer.TEMPLATE_ID;807808private _topStackFrameColor: Color | undefined;809private _focusedStackFrameColor: Color | undefined;810811constructor(812private readonly _disassemblyView: DisassemblyView,813@IThemeService themeService: IThemeService,814@IEditorService private readonly editorService: IEditorService,815@ITextModelService private readonly textModelService: ITextModelService,816@IUriIdentityService private readonly uriService: IUriIdentityService,817@ILogService private readonly logService: ILogService,818) {819super();820821this._topStackFrameColor = themeService.getColorTheme().getColor(topStackFrameColor);822this._focusedStackFrameColor = themeService.getColorTheme().getColor(focusedStackFrameColor);823824this._register(themeService.onDidColorThemeChange(e => {825this._topStackFrameColor = e.getColor(topStackFrameColor);826this._focusedStackFrameColor = e.getColor(focusedStackFrameColor);827}));828}829830renderTemplate(container: HTMLElement): IInstructionColumnTemplateData {831const sourcecode = append(container, $('.sourcecode'));832const instruction = append(container, $('.instruction'));833this.applyFontInfo(sourcecode);834this.applyFontInfo(instruction);835const currentElement: { element?: IDisassembledInstructionEntry } = { element: undefined };836const cellDisposable: IDisposable[] = [];837838const disposables = [839this._disassemblyView.onDidChangeStackFrame(() => this.rerenderBackground(instruction, sourcecode, currentElement.element)),840addStandardDisposableListener(sourcecode, 'dblclick', () => this.openSourceCode(currentElement.element?.instruction)),841];842843return { currentElement, instruction, sourcecode, cellDisposable, disposables };844}845846renderElement(element: IDisassembledInstructionEntry, index: number, templateData: IInstructionColumnTemplateData): void {847this.renderElementInner(element, index, templateData);848}849850private async renderElementInner(element: IDisassembledInstructionEntry, index: number, templateData: IInstructionColumnTemplateData): Promise<void> {851templateData.currentElement.element = element;852const instruction = element.instruction;853templateData.sourcecode.innerText = '';854const sb = new StringBuilder(1000);855856if (this._disassemblyView.isSourceCodeRender && element.showSourceLocation && instruction.location?.path && instruction.line !== undefined) {857const sourceURI = this.getUriFromSource(instruction);858859if (sourceURI) {860let textModel: ITextModel | undefined = undefined;861const sourceSB = new StringBuilder(10000);862const ref = await this.textModelService.createModelReference(sourceURI);863if (templateData.currentElement.element !== element) {864return; // avoid a race, #192831865}866textModel = ref.object.textEditorModel;867templateData.cellDisposable.push(ref);868869// templateData could have moved on during async. Double check if it is still the same source.870if (textModel && templateData.currentElement.element === element) {871let lineNumber = instruction.line;872873while (lineNumber && lineNumber >= 1 && lineNumber <= textModel.getLineCount()) {874const lineContent = textModel.getLineContent(lineNumber);875sourceSB.appendString(` ${lineNumber}: `);876sourceSB.appendString(lineContent + '\n');877878if (instruction.endLine && lineNumber < instruction.endLine) {879lineNumber++;880continue;881}882883break;884}885886templateData.sourcecode.innerText = sourceSB.build();887}888}889}890891let spacesToAppend = 10;892893if (instruction.address !== '-1') {894sb.appendString(instruction.address);895if (instruction.address.length < InstructionRenderer.INSTRUCTION_ADDR_MIN_LENGTH) {896spacesToAppend = InstructionRenderer.INSTRUCTION_ADDR_MIN_LENGTH - instruction.address.length;897}898for (let i = 0; i < spacesToAppend; i++) {899sb.appendString(' ');900}901}902903if (instruction.instructionBytes) {904sb.appendString(instruction.instructionBytes);905spacesToAppend = 10;906if (instruction.instructionBytes.length < InstructionRenderer.INSTRUCTION_BYTES_MIN_LENGTH) {907spacesToAppend = InstructionRenderer.INSTRUCTION_BYTES_MIN_LENGTH - instruction.instructionBytes.length;908}909for (let i = 0; i < spacesToAppend; i++) {910sb.appendString(' ');911}912}913914sb.appendString(instruction.instruction);915templateData.instruction.innerText = sb.build();916917this.rerenderBackground(templateData.instruction, templateData.sourcecode, element);918}919920disposeElement(element: IDisassembledInstructionEntry, index: number, templateData: IInstructionColumnTemplateData): void {921dispose(templateData.cellDisposable);922templateData.cellDisposable = [];923}924925disposeTemplate(templateData: IInstructionColumnTemplateData): void {926dispose(templateData.disposables);927templateData.disposables = [];928}929930private rerenderBackground(instruction: HTMLElement, sourceCode: HTMLElement, element?: IDisassembledInstructionEntry) {931if (element && this._disassemblyView.currentInstructionAddresses.includes(element.address)) {932instruction.style.background = this._topStackFrameColor?.toString() || 'transparent';933} else if (element?.address === this._disassemblyView.focusedInstructionAddress) {934instruction.style.background = this._focusedStackFrameColor?.toString() || 'transparent';935} else {936instruction.style.background = 'transparent';937}938}939940private openSourceCode(instruction: DebugProtocol.DisassembledInstruction | undefined) {941if (instruction) {942const sourceURI = this.getUriFromSource(instruction);943const selection = instruction.endLine ? {944startLineNumber: instruction.line!,945endLineNumber: instruction.endLine,946startColumn: instruction.column || 1,947endColumn: instruction.endColumn || Constants.MAX_SAFE_SMALL_INTEGER,948} : {949startLineNumber: instruction.line!,950endLineNumber: instruction.line!,951startColumn: instruction.column || 1,952endColumn: instruction.endColumn || Constants.MAX_SAFE_SMALL_INTEGER,953};954955this.editorService.openEditor({956resource: sourceURI,957description: localize('editorOpenedFromDisassemblyDescription', "from disassembly"),958options: {959preserveFocus: false,960selection: selection,961revealIfOpened: true,962selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport,963pinned: false,964}965});966}967}968969private getUriFromSource(instruction: DebugProtocol.DisassembledInstruction): URI {970// Try to resolve path before consulting the debugSession.971const path = instruction.location!.path;972if (path && isUriString(path)) { // path looks like a uri973return this.uriService.asCanonicalUri(URI.parse(path));974}975// assume a filesystem path976if (path && isAbsolute(path)) {977return this.uriService.asCanonicalUri(URI.file(path));978}979980return getUriFromSource(instruction.location!, instruction.location!.path, this._disassemblyView.debugSession!.getId(), this.uriService, this.logService);981}982983private applyFontInfo(element: HTMLElement) {984applyFontInfo(element, this._disassemblyView.fontInfo);985element.style.whiteSpace = 'pre';986}987}988989class AccessibilityProvider implements IListAccessibilityProvider<IDisassembledInstructionEntry> {990991getWidgetAriaLabel(): string {992return localize('disassemblyView', "Disassembly View");993}994995getAriaLabel(element: IDisassembledInstructionEntry): string | null {996let label = '';997998const instruction = element.instruction;999if (instruction.address !== '-1') {1000label += `${localize('instructionAddress', "Address")}: ${instruction.address}`;1001}1002if (instruction.instructionBytes) {1003label += `, ${localize('instructionBytes', "Bytes")}: ${instruction.instructionBytes}`;1004}1005label += `, ${localize(`instructionText`, "Instruction")}: ${instruction.instruction}`;10061007return label;1008}1009}10101011export class DisassemblyViewContribution implements IWorkbenchContribution {10121013private readonly _onDidActiveEditorChangeListener: IDisposable;1014private _onDidChangeModelLanguage: IDisposable | undefined;1015private _languageSupportsDisassembleRequest: IContextKey<boolean> | undefined;10161017constructor(1018@IEditorService editorService: IEditorService,1019@IDebugService debugService: IDebugService,1020@IContextKeyService contextKeyService: IContextKeyService1021) {1022contextKeyService.bufferChangeEvents(() => {1023this._languageSupportsDisassembleRequest = CONTEXT_LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST.bindTo(contextKeyService);1024});10251026const onDidActiveEditorChangeListener = () => {1027if (this._onDidChangeModelLanguage) {1028this._onDidChangeModelLanguage.dispose();1029this._onDidChangeModelLanguage = undefined;1030}10311032const activeTextEditorControl = editorService.activeTextEditorControl;1033if (isCodeEditor(activeTextEditorControl)) {1034const language = activeTextEditorControl.getModel()?.getLanguageId();1035// TODO: instead of using idDebuggerInterestedInLanguage, have a specific ext point for languages1036// support disassembly1037this._languageSupportsDisassembleRequest?.set(!!language && debugService.getAdapterManager().someDebuggerInterestedInLanguage(language));10381039this._onDidChangeModelLanguage = activeTextEditorControl.onDidChangeModelLanguage(e => {1040this._languageSupportsDisassembleRequest?.set(debugService.getAdapterManager().someDebuggerInterestedInLanguage(e.newLanguage));1041});1042} else {1043this._languageSupportsDisassembleRequest?.set(false);1044}1045};10461047onDidActiveEditorChangeListener();1048this._onDidActiveEditorChangeListener = editorService.onDidActiveEditorChange(onDidActiveEditorChangeListener);1049}10501051dispose(): void {1052this._onDidActiveEditorChangeListener.dispose();1053this._onDidChangeModelLanguage?.dispose();1054}1055}10561057CommandsRegistry.registerCommand({1058metadata: {1059description: COPY_ADDRESS_LABEL,1060},1061id: COPY_ADDRESS_ID,1062handler: async (accessor: ServicesAccessor, entry?: IDisassembledInstructionEntry) => {1063if (entry?.instruction?.address) {1064const clipboardService = accessor.get(IClipboardService);1065clipboardService.writeText(entry.instruction.address);1066}1067}1068});106910701071