Path: blob/main/src/vs/workbench/contrib/debug/browser/disassemblyView.ts
3296 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 { IRange, Range } from '../../../../editor/common/core/range.js';20import { StringBuilder } from '../../../../editor/common/core/stringBuilder.js';21import { ITextModel } from '../../../../editor/common/model.js';22import { ITextModelService } from '../../../../editor/common/services/resolverService.js';23import { localize } from '../../../../nls.js';24import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';25import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';26import { TextEditorSelectionRevealType } from '../../../../platform/editor/common/editor.js';27import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';28import { WorkbenchTable } from '../../../../platform/list/browser/listService.js';29import { ILogService } from '../../../../platform/log/common/log.js';30import { IStorageService } from '../../../../platform/storage/common/storage.js';31import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';32import { editorBackground } from '../../../../platform/theme/common/colorRegistry.js';33import { IThemeService } from '../../../../platform/theme/common/themeService.js';34import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';35import { EditorPane } from '../../../browser/parts/editor/editorPane.js';36import { IWorkbenchContribution } from '../../../common/contributions.js';37import { focusedStackFrameColor, topStackFrameColor } from './callStackEditorContribution.js';38import * as icons from './debugIcons.js';39import { CONTEXT_LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST, DISASSEMBLY_VIEW_ID, IDebugConfiguration, IDebugService, IDebugSession, IInstructionBreakpoint, State } from '../common/debug.js';40import { InstructionBreakpoint } from '../common/debugModel.js';41import { getUriFromSource } from '../common/debugSource.js';42import { isUri, sourcesEqual } from '../common/debugUtils.js';43import { IEditorService } from '../../../services/editor/common/editorService.js';44import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js';45import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';46import { IMenu, IMenuService, MenuId } from '../../../../platform/actions/common/actions.js';47import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';48import { COPY_ADDRESS_ID, COPY_ADDRESS_LABEL } from '../../../../workbench/contrib/debug/browser/debugCommands.js';49import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';50import { getFlatContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';5152export interface IDisassembledInstructionEntry {53allowBreakpoint: boolean;54isBreakpointSet: boolean;55isBreakpointEnabled: boolean;56/** Instruction reference from the DA */57instructionReference: string;58/** Offset from the instructionReference that's the basis for the `instructionOffset` */59instructionReferenceOffset: number;60/** The number of instructions (+/-) away from the instructionReference and instructionReferenceOffset this instruction lies */61instructionOffset: number;62/** Whether this is the first instruction on the target line. */63showSourceLocation?: boolean;64/** Original instruction from the debugger */65instruction: DebugProtocol.DisassembledInstruction;66/** Parsed instruction address */67address: bigint;68}6970// Special entry as a placeholer when disassembly is not available71const disassemblyNotAvailable: IDisassembledInstructionEntry = {72allowBreakpoint: false,73isBreakpointSet: false,74isBreakpointEnabled: false,75instructionReference: '',76instructionOffset: 0,77instructionReferenceOffset: 0,78address: 0n,79instruction: {80address: '-1',81instruction: localize('instructionNotAvailable', "Disassembly not available.")82},83};8485export class DisassemblyView extends EditorPane {8687private static readonly NUM_INSTRUCTIONS_TO_LOAD = 50;8889// Used in instruction renderer90private _fontInfo: BareFontInfo | undefined;91private _disassembledInstructions: WorkbenchTable<IDisassembledInstructionEntry> | undefined;92private _onDidChangeStackFrame: Emitter<void>;93private _previousDebuggingState: State;94private _instructionBpList: readonly IInstructionBreakpoint[] = [];95private _enableSourceCodeRender: boolean = true;96private _loadingLock: boolean = false;97private readonly _referenceToMemoryAddress = new Map<string, bigint>();98private menu: IMenu;99100constructor(101group: IEditorGroup,102@ITelemetryService telemetryService: ITelemetryService,103@IThemeService themeService: IThemeService,104@IStorageService storageService: IStorageService,105@IConfigurationService private readonly _configurationService: IConfigurationService,106@IInstantiationService private readonly _instantiationService: IInstantiationService,107@IDebugService private readonly _debugService: IDebugService,108@IContextMenuService private readonly _contextMenuService: IContextMenuService,109@IMenuService menuService: IMenuService,110@IContextKeyService contextKeyService: IContextKeyService,111) {112super(DISASSEMBLY_VIEW_ID, group, telemetryService, themeService, storageService);113114this.menu = menuService.createMenu(MenuId.DebugDisassemblyContext, contextKeyService);115this._register(this.menu);116this._disassembledInstructions = undefined;117this._onDidChangeStackFrame = this._register(new Emitter<void>({ leakWarningThreshold: 1000 }));118this._previousDebuggingState = _debugService.state;119this._register(_configurationService.onDidChangeConfiguration(e => {120if (e.affectsConfiguration('debug')) {121// show/hide source code requires changing height which WorkbenchTable doesn't support dynamic height, thus force a total reload.122const newValue = this._configurationService.getValue<IDebugConfiguration>('debug').disassemblyView.showSourceCode;123if (this._enableSourceCodeRender !== newValue) {124this._enableSourceCodeRender = newValue;125// todo: trigger rerender126} else {127this._disassembledInstructions?.rerender();128}129}130}));131}132133get fontInfo() {134if (!this._fontInfo) {135this._fontInfo = this.createFontInfo();136137this._register(this._configurationService.onDidChangeConfiguration(e => {138if (e.affectsConfiguration('editor')) {139this._fontInfo = this.createFontInfo();140}141}));142}143144return this._fontInfo;145}146147private createFontInfo() {148return BareFontInfo.createFromRawSettings(this._configurationService.getValue('editor'), PixelRatio.getInstance(this.window).value);149}150151get currentInstructionAddresses() {152return this._debugService.getModel().getSessions(false).153map(session => session.getAllThreads()).154reduce((prev, curr) => prev.concat(curr), []).155map(thread => thread.getTopStackFrame()).156map(frame => frame?.instructionPointerReference).157map(ref => ref ? this.getReferenceAddress(ref) : undefined);158}159160// Instruction reference of the top stack frame of the focused stack161get focusedCurrentInstructionReference() {162return this._debugService.getViewModel().focusedStackFrame?.thread.getTopStackFrame()?.instructionPointerReference;163}164165get focusedCurrentInstructionAddress() {166const ref = this.focusedCurrentInstructionReference;167return ref ? this.getReferenceAddress(ref) : undefined;168}169170get focusedInstructionReference() {171return this._debugService.getViewModel().focusedStackFrame?.instructionPointerReference;172}173174get focusedInstructionAddress() {175const ref = this.focusedInstructionReference;176return ref ? this.getReferenceAddress(ref) : undefined;177}178179get isSourceCodeRender() { return this._enableSourceCodeRender; }180181get debugSession(): IDebugSession | undefined {182return this._debugService.getViewModel().focusedSession;183}184185get onDidChangeStackFrame() { return this._onDidChangeStackFrame.event; }186187get focusedAddressAndOffset() {188const element = this._disassembledInstructions?.getFocusedElements()[0];189if (!element) {190return undefined;191}192193return this.getAddressAndOffset(element);194}195196getAddressAndOffset(element: IDisassembledInstructionEntry) {197const reference = element.instructionReference;198const offset = Number(element.address - this.getReferenceAddress(reference)!);199return { reference, offset, address: element.address };200}201202protected createEditor(parent: HTMLElement): void {203this._enableSourceCodeRender = this._configurationService.getValue<IDebugConfiguration>('debug').disassemblyView.showSourceCode;204const lineHeight = this.fontInfo.lineHeight;205const thisOM = this;206const delegate = new class implements ITableVirtualDelegate<IDisassembledInstructionEntry> {207headerRowHeight: number = 0; // No header208getHeight(row: IDisassembledInstructionEntry): number {209if (thisOM.isSourceCodeRender && row.showSourceLocation && row.instruction.location?.path && row.instruction.line) {210// instruction line + source lines211if (row.instruction.endLine) {212return lineHeight * Math.max(2, (row.instruction.endLine - row.instruction.line + 2));213} else {214// source is only a single line.215return lineHeight * 2;216}217}218219// just instruction line220return lineHeight;221}222};223224const instructionRenderer = this._register(this._instantiationService.createInstance(InstructionRenderer, this));225226this._disassembledInstructions = this._register(this._instantiationService.createInstance(WorkbenchTable,227'DisassemblyView', parent, delegate,228[229{230label: '',231tooltip: '',232weight: 0,233minimumWidth: this.fontInfo.lineHeight,234maximumWidth: this.fontInfo.lineHeight,235templateId: BreakpointRenderer.TEMPLATE_ID,236project(row: IDisassembledInstructionEntry): IDisassembledInstructionEntry { return row; }237},238{239label: localize('disassemblyTableColumnLabel', "instructions"),240tooltip: '',241weight: 0.3,242templateId: InstructionRenderer.TEMPLATE_ID,243project(row: IDisassembledInstructionEntry): IDisassembledInstructionEntry { return row; }244},245],246[247this._instantiationService.createInstance(BreakpointRenderer, this),248instructionRenderer,249],250{251identityProvider: { getId: (e: IDisassembledInstructionEntry) => e.instruction.address },252horizontalScrolling: false,253overrideStyles: {254listBackground: editorBackground255},256multipleSelectionSupport: false,257setRowLineHeight: false,258openOnSingleClick: false,259accessibilityProvider: new AccessibilityProvider(),260mouseSupport: false261}262)) as WorkbenchTable<IDisassembledInstructionEntry>;263264this._disassembledInstructions.domNode.classList.add('disassembly-view');265266if (this.focusedInstructionReference) {267this.reloadDisassembly(this.focusedInstructionReference, 0);268}269270this._register(this._disassembledInstructions.onDidScroll(e => {271if (this._loadingLock) {272return;273}274275if (e.oldScrollTop > e.scrollTop && e.scrollTop < e.height) {276this._loadingLock = true;277const prevTop = Math.floor(e.scrollTop / this.fontInfo.lineHeight);278this.scrollUp_LoadDisassembledInstructions(DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD).then((loaded) => {279if (loaded > 0) {280this._disassembledInstructions!.reveal(prevTop + loaded, 0);281}282this._loadingLock = false;283});284} else if (e.oldScrollTop < e.scrollTop && e.scrollTop + e.height > e.scrollHeight - e.height) {285this._loadingLock = true;286this.scrollDown_LoadDisassembledInstructions(DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD).then(() => { this._loadingLock = false; });287}288}));289290this._register(this._disassembledInstructions.onContextMenu(e => this.onContextMenu(e)));291292this._register(this._debugService.getViewModel().onDidFocusStackFrame(({ stackFrame }) => {293if (this._disassembledInstructions && stackFrame?.instructionPointerReference) {294this.goToInstructionAndOffset(stackFrame.instructionPointerReference, 0);295}296this._onDidChangeStackFrame.fire();297}));298299// refresh breakpoints view300this._register(this._debugService.getModel().onDidChangeBreakpoints(bpEvent => {301if (bpEvent && this._disassembledInstructions) {302// draw viewable BP303let changed = false;304bpEvent.added?.forEach((bp) => {305if (bp instanceof InstructionBreakpoint) {306const index = this.getIndexFromReferenceAndOffset(bp.instructionReference, bp.offset);307if (index >= 0) {308this._disassembledInstructions!.row(index).isBreakpointSet = true;309this._disassembledInstructions!.row(index).isBreakpointEnabled = bp.enabled;310changed = true;311}312}313});314315bpEvent.removed?.forEach((bp) => {316if (bp instanceof InstructionBreakpoint) {317const index = this.getIndexFromReferenceAndOffset(bp.instructionReference, bp.offset);318if (index >= 0) {319this._disassembledInstructions!.row(index).isBreakpointSet = false;320changed = true;321}322}323});324325bpEvent.changed?.forEach((bp) => {326if (bp instanceof InstructionBreakpoint) {327const index = this.getIndexFromReferenceAndOffset(bp.instructionReference, bp.offset);328if (index >= 0) {329if (this._disassembledInstructions!.row(index).isBreakpointEnabled !== bp.enabled) {330this._disassembledInstructions!.row(index).isBreakpointEnabled = bp.enabled;331changed = true;332}333}334}335});336337// get an updated list so that items beyond the current range would render when reached.338this._instructionBpList = this._debugService.getModel().getInstructionBreakpoints();339340// breakpoints restored from a previous session can be based on memory341// references that may no longer exist in the current session. Request342// those instructions to be loaded so the BP can be displayed.343for (const bp of this._instructionBpList) {344this.primeMemoryReference(bp.instructionReference);345}346347if (changed) {348this._onDidChangeStackFrame.fire();349}350}351}));352353this._register(this._debugService.onDidChangeState(e => {354if ((e === State.Running || e === State.Stopped) &&355(this._previousDebuggingState !== State.Running && this._previousDebuggingState !== State.Stopped)) {356// Just started debugging, clear the view357this.clear();358this._enableSourceCodeRender = this._configurationService.getValue<IDebugConfiguration>('debug').disassemblyView.showSourceCode;359}360361this._previousDebuggingState = e;362this._onDidChangeStackFrame.fire();363}));364}365366layout(dimension: Dimension): void {367this._disassembledInstructions?.layout(dimension.height);368}369370async goToInstructionAndOffset(instructionReference: string, offset: number, focus?: boolean) {371let addr = this._referenceToMemoryAddress.get(instructionReference);372if (addr === undefined) {373await this.loadDisassembledInstructions(instructionReference, 0, -DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD, DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD * 2);374addr = this._referenceToMemoryAddress.get(instructionReference);375}376377if (addr) {378this.goToAddress(addr + BigInt(offset), focus);379}380}381382/** Gets the address associated with the instruction reference. */383getReferenceAddress(instructionReference: string) {384return this._referenceToMemoryAddress.get(instructionReference);385}386387/**388* 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.389*/390private goToAddress(address: bigint, focus?: boolean): boolean {391if (!this._disassembledInstructions) {392return false;393}394395if (!address) {396return false;397}398399const index = this.getIndexFromAddress(address);400if (index >= 0) {401this._disassembledInstructions.reveal(index);402403if (focus) {404this._disassembledInstructions.domFocus();405this._disassembledInstructions.setFocus([index]);406}407return true;408}409410return false;411}412413private async scrollUp_LoadDisassembledInstructions(instructionCount: number): Promise<number> {414const first = this._disassembledInstructions?.row(0);415if (first) {416return this.loadDisassembledInstructions(417first.instructionReference,418first.instructionReferenceOffset,419first.instructionOffset - instructionCount,420instructionCount,421);422}423424return 0;425}426427private async scrollDown_LoadDisassembledInstructions(instructionCount: number): Promise<number> {428const last = this._disassembledInstructions?.row(this._disassembledInstructions?.length - 1);429if (last) {430return this.loadDisassembledInstructions(431last.instructionReference,432last.instructionReferenceOffset,433last.instructionOffset + 1,434instructionCount,435);436}437438return 0;439}440441/**442* Sets the memory reference address. We don't just loadDisassembledInstructions443* for this, since we can't really deal with discontiguous ranges (we can't444* detect _if_ a range is discontiguous since we don't know how much memory445* comes between instructions.)446*/447private async primeMemoryReference(instructionReference: string) {448if (this._referenceToMemoryAddress.has(instructionReference)) {449return true;450}451452const s = await this.debugSession?.disassemble(instructionReference, 0, 0, 1);453if (s && s.length > 0) {454try {455this._referenceToMemoryAddress.set(instructionReference, BigInt(s[0].address));456return true;457} catch {458return false;459}460}461462return false;463}464465/** Loads disasembled instructions. Returns the number of instructions that were loaded. */466private async loadDisassembledInstructions(instructionReference: string, offset: number, instructionOffset: number, instructionCount: number): Promise<number> {467const session = this.debugSession;468const resultEntries = await session?.disassemble(instructionReference, offset, instructionOffset, instructionCount);469470// Ensure we always load the baseline instructions so we know what address the instructionReference refers to.471if (!this._referenceToMemoryAddress.has(instructionReference) && instructionOffset !== 0) {472await this.loadDisassembledInstructions(instructionReference, 0, 0, DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD);473}474475if (session && resultEntries && this._disassembledInstructions) {476const newEntries: IDisassembledInstructionEntry[] = [];477478let lastLocation: DebugProtocol.Source | undefined;479let lastLine: IRange | undefined;480for (let i = 0; i < resultEntries.length; i++) {481const instruction = resultEntries[i];482const thisInstructionOffset = instructionOffset + i;483484// Forward fill the missing location as detailed in the DAP spec.485if (instruction.location) {486lastLocation = instruction.location;487lastLine = undefined;488}489490if (instruction.line) {491const currentLine: IRange = {492startLineNumber: instruction.line,493startColumn: instruction.column ?? 0,494endLineNumber: instruction.endLine ?? instruction.line,495endColumn: instruction.endColumn ?? 0,496};497498// Add location only to the first unique range. This will give the appearance of grouping of instructions.499if (!Range.equalsRange(currentLine, lastLine ?? null)) {500lastLine = currentLine;501instruction.location = lastLocation;502}503}504505let address: bigint;506try {507address = BigInt(instruction.address);508} catch {509console.error(`Could not parse disassembly address ${instruction.address} (in ${JSON.stringify(instruction)})`);510continue;511}512513if (address === -1n) {514// Ignore invalid instructions returned by the adapter.515continue;516}517518const entry: IDisassembledInstructionEntry = {519allowBreakpoint: true,520isBreakpointSet: false,521isBreakpointEnabled: false,522instructionReference,523instructionReferenceOffset: offset,524instructionOffset: thisInstructionOffset,525instruction,526address,527};528529newEntries.push(entry);530531// if we just loaded the first instruction for this reference, mark its address.532if (offset === 0 && thisInstructionOffset === 0) {533this._referenceToMemoryAddress.set(instructionReference, address);534}535}536537if (newEntries.length === 0) {538return 0;539}540541const refBaseAddress = this._referenceToMemoryAddress.get(instructionReference);542const bps = this._instructionBpList.map(p => {543const base = this._referenceToMemoryAddress.get(p.instructionReference);544if (!base) {545return undefined;546}547return {548enabled: p.enabled,549address: base + BigInt(p.offset || 0),550};551});552553if (refBaseAddress !== undefined) {554for (const entry of newEntries) {555const bp = bps.find(p => p?.address === entry.address);556if (bp) {557entry.isBreakpointSet = true;558entry.isBreakpointEnabled = bp.enabled;559}560}561}562563const da = this._disassembledInstructions;564if (da.length === 1 && this._disassembledInstructions.row(0) === disassemblyNotAvailable) {565da.splice(0, 1);566}567568const firstAddr = newEntries[0].address;569const lastAddr = newEntries[newEntries.length - 1].address;570571const startN = binarySearch2(da.length, i => Number(da.row(i).address - firstAddr));572const start = startN < 0 ? ~startN : startN;573const endN = binarySearch2(da.length, i => Number(da.row(i).address - lastAddr));574const end = endN < 0 ? ~endN : endN + 1;575const toDelete = end - start;576577// Go through everything we're about to add, and only show the source578// location if it's different from the previous one, "grouping" instructions by line579let lastLocated: undefined | DebugProtocol.DisassembledInstruction;580for (let i = start - 1; i >= 0; i--) {581const { instruction } = da.row(i);582if (instruction.location && instruction.line !== undefined) {583lastLocated = instruction;584break;585}586}587588const shouldShowLocation = (instruction: DebugProtocol.DisassembledInstruction) =>589instruction.line !== undefined && instruction.location !== undefined &&590(!lastLocated || !sourcesEqual(instruction.location, lastLocated.location) || instruction.line !== lastLocated.line);591592for (const entry of newEntries) {593if (shouldShowLocation(entry.instruction)) {594entry.showSourceLocation = true;595lastLocated = entry.instruction;596}597}598599da.splice(start, toDelete, newEntries);600601return newEntries.length - toDelete;602}603604return 0;605}606607private getIndexFromReferenceAndOffset(instructionReference: string, offset: number): number {608const addr = this._referenceToMemoryAddress.get(instructionReference);609if (addr === undefined) {610return -1;611}612613return this.getIndexFromAddress(addr + BigInt(offset));614}615616private getIndexFromAddress(address: bigint): number {617const disassembledInstructions = this._disassembledInstructions;618if (disassembledInstructions && disassembledInstructions.length > 0) {619return binarySearch2(disassembledInstructions.length, index => {620const row = disassembledInstructions.row(index);621return Number(row.address - address);622});623}624625return -1;626}627628/**629* Clears the table and reload instructions near the target address630*/631private reloadDisassembly(instructionReference: string, offset: number) {632if (!this._disassembledInstructions) {633return;634}635636this._loadingLock = true; // stop scrolling during the load.637this.clear();638this._instructionBpList = this._debugService.getModel().getInstructionBreakpoints();639this.loadDisassembledInstructions(instructionReference, offset, -DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD * 4, DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD * 8).then(() => {640// on load, set the target instruction in the middle of the page.641if (this._disassembledInstructions!.length > 0) {642const targetIndex = Math.floor(this._disassembledInstructions!.length / 2);643this._disassembledInstructions!.reveal(targetIndex, 0.5);644645// Always focus the target address on reload, or arrow key navigation would look terrible646this._disassembledInstructions!.domFocus();647this._disassembledInstructions!.setFocus([targetIndex]);648}649this._loadingLock = false;650});651}652653private clear() {654this._referenceToMemoryAddress.clear();655this._disassembledInstructions?.splice(0, this._disassembledInstructions.length, [disassemblyNotAvailable]);656}657658private onContextMenu(e: ITableContextMenuEvent<IDisassembledInstructionEntry>): void {659const actions = getFlatContextMenuActions(this.menu.getActions({ shouldForwardArgs: true }));660this._contextMenuService.showContextMenu({661getAnchor: () => e.anchor,662getActions: () => actions,663getActionsContext: () => e.element664});665}666}667668interface IBreakpointColumnTemplateData {669currentElement: { element?: IDisassembledInstructionEntry };670icon: HTMLElement;671disposables: IDisposable[];672}673674class BreakpointRenderer implements ITableRenderer<IDisassembledInstructionEntry, IBreakpointColumnTemplateData> {675676static readonly TEMPLATE_ID = 'breakpoint';677678templateId: string = BreakpointRenderer.TEMPLATE_ID;679680private readonly _breakpointIcon = 'codicon-' + icons.breakpoint.regular.id;681private readonly _breakpointDisabledIcon = 'codicon-' + icons.breakpoint.disabled.id;682private readonly _breakpointHintIcon = 'codicon-' + icons.debugBreakpointHint.id;683private readonly _debugStackframe = 'codicon-' + icons.debugStackframe.id;684private readonly _debugStackframeFocused = 'codicon-' + icons.debugStackframeFocused.id;685686constructor(687private readonly _disassemblyView: DisassemblyView,688@IDebugService private readonly _debugService: IDebugService689) {690}691692renderTemplate(container: HTMLElement): IBreakpointColumnTemplateData {693// align from the bottom so that it lines up with instruction when source code is present.694container.style.alignSelf = 'flex-end';695696const icon = append(container, $('.codicon'));697icon.style.display = 'flex';698icon.style.alignItems = 'center';699icon.style.justifyContent = 'center';700icon.style.height = this._disassemblyView.fontInfo.lineHeight + 'px';701702const currentElement: { element?: IDisassembledInstructionEntry } = { element: undefined };703704const disposables = [705this._disassemblyView.onDidChangeStackFrame(() => this.rerenderDebugStackframe(icon, currentElement.element)),706addStandardDisposableListener(container, 'mouseover', () => {707if (currentElement.element?.allowBreakpoint) {708icon.classList.add(this._breakpointHintIcon);709}710}),711addStandardDisposableListener(container, 'mouseout', () => {712if (currentElement.element?.allowBreakpoint) {713icon.classList.remove(this._breakpointHintIcon);714}715}),716addStandardDisposableListener(container, 'click', () => {717if (currentElement.element?.allowBreakpoint) {718// click show hint while waiting for BP to resolve.719icon.classList.add(this._breakpointHintIcon);720const reference = currentElement.element.instructionReference;721const offset = Number(currentElement.element.address - this._disassemblyView.getReferenceAddress(reference)!);722if (currentElement.element.isBreakpointSet) {723this._debugService.removeInstructionBreakpoints(reference, offset);724} else if (currentElement.element.allowBreakpoint && !currentElement.element.isBreakpointSet) {725this._debugService.addInstructionBreakpoint({ instructionReference: reference, offset, address: currentElement.element.address, canPersist: false });726}727}728})729];730731return { currentElement, icon, disposables };732}733734renderElement(element: IDisassembledInstructionEntry, index: number, templateData: IBreakpointColumnTemplateData): void {735templateData.currentElement.element = element;736this.rerenderDebugStackframe(templateData.icon, element);737}738739disposeTemplate(templateData: IBreakpointColumnTemplateData): void {740dispose(templateData.disposables);741templateData.disposables = [];742}743744private rerenderDebugStackframe(icon: HTMLElement, element?: IDisassembledInstructionEntry) {745if (element?.address === this._disassemblyView.focusedCurrentInstructionAddress) {746icon.classList.add(this._debugStackframe);747} else if (element?.address === this._disassemblyView.focusedInstructionAddress) {748icon.classList.add(this._debugStackframeFocused);749} else {750icon.classList.remove(this._debugStackframe);751icon.classList.remove(this._debugStackframeFocused);752}753754icon.classList.remove(this._breakpointHintIcon);755756if (element?.isBreakpointSet) {757if (element.isBreakpointEnabled) {758icon.classList.add(this._breakpointIcon);759icon.classList.remove(this._breakpointDisabledIcon);760} else {761icon.classList.remove(this._breakpointIcon);762icon.classList.add(this._breakpointDisabledIcon);763}764} else {765icon.classList.remove(this._breakpointIcon);766icon.classList.remove(this._breakpointDisabledIcon);767}768}769}770771interface IInstructionColumnTemplateData {772currentElement: { element?: IDisassembledInstructionEntry };773// TODO: hover widget?774instruction: HTMLElement;775sourcecode: HTMLElement;776// disposed when cell is closed.777cellDisposable: IDisposable[];778// disposed when template is closed.779disposables: IDisposable[];780}781782class InstructionRenderer extends Disposable implements ITableRenderer<IDisassembledInstructionEntry, IInstructionColumnTemplateData> {783784static readonly TEMPLATE_ID = 'instruction';785786private static readonly INSTRUCTION_ADDR_MIN_LENGTH = 25;787private static readonly INSTRUCTION_BYTES_MIN_LENGTH = 30;788789templateId: string = InstructionRenderer.TEMPLATE_ID;790791private _topStackFrameColor: Color | undefined;792private _focusedStackFrameColor: Color | undefined;793794constructor(795private readonly _disassemblyView: DisassemblyView,796@IThemeService themeService: IThemeService,797@IEditorService private readonly editorService: IEditorService,798@ITextModelService private readonly textModelService: ITextModelService,799@IUriIdentityService private readonly uriService: IUriIdentityService,800@ILogService private readonly logService: ILogService,801) {802super();803804this._topStackFrameColor = themeService.getColorTheme().getColor(topStackFrameColor);805this._focusedStackFrameColor = themeService.getColorTheme().getColor(focusedStackFrameColor);806807this._register(themeService.onDidColorThemeChange(e => {808this._topStackFrameColor = e.getColor(topStackFrameColor);809this._focusedStackFrameColor = e.getColor(focusedStackFrameColor);810}));811}812813renderTemplate(container: HTMLElement): IInstructionColumnTemplateData {814const sourcecode = append(container, $('.sourcecode'));815const instruction = append(container, $('.instruction'));816this.applyFontInfo(sourcecode);817this.applyFontInfo(instruction);818const currentElement: { element?: IDisassembledInstructionEntry } = { element: undefined };819const cellDisposable: IDisposable[] = [];820821const disposables = [822this._disassemblyView.onDidChangeStackFrame(() => this.rerenderBackground(instruction, sourcecode, currentElement.element)),823addStandardDisposableListener(sourcecode, 'dblclick', () => this.openSourceCode(currentElement.element?.instruction)),824];825826return { currentElement, instruction, sourcecode, cellDisposable, disposables };827}828829renderElement(element: IDisassembledInstructionEntry, index: number, templateData: IInstructionColumnTemplateData): void {830this.renderElementInner(element, index, templateData);831}832833private async renderElementInner(element: IDisassembledInstructionEntry, index: number, templateData: IInstructionColumnTemplateData): Promise<void> {834templateData.currentElement.element = element;835const instruction = element.instruction;836templateData.sourcecode.innerText = '';837const sb = new StringBuilder(1000);838839if (this._disassemblyView.isSourceCodeRender && element.showSourceLocation && instruction.location?.path && instruction.line !== undefined) {840const sourceURI = this.getUriFromSource(instruction);841842if (sourceURI) {843let textModel: ITextModel | undefined = undefined;844const sourceSB = new StringBuilder(10000);845const ref = await this.textModelService.createModelReference(sourceURI);846if (templateData.currentElement.element !== element) {847return; // avoid a race, #192831848}849textModel = ref.object.textEditorModel;850templateData.cellDisposable.push(ref);851852// templateData could have moved on during async. Double check if it is still the same source.853if (textModel && templateData.currentElement.element === element) {854let lineNumber = instruction.line;855856while (lineNumber && lineNumber >= 1 && lineNumber <= textModel.getLineCount()) {857const lineContent = textModel.getLineContent(lineNumber);858sourceSB.appendString(` ${lineNumber}: `);859sourceSB.appendString(lineContent + '\n');860861if (instruction.endLine && lineNumber < instruction.endLine) {862lineNumber++;863continue;864}865866break;867}868869templateData.sourcecode.innerText = sourceSB.build();870}871}872}873874let spacesToAppend = 10;875876if (instruction.address !== '-1') {877sb.appendString(instruction.address);878if (instruction.address.length < InstructionRenderer.INSTRUCTION_ADDR_MIN_LENGTH) {879spacesToAppend = InstructionRenderer.INSTRUCTION_ADDR_MIN_LENGTH - instruction.address.length;880}881for (let i = 0; i < spacesToAppend; i++) {882sb.appendString(' ');883}884}885886if (instruction.instructionBytes) {887sb.appendString(instruction.instructionBytes);888spacesToAppend = 10;889if (instruction.instructionBytes.length < InstructionRenderer.INSTRUCTION_BYTES_MIN_LENGTH) {890spacesToAppend = InstructionRenderer.INSTRUCTION_BYTES_MIN_LENGTH - instruction.instructionBytes.length;891}892for (let i = 0; i < spacesToAppend; i++) {893sb.appendString(' ');894}895}896897sb.appendString(instruction.instruction);898templateData.instruction.innerText = sb.build();899900this.rerenderBackground(templateData.instruction, templateData.sourcecode, element);901}902903disposeElement(element: IDisassembledInstructionEntry, index: number, templateData: IInstructionColumnTemplateData): void {904dispose(templateData.cellDisposable);905templateData.cellDisposable = [];906}907908disposeTemplate(templateData: IInstructionColumnTemplateData): void {909dispose(templateData.disposables);910templateData.disposables = [];911}912913private rerenderBackground(instruction: HTMLElement, sourceCode: HTMLElement, element?: IDisassembledInstructionEntry) {914if (element && this._disassemblyView.currentInstructionAddresses.includes(element.address)) {915instruction.style.background = this._topStackFrameColor?.toString() || 'transparent';916} else if (element?.address === this._disassemblyView.focusedInstructionAddress) {917instruction.style.background = this._focusedStackFrameColor?.toString() || 'transparent';918} else {919instruction.style.background = 'transparent';920}921}922923private openSourceCode(instruction: DebugProtocol.DisassembledInstruction | undefined) {924if (instruction) {925const sourceURI = this.getUriFromSource(instruction);926const selection = instruction.endLine ? {927startLineNumber: instruction.line!,928endLineNumber: instruction.endLine,929startColumn: instruction.column || 1,930endColumn: instruction.endColumn || Constants.MAX_SAFE_SMALL_INTEGER,931} : {932startLineNumber: instruction.line!,933endLineNumber: instruction.line!,934startColumn: instruction.column || 1,935endColumn: instruction.endColumn || Constants.MAX_SAFE_SMALL_INTEGER,936};937938this.editorService.openEditor({939resource: sourceURI,940description: localize('editorOpenedFromDisassemblyDescription', "from disassembly"),941options: {942preserveFocus: false,943selection: selection,944revealIfOpened: true,945selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport,946pinned: false,947}948});949}950}951952private getUriFromSource(instruction: DebugProtocol.DisassembledInstruction): URI {953// Try to resolve path before consulting the debugSession.954const path = instruction.location!.path;955if (path && isUri(path)) { // path looks like a uri956return this.uriService.asCanonicalUri(URI.parse(path));957}958// assume a filesystem path959if (path && isAbsolute(path)) {960return this.uriService.asCanonicalUri(URI.file(path));961}962963return getUriFromSource(instruction.location!, instruction.location!.path, this._disassemblyView.debugSession!.getId(), this.uriService, this.logService);964}965966private applyFontInfo(element: HTMLElement) {967applyFontInfo(element, this._disassemblyView.fontInfo);968element.style.whiteSpace = 'pre';969}970}971972class AccessibilityProvider implements IListAccessibilityProvider<IDisassembledInstructionEntry> {973974getWidgetAriaLabel(): string {975return localize('disassemblyView', "Disassembly View");976}977978getAriaLabel(element: IDisassembledInstructionEntry): string | null {979let label = '';980981const instruction = element.instruction;982if (instruction.address !== '-1') {983label += `${localize('instructionAddress', "Address")}: ${instruction.address}`;984}985if (instruction.instructionBytes) {986label += `, ${localize('instructionBytes', "Bytes")}: ${instruction.instructionBytes}`;987}988label += `, ${localize(`instructionText`, "Instruction")}: ${instruction.instruction}`;989990return label;991}992}993994export class DisassemblyViewContribution implements IWorkbenchContribution {995996private readonly _onDidActiveEditorChangeListener: IDisposable;997private _onDidChangeModelLanguage: IDisposable | undefined;998private _languageSupportsDisassembleRequest: IContextKey<boolean> | undefined;9991000constructor(1001@IEditorService editorService: IEditorService,1002@IDebugService debugService: IDebugService,1003@IContextKeyService contextKeyService: IContextKeyService1004) {1005contextKeyService.bufferChangeEvents(() => {1006this._languageSupportsDisassembleRequest = CONTEXT_LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST.bindTo(contextKeyService);1007});10081009const onDidActiveEditorChangeListener = () => {1010if (this._onDidChangeModelLanguage) {1011this._onDidChangeModelLanguage.dispose();1012this._onDidChangeModelLanguage = undefined;1013}10141015const activeTextEditorControl = editorService.activeTextEditorControl;1016if (isCodeEditor(activeTextEditorControl)) {1017const language = activeTextEditorControl.getModel()?.getLanguageId();1018// TODO: instead of using idDebuggerInterestedInLanguage, have a specific ext point for languages1019// support disassembly1020this._languageSupportsDisassembleRequest?.set(!!language && debugService.getAdapterManager().someDebuggerInterestedInLanguage(language));10211022this._onDidChangeModelLanguage = activeTextEditorControl.onDidChangeModelLanguage(e => {1023this._languageSupportsDisassembleRequest?.set(debugService.getAdapterManager().someDebuggerInterestedInLanguage(e.newLanguage));1024});1025} else {1026this._languageSupportsDisassembleRequest?.set(false);1027}1028};10291030onDidActiveEditorChangeListener();1031this._onDidActiveEditorChangeListener = editorService.onDidActiveEditorChange(onDidActiveEditorChangeListener);1032}10331034dispose(): void {1035this._onDidActiveEditorChangeListener.dispose();1036this._onDidChangeModelLanguage?.dispose();1037}1038}10391040CommandsRegistry.registerCommand({1041metadata: {1042description: COPY_ADDRESS_LABEL,1043},1044id: COPY_ADDRESS_ID,1045handler: async (accessor: ServicesAccessor, entry?: IDisassembledInstructionEntry) => {1046if (entry?.instruction?.address) {1047const clipboardService = accessor.get(IClipboardService);1048clipboardService.writeText(entry.instruction.address);1049}1050}1051});105210531054