mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-10 05:35:05 +08:00
This CL enforces general camelCase naming for properties,
with the following exceptions:
- public properties are allowed to have a leading underscore until
we've migrated them (they were probably introduced by the
JSDOC -> TS automated script).
- static readonly class properties must use CONSTANT_CASE. For
non-static readonly class properties camelCase is enforced,
and I think this gives the right intuition.
Bug: chromium:1057042
Change-Id: I482b5b7cfe2b856e83fba8b78be222d60761db19
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2692210
Reviewed-by: Mathias Bynens <mathias@chromium.org>
Reviewed-by: Peter Marshall <petermarshall@chromium.org>
Commit-Queue: Sigurd Schneider <sigurds@chromium.org>
339 lines
11 KiB
TypeScript
339 lines
11 KiB
TypeScript
// Copyright (c) 2020 The Chromium Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
import * as ComponentHelpers from '../component_helpers/component_helpers.js';
|
|
import * as LitHtml from '../third_party/lit-html/lit-html.js';
|
|
|
|
import {toHexString} from './LinearMemoryInspectorUtils.js';
|
|
|
|
const {render, html} = LitHtml;
|
|
|
|
export interface LinearMemoryViewerData {
|
|
memory: Uint8Array;
|
|
address: number;
|
|
memoryOffset: number;
|
|
focus: boolean;
|
|
}
|
|
|
|
export class ByteSelectedEvent extends Event {
|
|
data: number;
|
|
|
|
constructor(address: number) {
|
|
super('byte-selected');
|
|
this.data = address;
|
|
}
|
|
}
|
|
|
|
export class ResizeEvent extends Event {
|
|
data: number;
|
|
|
|
constructor(numBytesPerPage: number) {
|
|
super('resize');
|
|
this.data = numBytesPerPage;
|
|
}
|
|
}
|
|
export class LinearMemoryViewer extends HTMLElement {
|
|
private static readonly BYTE_GROUP_MARGIN = 8;
|
|
private static readonly BYTE_GROUP_SIZE = 4;
|
|
private readonly shadow = this.attachShadow({mode: 'open'});
|
|
|
|
private readonly resizeObserver = new ResizeObserver(() => this.resize());
|
|
private isObservingResize = false;
|
|
|
|
private memory = new Uint8Array();
|
|
private address = 0;
|
|
private memoryOffset = 0;
|
|
|
|
private numRows = 1;
|
|
private numBytesInRow = LinearMemoryViewer.BYTE_GROUP_SIZE;
|
|
|
|
private focusOnByte = true;
|
|
|
|
private lastKeyUpdateSent: number|undefined = undefined;
|
|
|
|
set data(data: LinearMemoryViewerData) {
|
|
if (data.address < data.memoryOffset || data.address > data.memoryOffset + data.memory.length || data.address < 0) {
|
|
throw new Error('Address is out of bounds.');
|
|
}
|
|
|
|
if (data.memoryOffset < 0) {
|
|
throw new Error('Memory offset has to be greater or equal to zero.');
|
|
}
|
|
|
|
this.memory = data.memory;
|
|
this.address = data.address;
|
|
this.memoryOffset = data.memoryOffset;
|
|
this.focusOnByte = data.focus;
|
|
this.update();
|
|
}
|
|
|
|
connectedCallback(): void {
|
|
ComponentHelpers.SetCSSProperty.set(this, '--byte-group-margin', `${LinearMemoryViewer.BYTE_GROUP_MARGIN}px`);
|
|
}
|
|
|
|
disconnectedCallback(): void {
|
|
this.isObservingResize = false;
|
|
this.resizeObserver.disconnect();
|
|
}
|
|
|
|
private update(): void {
|
|
this.updateDimensions();
|
|
this.render();
|
|
this.focusOnView();
|
|
this.engageResizeObserver();
|
|
}
|
|
|
|
private focusOnView(): void {
|
|
if (this.focusOnByte) {
|
|
const view = this.shadow.querySelector<HTMLDivElement>('.view');
|
|
if (view) {
|
|
view.focus();
|
|
}
|
|
}
|
|
}
|
|
|
|
private resize(): void {
|
|
// A memory request currently takes too much time, so for the time being
|
|
// update with whatever data we have, and request for more memory to fill
|
|
// the screen if applicable after.
|
|
this.update();
|
|
this.dispatchEvent(new ResizeEvent(this.numBytesInRow * this.numRows));
|
|
}
|
|
|
|
/** Recomputes the number of rows and (byte) columns that fit into the current view. */
|
|
private updateDimensions(): void {
|
|
if (this.clientWidth === 0 || this.clientHeight === 0 || !this.shadowRoot) {
|
|
this.numBytesInRow = LinearMemoryViewer.BYTE_GROUP_SIZE;
|
|
this.numRows = 1;
|
|
return;
|
|
}
|
|
|
|
// We initially just plot one row with one byte group (here: byte group size of 4).
|
|
// Depending on that initially plotted row we can determine how many rows and
|
|
// bytes per row we can fit:
|
|
// > 0000000 | b0 b1 b2 b4 | a0 a1 a2 a3 <
|
|
// ^-^ ^-^
|
|
// byteCellWidth textCellWidth
|
|
// ^-------------------------------^
|
|
// widthToFill
|
|
|
|
const firstByteCell = this.shadowRoot.querySelector('.byte-cell');
|
|
const textCell = this.shadowRoot.querySelector('.text-cell');
|
|
const divider = this.shadowRoot.querySelector('.divider');
|
|
const rowElement = this.shadowRoot.querySelector('.row');
|
|
|
|
if (!firstByteCell || !textCell || !divider || !rowElement) {
|
|
this.numBytesInRow = LinearMemoryViewer.BYTE_GROUP_SIZE;
|
|
this.numRows = 1;
|
|
return;
|
|
}
|
|
|
|
// Calculate the width required for each (unsplittable) group of bytes.
|
|
const byteCellWidth = firstByteCell.getBoundingClientRect().width;
|
|
const textCellWidth = textCell.getBoundingClientRect().width;
|
|
const groupWidth =
|
|
LinearMemoryViewer.BYTE_GROUP_SIZE * (byteCellWidth + textCellWidth) + LinearMemoryViewer.BYTE_GROUP_MARGIN;
|
|
|
|
// Calculate the width to fill.
|
|
const dividerWidth = divider.getBoundingClientRect().width;
|
|
const widthToFill = this.clientWidth -
|
|
(firstByteCell.getBoundingClientRect().left - this.getBoundingClientRect().left) - dividerWidth;
|
|
if (widthToFill < groupWidth) {
|
|
this.numBytesInRow = LinearMemoryViewer.BYTE_GROUP_SIZE;
|
|
this.numRows = 1;
|
|
return;
|
|
}
|
|
|
|
this.numBytesInRow = Math.floor(widthToFill / groupWidth) * LinearMemoryViewer.BYTE_GROUP_SIZE;
|
|
this.numRows = Math.floor(this.clientHeight / rowElement.clientHeight);
|
|
}
|
|
|
|
private engageResizeObserver(): void {
|
|
if (!this.resizeObserver || this.isObservingResize) {
|
|
return;
|
|
}
|
|
|
|
this.resizeObserver.observe(this);
|
|
this.isObservingResize = true;
|
|
}
|
|
|
|
private render(): void {
|
|
// Disabled until https://crbug.com/1079231 is fixed.
|
|
// clang-format off
|
|
render(html`
|
|
<style>
|
|
:host {
|
|
flex: auto;
|
|
display: flex;
|
|
min-height: 20px;
|
|
}
|
|
|
|
.view {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
box-sizing: border-box;
|
|
background: var(--color-background);
|
|
outline: none;
|
|
}
|
|
|
|
.row {
|
|
display: flex;
|
|
height: 20px;
|
|
align-items: center;
|
|
}
|
|
|
|
.cell {
|
|
text-align: center;
|
|
border: 1px solid transparent;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.cell.selected {
|
|
border-color: var(--color-syntax-3);
|
|
color: var(--color-syntax-3);
|
|
background-color: var(--item-selection-bg-color);
|
|
}
|
|
|
|
.byte-cell {
|
|
min-width: 21px;
|
|
color: var(--color-text-primary);
|
|
}
|
|
|
|
.byte-group-margin {
|
|
margin-left: var(--byte-group-margin);
|
|
}
|
|
|
|
.text-cell {
|
|
min-width: 14px;
|
|
color: var(--color-syntax-3);
|
|
}
|
|
|
|
.address {
|
|
color: var(--color-text-disabled);
|
|
}
|
|
|
|
.address.selected {
|
|
font-weight: bold;
|
|
color: var(--color-text-primary);
|
|
}
|
|
|
|
.divider {
|
|
width: 1px;
|
|
height: inherit;
|
|
background-color: var(--divider-color);
|
|
margin: 0 4px 0 4px;
|
|
}
|
|
</style>
|
|
<div class="view" tabindex="0" @keydown=${this.onKeyDown}>
|
|
${this.renderView()}
|
|
</div>
|
|
`, this.shadow, {eventContext: this});
|
|
}
|
|
|
|
private onKeyDown(event: Event): void {
|
|
const keyboardEvent = event as KeyboardEvent;
|
|
let newAddress = undefined;
|
|
if (keyboardEvent.code === 'ArrowUp') {
|
|
newAddress = this.address - this.numBytesInRow;
|
|
} else if (keyboardEvent.code === 'ArrowDown') {
|
|
newAddress = this.address + this.numBytesInRow;
|
|
} else if (keyboardEvent.code === 'ArrowLeft') {
|
|
newAddress = this.address - 1;
|
|
} else if (keyboardEvent.code === 'ArrowRight') {
|
|
newAddress = this.address + 1;
|
|
} else if (keyboardEvent.code === 'PageUp') {
|
|
newAddress = this.address - this.numBytesInRow * this.numRows;
|
|
} else if (keyboardEvent.code === 'PageDown') {
|
|
newAddress = this.address + this.numBytesInRow * this.numRows;
|
|
}
|
|
|
|
if (newAddress !== undefined && newAddress !== this.lastKeyUpdateSent) {
|
|
this.lastKeyUpdateSent = newAddress;
|
|
this.dispatchEvent(new ByteSelectedEvent(newAddress));
|
|
}
|
|
}
|
|
|
|
private renderView(): LitHtml.TemplateResult {
|
|
const itemTemplates = [];
|
|
for (let i = 0; i < this.numRows; ++i) {
|
|
itemTemplates.push(this.renderRow(i));
|
|
}
|
|
return html`${itemTemplates}`;
|
|
}
|
|
|
|
private renderRow(row: number): LitHtml.TemplateResult {
|
|
const {startIndex, endIndex} = {startIndex: row * this.numBytesInRow, endIndex: (row + 1) * this.numBytesInRow};
|
|
|
|
const classMap = {
|
|
address: true,
|
|
selected: Math.floor((this.address - this.memoryOffset) / this.numBytesInRow) === row,
|
|
};
|
|
return html`
|
|
<div class="row">
|
|
<span class="${LitHtml.Directives.classMap(classMap)}">${toHexString({number: startIndex + this.memoryOffset, pad: 8, prefix: false})}</span>
|
|
<span class="divider"></span>
|
|
${this.renderByteValues(startIndex, endIndex)}
|
|
<span class="divider"></span>
|
|
${this.renderCharacterValues(startIndex, endIndex)}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderByteValues(startIndex: number, endIndex: number): LitHtml.TemplateResult {
|
|
const cells = [];
|
|
for (let i = startIndex; i < endIndex; ++i) {
|
|
// Add margin after each group of bytes of size byteGroupSize.
|
|
const addMargin = i !== startIndex && (i - startIndex) % LinearMemoryViewer.BYTE_GROUP_SIZE === 0;
|
|
const selected = i === this.address - this.memoryOffset;
|
|
const classMap = {
|
|
'cell': true,
|
|
'byte-cell': true,
|
|
'byte-group-margin': addMargin,
|
|
selected,
|
|
};
|
|
const isSelectableCell = i < this.memory.length;
|
|
const byteValue = isSelectableCell ? html`${toHexString({number: this.memory[i], pad: 2, prefix: false})}` : '';
|
|
const actualIndex = i + this.memoryOffset;
|
|
const onSelectedByte = isSelectableCell ? this.onSelectedByte.bind(this, actualIndex) : '';
|
|
cells.push(html`<span class="${LitHtml.Directives.classMap(classMap)}" @click=${onSelectedByte}>${byteValue}</span>`);
|
|
}
|
|
return html`${cells}`;
|
|
}
|
|
|
|
private renderCharacterValues(startIndex: number, endIndex: number): LitHtml.TemplateResult {
|
|
const cells = [];
|
|
for (let i = startIndex; i < endIndex; ++i) {
|
|
const classMap = {
|
|
'cell': true,
|
|
'text-cell': true,
|
|
selected: this.address - this.memoryOffset === i,
|
|
};
|
|
const isSelectableCell = i < this.memory.length;
|
|
const value = isSelectableCell ? html`${this.toAscii(this.memory[i])}` : '';
|
|
const onSelectedByte = isSelectableCell ? this.onSelectedByte.bind(this, i + this.memoryOffset) : '';
|
|
cells.push(html`<span class="${LitHtml.Directives.classMap(classMap)}" @click=${onSelectedByte}>${value}</span>`);
|
|
}
|
|
return html`${cells}`;
|
|
}
|
|
|
|
private toAscii(byte: number): string {
|
|
if (byte >= 20 && byte <= 0x7F) {
|
|
return String.fromCharCode(byte);
|
|
}
|
|
return '.';
|
|
}
|
|
|
|
private onSelectedByte(index: number): void {
|
|
this.dispatchEvent(new ByteSelectedEvent(index));
|
|
}
|
|
}
|
|
|
|
customElements.define('devtools-linear-memory-inspector-viewer', LinearMemoryViewer);
|
|
|
|
declare global {
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
interface HTMLElementTagNameMap {
|
|
'devtools-linear-memory-inspector-viewer': LinearMemoryViewer;
|
|
}
|
|
}
|