// ***************************************************************************** // 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-only 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 { StringBuilder } 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): void { this.renderElementInner(element, index, templateData); } protected async renderElementInner(element: DisassembledInstructionEntry, index: number, column: InstructionColumnTemplateData): Promise { column.currentElement.element = element; const instruction = element.instruction; column.sourcecode.innerText = ''; const sb = new StringBuilder(1000); if (this._disassemblyView.isSourceCodeRender && instruction.location?.path && instruction.line) { const sourceURI = this.getUriFromSource(instruction); if (sourceURI) { let textModel: ITextModel | undefined = undefined; const sourceSB = new StringBuilder(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.appendString(` ${lineNumber}: `); sourceSB.appendString(lineContent + '\n'); if (instruction.endLine && lineNumber < instruction.endLine) { lineNumber++; continue; } break; } column.sourcecode.innerText = sourceSB.build(); } } } let spacesToAppend = 10; if (instruction.address !== '-1') { sb.appendString(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.appendString(' '); } } if (instruction.instructionBytes) { sb.appendString(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.appendString(' '); } } sb.appendString(instruction.instruction); column.instruction.innerText = sb.build(); this.rerenderBackground(column.instruction, column.sourcecode, element); } disposeElement(element: DisassembledInstructionEntry, index: number, templateData: InstructionColumnTemplateData): 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.toString()), 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)); }