diff --git a/packages/core/src/common/types.ts b/packages/core/src/common/types.ts index 1e8479fd94f19..cdabbeb69a96c 100644 --- a/packages/core/src/common/types.ts +++ b/packages/core/src/common/types.ts @@ -128,6 +128,40 @@ export namespace ArrayUtils { RightBeforeLeft = 1, Equal = 0, } + + // Copied from https://github.com/microsoft/vscode/blob/9c29becfad5f68270b9b23efeafb147722c5feba/src/vs/base/common/arrays.ts + /** + * Performs a binary search algorithm over a sorted collection. Useful for cases + * when we need to perform a binary search over something that isn't actually an + * array, and converting data to an array would defeat the use of binary search + * in the first place. + * + * @param length The collection length. + * @param compareToKey A function that takes an index of an element in the + * collection and returns zero if the value at this index is equal to the + * search key, a negative number if the value precedes the search key in the + * sorting order, or a positive number if the search key precedes the value. + * @return A non-negative index of an element, if found. If not found, the + * result is -(n+1) (or ~n, using bitwise notation), where n is the index + * where the key should be inserted to maintain the sorting order. + */ + export function binarySearch2(length: number, compareToKey: (index: number) => number): number { + let low = 0; + let high = length - 1; + + while (low <= high) { + const mid = ((low + high) / 2) | 0; + const comp = compareToKey(mid); + if (comp < 0) { + low = mid + 1; + } else if (comp > 0) { + high = mid - 1; + } else { + return mid; + } + } + return -(low + 1); + } } /** diff --git a/packages/debug/src/browser/breakpoint/breakpoint-manager.ts b/packages/debug/src/browser/breakpoint/breakpoint-manager.ts index 3a576231b829c..93eefc9b177aa 100644 --- a/packages/debug/src/browser/breakpoint/breakpoint-manager.ts +++ b/packages/debug/src/browser/breakpoint/breakpoint-manager.ts @@ -20,7 +20,7 @@ import { StorageService } from '@theia/core/lib/browser'; import { Marker } from '@theia/markers/lib/common/marker'; import { MarkerManager } from '@theia/markers/lib/browser/marker-manager'; import URI from '@theia/core/lib/common/uri'; -import { SourceBreakpoint, BREAKPOINT_KIND, ExceptionBreakpoint, FunctionBreakpoint, BaseBreakpoint } from './breakpoint-marker'; +import { SourceBreakpoint, BREAKPOINT_KIND, ExceptionBreakpoint, FunctionBreakpoint, BaseBreakpoint, InstructionBreakpoint } from './breakpoint-marker'; export interface BreakpointsChangeEvent { uri: URI @@ -30,6 +30,7 @@ export interface BreakpointsChangeEvent { } export type SourceBreakpointsChangeEvent = BreakpointsChangeEvent; export type FunctionBreakpointsChangeEvent = BreakpointsChangeEvent; +export type InstructionBreakpointsChangeEvent = BreakpointsChangeEvent; @injectable() export class BreakpointManager extends MarkerManager { @@ -38,6 +39,8 @@ export class BreakpointManager extends MarkerManager { static FUNCTION_URI = new URI('debug:function://'); + static INSTRUCTION_URI = new URI('debug:instruction://'); + protected readonly owner = 'breakpoint'; @inject(StorageService) @@ -53,6 +56,9 @@ export class BreakpointManager extends MarkerManager { protected readonly onDidChangeFunctionBreakpointsEmitter = new Emitter(); readonly onDidChangeFunctionBreakpoints = this.onDidChangeFunctionBreakpointsEmitter.event; + protected readonly onDidChangeInstructionBreakpointsEmitter = new Emitter(); + readonly onDidChangeInstructionBreakpoints = this.onDidChangeInstructionBreakpointsEmitter.event; + override setMarkers(uri: URI, owner: string, newMarkers: SourceBreakpoint[]): Marker[] { const result = super.setMarkers(uri, owner, newMarkers); const added: SourceBreakpoint[] = []; @@ -128,7 +134,7 @@ export class BreakpointManager extends MarkerManager { } } let didChangeFunction = false; - for (const breakpoint of this.getFunctionBreakpoints()) { + for (const breakpoint of (this.getFunctionBreakpoints() as BaseBreakpoint[]).concat(this.getInstructionBreakpoints())) { if (breakpoint.enabled !== enabled) { breakpoint.enabled = enabled; didChangeFunction = true; @@ -219,13 +225,74 @@ export class BreakpointManager extends MarkerManager { this.onDidChangeFunctionBreakpointsEmitter.fire({ uri: BreakpointManager.FUNCTION_URI, added, removed, changed }); } + protected instructionBreakpoints: InstructionBreakpoint[] = []; + + getInstructionBreakpoints(): ReadonlyArray { + return Object.freeze(this.instructionBreakpoints.slice()); + } + hasBreakpoints(): boolean { - return !!this.getUris().next().value || !!this.functionBreakpoints.length; + return Boolean(this.getUris().next().value || this.functionBreakpoints.length || this.instructionBreakpoints.length); + } + + protected setInstructionBreakpoints(newBreakpoints: InstructionBreakpoint[]): void { + const oldBreakpoints = new Map(this.instructionBreakpoints.map(breakpoint => [breakpoint.id, breakpoint])); + const currentBreakpoints = new Map(newBreakpoints.map(breakpoint => [breakpoint.id, breakpoint])); + const added = []; + const changed = []; + for (const [id, breakpoint] of currentBreakpoints.entries()) { + const old = oldBreakpoints.get(id); + if (old) { + changed.push(old); + } else { + added.push(breakpoint); + } + oldBreakpoints.delete(id); + } + const removed = Array.from(oldBreakpoints.values()); + this.instructionBreakpoints = Array.from(currentBreakpoints.values()); + this.fireOnDidChangeMarkers(BreakpointManager.INSTRUCTION_URI); + this.onDidChangeInstructionBreakpointsEmitter.fire({ uri: BreakpointManager.INSTRUCTION_URI, added, removed, changed }); + } + + addInstructionBreakpoint(address: string, offset: number, condition?: string, hitCondition?: string): void { + this.setInstructionBreakpoints(this.instructionBreakpoints.concat(InstructionBreakpoint.create({ + instructionReference: address, + offset, + condition, + hitCondition, + }))); + } + + updateInstructionBreakpoint(id: string, options: Partial>): void { + const breakpoint = this.instructionBreakpoints.find(candidate => id === candidate.id); + if (breakpoint) { + Object.assign(breakpoint, options); + this.fireOnDidChangeMarkers(BreakpointManager.INSTRUCTION_URI); + this.onDidChangeInstructionBreakpointsEmitter.fire({ uri: BreakpointManager.INSTRUCTION_URI, changed: [breakpoint], added: [], removed: [] }); + } + } + + removeInstructionBreakpoint(address?: string): void { + if (!address) { + this.clearInstructionBreakpoints(); + } + const breakpointIndex = this.instructionBreakpoints.findIndex(breakpoint => breakpoint.instructionReference === address); + if (breakpointIndex !== -1) { + const removed = this.instructionBreakpoints.splice(breakpointIndex, 1); + this.fireOnDidChangeMarkers(BreakpointManager.INSTRUCTION_URI); + this.onDidChangeInstructionBreakpointsEmitter.fire({ uri: BreakpointManager.INSTRUCTION_URI, added: [], changed: [], removed }); + } + } + + clearInstructionBreakpoints(): void { + this.setInstructionBreakpoints([]); } removeBreakpoints(): void { this.cleanAllMarkers(); this.setFunctionBreakpoints([]); + this.setInstructionBreakpoints([]); } async load(): Promise { @@ -244,6 +311,9 @@ export class BreakpointManager extends MarkerManager { if (data.exceptionBreakpoints) { this.setExceptionBreakpoints(data.exceptionBreakpoints); } + if (data.instructionBreakpoints) { + this.setInstructionBreakpoints(data.instructionBreakpoints); + } } save(): void { @@ -261,17 +331,21 @@ export class BreakpointManager extends MarkerManager { if (this.exceptionBreakpoints.size) { data.exceptionBreakpoints = [...this.exceptionBreakpoints.values()]; } + if (this.instructionBreakpoints.length) { + data.instructionBreakpoints = this.instructionBreakpoints; + } this.storage.setData('breakpoints', data); } } export namespace BreakpointManager { export interface Data { - breakpointsEnabled: boolean + breakpointsEnabled: boolean; breakpoints: { - [uri: string]: SourceBreakpoint[] + [uri: string]: SourceBreakpoint[]; } - exceptionBreakpoints?: ExceptionBreakpoint[] - functionBreakpoints?: FunctionBreakpoint[] + exceptionBreakpoints?: ExceptionBreakpoint[]; + functionBreakpoints?: FunctionBreakpoint[]; + instructionBreakpoints?: InstructionBreakpoint[]; } } diff --git a/packages/debug/src/browser/breakpoint/breakpoint-marker.ts b/packages/debug/src/browser/breakpoint/breakpoint-marker.ts index 94e055fc62f0e..b53d372606e78 100644 --- a/packages/debug/src/browser/breakpoint/breakpoint-marker.ts +++ b/packages/debug/src/browser/breakpoint/breakpoint-marker.ts @@ -84,3 +84,20 @@ export namespace FunctionBreakpoint { }; } } + +export interface InstructionBreakpoint extends BaseBreakpoint, DebugProtocol.InstructionBreakpoint { } + +export namespace InstructionBreakpoint { + export function create(raw: DebugProtocol.InstructionBreakpoint, existing?: InstructionBreakpoint): InstructionBreakpoint { + return { + ...raw, + id: existing?.id ?? UUID.uuid4(), + enabled: existing?.enabled ?? true, + }; + } + + export function is(thing: BaseBreakpoint): thing is InstructionBreakpoint { + const candidate = thing as InstructionBreakpoint; + return 'instructionReference' in candidate && typeof candidate.instructionReference === 'string'; + } +} diff --git a/packages/debug/src/browser/debug-frontend-application-contribution.ts b/packages/debug/src/browser/debug-frontend-application-contribution.ts index f66e131e07e34..e05801970c70c 100644 --- a/packages/debug/src/browser/debug-frontend-application-contribution.ts +++ b/packages/debug/src/browser/debug-frontend-application-contribution.ts @@ -51,6 +51,7 @@ import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; import { DebugFunctionBreakpoint } from './model/debug-function-breakpoint'; import { DebugBreakpoint } from './model/debug-breakpoint'; import { nls } from '@theia/core/lib/common/nls'; +import { DebugInstructionBreakpoint } from './model/debug-instruction-breakpoint'; export namespace DebugMenus { export const DEBUG = [...MAIN_MENU_BAR, '6_debug']; @@ -763,13 +764,13 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi }); registry.registerCommand(DebugCommands.REMOVE_BREAKPOINT, { execute: () => { - const selectedBreakpoint = this.selectedBreakpoint || this.selectedFunctionBreakpoint; + const selectedBreakpoint = this.selectedSettableBreakpoint; if (selectedBreakpoint) { selectedBreakpoint.remove(); } }, - isEnabled: () => !!this.selectedBreakpoint || !!this.selectedFunctionBreakpoint, - isVisible: () => !!this.selectedBreakpoint || !!this.selectedFunctionBreakpoint, + isEnabled: () => Boolean(this.selectedSettableBreakpoint), + isVisible: () => Boolean(this.selectedSettableBreakpoint), }); registry.registerCommand(DebugCommands.REMOVE_LOGPOINT, { execute: () => { @@ -1178,6 +1179,18 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi const breakpoint = this.selectedAnyBreakpoint; return breakpoint && breakpoint instanceof DebugFunctionBreakpoint ? breakpoint : undefined; } + get selectedInstructionBreakpoint(): DebugInstructionBreakpoint | undefined { + if (this.selectedAnyBreakpoint instanceof DebugInstructionBreakpoint) { + return this.selectedAnyBreakpoint; + } + } + + get selectedSettableBreakpoint(): DebugFunctionBreakpoint | DebugInstructionBreakpoint | DebugSourceBreakpoint | undefined { + const selected = this.selectedAnyBreakpoint; + if (selected instanceof DebugFunctionBreakpoint || selected instanceof DebugInstructionBreakpoint || selected instanceof DebugSourceBreakpoint) { + return selected; + } + } get variables(): DebugVariablesWidget | undefined { const { currentWidget } = this.shell; @@ -1431,5 +1444,4 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi return true; } - } diff --git a/packages/debug/src/browser/debug-frontend-module.ts b/packages/debug/src/browser/debug-frontend-module.ts index 7379687c8b852..1d9d44ea61bc0 100644 --- a/packages/debug/src/browser/debug-frontend-module.ts +++ b/packages/debug/src/browser/debug-frontend-module.ts @@ -61,6 +61,7 @@ import { QuickAccessContribution } from '@theia/core/lib/browser/quick-input/qui import { DebugViewModel } from './view/debug-view-model'; import { DebugToolBar } from './view/debug-toolbar-widget'; import { DebugSessionWidget } from './view/debug-session-widget'; +import { bindDisassemblyView } from './disassembly-view/disassembly-view-contribution'; export default new ContainerModule((bind: interfaces.Bind) => { bindContributionProvider(bind, DebugContribution); @@ -131,4 +132,5 @@ export default new ContainerModule((bind: interfaces.Bind) => { createWidget: () => subwidget.createWidget(container), })); } + bindDisassemblyView(bind); }); diff --git a/packages/debug/src/browser/debug-preferences.ts b/packages/debug/src/browser/debug-preferences.ts index 75c5a39d494cb..a3133f7819b48 100644 --- a/packages/debug/src/browser/debug-preferences.ts +++ b/packages/debug/src/browser/debug-preferences.ts @@ -60,6 +60,11 @@ export const debugPreferencesSchema: PreferenceSchema = { 'Always confirm if there are debug sessions.', ], default: 'never' + }, + 'debug.disassemblyView.showSourceCode': { + description: nls.localize('theia/debug/disassembly-view/show-source-code', 'Show Source Code in Disassembly View.'), + type: 'boolean', + default: true, } } }; @@ -71,6 +76,7 @@ export class DebugConfiguration { 'debug.inlineValues': boolean; 'debug.showInStatusBar': 'never' | 'always' | 'onFirstSessionStart'; 'debug.confirmOnExit': 'never' | 'always'; + 'debug.disassemblyView.showSourceCode': boolean; } export const DebugPreferenceContribution = Symbol('DebugPreferenceContribution'); diff --git a/packages/debug/src/browser/debug-session-manager.ts b/packages/debug/src/browser/debug-session-manager.ts index f48e0139d2f81..8d4d633574460 100644 --- a/packages/debug/src/browser/debug-session-manager.ts +++ b/packages/debug/src/browser/debug-session-manager.ts @@ -14,8 +14,6 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -/* eslint-disable @typescript-eslint/no-explicit-any */ - import { DisposableCollection, Emitter, Event, MessageService, ProgressService, WaitUntilEvent } from '@theia/core'; import { LabelProvider, ApplicationShell } from '@theia/core/lib/browser'; import { ContextKey, ContextKeyService } from '@theia/core/lib/browser/context-key-service'; @@ -38,6 +36,7 @@ import { TaskIdentifier } from '@theia/task/lib/common'; import { DebugSourceBreakpoint } from './model/debug-source-breakpoint'; import { DebugFunctionBreakpoint } from './model/debug-function-breakpoint'; import * as monaco from '@theia/monaco-editor-core'; +import { DebugInstructionBreakpoint } from './model/debug-instruction-breakpoint'; export interface WillStartDebugSession extends WaitUntilEvent { } @@ -56,8 +55,13 @@ export interface DidChangeBreakpointsEvent { uri: URI } +export interface DidFocusStackFrameEvent { + session: DebugSession; + frame: DebugStackFrame | undefined; +} + export interface DebugSessionCustomEvent { - readonly body?: any + readonly body?: any // eslint-disable-line @typescript-eslint/no-explicit-any readonly event: string readonly session: DebugSession } @@ -90,8 +94,11 @@ export class DebugSessionManager { protected readonly onDidReceiveDebugSessionCustomEventEmitter = new Emitter(); readonly onDidReceiveDebugSessionCustomEvent: Event = this.onDidReceiveDebugSessionCustomEventEmitter.event; + protected readonly onDidFocusStackFrameEmitter = new Emitter(); + readonly onDidFocusStackFrame = this.onDidFocusStackFrameEmitter.event; + protected readonly onDidChangeBreakpointsEmitter = new Emitter(); - readonly onDidChangeBreakpoints: Event = this.onDidChangeBreakpointsEmitter.event; + readonly onDidChangeBreakpoints = this.onDidChangeBreakpointsEmitter.event; protected fireDidChangeBreakpoints(event: DidChangeBreakpointsEvent): void { this.onDidChangeBreakpointsEmitter.fire(event); } @@ -417,6 +424,7 @@ export class DebugSessionManager { } this.fireDidChange(current); })); + this.disposeOnCurrentSessionChanged.push(current.onDidFocusStackFrame(frame => this.onDidFocusStackFrameEmitter.fire({ session: current, frame }))); } this.updateBreakpoints(previous, current); this.open(); @@ -475,6 +483,14 @@ export class DebugSessionManager { return this.breakpoints.getFunctionBreakpoints().map(origin => new DebugFunctionBreakpoint(origin, { labelProvider, breakpoints, editorManager })); } + getInstructionBreakpoints(session = this.currentSession): DebugInstructionBreakpoint[] { + if (session && session.state > DebugState.Initializing) { + return session.getInstructionBreakpoints(); + } + const { labelProvider, breakpoints, editorManager } = this; + return this.breakpoints.getInstructionBreakpoints().map(origin => new DebugInstructionBreakpoint(origin, { labelProvider, breakpoints, editorManager })); + } + getBreakpoints(session?: DebugSession): DebugSourceBreakpoint[]; getBreakpoints(uri: URI, session?: DebugSession): DebugSourceBreakpoint[]; getBreakpoints(arg?: URI | DebugSession, arg2?: DebugSession): DebugSourceBreakpoint[] { diff --git a/packages/debug/src/browser/debug-session.tsx b/packages/debug/src/browser/debug-session.tsx index 1c0bcde67e766..846a8694dcacf 100644 --- a/packages/debug/src/browser/debug-session.tsx +++ b/packages/debug/src/browser/debug-session.tsx @@ -42,6 +42,7 @@ import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { DebugContribution } from './debug-contribution'; import { waitForEvent } from '@theia/core/lib/common/promise-util'; import { WorkspaceService } from '@theia/workspace/lib/browser'; +import { DebugInstructionBreakpoint } from './model/debug-instruction-breakpoint'; export enum DebugState { Inactive, @@ -57,6 +58,10 @@ export class DebugSession implements CompositeTreeElement { protected fireDidChange(): void { this.onDidChangeEmitter.fire(undefined); } + protected readonly onDidFocusStackFrameEmitter = new Emitter(); + get onDidFocusStackFrame(): Event { + return this.onDidFocusStackFrameEmitter.event; + } protected readonly onDidChangeBreakpointsEmitter = new Emitter(); readonly onDidChangeBreakpoints: Event = this.onDidChangeBreakpointsEmitter.event; @@ -235,6 +240,7 @@ export class DebugSession implements CompositeTreeElement { this.fireDidChange(); if (thread) { this.toDisposeOnCurrentThread.push(thread.onDidChanged(() => this.fireDidChange())); + this.toDisposeOnCurrentThread.push(thread.onDidFocusStackFrame(frame => this.onDidFocusStackFrameEmitter.fire(frame))); // If this thread is missing stack frame information, then load that. this.updateFrames(); @@ -513,13 +519,15 @@ export class DebugSession implements CompositeTreeElement { } getFunctionBreakpoints(): DebugFunctionBreakpoint[] { - const breakpoints = []; - for (const breakpoint of this.getBreakpoints(BreakpointManager.FUNCTION_URI)) { - if (breakpoint instanceof DebugFunctionBreakpoint) { - breakpoints.push(breakpoint); - } + return this.getBreakpoints(BreakpointManager.FUNCTION_URI).filter((breakpoint): breakpoint is DebugFunctionBreakpoint => breakpoint instanceof DebugFunctionBreakpoint); + } + + getInstructionBreakpoints(): DebugInstructionBreakpoint[] { + if (this.capabilities.supportsInstructionBreakpoints) { + return this.getBreakpoints(BreakpointManager.FUNCTION_URI) + .filter((breakpoint): breakpoint is DebugInstructionBreakpoint => breakpoint instanceof DebugInstructionBreakpoint); } - return breakpoints; + return this.breakpoints.getInstructionBreakpoints().map(origin => new DebugInstructionBreakpoint(origin, this.asDebugBreakpointOptions())); } getBreakpoints(uri?: URI): DebugBreakpoint[] { @@ -614,6 +622,8 @@ export class DebugSession implements CompositeTreeElement { await this.sendExceptionBreakpoints(); } else if (affectedUri.toString() === BreakpointManager.FUNCTION_URI.toString()) { await this.sendFunctionBreakpoints(affectedUri); + } else if (affectedUri.toString() === BreakpointManager.INSTRUCTION_URI.toString()) { + await this.sendInstructionBreakpoints(); } else { await this.sendSourceBreakpoints(affectedUri, sourceModified); } @@ -640,7 +650,7 @@ export class DebugSession implements CompositeTreeElement { const response = await this.sendRequest('setFunctionBreakpoints', { breakpoints: enabled.map(b => b.origin.raw) }); - response.body.breakpoints.map((raw, index) => { + response.body.breakpoints.forEach((raw, index) => { // node debug adapter returns more breakpoints sometimes if (enabled[index]) { enabled[index].update({ raw }); @@ -679,7 +689,7 @@ export class DebugSession implements CompositeTreeElement { sourceModified, breakpoints: enabled.map(({ origin }) => origin.raw) }); - response.body.breakpoints.map((raw, index) => { + response.body.breakpoints.forEach((raw, index) => { // node debug adapter returns more breakpoints sometimes if (enabled[index]) { enabled[index].update({ raw }); @@ -705,6 +715,23 @@ export class DebugSession implements CompositeTreeElement { this.setSourceBreakpoints(affectedUri, all); } + protected async sendInstructionBreakpoints(): Promise { + if (!this.capabilities.supportsInstructionBreakpoints) { + return; + } + const all = this.breakpoints.getInstructionBreakpoints().map(breakpoint => new DebugInstructionBreakpoint(breakpoint, this.asDebugBreakpointOptions())); + const enabled = all.filter(breakpoint => breakpoint.enabled); + try { + const response = await this.sendRequest('setInstructionBreakpoints', { + breakpoints: enabled.map(renderable => renderable.origin), + }); + response.body.breakpoints.forEach((raw, index) => enabled[index]?.update({ raw })); + } catch { + enabled.forEach(breakpoint => breakpoint.update({ raw: { verified: false } })); + } + this.setBreakpoints(BreakpointManager.INSTRUCTION_URI, all); + } + protected setBreakpoints(uri: URI, breakpoints: DebugBreakpoint[]): void { this._breakpoints.set(uri.toString(), breakpoints); this.fireDidChangeBreakpoints(uri); diff --git a/packages/debug/src/browser/disassembly-view/disassembly-view-accessibility-provider.ts b/packages/debug/src/browser/disassembly-view/disassembly-view-accessibility-provider.ts new file mode 100644 index 0000000000000..44b89dfe9fb87 --- /dev/null +++ b/packages/debug/src/browser/disassembly-view/disassembly-view-accessibility-provider.ts @@ -0,0 +1,43 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { nls } from '@theia/core'; +import { IListAccessibilityProvider } from '@theia/monaco-editor-core/esm/vs/base/browser/ui/list/listWidget'; +import { DisassembledInstructionEntry } from './disassembly-view-utilities'; + +// This file is adapted from https://github.com/microsoft/vscode/blob/c061ce5c24fc480342fbc5f23244289d633c56eb/src/vs/workbench/contrib/debug/browser/disassemblyView.ts + +export class AccessibilityProvider implements IListAccessibilityProvider { + + getWidgetAriaLabel(): string { + return nls.localize('disassemblyView', 'Disassembly View'); + } + + getAriaLabel(element: DisassembledInstructionEntry): string | null { + let label = ''; + + const instruction = element.instruction; + if (instruction.address !== '-1') { + label += `${nls.localize('theia/debug/instructionAddress', 'Address')}: ${instruction.address}`; + } + if (instruction.instructionBytes) { + label += `, ${nls.localize('theia/debug/instructionBytes', 'Bytes')}: ${instruction.instructionBytes}`; + } + label += `, ${nls.localize('theia/debug/instructionText', 'Instruction')}: ${instruction.instruction}`; + + return label; + } +} diff --git a/packages/debug/src/browser/disassembly-view/disassembly-view-breakpoint-renderer.ts b/packages/debug/src/browser/disassembly-view/disassembly-view-breakpoint-renderer.ts new file mode 100644 index 0000000000000..c688b5435f8d9 --- /dev/null +++ b/packages/debug/src/browser/disassembly-view/disassembly-view-breakpoint-renderer.ts @@ -0,0 +1,119 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { append, $, addStandardDisposableListener } from '@theia/monaco-editor-core/esm/vs/base/browser/dom'; +import { ITableRenderer } from '@theia/monaco-editor-core/esm/vs/base/browser/ui/table/table'; +import { dispose } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle'; +import { BreakpointManager } from '../breakpoint/breakpoint-manager'; +import { BreakpointColumnTemplateData, DisassembledInstructionEntry, DisassemblyViewRendererReference } from './disassembly-view-utilities'; + +// This file is adapted from https://github.com/microsoft/vscode/blob/c061ce5c24fc480342fbc5f23244289d633c56eb/src/vs/workbench/contrib/debug/browser/disassemblyView.ts + +export class BreakpointRenderer implements ITableRenderer { + + static readonly TEMPLATE_ID = 'breakpoint'; + + templateId: string = BreakpointRenderer.TEMPLATE_ID; + + protected readonly _breakpointIcon = 'codicon-debug-breakpoint'; + protected readonly _breakpointDisabledIcon = 'codicon-debug-breakpoint-disabled'; + protected readonly _breakpointHintIcon = 'codicon-debug-hint'; + protected readonly _debugStackframe = 'codicon-debug-stackframe'; + protected readonly _debugStackframeFocused = 'codicon-debug-stackframe-focused'; + + constructor( + protected readonly _disassemblyView: DisassemblyViewRendererReference, + protected readonly _debugService: BreakpointManager, + ) { } + + renderTemplate(container: HTMLElement): BreakpointColumnTemplateData { + // align from the bottom so that it lines up with instruction when source code is present. + container.style.alignSelf = 'flex-end'; + + const icon = append(container, $('.disassembly-view')); + icon.classList.add('codicon'); + icon.style.display = 'flex'; + icon.style.alignItems = 'center'; + icon.style.justifyContent = 'center'; + icon.style.height = this._disassemblyView.fontInfo.lineHeight + 'px'; + + const currentElement: { element?: DisassembledInstructionEntry } = { element: undefined }; + + const disposables = [ + this._disassemblyView.onDidChangeStackFrame(() => this.rerenderDebugStackframe(icon, currentElement.element)), + addStandardDisposableListener(container, 'mouseover', () => { + if (currentElement.element?.allowBreakpoint) { + icon.classList.add(this._breakpointHintIcon); + } + }), + addStandardDisposableListener(container, 'mouseout', () => { + if (currentElement.element?.allowBreakpoint) { + icon.classList.remove(this._breakpointHintIcon); + } + }), + addStandardDisposableListener(container, 'click', () => { + if (currentElement.element?.allowBreakpoint) { + // click show hint while waiting for BP to resolve. + icon.classList.add(this._breakpointHintIcon); + if (currentElement.element.isBreakpointSet) { + this._debugService.removeInstructionBreakpoint(currentElement.element.instruction.address); + + } else if (currentElement.element.allowBreakpoint && !currentElement.element.isBreakpointSet) { + this._debugService.addInstructionBreakpoint(currentElement.element.instruction.address, 0); + } + } + }) + ]; + + return { currentElement, icon, disposables }; + } + + renderElement(element: DisassembledInstructionEntry, index: number, templateData: BreakpointColumnTemplateData, height: number | undefined): void { + templateData.currentElement.element = element; + this.rerenderDebugStackframe(templateData.icon, element); + } + + disposeTemplate(templateData: BreakpointColumnTemplateData): void { + dispose(templateData.disposables); + templateData.disposables = []; + } + + protected rerenderDebugStackframe(icon: HTMLElement, element?: DisassembledInstructionEntry): void { + if (element?.instruction.address === this._disassemblyView.focusedCurrentInstructionAddress) { + icon.classList.add(this._debugStackframe); + } else if (element?.instruction.address === this._disassemblyView.focusedInstructionAddress) { + icon.classList.add(this._debugStackframeFocused); + } else { + icon.classList.remove(this._debugStackframe); + icon.classList.remove(this._debugStackframeFocused); + } + + icon.classList.remove(this._breakpointHintIcon); + + if (element?.isBreakpointSet) { + if (element.isBreakpointEnabled) { + icon.classList.add(this._breakpointIcon); + icon.classList.remove(this._breakpointDisabledIcon); + } else { + icon.classList.remove(this._breakpointIcon); + icon.classList.add(this._breakpointDisabledIcon); + } + } else { + icon.classList.remove(this._breakpointIcon); + icon.classList.remove(this._breakpointDisabledIcon); + } + } +} diff --git a/packages/debug/src/browser/disassembly-view/disassembly-view-contribution.ts b/packages/debug/src/browser/disassembly-view/disassembly-view-contribution.ts new file mode 100644 index 0000000000000..5d6ea18e35164 --- /dev/null +++ b/packages/debug/src/browser/disassembly-view/disassembly-view-contribution.ts @@ -0,0 +1,109 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify'; +import { AbstractViewContribution, bindViewContribution, WidgetFactory } from '@theia/core/lib/browser'; +import { DisassemblyViewWidget } from './disassembly-view-widget'; +import { Command, CommandRegistry, MenuModelRegistry, nls } from '@theia/core'; +import { DebugService } from '../../common/debug-service'; +import { EditorManager, EDITOR_CONTEXT_MENU } from '@theia/editor/lib/browser'; +import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; +import { DebugSessionManager } from '../debug-session-manager'; +import { DebugStackFrame } from '../model/debug-stack-frame'; +import { DebugSession, DebugState } from '../debug-session'; +import { DebugStackFramesWidget } from '../view/debug-stack-frames-widget'; + +export const OPEN_DISASSEMBLY_VIEW_COMMAND: Command = { + id: 'open-disassembly-view', + label: nls.localize('theia/debug/open-disassembly-view', 'Open Disassembly View') +}; + +export const LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST = 'languageSupportsDisassembleRequest'; +export const FOCUSED_STACK_FRAME_HAS_INSTRUCTION_REFERENCE = 'focusedStackFrameHasInstructionReference'; +export const DISASSEMBLE_REQUEST_SUPPORTED = 'disassembleRequestSupported'; +export const DISASSEMBLY_VIEW_FOCUS = 'disassemblyViewFocus'; + +@injectable() +export class DisassemblyViewContribution extends AbstractViewContribution { + @inject(DebugService) protected readonly debugService: DebugService; + @inject(EditorManager) protected readonly editorManager: EditorManager; + @inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService; + @inject(DebugSessionManager) protected readonly debugSessionManager: DebugSessionManager; + + constructor() { + super({ + widgetId: DisassemblyViewWidget.ID, + widgetName: 'Disassembly View', + defaultWidgetOptions: { area: 'main' } + }); + } + + @postConstruct() + protected init(): void { + let activeEditorChangeCancellation = { cancelled: false }; + const updateLanguageSupportsDisassemblyKey = async () => { + const editor = this.editorManager.currentEditor; + activeEditorChangeCancellation.cancelled = true; + const localCancellation = activeEditorChangeCancellation = { cancelled: false }; + + const language = editor?.editor.document.languageId; + const debuggersForLanguage = language && await this.debugService.getDebuggersForLanguage(language); + if (!localCancellation.cancelled) { + this.contextKeyService.setContext(LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST, Boolean(debuggersForLanguage?.length)); + } + }; + this.editorManager.onCurrentEditorChanged(updateLanguageSupportsDisassemblyKey); + this.debugService.onDidChangeDebuggers?.(updateLanguageSupportsDisassemblyKey); + let lastSession: DebugSession | undefined; + let lastFrame: DebugStackFrame | undefined; + this.debugSessionManager.onDidChange(() => { + const { currentFrame, currentSession } = this.debugSessionManager; + if (currentFrame !== lastFrame) { + lastFrame = currentFrame; + this.contextKeyService.setContext(FOCUSED_STACK_FRAME_HAS_INSTRUCTION_REFERENCE, Boolean(currentFrame?.raw.instructionPointerReference)); + } + if (currentSession !== lastSession) { + lastSession = currentSession; + this.contextKeyService.setContext(DISASSEMBLE_REQUEST_SUPPORTED, Boolean(currentSession?.capabilities.supportsDisassembleRequest)); + } + }); + this.shell.onDidChangeCurrentWidget(widget => { + this.contextKeyService.setContext(DISASSEMBLY_VIEW_FOCUS, widget instanceof DisassemblyViewWidget); + }); + } + + override registerCommands(commands: CommandRegistry): void { + commands.registerCommand(OPEN_DISASSEMBLY_VIEW_COMMAND, { + isEnabled: () => this.debugSessionManager.inDebugMode + && this.debugSessionManager.state === DebugState.Stopped + && this.contextKeyService.match('focusedStackFrameHasInstructionReference'), + execute: () => this.openView({ activate: true }), + }); + } + + override registerMenus(menus: MenuModelRegistry): void { + menus.registerMenuAction(DebugStackFramesWidget.CONTEXT_MENU, + { commandId: OPEN_DISASSEMBLY_VIEW_COMMAND.id, label: OPEN_DISASSEMBLY_VIEW_COMMAND.label }); + menus.registerMenuAction([...EDITOR_CONTEXT_MENU, 'a_debug'], + { commandId: OPEN_DISASSEMBLY_VIEW_COMMAND.id, label: OPEN_DISASSEMBLY_VIEW_COMMAND.label, when: LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST }); + } +} + +export function bindDisassemblyView(bind: interfaces.Bind): void { + bind(DisassemblyViewWidget).toSelf(); + bind(WidgetFactory).toDynamicValue(({ container }) => ({ id: DisassemblyViewWidget.ID, createWidget: () => container.get(DisassemblyViewWidget) })); + bindViewContribution(bind, DisassemblyViewContribution); +} diff --git a/packages/debug/src/browser/disassembly-view/disassembly-view-instruction-renderer.ts b/packages/debug/src/browser/disassembly-view/disassembly-view-instruction-renderer.ts new file mode 100644 index 0000000000000..42b82f0156c95 --- /dev/null +++ b/packages/debug/src/browser/disassembly-view/disassembly-view-instruction-renderer.ts @@ -0,0 +1,245 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { open, OpenerService } from '@theia/core/lib/browser'; +import { URI as TheiaURI } from '@theia/core/lib/common/uri'; +import { EditorOpenerOptions } from '@theia/editor/lib/browser'; +import { IDisposable, Uri as URI } from '@theia/monaco-editor-core'; +import { $, addStandardDisposableListener, append } from '@theia/monaco-editor-core/esm/vs/base/browser/dom'; +import { ITableRenderer } from '@theia/monaco-editor-core/esm/vs/base/browser/ui/table/table'; +import { Color } from '@theia/monaco-editor-core/esm/vs/base/common/color'; +import { Disposable, dispose } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle'; +import { isAbsolute } from '@theia/monaco-editor-core/esm/vs/base/common/path'; +import { Constants } from '@theia/monaco-editor-core/esm/vs/base/common/uint'; +import { applyFontInfo } from '@theia/monaco-editor-core/esm/vs/editor/browser/config/domFontInfo'; +import { createStringBuilder } from '@theia/monaco-editor-core/esm/vs/editor/common/core/stringBuilder'; +import { ITextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model'; +import { ITextModelService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/resolverService'; +import { IThemeService } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { DebugSource } from '../model/debug-source'; +import { DisassembledInstructionEntry, DisassemblyViewRendererReference, InstructionColumnTemplateData } from './disassembly-view-utilities'; + +// This file is adapted from https://github.com/microsoft/vscode/blob/c061ce5c24fc480342fbc5f23244289d633c56eb/src/vs/workbench/contrib/debug/browser/disassemblyView.ts + +const topStackFrameColor = 'editor.stackFrameHighlightBackground'; +const focusedStackFrameColor = 'editor.focusedStackFrameHighlightBackground'; + +export class InstructionRenderer extends Disposable implements ITableRenderer { + + static readonly TEMPLATE_ID = 'instruction'; + + protected static readonly INSTRUCTION_ADDR_MIN_LENGTH = 25; + protected static readonly INSTRUCTION_BYTES_MIN_LENGTH = 30; + + templateId: string = InstructionRenderer.TEMPLATE_ID; + + protected _topStackFrameColor: Color | undefined; + protected _focusedStackFrameColor: Color | undefined; + + constructor( + protected readonly _disassemblyView: DisassemblyViewRendererReference, + protected readonly openerService: OpenerService, + protected readonly uriService: { asCanonicalUri(uri: URI): URI }, + @IThemeService themeService: IThemeService, + @ITextModelService protected readonly textModelService: ITextModelService, + ) { + super(); + + this._topStackFrameColor = themeService.getColorTheme().getColor(topStackFrameColor); + this._focusedStackFrameColor = themeService.getColorTheme().getColor(focusedStackFrameColor); + + this._register(themeService.onDidColorThemeChange(e => { + this._topStackFrameColor = e.getColor(topStackFrameColor); + this._focusedStackFrameColor = e.getColor(focusedStackFrameColor); + })); + } + + renderTemplate(container: HTMLElement): InstructionColumnTemplateData { + const sourcecode = append(container, $('.sourcecode')); + const instruction = append(container, $('.instruction')); + this.applyFontInfo(sourcecode); + this.applyFontInfo(instruction); + const currentElement: { element?: DisassembledInstructionEntry } = { element: undefined }; + const cellDisposable: IDisposable[] = []; + + const disposables = [ + this._disassemblyView.onDidChangeStackFrame(() => this.rerenderBackground(instruction, sourcecode, currentElement.element)), + addStandardDisposableListener(sourcecode, 'dblclick', () => this.openSourceCode(currentElement.element?.instruction!)), + ]; + + return { currentElement, instruction, sourcecode, cellDisposable, disposables }; + } + + renderElement(element: DisassembledInstructionEntry, index: number, templateData: InstructionColumnTemplateData, height: number | undefined): void { + this.renderElementInner(element, index, templateData, height); + } + + protected async renderElementInner(element: DisassembledInstructionEntry, index: number, column: InstructionColumnTemplateData, height: number | undefined): Promise { + column.currentElement.element = element; + const instruction = element.instruction; + column.sourcecode.innerText = ''; + const sb = createStringBuilder(1000); + + if (this._disassemblyView.isSourceCodeRender && instruction.location?.path && instruction.line) { + const sourceURI = this.getUriFromSource(instruction); + + if (sourceURI) { + let textModel: ITextModel | undefined = undefined; + const sourceSB = createStringBuilder(10000); + const ref = await this.textModelService.createModelReference(sourceURI); + textModel = ref.object.textEditorModel; + column.cellDisposable.push(ref); + + // templateData could have moved on during async. Double check if it is still the same source. + if (textModel && column.currentElement.element === element) { + let lineNumber = instruction.line; + + while (lineNumber && lineNumber >= 1 && lineNumber <= textModel.getLineCount()) { + const lineContent = textModel.getLineContent(lineNumber); + sourceSB.appendASCIIString(` ${lineNumber}: `); + sourceSB.appendASCIIString(lineContent + '\n'); + + if (instruction.endLine && lineNumber < instruction.endLine) { + lineNumber++; + continue; + } + + break; + } + + column.sourcecode.innerText = sourceSB.build(); + } + } + } + + let spacesToAppend = 10; + + if (instruction.address !== '-1') { + sb.appendASCIIString(instruction.address); + if (instruction.address.length < InstructionRenderer.INSTRUCTION_ADDR_MIN_LENGTH) { + spacesToAppend = InstructionRenderer.INSTRUCTION_ADDR_MIN_LENGTH - instruction.address.length; + } + for (let i = 0; i < spacesToAppend; i++) { + sb.appendASCIIString(' '); + } + } + + if (instruction.instructionBytes) { + sb.appendASCIIString(instruction.instructionBytes); + spacesToAppend = 10; + if (instruction.instructionBytes.length < InstructionRenderer.INSTRUCTION_BYTES_MIN_LENGTH) { + spacesToAppend = InstructionRenderer.INSTRUCTION_BYTES_MIN_LENGTH - instruction.instructionBytes.length; + } + for (let i = 0; i < spacesToAppend; i++) { + sb.appendASCIIString(' '); + } + } + + sb.appendASCIIString(instruction.instruction); + column.instruction.innerText = sb.build(); + + this.rerenderBackground(column.instruction, column.sourcecode, element); + } + + disposeElement(element: DisassembledInstructionEntry, index: number, templateData: InstructionColumnTemplateData, height: number | undefined): void { + dispose(templateData.cellDisposable); + templateData.cellDisposable = []; + } + + disposeTemplate(templateData: InstructionColumnTemplateData): void { + dispose(templateData.disposables); + templateData.disposables = []; + } + + protected rerenderBackground(instruction: HTMLElement, sourceCode: HTMLElement, element?: DisassembledInstructionEntry): void { + if (element && this._disassemblyView.currentInstructionAddresses.includes(element.instruction.address)) { + instruction.style.background = this._topStackFrameColor?.toString() || 'transparent'; + } else if (element?.instruction.address === this._disassemblyView.focusedInstructionAddress) { + instruction.style.background = this._focusedStackFrameColor?.toString() || 'transparent'; + } else { + instruction.style.background = 'transparent'; + } + } + + protected openSourceCode(instruction: DebugProtocol.DisassembledInstruction | undefined): void { + if (instruction) { + const sourceURI = this.getUriFromSource(instruction); + const selection: EditorOpenerOptions['selection'] = instruction.endLine ? { + start: { line: instruction.line!, character: instruction.column ?? 0 }, + end: { line: instruction.endLine, character: instruction.endColumn ?? Constants.MAX_SAFE_SMALL_INTEGER } + } : { + start: { line: instruction.line!, character: instruction.column ?? 0 }, + end: { line: instruction.line, character: instruction.endColumn ?? Constants.MAX_SAFE_SMALL_INTEGER } + }; + + const openerOptions: EditorOpenerOptions = { + selection, + mode: 'activate', + widgetOptions: { area: 'main' } + }; + open(this.openerService, new TheiaURI(sourceURI), openerOptions); + } + } + + protected getUriFromSource(instruction: DebugProtocol.DisassembledInstruction): URI { + // Try to resolve path before consulting the debugSession. + const path = instruction.location!.path; + if (path && isUri(path)) { // path looks like a uri + return this.uriService.asCanonicalUri(URI.parse(path)); + } + // assume a filesystem path + if (path && isAbsolute(path)) { + return this.uriService.asCanonicalUri(URI.file(path)); + } + + return getUriFromSource(instruction.location!, instruction.location!.path, this._disassemblyView.debugSession!.id, this.uriService); + } + + protected applyFontInfo(element: HTMLElement): void { + applyFontInfo(element, this._disassemblyView.fontInfo); + element.style.whiteSpace = 'pre'; + } +} + +export function getUriFromSource(raw: DebugProtocol.Source, path: string | undefined, sessionId: string, uriIdentityService: { asCanonicalUri(uri: URI): URI }): URI { + if (typeof raw.sourceReference === 'number' && raw.sourceReference > 0) { + return URI.from({ + scheme: DebugSource.SCHEME, + path, + query: `session=${sessionId}&ref=${raw.sourceReference}` + }); + } + + if (path && isUri(path)) { // path looks like a uri + return uriIdentityService.asCanonicalUri(URI.parse(path)); + } + // assume a filesystem path + if (path && isAbsolute(path)) { + return uriIdentityService.asCanonicalUri(URI.file(path)); + } + // path is relative: since VS Code cannot deal with this by itself + // create a debug url that will result in a DAP 'source' request when the url is resolved. + return uriIdentityService.asCanonicalUri(URI.from({ + scheme: DebugSource.SCHEME, + path, + query: `session=${sessionId}` + })); +} + +function isUri(candidate: string | undefined): boolean { + return Boolean(candidate && candidate.match(DebugSource.SCHEME_PATTERN)); +} diff --git a/packages/debug/src/browser/disassembly-view/disassembly-view-table-delegate.ts b/packages/debug/src/browser/disassembly-view/disassembly-view-table-delegate.ts new file mode 100644 index 0000000000000..c52adefc80fc5 --- /dev/null +++ b/packages/debug/src/browser/disassembly-view/disassembly-view-table-delegate.ts @@ -0,0 +1,39 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { ITableVirtualDelegate } from '@theia/monaco-editor-core/esm/vs/base/browser/ui/table/table'; +import { BareFontInfo } from '@theia/monaco-editor-core/esm/vs/editor/common/config/fontInfo'; +import { DisassembledInstructionEntry } from './disassembly-view-utilities'; + +// This file is adapted from https://github.com/microsoft/vscode/blob/c061ce5c24fc480342fbc5f23244289d633c56eb/src/vs/workbench/contrib/debug/browser/disassemblyView.ts + +export class DisassemblyViewTableDelegate implements ITableVirtualDelegate { + constructor(protected readonly fontInfoProvider: { fontInfo: BareFontInfo, isSourceCodeRender: boolean }) { } + + headerRowHeight = 0; + + getHeight(row: DisassembledInstructionEntry): number { + if (this.fontInfoProvider.isSourceCodeRender && row.instruction.location?.path && row.instruction.line !== undefined) { + if (row.instruction.endLine !== undefined) { + return this.fontInfoProvider.fontInfo.lineHeight + (row.instruction.endLine - row.instruction.line + 2); + } else { + return this.fontInfoProvider.fontInfo.lineHeight * 2; + } + } + + return this.fontInfoProvider.fontInfo.lineHeight; + } +} diff --git a/packages/debug/src/browser/disassembly-view/disassembly-view-utilities.ts b/packages/debug/src/browser/disassembly-view/disassembly-view-utilities.ts new file mode 100644 index 0000000000000..263fdc2713d21 --- /dev/null +++ b/packages/debug/src/browser/disassembly-view/disassembly-view-utilities.ts @@ -0,0 +1,55 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { IDisposable, IEvent } from '@theia/monaco-editor-core'; +import { BareFontInfo } from '@theia/monaco-editor-core/esm/vs/editor/common/config/fontInfo'; +import { DebugProtocol } from 'vscode-debugprotocol'; + +export interface DisassemblyViewRendererReference { + onDidChangeStackFrame: IEvent; + isSourceCodeRender: boolean; + currentInstructionAddresses: Array; + focusedInstructionAddress: string | undefined; + focusedCurrentInstructionAddress: string | undefined; + debugSession: { id: string } | undefined; + fontInfo: BareFontInfo; +} + +// The rest of the file is adapted from https://github.com/microsoft/vscode/blob/c061ce5c24fc480342fbc5f23244289d633c56eb/src/vs/workbench/contrib/debug/browser/disassemblyView.ts +export interface DisassembledInstructionEntry { + allowBreakpoint: boolean; + isBreakpointSet: boolean; + isBreakpointEnabled: boolean; + instruction: DebugProtocol.DisassembledInstruction; + instructionAddress?: bigint; +} + +export interface InstructionColumnTemplateData { + currentElement: { element?: DisassembledInstructionEntry }; + // TODO: hover widget? + instruction: HTMLElement; + sourcecode: HTMLElement; + // disposed when cell is closed. + cellDisposable: IDisposable[]; + // disposed when template is closed. + disposables: IDisposable[]; +} + +export interface BreakpointColumnTemplateData { + currentElement: { element?: DisassembledInstructionEntry }; + icon: HTMLElement; + disposables: IDisposable[]; +} diff --git a/packages/debug/src/browser/disassembly-view/disassembly-view-widget.ts b/packages/debug/src/browser/disassembly-view/disassembly-view-widget.ts new file mode 100644 index 0000000000000..958d93da6a6cd --- /dev/null +++ b/packages/debug/src/browser/disassembly-view/disassembly-view-widget.ts @@ -0,0 +1,464 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +import { BaseWidget, LabelProvider, Message, OpenerService, Widget } from '@theia/core/lib/browser'; +import { ArrayUtils } from '@theia/core/lib/common/types'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { InstructionBreakpoint } from '../breakpoint/breakpoint-marker'; +import { BreakpointManager } from '../breakpoint/breakpoint-manager'; +import { DebugSessionManager } from '../debug-session-manager'; +import { Emitter, IDisposable, IRange, Range, Uri } from '@theia/monaco-editor-core'; +import { nls } from '@theia/core'; +import { BareFontInfo } from '@theia/monaco-editor-core/esm/vs/editor/common/config/fontInfo'; +import { WorkbenchTable } from '@theia/monaco-editor-core/esm/vs/platform/list/browser/listService'; +import { DebugState, DebugSession } from '../debug-session'; +import { EditorPreferences } from '@theia/editor/lib/browser'; +import { PixelRatio } from '@theia/monaco-editor-core/esm/vs/base/browser/browser'; +import { DebugPreferences } from '../debug-preferences'; +import { DebugThread } from '../model/debug-thread'; +import { Event } from '@theia/monaco-editor-core/esm/vs/base/common/event'; +import { DisassembledInstructionEntry } from './disassembly-view-utilities'; +import { DisassemblyViewTableDelegate } from './disassembly-view-table-delegate'; +import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; +import { InstructionRenderer } from './disassembly-view-instruction-renderer'; +import { IInstantiationService } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/instantiation'; +import { BreakpointRenderer } from './disassembly-view-breakpoint-renderer'; +import { AccessibilityProvider } from './disassembly-view-accessibility-provider'; +import { editorBackground } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/colorRegistry'; +import { Dimension } from '@theia/monaco-editor-core/esm/vs/base/browser/dom'; +import { URI } from '@theia/core/lib/common/uri'; + +// This file is adapted from https://github.com/microsoft/vscode/blob/c061ce5c24fc480342fbc5f23244289d633c56eb/src/vs/workbench/contrib/debug/browser/disassemblyView.ts + +// Special entry as a placeholer when disassembly is not available +const disassemblyNotAvailable: DisassembledInstructionEntry = { + allowBreakpoint: false, + isBreakpointSet: false, + isBreakpointEnabled: false, + instruction: { + address: '-1', + instruction: nls.localize('theia/debug/instructionNotAvailable', 'Disassembly not available.') + }, + instructionAddress: BigInt(-1) +} as const; + +@injectable() +export class DisassemblyViewWidget extends BaseWidget { + static readonly ID = 'disassembly-view-widget'; + protected static readonly NUM_INSTRUCTIONS_TO_LOAD = 50; + protected readonly iconReferenceUri = new URI().withScheme('file').withPath('disassembly-view.disassembly-view'); + + @inject(BreakpointManager) protected readonly breakpointManager: BreakpointManager; + @inject(DebugSessionManager) protected readonly debugSessionManager: DebugSessionManager; + @inject(EditorPreferences) protected readonly editorPreferences: EditorPreferences; + @inject(DebugPreferences) protected readonly debugPreferences: DebugPreferences; + @inject(OpenerService) protected readonly openerService: OpenerService; + @inject(LabelProvider) protected readonly labelProvider: LabelProvider; + + protected _fontInfo: BareFontInfo; + protected _disassembledInstructions: WorkbenchTable | undefined = undefined; + protected _onDidChangeStackFrame = new Emitter(); + protected _previousDebuggingState: DebugState; + protected _instructionBpList: readonly InstructionBreakpoint[] = []; + protected _enableSourceCodeRender: boolean = true; + protected _loadingLock: boolean = false; + + @postConstruct() + protected init(): void { + this.id = DisassemblyViewWidget.ID; + this.addClass(DisassemblyViewWidget.ID); + this.title.closable = true; + this.title.label = 'Disassembly'; + const updateIcon = () => this.title.iconClass = this.labelProvider.getIcon(this.iconReferenceUri) + ' file-icon'; + updateIcon(); + this.toDispose.push(this.labelProvider.onDidChange(updateIcon)); + this.node.tabIndex = -1; + this.node.style.outline = 'none'; + this._previousDebuggingState = this.debugSessionManager.currentSession?.state ?? DebugState.Inactive; + this._fontInfo = BareFontInfo.createFromRawSettings(this.toFontInfo(), PixelRatio.value); + this.editorPreferences.onPreferenceChanged(() => this._fontInfo = BareFontInfo.createFromRawSettings(this.toFontInfo(), PixelRatio.value)); + this.debugPreferences.onPreferenceChanged(e => { + if (e.preferenceName === 'debug.disassemblyView.showSourceCode' && e.newValue !== this._enableSourceCodeRender) { + this._enableSourceCodeRender = e.newValue; + this.reloadDisassembly(undefined); + } else { + this._disassembledInstructions?.rerender(); + } + }); + this.createPane(); + } + + get fontInfo(): BareFontInfo { return this._fontInfo; } + + get currentInstructionAddresses(): Array { + return this.debugSessionManager.sessions + .map(session => session.getThreads(() => true)) + .reduce((prev, curr) => prev.concat(Array.from(curr)), []) + .map(thread => thread.topFrame) + .map(frame => frame?.raw.instructionPointerReference); + } + + get focusedCurrentInstructionAddress(): string | undefined { + return this.debugSessionManager.currentFrame?.thread.topFrame?.raw.instructionPointerReference; + } + + get isSourceCodeRender(): boolean { return this._enableSourceCodeRender; } + + get debugSession(): DebugSession | undefined { return this.debugSessionManager.currentSession; } + + get focusedInstructionAddress(): string | undefined { + return this.debugSessionManager.currentFrame?.raw.instructionPointerReference; + } + + get onDidChangeStackFrame(): Event { return this._onDidChangeStackFrame.event; } + + protected createPane(): void { + this._enableSourceCodeRender = this.debugPreferences['debug.disassemblyView.showSourceCode']; + const monacoInstantiationService = StandaloneServices.initialize({}); + const tableDelegate = new DisassemblyViewTableDelegate(this); + const instructionRenderer = monacoInstantiationService.createInstance(InstructionRenderer, this, this.openerService, { asCanonicalUri(thing: Uri): Uri { return thing; } }); + this.toDispose.push(instructionRenderer); + this.getTable(monacoInstantiationService, tableDelegate, instructionRenderer); + this.reloadDisassembly(); + this._register(this._disassembledInstructions!.onDidScroll(e => { + if (this._loadingLock) { + return; + } + + if (e.oldScrollTop > e.scrollTop && e.scrollTop < e.height) { + this._loadingLock = true; + const topElement = Math.floor(e.scrollTop / this.fontInfo.lineHeight) + DisassemblyViewWidget.NUM_INSTRUCTIONS_TO_LOAD; + this.scrollUp_LoadDisassembledInstructions(DisassemblyViewWidget.NUM_INSTRUCTIONS_TO_LOAD).then(success => { + if (success) { + this._disassembledInstructions!.reveal(topElement, 0); + } + this._loadingLock = false; + }); + } else if (e.oldScrollTop < e.scrollTop && e.scrollTop + e.height > e.scrollHeight - e.height) { + this._loadingLock = true; + this.scrollDown_LoadDisassembledInstructions(DisassemblyViewWidget.NUM_INSTRUCTIONS_TO_LOAD).then(() => { this._loadingLock = false; }); + } + })); + this._register(this.debugSessionManager.onDidFocusStackFrame(() => { + if (this._disassembledInstructions) { + this.goToAddress(); + this._onDidChangeStackFrame.fire(); + } + })); + this._register(this.breakpointManager.onDidChangeInstructionBreakpoints(bpEvent => { + if (bpEvent && this._disassembledInstructions) { + // draw viewable BP + let changed = false; + bpEvent.added?.forEach(bp => { + if (InstructionBreakpoint.is(bp)) { + const index = this.getIndexFromAddress(bp.instructionReference); + if (index >= 0) { + this._disassembledInstructions!.row(index).isBreakpointSet = true; + this._disassembledInstructions!.row(index).isBreakpointEnabled = bp.enabled; + changed = true; + } + } + }); + + bpEvent.removed?.forEach(bp => { + if (InstructionBreakpoint.is(bp)) { + const index = this.getIndexFromAddress(bp.instructionReference); + if (index >= 0) { + this._disassembledInstructions!.row(index).isBreakpointSet = false; + changed = true; + } + } + }); + + bpEvent.changed?.forEach(bp => { + if (InstructionBreakpoint.is(bp)) { + const index = this.getIndexFromAddress(bp.instructionReference); + if (index >= 0) { + if (this._disassembledInstructions!.row(index).isBreakpointEnabled !== bp.enabled) { + this._disassembledInstructions!.row(index).isBreakpointEnabled = bp.enabled; + changed = true; + } + } + } + }); + + // get an updated list so that items beyond the current range would render when reached. + this._instructionBpList = this.breakpointManager.getInstructionBreakpoints(); + + if (changed) { + this._onDidChangeStackFrame.fire(); + } + } + })); + + // This would like to be more specific: onDidChangeState + this._register(this.debugSessionManager.onDidChange(() => { + const state = this.debugSession?.state; + + if ((state === DebugState.Running || state === DebugState.Stopped) && + (this._previousDebuggingState !== DebugState.Running && this._previousDebuggingState !== DebugState.Stopped)) { + // Just started debugging, clear the view + this._disassembledInstructions?.splice(0, this._disassembledInstructions.length, [disassemblyNotAvailable]); + this._enableSourceCodeRender = this.debugPreferences['debug.disassemblyView.showSourceCode']; + } + if (state !== undefined && state !== this._previousDebuggingState) { + this._previousDebuggingState = state; + } + })); + } + + protected getTable( + monacoInstantiationService: IInstantiationService, + tableDelegate: DisassemblyViewTableDelegate, + instructionRenderer: InstructionRenderer + ): WorkbenchTable { + return this._disassembledInstructions = this._register(monacoInstantiationService.createInstance(WorkbenchTable, + 'DisassemblyView', this.node, tableDelegate, + [ + { + label: '', + tooltip: '', + weight: 0, + minimumWidth: this.fontInfo.lineHeight, + maximumWidth: this.fontInfo.lineHeight, + templateId: BreakpointRenderer.TEMPLATE_ID, + project(row: DisassembledInstructionEntry): DisassembledInstructionEntry { return row; } + }, + { + label: nls.localize('theia/disassembly-view/disassemblyTableColumnLabel', 'instructions'), + tooltip: '', + weight: 0.3, + templateId: InstructionRenderer.TEMPLATE_ID, + project(row: DisassembledInstructionEntry): DisassembledInstructionEntry { return row; } + }, + ], + [ + new BreakpointRenderer(this, this.breakpointManager), + instructionRenderer, + ], + { + identityProvider: { getId: (e: DisassembledInstructionEntry) => e.instruction.address }, + horizontalScrolling: false, + overrideStyles: { + listBackground: editorBackground + }, + multipleSelectionSupport: false, + setRowLineHeight: false, + openOnSingleClick: false, + accessibilityProvider: new AccessibilityProvider(), + mouseSupport: false + } + )) as WorkbenchTable; + } + + adjustLayout(dimension: Dimension): void { + if (this._disassembledInstructions) { + this._disassembledInstructions.layout(dimension.height); + } + } + + goToAddress(address?: string, focus?: boolean): void { + if (!this._disassembledInstructions) { + return; + } + + if (!address) { + address = this.focusedInstructionAddress; + } + if (!address) { + return; + } + + const index = this.getIndexFromAddress(address); + if (index >= 0) { + this._disassembledInstructions.reveal(index); + + if (focus) { + this._disassembledInstructions.domFocus(); + this._disassembledInstructions.setFocus([index]); + } + } else if (this.debugSessionManager.state === DebugState.Stopped) { + // Address is not provided or not in the table currently, clear the table + // and reload if we are in the state where we can load disassembly. + this.reloadDisassembly(address); + } + } + + protected async scrollUp_LoadDisassembledInstructions(instructionCount: number): Promise { + if (this._disassembledInstructions && this._disassembledInstructions.length > 0) { + const address: string | undefined = this._disassembledInstructions?.row(0).instruction.address; + return this.loadDisassembledInstructions(address, -instructionCount, instructionCount); + } + + return false; + } + + protected async scrollDown_LoadDisassembledInstructions(instructionCount: number): Promise { + if (this._disassembledInstructions && this._disassembledInstructions.length > 0) { + const address: string | undefined = this._disassembledInstructions?.row(this._disassembledInstructions?.length - 1).instruction.address; + return this.loadDisassembledInstructions(address, 1, instructionCount); + } + + return false; + } + + protected async loadDisassembledInstructions(memoryReference: string | undefined, instructionOffset: number, instructionCount: number): Promise { + // if address is null, then use current stack frame. + if (!memoryReference || memoryReference === '-1') { + memoryReference = this.focusedInstructionAddress; + } + if (!memoryReference) { + return false; + } + + const session = this.debugSession; + const resultEntries = (await session?.sendRequest('disassemble', { + instructionCount, + memoryReference, + instructionOffset, + offset: 0, + resolveSymbols: true, + }))?.body?.instructions; + if (session && resultEntries && this._disassembledInstructions) { + const newEntries: DisassembledInstructionEntry[] = []; + const allowBreakpoint = Boolean(session.capabilities.supportsInstructionBreakpoints); + + let lastLocation: DebugProtocol.Source | undefined; + let lastLine: IRange | undefined; + for (let i = 0; i < resultEntries.length; i++) { + const found = this._instructionBpList.find(p => p.instructionReference === resultEntries[i].address); + const instruction = resultEntries[i]; + + // Forward fill the missing location as detailed in the DAP spec. + if (instruction.location) { + lastLocation = instruction.location; + lastLine = undefined; + } + + if (instruction.line) { + const currentLine: IRange = { + startLineNumber: instruction.line, + startColumn: instruction.column ?? 0, + endLineNumber: instruction.endLine ?? instruction.line!, + endColumn: instruction.endColumn ?? 0, + }; + + // Add location only to the first unique range. This will give the appearance of grouping of instructions. + if (!Range.equalsRange(currentLine, lastLine ?? null)) { // eslint-disable-line no-null/no-null + lastLine = currentLine; + instruction.location = lastLocation; + } + } + + newEntries.push({ allowBreakpoint, isBreakpointSet: found !== undefined, isBreakpointEnabled: !!found?.enabled, instruction: instruction }); + } + + const specialEntriesToRemove = this._disassembledInstructions.length === 1 ? 1 : 0; + + // request is either at the start or end + if (instructionOffset >= 0) { + this._disassembledInstructions.splice(this._disassembledInstructions.length, specialEntriesToRemove, newEntries); + } else { + this._disassembledInstructions.splice(0, specialEntriesToRemove, newEntries); + } + + return true; + } + + return false; + } + + protected getIndexFromAddress(instructionAddress: string): number { + const disassembledInstructions = this._disassembledInstructions; + if (disassembledInstructions && disassembledInstructions.length > 0) { + const address = BigInt(instructionAddress); + if (address) { + return ArrayUtils.binarySearch2(disassembledInstructions.length, index => { + const row = disassembledInstructions.row(index); + + this.ensureAddressParsed(row); + if (row.instructionAddress! > address) { + return 1; + } else if (row.instructionAddress! < address) { + return -1; + } else { + return 0; + } + }); + } + } + + return -1; + } + + protected ensureAddressParsed(entry: DisassembledInstructionEntry): void { + if (entry.instructionAddress !== undefined) { + return; + } else { + entry.instructionAddress = BigInt(entry.instruction.address); + } + } + + /** + * Clears the table and reload instructions near the target address + */ + protected reloadDisassembly(targetAddress?: string): void { + if (this._disassembledInstructions) { + this._loadingLock = true; // stop scrolling during the load. + this._disassembledInstructions.splice(0, this._disassembledInstructions.length, [disassemblyNotAvailable]); + this._instructionBpList = this.breakpointManager.getInstructionBreakpoints(); + this.loadDisassembledInstructions(targetAddress, -DisassemblyViewWidget.NUM_INSTRUCTIONS_TO_LOAD * 4, DisassemblyViewWidget.NUM_INSTRUCTIONS_TO_LOAD * 8).then(() => { + // on load, set the target instruction in the middle of the page. + if (this._disassembledInstructions!.length > 0) { + const targetIndex = Math.floor(this._disassembledInstructions!.length / 2); + this._disassembledInstructions!.reveal(targetIndex, 0.5); + + // Always focus the target address on reload, or arrow key navigation would look terrible + this._disassembledInstructions!.domFocus(); + this._disassembledInstructions!.setFocus([targetIndex]); + } + this._loadingLock = false; + }); + } + } + + protected override onResize(msg: Widget.ResizeMessage): void { + this.adjustLayout(new Dimension(msg.width, msg.height)); + } + + protected override onActivateRequest(msg: Message): void { + this.node.focus(); + super.onActivateRequest(msg); + } + + protected toFontInfo(): Parameters[0] { + return { + fontFamily: this.editorPreferences['editor.fontFamily'], + fontWeight: String(this.editorPreferences['editor.fontWeight']), + fontSize: this.editorPreferences['editor.fontSize'], + fontLigatures: this.editorPreferences['editor.fontLigatures'], + lineHeight: this.editorPreferences['editor.lineHeight'], + letterSpacing: this.editorPreferences['editor.letterSpacing'], + }; + } + + protected _register(disposable: T): T { + this.toDispose.push(disposable); + return disposable; + } +} + diff --git a/packages/debug/src/browser/model/debug-instruction-breakpoint.tsx b/packages/debug/src/browser/model/debug-instruction-breakpoint.tsx new file mode 100644 index 0000000000000..bbc32e6823d86 --- /dev/null +++ b/packages/debug/src/browser/model/debug-instruction-breakpoint.tsx @@ -0,0 +1,68 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { nls } from '@theia/core'; +import * as React from '@theia/core/shared/react'; +import { BreakpointManager } from '../breakpoint/breakpoint-manager'; +import { InstructionBreakpoint } from '../breakpoint/breakpoint-marker'; +import { DebugBreakpoint, DebugBreakpointDecoration, DebugBreakpointOptions } from './debug-breakpoint'; + +export class DebugInstructionBreakpoint extends DebugBreakpoint { + constructor(readonly origin: InstructionBreakpoint, options: DebugBreakpointOptions) { + super(BreakpointManager.INSTRUCTION_URI, options); + } + + setEnabled(enabled: boolean): void { + if (enabled !== this.origin.enabled) { + this.breakpoints.updateInstructionBreakpoint(this.origin.id, { enabled }); + } + } + + protected override isEnabled(): boolean { + return super.isEnabled() && this.isSupported(); + } + + protected isSupported(): boolean { + return Boolean(this.session?.capabilities.supportsInstructionBreakpoints); + } + + remove(): void { + this.breakpoints.removeInstructionBreakpoint(this.origin.instructionReference); + } + + protected doRender(): React.ReactNode { + return {this.origin.instructionReference}; + } + + protected getBreakpointDecoration(message?: string[]): DebugBreakpointDecoration { + if (!this.isSupported()) { + return { + className: 'codicon-debug-breakpoint-unsupported', + message: message ?? [nls.localize('theia/debug/instruction-breakpoint', 'Instruction Breakpoint')], + }; + } + if (this.origin.condition || this.origin.hitCondition) { + return { + className: 'codicon-debug-breakpoint-conditional', + message: message || [nls.localizeByDefault('Conditional Breakpoint...')] + }; + } + return { + className: 'codicon-debug-breakpoint', + message: message || [nls.localize('theia/debug/instruction-breakpoint', 'Instruction Breakpoint')] + }; + } +} diff --git a/packages/debug/src/browser/model/debug-thread.tsx b/packages/debug/src/browser/model/debug-thread.tsx index 3a59f3a7fec0a..7fc5c957eebbf 100644 --- a/packages/debug/src/browser/model/debug-thread.tsx +++ b/packages/debug/src/browser/model/debug-thread.tsx @@ -41,6 +41,10 @@ export class DebugThread extends DebugThreadData implements TreeElement { protected readonly onDidChangedEmitter = new Emitter(); readonly onDidChanged: Event = this.onDidChangedEmitter.event; + protected readonly onDidFocusStackFrameEmitter = new Emitter(); + get onDidFocusStackFrame(): Event { + return this.onDidFocusStackFrameEmitter.event; + } constructor( readonly session: DebugSession @@ -59,6 +63,7 @@ export class DebugThread extends DebugThreadData implements TreeElement { set currentFrame(frame: DebugStackFrame | undefined) { this._currentFrame = frame; this.onDidChangedEmitter.fire(undefined); + this.onDidFocusStackFrameEmitter.fire(frame); } get stopped(): boolean { diff --git a/packages/debug/src/browser/view/debug-breakpoints-source.tsx b/packages/debug/src/browser/view/debug-breakpoints-source.tsx index e1a159fb1638c..be4b474ad4819 100644 --- a/packages/debug/src/browser/view/debug-breakpoints-source.tsx +++ b/packages/debug/src/browser/view/debug-breakpoints-source.tsx @@ -42,6 +42,9 @@ export class DebugBreakpointsSource extends TreeSource { for (const functionBreakpoint of this.model.functionBreakpoints) { yield functionBreakpoint; } + for (const instructionBreakpoint of this.model.instructionBreakpoints) { + yield instructionBreakpoint; + } for (const breakpoint of this.model.breakpoints) { yield breakpoint; } diff --git a/packages/debug/src/browser/view/debug-view-model.ts b/packages/debug/src/browser/view/debug-view-model.ts index a35a73f25329a..b74c2de98a1e9 100644 --- a/packages/debug/src/browser/view/debug-view-model.ts +++ b/packages/debug/src/browser/view/debug-view-model.ts @@ -26,6 +26,7 @@ import { DebugSourceBreakpoint } from '../model/debug-source-breakpoint'; import { DebugWatchExpression } from './debug-watch-expression'; import { DebugWatchManager } from '../debug-watch-manager'; import { DebugFunctionBreakpoint } from '../model/debug-function-breakpoint'; +import { DebugInstructionBreakpoint } from '../model/debug-instruction-breakpoint'; @injectable() export class DebugViewModel implements Disposable { @@ -54,7 +55,7 @@ export class DebugViewModel implements Disposable { protected readonly toDispose = new DisposableCollection( this.onDidChangeEmitter, this.onDidChangeBreakpointsEmitter, - this.onDidChangeWatchExpressionsEmitter + this.onDidChangeWatchExpressionsEmitter, ); @inject(DebugSessionManager) @@ -131,6 +132,10 @@ export class DebugViewModel implements Disposable { return this.manager.getFunctionBreakpoints(this.currentSession); } + get instructionBreakpoints(): DebugInstructionBreakpoint[] { + return this.manager.getInstructionBreakpoints(this.currentSession); + } + async start(): Promise { const { session } = this; if (!session) { diff --git a/packages/debug/src/common/debug-service.ts b/packages/debug/src/common/debug-service.ts index 78a81643b2fe1..67ee916e90968 100644 --- a/packages/debug/src/common/debug-service.ts +++ b/packages/debug/src/common/debug-service.ts @@ -14,8 +14,6 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -/* eslint-disable @typescript-eslint/no-explicit-any */ - import { Channel, Disposable, Emitter, Event } from '@theia/core'; import { ApplicationError } from '@theia/core/lib/common/application-error'; import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema'; @@ -47,6 +45,8 @@ export const DebugService = Symbol('DebugService'); * #resolveDebugConfiguration method is invoked. After that the debug adapter session will be started. */ export interface DebugService extends Disposable { + onDidChangeDebuggers?: Event; + /** * Finds and returns an array of registered debug types. * @returns An array of registered debug types @@ -147,8 +147,7 @@ export namespace DebugError { export interface DebugChannel { send(content: string): void; onMessage(cb: (message: string) => void): void; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onError(cb: (reason: any) => void): void; + onError(cb: (reason: unknown) => void): void; onClose(cb: (code: number, reason: string) => void): void; close(): void; } @@ -170,8 +169,7 @@ export class ForwardingDebugChannel implements DebugChannel { onMessage(cb: (message: string) => void): void { this.onMessageEmitter.event(cb); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onError(cb: (reason: any) => void): void { + onError(cb: (reason: unknown) => void): void { this.underlyingChannel.onError(cb); } onClose(cb: (code: number, reason: string) => void): void { diff --git a/packages/plugin-ext/src/main/browser/debug/plugin-debug-service.ts b/packages/plugin-ext/src/main/browser/debug/plugin-debug-service.ts index b82466b09fa83..1e7576edeab2a 100644 --- a/packages/plugin-ext/src/main/browser/debug/plugin-debug-service.ts +++ b/packages/plugin-ext/src/main/browser/debug/plugin-debug-service.ts @@ -37,10 +37,15 @@ import * as theia from '@theia/plugin'; @injectable() export class PluginDebugService implements DebugService { + protected readonly onDidChangeDebuggersEmitter = new Emitter(); + get onDidChangeDebuggers(): Event { + return this.onDidChangeDebuggersEmitter.event; + } + protected readonly debuggers: DebuggerContribution[] = []; protected readonly contributors = new Map(); protected readonly configurationProviders = new Map(); - protected readonly toDispose = new DisposableCollection(); + protected readonly toDispose = new DisposableCollection(this.onDidChangeDebuggersEmitter); protected readonly onDidChangeDebugConfigurationProvidersEmitter = new Emitter(); get onDidChangeDebugConfigurationProviders(): Event {