deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [
'../../configs/build.eslintrc.json'
],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.json'
}
};

View File

@@ -0,0 +1,71 @@
<div align='center'>
<br />
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
<h2>ECLIPSE THEIA - MEMORY-INSPECTOR EXTENSION</h2>
<hr />
</div>
## Description
This extension contributes a set of widgets for viewing memory in different ways.
## Requirements
This extension must be used in conjunction with a Debug Adapter that implements a `ReadMemoryRequest` handler or alternative custom request that returns memory data.
It has been tested against the [CDT-GDB Adapter](https://github.com/eclipse-cdt/cdt-gdb-adapter) used as the backend for the
[CDT-GDB VSCode](https://github.com/eclipse-cdt/cdt-gdb-vscode) plugin. This repository is configured to download that plugin as part of its build routine.
If you intend to use this extension with a different debug adapter, you may need to implement a custom
[`MemoryProvider`](./src/browser/memory-provider/memory-provider-service.ts) to handle any peculiarities of the requests and responses used by your adapter.
## Widgets
### Memory Widget
The basic [`MemoryWidget` class](./src/browser/memory-widget/memory-widget.ts) is a wrapper around two functional widgets, a `MemoryOptionsWidget` and
a`MemoryTableWidget`. The [`MemoryOptionsWidget`](./src/browser/memory-widget/memory-options-widget.tsx) is responsible for configuring the display
and fetching memory, and the [`MemoryTableWidget`](./src/browser/memory-widget/memory-table-widget.tsx) renders the memory according to the options
specified by the user in the `MemoryOptionsWidget`. The basic combination of these three classes offers variable highlighting, ascii display, and
dynamic updating in response to events from the debug session, as well as the option to lock the view to ignore changes from the session.
### Diff Widget
The [`MemoryDiffWidget`](./src/browser/diff-widget/memory-diff-widget-types.ts) is an elaboration of the `MemoryWidget` type that allows side-by-side
comparison of the contents of two `MemoryWidgets`.
### Register Widget
The [`RegisterWidget`](./src/browser/register-widget/register-widget-types.ts) offers functionality to view and
manipulate those values when using a debug adapter that reports register contents.
### Editable Widget
The [`MemoryEditableTableWidget`](./src/browser/editable-widget/memory-editable-table-widget.tsx) adds UI functionality to allow users to modify values in
the table display and send them to a backend that supports that operation.
## Using the Widgets
The widgets are created by the [`MemoryWidgetManager`](./src/browser/utils/memory-widget-manager.ts), and modifying the `createNewMemoryWidget()`
method of that service allows you to change what kind of widget is instantiated and under what circumstances. The widgets get memory through the
[`MemoryProviderService`](./src/browser/memory-provider/memory-provider-service.ts), which delegates to implementations `MemoryProvider` interface
that are bound as `MemoryProvider` contributions.
## Additional Information
- [API documentation for `@theia/memory-inspector`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_memory-inspector.html)
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
- [Theia - Website](https://theia-ide.org/)
## License
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
## Trademark
"Theia" is a trademark of the Eclipse Foundation
<https://www.eclipse.org/theia>

View File

@@ -0,0 +1,48 @@
{
"name": "@theia/memory-inspector",
"version": "1.68.0",
"description": "Theia - Memory Inspector",
"keywords": [
"theia-extension"
],
"homepage": "https://github.com/eclipse-theia/theia",
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
"repository": {
"type": "git",
"url": "https://github.com/eclipse-theia/theia.git"
},
"bugs": {
"url": "https://github.com/eclipse-theia/theia/issues"
},
"files": [
"lib",
"src"
],
"scripts": {
"build": "theiaext build",
"clean": "theiaext clean",
"compile": "theiaext compile",
"lint": "theiaext lint",
"test": "theiaext test",
"watch": "theiaext watch"
},
"dependencies": {
"@theia/core": "1.68.0",
"@theia/debug": "1.68.0",
"@vscode/debugprotocol": "^1.51.0",
"long": "^4.0.0",
"tslib": "^2.6.2"
},
"devDependencies": {
"@types/long": "^4.0.0"
},
"theiaExtensions": [
{
"frontend": "lib/browser/memory-inspector-frontend-module"
}
],
"publishConfig": {
"access": "public"
},
"gitHead": "21358137e41342742707f660b8e222f940a27652"
}

View File

@@ -0,0 +1,152 @@
/********************************************************************************
* Copyright (C) 2021 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 { Key, KeyCode } from '@theia/core/lib/browser';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
import { ThemeType } from '@theia/core/lib/common/theme';
import { LENGTH_FIELD_ID, LOCATION_OFFSET_FIELD_ID, MemoryOptionsWidget } from '../memory-widget/memory-options-widget';
import { MWInput } from '../utils/memory-widget-components';
import { Interfaces, MemoryDiffWidgetData, Utils } from '../utils/memory-widget-utils';
import { DiffLabels } from './memory-diff-widget-types';
import { nls } from '@theia/core/lib/common/nls';
export interface DiffMemoryOptions extends Interfaces.MemoryOptions {
beforeOffset: number;
afterOffset: number;
}
@injectable()
export class MemoryDiffOptionsWidget extends MemoryOptionsWidget {
@inject(MemoryDiffWidgetData) protected override memoryWidgetOptions: MemoryDiffWidgetData;
protected themeType: ThemeType;
override get options(): DiffMemoryOptions {
return this.storeState();
}
updateDiffData(newDiffData: Partial<MemoryDiffWidgetData>): void {
this.memoryWidgetOptions = { ...this.memoryWidgetOptions, ...newDiffData };
this.init();
}
@postConstruct()
protected override init(): void {
this.addClass(MemoryOptionsWidget.ID);
this.addClass('diff-options-widget');
const { identifier, beforeBytes, afterBytes } = this.memoryWidgetOptions;
this.id = `${MemoryDiffOptionsWidget.ID}-${identifier}`;
this.title.label = nls.localize('theia/memory-inspector/diff/label', 'Diff: {0}', identifier);
this.title.caption = this.title.label;
this.title.iconClass = this.iconClass;
this.title.closable = true;
this.toDispose.push(this.onOptionsChanged(() => this.update()));
beforeBytes.label = DiffLabels.Before;
afterBytes.label = DiffLabels.After;
this.columnsDisplayed = {
beforeAddress: {
label: nls.localizeByDefault('Address'),
doRender: true
},
beforeData: {
label: this.memoryWidgetOptions.titles[0],
doRender: true
},
afterAddress: {
label: nls.localizeByDefault('Address'),
doRender: true
},
afterData: {
label: this.memoryWidgetOptions.titles[1],
doRender: true
},
variables: {
label: nls.localizeByDefault('Variables'),
doRender: false
},
ascii: {
label: nls.localize('theia/memory-inspector/ascii', 'ASCII'),
doRender: false
},
};
this.update();
}
protected override acceptFocus(): void {
const settingsCog = this.node.querySelector('.toggle-settings-click-zone') as HTMLDivElement;
settingsCog?.focus();
}
protected override renderMemoryLocationGroup(): React.ReactNode {
const { titles: [beforeTitle, afterTitle] } = this.memoryWidgetOptions;
return (
<div className='t-mv-group view-group'>
<MWInput
id={LOCATION_OFFSET_FIELD_ID}
label={nls.localize('theia/memory-inspector/diff-widget/offset-label', '{0} Offset', beforeTitle)}
title={nls.localize('theia/memory-inspector/diff-widget/offset-title', 'Bytes to offset the memory from {0}', beforeTitle)}
defaultValue='0'
passRef={this.assignOffsetRef}
onChange={Utils.validateNumericalInputs}
onKeyDown={this.doRefresh}
/>
<MWInput
id={LENGTH_FIELD_ID}
label={nls.localize('theia/memory-inspector/diff-widget/offset-label', '{0} Offset', afterTitle)}
title={nls.localize('theia/memory-inspector/diff-widget/offset-title', 'Bytes to offset the memory from {0}', afterTitle)}
defaultValue='0'
passRef={this.assignReadLengthRef}
onChange={Utils.validateNumericalInputs}
onKeyDown={this.doRefresh}
/>
<button
type='button'
className='theia-button main view-group-go-button'
title={nls.localizeByDefault('Go')}
onClick={this.doRefresh}
>
{nls.localizeByDefault('Go')}
</button>
</div>
);
}
protected override getObligatoryColumnIds(): string[] {
return ['beforeAddress', 'beforeData', 'afterAddress', 'afterData'];
}
protected override doRefresh = (event: React.KeyboardEvent<HTMLInputElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>): void => {
if ('key' in event && KeyCode.createKeyCode(event.nativeEvent).key?.keyCode !== Key.ENTER.keyCode) {
return;
}
this.fireDidChangeOptions();
};
override storeState(): DiffMemoryOptions {
return {
...super.storeState(),
// prefix a 0. It'll do nothing if it's a number, but if it's an empty string or garbage, it'll make parseInt return 0.
beforeOffset: parseInt(`0${this.offsetField?.value ?? 0}`),
afterOffset: parseInt(`0${this.readLengthField?.value ?? 0}`),
};
}
}

View File

@@ -0,0 +1,163 @@
/********************************************************************************
* Copyright (C) 2021 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 { Key, KeyCode, Message, ReactWidget } from '@theia/core/lib/browser';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
import Long from 'long';
import { MemoryWidget } from '../memory-widget/memory-widget';
import { RegisterWidget } from '../register-widget/register-widget-types';
import { MWSelect } from '../utils/memory-widget-components';
import { MemoryWidgetManager } from '../utils/memory-widget-manager';
import { Interfaces } from '../utils/memory-widget-utils';
import { VariableRange } from '../utils/memory-widget-variable-utils';
import { MemoryDiffWidget } from './memory-diff-table-widget';
import { nls } from '@theia/core/lib/common/nls';
export interface DiffMemory {
beforeAddress: Long;
beforeBytes: Interfaces.LabeledUint8Array;
beforeVariables: VariableRange[];
afterAddress: Long;
afterBytes: Interfaces.LabeledUint8Array;
afterVariables: VariableRange[];
}
@injectable()
export class MemoryDiffSelectWidget extends ReactWidget {
static DIFF_SELECT_CLASS = 'memory-diff-select';
protected beforeWidgetLabel = '';
protected afterWidgetLabel = '';
protected labelToWidgetMap = new Map<string, MemoryWidget>();
@inject(MemoryWidgetManager) protected readonly memoryWidgetManager: MemoryWidgetManager;
@postConstruct()
protected init(): void {
this.addClass(MemoryDiffSelectWidget.DIFF_SELECT_CLASS);
this.id = MemoryDiffSelectWidget.DIFF_SELECT_CLASS;
this.updateWidgetMap();
this.update();
this.toDispose.push(this.memoryWidgetManager.onChanged(() => this.updateWidgetMap()));
this.scrollOptions = { ...this.scrollOptions, suppressScrollX: false };
this.hide();
}
override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.node.querySelector('select')?.focus();
}
protected assignBaseValue = (e: React.ChangeEvent<HTMLSelectElement>): void => {
this.beforeWidgetLabel = e.target.value;
this.update();
};
protected assignLaterValue = (e: React.ChangeEvent<HTMLSelectElement>): void => {
this.afterWidgetLabel = e.target.value;
this.update();
};
render(): React.ReactNode {
const optionLabels = [...this.labelToWidgetMap.keys()];
const currentBase = this.getBeforeLabel(optionLabels);
const currentChanged = this.getAfterLabel(optionLabels, currentBase);
return optionLabels.length > 1 && (
<div className='memory-diff-select-wrapper'>
<div className='diff-select-input-wrapper'>
<div className='t-mv-diff-select-widget-options-wrapper'>
<MWSelect
id='diff-selector-before'
label='compare'
value={currentBase}
options={optionLabels}
onChange={this.assignBaseValue}
/>
</div>
<div className='t-mv-diff-select-widget-options-wrapper'>
<MWSelect
id='diff-selector-after'
label='with'
value={currentChanged}
options={optionLabels.filter(label => label !== currentBase)}
onChange={this.assignLaterValue}
onKeyDown={this.diffIfEnter}
/>
</div>
</div>
<button
type='button'
className='theia-button main memory-diff-select-go'
title={nls.localizeByDefault('Go')}
onClick={this.diff}
>
{nls.localizeByDefault('Go')}
</button>
</div>
);
}
protected diffIfEnter = (e: React.KeyboardEvent<HTMLInputElement>): void => {
if (KeyCode.createKeyCode(e.nativeEvent).key?.keyCode === Key.ENTER.keyCode) {
this.doDiff();
}
};
protected updateWidgetMap(): void {
const widgets = this.memoryWidgetManager.availableWidgets.filter(widget => !MemoryDiffWidget.is(widget) && !RegisterWidget.is(widget));
this.labelToWidgetMap = new Map<string, MemoryWidget>(widgets.map((widget): [string, MemoryWidget] => [widget.title.label, widget]));
this.update();
}
protected getBeforeLabel(optionLabels: string[] = [...this.labelToWidgetMap.keys()]): string {
return this.labelToWidgetMap.has(this.beforeWidgetLabel) && this.beforeWidgetLabel || optionLabels[0];
}
protected getAfterLabel(optionLabels: string[], beforeWidgetLabel: string = this.getBeforeLabel(optionLabels)): string {
return (this.afterWidgetLabel && this.afterWidgetLabel !== beforeWidgetLabel
? this.afterWidgetLabel
: optionLabels.find(label => label !== beforeWidgetLabel)) ?? '';
}
protected diff = (): void => this.doDiff();
protected doDiff(): void {
const labels = [...this.labelToWidgetMap.keys()];
const baseLabel = this.getBeforeLabel(labels);
const changedLabel = this.getAfterLabel(labels, baseLabel);
const baseWidget = this.labelToWidgetMap.get(baseLabel);
const changedWidget = this.labelToWidgetMap.get(changedLabel);
if (baseWidget && changedWidget) {
const memoryAndAddresses = this.getMemoryArrays(baseWidget, changedWidget);
this.memoryWidgetManager.doDiff({ ...memoryAndAddresses, titles: [baseLabel, changedLabel] });
}
}
protected getMemoryArrays(beforeWidget: MemoryWidget, afterWidget: MemoryWidget): DiffMemory {
const { memory: beforeMemory } = beforeWidget.optionsWidget;
const { memory: afterMemory } = afterWidget.optionsWidget;
return {
beforeBytes: beforeMemory.bytes,
afterBytes: afterMemory.bytes,
beforeAddress: beforeMemory.address,
afterAddress: afterMemory.address,
beforeVariables: beforeMemory.variables,
afterVariables: afterMemory.variables,
};
}
}

View File

@@ -0,0 +1,366 @@
/********************************************************************************
* Copyright (C) 2021 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 { inject, injectable } from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
import Long from 'long';
import { MemoryTable, MemoryTableWidget } from '../memory-widget/memory-table-widget';
import { MemoryWidget } from '../memory-widget/memory-widget';
import { EasilyMappedObject } from '../utils/memory-hover-renderer';
import { Interfaces, MemoryDiffWidgetData } from '../utils/memory-widget-utils';
import { VariableDecoration, VariableFinder } from '../utils/memory-widget-variable-utils';
import { DiffMemoryOptions, MemoryDiffOptionsWidget } from './memory-diff-options-widget';
import { DiffExtraColumnOptions, DiffLabels, DiffRowOptions, RowData } from './memory-diff-widget-types';
export type MemoryDiffWidget = MemoryWidget<MemoryDiffOptionsWidget, MemoryDiffTableWidget>;
export namespace MemoryDiffWidget {
export const ID = 'memory.diff.view';
export const is = (widget: MemoryWidget): boolean => widget.optionsWidget instanceof MemoryDiffOptionsWidget;
}
interface DummyCounts {
leading: number;
trailing: number;
}
interface OffsetData {
before: DummyCounts;
after: DummyCounts;
}
@injectable()
export class MemoryDiffTableWidget extends MemoryTableWidget {
@inject(MemoryDiffWidgetData) protected diffData: MemoryDiffWidgetData;
@inject(MemoryDiffOptionsWidget) override readonly optionsWidget: MemoryDiffOptionsWidget;
protected diffedSpanCounter = 0;
protected beforeVariableFinder: VariableFinder;
protected afterVariableFinder: VariableFinder;
protected isHighContrast = false;
protected override options: DiffMemoryOptions;
protected offsetData: OffsetData;
updateDiffData(newDiffData: Partial<MemoryDiffWidgetData>): void {
this.optionsWidget.updateDiffData(newDiffData);
this.diffData = { ...this.diffData, ...newDiffData };
this.getStateAndUpdate();
}
protected override getState(): void {
this.options = this.optionsWidget.options;
this.isHighContrast = this.themeService.getCurrentTheme().type === 'hc';
this.beforeVariableFinder = new VariableFinder(this.diffData.beforeVariables, this.isHighContrast);
this.afterVariableFinder = new VariableFinder(this.diffData.afterVariables, this.isHighContrast);
this.memory = { bytes: this.diffData.beforeBytes, address: new Long(0), variables: this.diffData.beforeVariables };
this.offsetData = this.getOffsetData();
}
protected getOffsetData(): OffsetData {
const offsetData: OffsetData = {
before: {
leading: this.options.beforeOffset * this.options.byteSize / 8,
trailing: 0,
},
after: {
leading: this.options.afterOffset * this.options.byteSize / 8,
trailing: 0,
},
};
this.setTrailing(offsetData);
return offsetData;
}
protected setTrailing(offsetData: OffsetData): void {
const totalBeforeLength = this.diffData.beforeBytes.length - offsetData.before.leading;
const totalAfterLength = this.diffData.afterBytes.length - offsetData.after.leading;
const totalDifference = totalBeforeLength - totalAfterLength;
const realDifference = Math.abs(totalDifference);
const beforeShorter = totalDifference < 0;
if (beforeShorter) {
offsetData.before.trailing = realDifference;
} else {
offsetData.after.trailing = realDifference;
}
}
/* eslint-enable no-param-reassign */
protected override getWrapperClass(): string {
return `${super.getWrapperClass()} diff-table`;
}
protected override getTableHeaderClass(): string {
return `${super.getTableHeaderClass()} diff-table`;
}
protected override *renderRows(): IterableIterator<React.ReactNode> {
const bytesPerRow = this.options.bytesPerGroup * this.options.groupsPerRow;
const oldGroupIterator = this.renderGroups(this.diffData.beforeBytes);
const changeGroupIterator = this.renderGroups(this.diffData.afterBytes);
let rowsYielded = 0;
let before = this.getNewRowData();
let after = this.getNewRowData();
let isModified = false;
for (const oldGroup of oldGroupIterator) {
const nextChanged: IteratorResult<MemoryTable.GroupData, undefined> = changeGroupIterator.next();
isModified = isModified || !!oldGroup.isHighlighted;
this.aggregate(before, oldGroup);
this.aggregate(after, nextChanged.value);
if (before.groups.length === this.options.groupsPerRow || oldGroup.index === this.diffData.beforeBytes.length - 1) {
const beforeID = this.diffData.beforeAddress.add(this.options.beforeOffset + (bytesPerRow * rowsYielded));
const afterID = this.diffData.afterAddress.add(this.options.afterOffset + (bytesPerRow * rowsYielded));
const beforeAddress = `0x${beforeID.toString(16)}`;
const afterAddress = `0x${afterID.toString(16)}`;
const doShowDivider = (rowsYielded % 4) === 3;
yield this.renderSingleRow({ beforeAddress, afterAddress, doShowDivider, before, after, isModified });
rowsYielded += 1;
isModified = false;
before = this.getNewRowData();
after = this.getNewRowData();
}
}
}
protected renderSingleRow(
options: DiffRowOptions,
getRowAttributes: Interfaces.RowDecorator = this.getRowAttributes.bind(this),
): React.ReactNode {
const { beforeAddress, afterAddress, before, after, isModified, doShowDivider } = options;
const { className } = getRowAttributes({ doShowDivider });
return (
<tr key={beforeAddress} className={className}>
<td className={MemoryTable.ADDRESS_DATA_CLASS}>{beforeAddress}</td>
<td className={this.getDataCellClass('before', isModified)}>{before.groups}</td>
<td className={MemoryTable.ADDRESS_DATA_CLASS}>{afterAddress}</td>
<td className={this.getDataCellClass('after', isModified)}>{after.groups}</td>
{this.getExtraColumn({
variables: before.variables.slice(),
ascii: before.ascii,
afterVariables: after.variables.slice(),
afterAscii: after.ascii,
})}
</tr>
);
}
protected override getExtraColumn(options: DiffExtraColumnOptions): React.ReactNode[] {
const additionalColumns = [];
if (this.options.columnsDisplayed.variables.doRender) {
additionalColumns.push(this.getDiffedVariables(options));
}
if (this.options.columnsDisplayed.ascii.doRender) {
additionalColumns.push(this.getDiffedAscii(options));
}
return additionalColumns;
}
protected getDiffedAscii(options: DiffExtraColumnOptions): React.ReactNode {
const { ascii: beforeAscii, afterAscii } = options;
const highContrastClass = this.isHighContrast ? ' hc' : '';
if (beforeAscii === afterAscii) {
return super.getExtraColumn({ ascii: beforeAscii });
}
const EMPTY_TEXT = {
before: '',
after: '',
};
let currentText = { ...EMPTY_TEXT };
const beforeSpans: React.ReactNode[] = [];
const afterSpans: React.ReactNode[] = [];
let lastWasSame = true;
for (let i = 0; i < beforeAscii.length; i += 1) {
const beforeLetter = beforeAscii[i];
const afterLetter = afterAscii[i];
const thisIsSame = beforeLetter === afterLetter;
if (thisIsSame !== lastWasSame) {
lastWasSame = thisIsSame;
this.addTextBits(beforeSpans, afterSpans, currentText);
currentText = { ...EMPTY_TEXT };
}
currentText.before += beforeLetter;
currentText.after += afterLetter;
}
this.addTextBits(beforeSpans, afterSpans, currentText);
return (
<td key='ascii' className={MemoryTable.EXTRA_COLUMN_DATA_CLASS}>
<span className={`different t-mv-diffed-ascii before${highContrastClass}`}>{beforeSpans}</span>
<span className={`different t-mv-diffed-ascii after${highContrastClass}`}>{afterSpans}</span>
</td>
);
}
protected addTextBits(beforeSpans: React.ReactNode[], afterSpans: React.ReactNode[], texts: { before: string; after: string }): void {
const [newBeforeSpans, newAfterSpans] = this.getAsciiSpan(texts);
beforeSpans.push(newBeforeSpans);
afterSpans.push(newAfterSpans);
}
protected getAsciiSpan({ before, after }: { before: string; after: string }): [React.ReactNode, React.ReactNode] {
if (!before) {
return [undefined, undefined];
}
const differentClass = before === after ? '' : 'different';
const highContrastClass = this.isHighContrast ? ' hc' : '';
// use non-breaking spaces so they show up in the diff.
return [
<span key={before + after + (this.diffedSpanCounter += 1)} className={`before ${differentClass}${highContrastClass}`}>
{before.replace(/ /g, '\xa0')}
</span>,
<span key={before + after + (this.diffedSpanCounter += 1)} className={`after ${differentClass}${highContrastClass}`}>
{after.replace(/ /g, '\xa0')}
</span>,
];
}
protected getDiffedVariables(options: DiffExtraColumnOptions): React.ReactNode {
const { variables: beforeVariables, afterVariables } = options;
const variableSpans: React.ReactNode[] = [];
let areDifferent = false;
for (const beforeVariable of beforeVariables) {
const placeInAfterVariables = afterVariables.findIndex(afterVariable => afterVariable.name === beforeVariable.name);
if (placeInAfterVariables > -1) {
afterVariables.splice(placeInAfterVariables, 1);
variableSpans.push(this.getVariableSpan(beforeVariable, DiffLabels.Before, false));
} else {
areDifferent = true;
variableSpans.push(this.getVariableSpan(beforeVariable, DiffLabels.Before, true));
}
}
for (const afterVariable of afterVariables) {
variableSpans.push(this.getVariableSpan(afterVariable, DiffLabels.After, true));
}
return <td key='variables' className={`${MemoryTable.EXTRA_COLUMN_DATA_CLASS}${areDifferent ? ' different' : ''}`}>{variableSpans}</td>;
}
protected getVariableSpan({ name, color }: VariableDecoration, origin: DiffLabels, isChanged: boolean): React.ReactNode {
return (
<span
key={name}
className={`t-mv-variable-label ${origin} ${isChanged ? ' different' : ''}`}
style={{ color }}
>
{name}
</span>
);
}
protected getDataCellClass(modifier: 'before' | 'after', isModified?: boolean): string {
const highContrastClass = this.isHighContrast ? 'hc' : '';
let base = `${MemoryTable.MEMORY_DATA_CLASS} ${modifier} ${highContrastClass}`;
if (isModified) {
base += ' different';
}
return base;
}
protected getNewRowData(): RowData {
return {
groups: [],
variables: [],
ascii: '',
};
}
protected aggregate(container: RowData, newData?: MemoryTable.GroupData): void {
if (newData) {
container.groups.push(newData.node);
container.variables.push(...newData.variables);
container.ascii += newData.ascii;
}
}
protected override *renderArrayItems(
iteratee: Interfaces.LabeledUint8Array = this.memory.bytes,
getBitAttributes: Interfaces.BitDecorator = this.getBitAttributes.bind(this),
): IterableIterator<MemoryTable.ItemData> {
let ignoredItems = 0;
const iterateeOffsetData = iteratee.label === DiffLabels.Before ? this.offsetData.before : this.offsetData.after;
for (const item of super.renderArrayItems(iteratee, getBitAttributes)) {
if (ignoredItems < iterateeOffsetData.leading) {
ignoredItems += 1;
continue;
}
yield item;
}
for (let i = 0; i < iterateeOffsetData.trailing; i += 1) {
yield this.getDummySpan(i);
}
}
protected getDummySpan(key: number): MemoryTable.ItemData {
const node = <span key={key}>{'\xa0'.repeat(2)}</span>;
return {
node,
content: '',
index: -1 * key,
};
}
protected override getBitAttributes(arrayOffset: number, iteratee: Interfaces.LabeledUint8Array): Partial<Interfaces.FullNodeAttributes> {
const isHighlighted = this.getHighlightStatus(arrayOffset, iteratee);
const content = iteratee[arrayOffset].toString(16).padStart(2, '0');
let className = `${MemoryTable.EIGHT_BIT_SPAN_CLASS} ${iteratee.label ?? ''}`;
const highContrastClass = this.isHighContrast ? 'hc' : '';
if (isHighlighted) {
className += ` different ${highContrastClass}`;
}
const isBeforeChunk = iteratee.label === DiffLabels.Before;
const baseAddress = isBeforeChunk ? this.diffData.beforeAddress : this.diffData.afterAddress;
const itemAddress = baseAddress.add(arrayOffset * 8 / this.options.byteSize);
const variable = isBeforeChunk
? this.beforeVariableFinder.getVariableForAddress(itemAddress)
: this.afterVariableFinder.getVariableForAddress(itemAddress);
return { className, content, isHighlighted, variable, style: { color: variable?.color } };
}
protected getHighlightStatus(arrayOffset: number, iteratee: Interfaces.LabeledUint8Array): boolean {
const source = iteratee.label === DiffLabels.Before ? DiffLabels.Before : DiffLabels.After;
const targetArray = source === DiffLabels.Before ? this.diffData.afterBytes : this.diffData.beforeBytes;
const sourceValue = iteratee[arrayOffset];
const targetIndex = this.translateBetweenShiftedArrays(arrayOffset, source);
const targetValue = targetArray[targetIndex];
return sourceValue !== undefined &&
targetValue !== undefined &&
sourceValue !== targetValue;
}
protected translateBetweenShiftedArrays(sourceIndex: number, source: DiffLabels): number {
const sourceOffsets = source === DiffLabels.Before ? this.offsetData.before : this.offsetData.after;
const targetOffsets = source === DiffLabels.Before ? this.offsetData.after : this.offsetData.before;
return sourceIndex - sourceOffsets.leading + targetOffsets.leading;
}
protected override getHoverForVariable(span: HTMLElement): EasilyMappedObject | undefined {
const name = span.textContent ?? '';
const variable = this.beforeVariableFinder.searchForVariable(name) ??
this.afterVariableFinder.searchForVariable(name);
if (variable?.type) {
return { type: variable.type };
}
return undefined;
}
}

View File

@@ -0,0 +1,45 @@
/********************************************************************************
* Copyright (C) 2021 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 { MemoryTable } from '../memory-widget/memory-table-widget';
import { VariableDecoration } from '../utils/memory-widget-variable-utils';
export enum DiffLabels {
Before = 'before',
After = 'after'
}
export interface RowData {
groups: React.ReactNode[];
variables: VariableDecoration[];
ascii: string;
}
export interface DiffRowOptions {
beforeAddress: string;
afterAddress: string;
before: RowData;
after: RowData;
doShowDivider: boolean;
isModified: boolean;
}
export interface DiffExtraColumnOptions extends Pick<MemoryTable.RowOptions, 'ascii' | 'variables'> {
afterAscii: string;
afterVariables: VariableDecoration[];
variables: VariableDecoration[];
ascii: string;
}

View File

@@ -0,0 +1,358 @@
/********************************************************************************
* Copyright (C) 2021 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 { Key, KeyCode } from '@theia/core/lib/browser';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { injectable } from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
import Long from 'long';
import { DebugProtocol } from '@vscode/debugprotocol';
import { hexStrToUnsignedLong } from '../../common/util';
import { MemoryOptionsWidget } from '../memory-widget/memory-options-widget';
import { MemoryTable, MemoryTableWidget } from '../memory-widget/memory-table-widget';
import { MemoryWidget } from '../memory-widget/memory-widget';
import { EasilyMappedObject } from '../utils/memory-hover-renderer';
import { Constants, Interfaces } from '../utils/memory-widget-utils';
import { nls } from '@theia/core/lib/common/nls';
export type EditableMemoryWidget = MemoryWidget<MemoryOptionsWidget, MemoryEditableTableWidget>;
export namespace EditableMemoryWidget {
export const ID = 'editable.memory.widget';
}
@injectable()
export class MemoryEditableTableWidget extends MemoryTableWidget {
protected pendingMemoryEdits = new Map<string, string>();
protected memoryEditsCompleted = new Deferred<void>();
protected highlightedField: Long = Long.fromInt(-1);
protected writeErrorInfo: { location: string, error: string } | undefined;
protected currentErrorTimeout: number | undefined;
protected doShowMoreMemoryBefore = false;
protected doShowMoreMemoryAfter = false;
protected override async doInit(): Promise<void> {
this.memoryEditsCompleted.resolve();
await super.doInit();
this.addClass('editable');
}
resetModifiedValue(valueAddress: Long): void {
const didChange = this.pendingMemoryEdits.delete(valueAddress.toString());
if (didChange) {
this.update();
}
}
protected override getState(): void {
super.getState();
if (!this.isInBounds(this.highlightedField)) {
this.highlightedField = this.memory.address;
}
}
protected override async handleMemoryChange(newMemory: Interfaces.MemoryReadResult): Promise<void> {
await this.memoryEditsCompleted.promise;
if (newMemory.bytes.length === 0) {
this.pendingMemoryEdits.clear();
}
super.handleMemoryChange(newMemory);
}
protected override areSameRegion(a: Interfaces.MemoryReadResult, b?: Interfaces.MemoryReadResult): boolean {
return b !== undefined && a.address.equals(b.address) && a.bytes.length === b.bytes.length;
}
protected override getTableFooter(): React.ReactNode {
const showButtons = !!this.pendingMemoryEdits.size && !this.writeErrorInfo;
return (
(showButtons || this.writeErrorInfo) && (
<div className='memory-edit-button-container'>
{showButtons && <button
className='theia-button secondary'
onClick={this.handleClearEditClick}
type='reset'
title={nls.localize('theia/memory-inspector/editable/clear', 'Clear Changes')}
>
{nls.localize('theia/memory-inspector/editable/clear', 'Clear Changes')}
</button>}
{showButtons && <button
className='theia-button main'
onClick={this.submitMemoryEdits}
type='submit'
title={nls.localize('theia/memory-inspector/editable/apply', 'Apply Changes')}
>
{nls.localize('theia/memory-inspector/editable/apply', 'Apply Changes')}
</button>}
{!!this.writeErrorInfo && <div className='memory-edit-error'>
<div className='memory-edit-error-location'>{`Error writing to 0x${Long.fromString(this.writeErrorInfo?.location).toString(16)}`}</div>
<div className='memory-edit-error-details'>{this.writeErrorInfo?.error}</div>
</div>}
</div>)
);
}
protected override getBitAttributes(arrayOffset: number, iteratee: Interfaces.LabeledUint8Array): Partial<Interfaces.FullNodeAttributes> {
const attributes = super.getBitAttributes(arrayOffset, iteratee);
const classNames = attributes.className?.split(' ') ?? [];
const itemID = this.memory.address.add(arrayOffset);
const isHighlight = itemID.equals(this.highlightedField);
const isEditPending = this.pendingMemoryEdits.has(itemID.toString());
const padder = isHighlight && isEditPending ? '\xa0' : '0'; // non-breaking space so it doesn't look like plain whitespace.
const stringValue = (this.pendingMemoryEdits.get(itemID.toString()) ?? this.memory.bytes[arrayOffset].toString(16)).padStart(2, padder);
if (!this.options.isFrozen) {
if (isHighlight) {
classNames.push('highlight');
}
if (isEditPending) {
classNames.push('modified');
}
}
return {
...attributes,
className: classNames.join(' '),
content: stringValue
};
}
protected override getHoverForChunk(span: HTMLElement): EasilyMappedObject | undefined {
const addressAsString = span.getAttribute('data-id');
if (addressAsString) {
const address = hexStrToUnsignedLong(addressAsString);
const { value } = this.composeByte(address, true);
const { value: inMemory } = this.composeByte(address, false);
const oldValue = this.previousBytes && this.composeByte(address, false, this.previousBytes).value;
const decimal = parseInt(value, 16);
const octal = decimal.toString(8).padStart(this.options.byteSize / 8, '0');
const UTF8 = String.fromCharCode(decimal);
const binary = this.getPaddedBinary(decimal);
const toSend: EasilyMappedObject = { hex: value, octal, binary, decimal };
if (UTF8) {
toSend.UTF8 = UTF8;
}
if (inMemory !== value) {
toSend['Current Value'] = inMemory;
}
if (oldValue !== undefined && oldValue !== value) {
toSend['Previous Value'] = oldValue;
}
return toSend;
}
return undefined;
}
protected composeByte(
addressPlusArrayOffset: Long,
usePendingEdits: boolean,
dataSource: Uint8Array = this.memory.bytes,
): Interfaces.ByteFromChunkData {
let value = '';
const offset = addressPlusArrayOffset.subtract(this.memory.address);
const chunksPerByte = this.options.byteSize / 8;
const startingChunkIndex = offset.subtract(offset.modulo(chunksPerByte));
const address = this.memory.address.add(startingChunkIndex.divide(chunksPerByte));
for (let i = 0; i < chunksPerByte; i += 1) {
const targetOffset = startingChunkIndex.add(i);
const targetChunk = this.getFromMapOrArray(targetOffset, usePendingEdits, dataSource);
value += targetChunk.padStart(2, '0');
}
return { address, value };
}
protected getFromMapOrArray(arrayOffset: Long, usePendingEdits: boolean, dataSource: Uint8Array = this.memory.bytes): string {
let value = usePendingEdits ? this.pendingMemoryEdits.get(arrayOffset.add(this.memory.address).toString()) : undefined;
if (value === undefined) {
value = dataSource[arrayOffset.toInt()]?.toString(16) ?? '';
}
return value;
}
protected handleClearEditClick = (): void => this.clearEdits();
protected clearEdits(address?: Long): void {
if (typeof address === 'number') {
this.pendingMemoryEdits.delete(address);
} else {
this.pendingMemoryEdits.clear();
}
this.update();
}
protected submitMemoryEdits = async (): Promise<void> => {
this.memoryEditsCompleted = new Deferred();
let didUpdateMemory = false;
for (const [key, edit] of this.createUniqueEdits()) {
try {
await this.doWriteMemory(edit);
didUpdateMemory = true;
this.pendingMemoryEdits.delete(key);
} catch (e) {
console.warn('Problem writing memory with arguments', edit, '\n', e);
const text = e instanceof Error ? e.message : 'Unknown error';
this.showWriteError(key, text);
break;
}
}
this.memoryEditsCompleted.resolve();
if (didUpdateMemory) {
this.optionsWidget.fetchNewMemory();
}
};
protected createUniqueEdits(): Array<[string, DebugProtocol.WriteMemoryArguments]> {
const addressesSubmitted = new Set<string>();
const edits: Array<[string, DebugProtocol.WriteMemoryArguments]> = [];
for (const k of this.pendingMemoryEdits.keys()) {
const address = Long.fromString(k);
const { address: addressToSend, value: valueToSend } = this.composeByte(address, true);
const memoryReference = '0x' + addressToSend.toString(16);
if (!addressesSubmitted.has(memoryReference)) {
const data = Buffer.from(valueToSend, 'hex').toString('base64');
edits.push([k, { memoryReference, data }]);
addressesSubmitted.add(memoryReference);
}
}
return edits;
}
protected doWriteMemory(writeMemoryArgs: DebugProtocol.WriteMemoryArguments): Promise<DebugProtocol.WriteMemoryResponse> {
return this.memoryProvider.writeMemory(writeMemoryArgs);
}
protected showWriteError(location: string, error: string): void {
if (this.currentErrorTimeout !== undefined) {
clearTimeout(this.currentErrorTimeout);
}
this.writeErrorInfo = { location, error };
this.update();
this.currentErrorTimeout = window.setTimeout(() => this.hideWriteError(), Constants.ERROR_TIMEOUT);
}
protected hideWriteError(): void {
this.currentErrorTimeout = undefined;
this.writeErrorInfo = undefined;
this.update();
}
protected override getWrapperHandlers(): MemoryTable.WrapperHandlers {
return this.options.isFrozen
? super.getWrapperHandlers()
: {
onClick: this.handleTableClick,
onContextMenu: this.handleTableRightClick,
onKeyDown: this.handleTableInput,
onMouseMove: this.handleTableMouseMove,
};
}
protected handleTableClick = (event: React.MouseEvent): void => {
const target = event.target as HTMLElement;
if (target.classList?.contains('eight-bits')) {
this.highlightedField = hexStrToUnsignedLong(target.getAttribute('data-id') ?? '-0x1');
this.update();
event.stopPropagation();
}
};
protected override doHandleTableRightClick(event: React.MouseEvent): void {
const target = event.target as HTMLElement;
if (target.classList?.contains('eight-bits')) {
this.highlightedField = hexStrToUnsignedLong(target.getAttribute('data-id') ?? '-0x1');
}
super.doHandleTableRightClick(event);
}
protected handleTableInput = (event: React.KeyboardEvent): void => {
if (this.highlightedField.lessThan(0)) {
return;
}
const keyCode = KeyCode.createKeyCode(event.nativeEvent).key?.keyCode;
const initialHighlight = this.highlightedField;
const initialHighlightIndex = initialHighlight.subtract(this.memory.address);
if (keyCode === Key.TAB.keyCode) {
return;
}
const arrayElementsPerRow = (this.options.byteSize / 8) * this.options.bytesPerGroup * this.options.groupsPerRow;
const isAlreadyEdited = this.pendingMemoryEdits.has(this.highlightedField.toString());
const oldValue = this.pendingMemoryEdits.get(initialHighlight.toString()) ??
this.memory.bytes[initialHighlightIndex.toInt()].toString(16).padStart(2, '0');
let possibleNewHighlight = new Long(-1);
let newValue = oldValue;
switch (keyCode) {
case Key.ARROW_DOWN.keyCode:
possibleNewHighlight = initialHighlight.add(arrayElementsPerRow);
event.preventDefault();
event.stopPropagation();
break;
case Key.ARROW_UP.keyCode:
possibleNewHighlight = initialHighlight.greaterThan(arrayElementsPerRow) ? initialHighlight.subtract(arrayElementsPerRow) : possibleNewHighlight;
event.preventDefault();
event.stopPropagation();
break;
case Key.ARROW_RIGHT.keyCode:
possibleNewHighlight = initialHighlight.add(1);
event.preventDefault();
event.stopPropagation();
break;
case Key.ARROW_LEFT.keyCode:
possibleNewHighlight = initialHighlight.greaterThan(0) ? initialHighlight.subtract(1) : possibleNewHighlight;
break;
case Key.BACKSPACE.keyCode:
newValue = oldValue.slice(0, oldValue.length - 1);
break;
case Key.DELETE.keyCode:
newValue = '';
break;
case Key.ENTER.keyCode:
this.submitMemoryEdits();
break;
case Key.ESCAPE.keyCode:
if (isAlreadyEdited) {
this.clearEdits(this.highlightedField);
} else {
this.clearEdits();
}
break;
default: {
const keyValue = parseInt(KeyCode.createKeyCode(event.nativeEvent).toString(), 16);
if (!Number.isNaN(keyValue)) {
newValue = isAlreadyEdited ? oldValue : '';
if (newValue.length < 2) {
newValue += keyValue.toString(16);
}
}
}
}
if (this.isInBounds(possibleNewHighlight)) {
this.highlightedField = possibleNewHighlight;
}
const valueWasChanged = newValue !== oldValue;
if (valueWasChanged) {
this.pendingMemoryEdits.set(this.highlightedField.toString(), newValue);
}
if (valueWasChanged || !this.highlightedField.equals(initialHighlight)) {
this.update();
}
};
protected isInBounds(candidateAddress: Long): boolean {
const { address, bytes } = this.memory;
return candidateAddress.greaterThanOrEqual(address) &&
candidateAddress.lessThan(address.add(bytes.length));
}
}

View File

@@ -0,0 +1,301 @@
/********************************************************************************
* Copyright (C) 2019 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 { AbstractViewContribution, FrontendApplicationContribution, Widget } from '@theia/core/lib/browser';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { Command, CommandRegistry, MenuModelRegistry } from '@theia/core/lib/common';
import { Color } from '@theia/core/lib/common/color';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { DebugScope, DebugVariable } from '@theia/debug/lib/browser/console/debug-console-items';
import { DebugFrontendApplicationContribution } from '@theia/debug/lib/browser/debug-frontend-application-contribution';
import { DebugVariablesWidget } from '@theia/debug/lib/browser/view/debug-variables-widget';
import { MemoryEditableTableWidget } from './editable-widget/memory-editable-table-widget';
import { MemoryProviderService } from './memory-provider/memory-provider-service';
import { MemoryTableWidget } from './memory-widget/memory-table-widget';
import { MemoryWidget } from './memory-widget/memory-widget';
import { RegisterTableWidget } from './register-widget/register-table-widget';
import { RegisterWidget } from './register-widget/register-widget-types';
import {
CreateNewMemoryViewCommand, CreateNewRegisterViewCommand, FollowPointerDebugCommand, FollowPointerTableCommand, MemoryCommand,
RegisterSetVariableCommand, ResetModifiedCellCommand, ToggleDiffSelectWidgetVisibilityCommand, ViewVariableInMemoryCommand, ViewVariableInRegisterViewCommand
} from './utils/memory-commands';
import { MemoryWidgetManager } from './utils/memory-widget-manager';
import { VariableRange } from './utils/memory-widget-variable-utils';
import { MemoryDockPanel } from './wrapper-widgets/memory-dock-panel';
import { MemoryLayoutWidget } from './wrapper-widgets/memory-layout-widget';
import { nls } from '@theia/core/lib/common/nls';
import Long from 'long';
const ONE_HALF_OPACITY = 0.5;
@injectable()
export class DebugFrontendContribution extends AbstractViewContribution<MemoryLayoutWidget>
implements FrontendApplicationContribution,
TabBarToolbarContribution,
ColorContribution {
@inject(DebugFrontendApplicationContribution) protected readonly debugContribution: DebugFrontendApplicationContribution;
@inject(MemoryWidgetManager) protected readonly memoryWidgetManager: MemoryWidgetManager;
@inject(FrontendApplicationStateService) protected readonly stateService: FrontendApplicationStateService;
@inject(MemoryProviderService) protected readonly memoryProvider: MemoryProviderService;
constructor() {
super({
widgetId: MemoryLayoutWidget.ID,
widgetName: MemoryLayoutWidget.LABEL,
defaultWidgetOptions: {
area: 'right',
},
toggleCommandId: MemoryCommand.id,
});
}
@postConstruct()
init(): void {
this.stateService.reachedState('initialized_layout').then(() => {
// Close leftover widgets from previous sessions.
this.memoryWidgetManager.availableWidgets.forEach(widget => {
if (!(widget.parent instanceof MemoryDockPanel)) {
widget.close();
}
});
});
}
async initializeLayout(): Promise<void> {
await this.openView({ activate: false });
}
override registerCommands(registry: CommandRegistry): void {
super.registerCommands(registry);
registry.registerCommand(ViewVariableInMemoryCommand, {
execute: async () => {
const { selectedVariable } = this.debugContribution;
const referenceText = this.memoryProvider.formatVariableReference(selectedVariable);
if (referenceText) {
await this.openMemoryWidgetAt(referenceText);
}
},
isVisible: () => {
const { selectedVariable } = this.debugContribution;
return Boolean(this.memoryProvider.supportsVariableReferenceSyntax(selectedVariable) && this.memoryProvider.formatVariableReference(selectedVariable));
},
});
registry.registerCommand(ViewVariableInRegisterViewCommand, {
execute: async () => {
const name = this.debugContribution.selectedVariable?.name;
if (name) {
await this.openRegisterWidgetWithReg(name);
}
},
isVisible: () => {
let { selectedVariable: currentLevel } = this.debugContribution;
if (!currentLevel) {
return false;
}
// Make sure it looks like it has a numerical value.
try { BigInt(currentLevel.value); } catch { return false; }
while (currentLevel.parent instanceof DebugVariable) {
currentLevel = currentLevel.parent;
}
return currentLevel.parent instanceof DebugScope && currentLevel.parent?.['raw']?.name === 'Registers';
},
});
registry.registerCommand(FollowPointerDebugCommand, {
isVisible: () => !!this.isPointer(this.debugContribution.selectedVariable?.type),
isEnabled: () => !!this.isPointer(this.debugContribution.selectedVariable?.type),
execute: async () => {
const name = this.debugContribution.selectedVariable?.name;
if (name) {
await this.openMemoryWidgetAt(name);
}
},
});
registry.registerCommand(ResetModifiedCellCommand, {
isEnabled: (widgetToActOn, address: Long) => Long.isLong(address) && widgetToActOn instanceof MemoryEditableTableWidget,
isVisible: (widgetToActOn, address: Long) => Long.isLong(address) && widgetToActOn instanceof MemoryEditableTableWidget,
execute: (widgetToActOn: MemoryEditableTableWidget, address: Long) => widgetToActOn.resetModifiedValue(address),
});
registry.registerCommand(FollowPointerTableCommand, {
isEnabled: (widgetToActOn, address, variable?: VariableRange) => widgetToActOn instanceof MemoryTableWidget &&
this.isPointer(variable?.type),
isVisible: (widgetToActOn, address, variable?: VariableRange) => widgetToActOn instanceof MemoryTableWidget &&
this.isPointer(variable?.type),
execute: (widgetToActOn: MemoryTableWidget, address, variable: VariableRange) => {
if (variable?.name) {
widgetToActOn.optionsWidget.setAddressAndGo(variable.name);
}
},
});
registry.registerCommand(CreateNewMemoryViewCommand, {
isEnabled: w => this.withWidget(() => true, w),
isVisible: w => this.withWidget(() => true, w),
execute: () => this.memoryWidgetManager.createNewMemoryWidget(),
});
registry.registerCommand(CreateNewRegisterViewCommand, {
isEnabled: w => this.withWidget(() => true, w),
isVisible: w => this.withWidget(() => true, w),
execute: () => this.memoryWidgetManager.createNewMemoryWidget('register'),
});
registry.registerCommand(RegisterSetVariableCommand, {
isEnabled: (widgetToActOn, dVar: DebugVariable) => widgetToActOn instanceof RegisterTableWidget &&
dVar && dVar.supportSetVariable,
isVisible: (widgetToActOn, dVar: DebugVariable) => widgetToActOn instanceof RegisterTableWidget &&
dVar && dVar.supportSetVariable,
execute: (widgetToActOn: RegisterTableWidget, dVar: DebugVariable) => dVar && widgetToActOn.handleSetValue(dVar),
});
registry.registerCommand(ToggleDiffSelectWidgetVisibilityCommand, {
isVisible: widget => this.withWidget(() => this.memoryWidgetManager.canCompare, widget),
execute: (widget: MemoryLayoutWidget) => {
widget.toggleComparisonVisibility();
},
});
}
protected isPointer(type?: string): boolean {
return !!type?.includes('*');
}
/**
* @param {string} addressReference Should be the exact string to be used in the address bar. I.e. it must resolve to an address value.
*/
protected async openMemoryWidgetAt(addressReference: string): Promise<MemoryWidget> {
await this.openView({ activate: false });
const newWidget = await this.memoryWidgetManager.createNewMemoryWidget();
await this.shell.activateWidget(newWidget.id);
if (newWidget) {
newWidget.optionsWidget.setAddressAndGo(addressReference);
}
return newWidget;
}
protected async openRegisterWidgetWithReg(name: string): Promise<MemoryWidget> {
await this.openView({ activate: false });
const newWidget = await this.memoryWidgetManager.createNewMemoryWidget<RegisterWidget>('register');
await this.shell.activateWidget(newWidget.id);
if (newWidget) {
newWidget.optionsWidget.setRegAndUpdate(name);
}
return newWidget;
}
protected withWidget(fn: (widget: MemoryLayoutWidget) => boolean, widget: Widget | undefined = this.tryGetWidget()): boolean {
if (widget instanceof MemoryLayoutWidget && widget.id === MemoryLayoutWidget.ID) {
return fn(widget);
}
return false;
}
override registerMenus(registry: MenuModelRegistry): void {
super.registerMenus(registry);
const registerMenuActions = (menuPath: string[], ...commands: Command[]): void => {
for (const [index, command] of commands.entries()) {
registry.registerMenuAction(menuPath, {
commandId: command.id,
label: command.label,
icon: command.iconClass,
order: String.fromCharCode('a'.charCodeAt(0) + index),
});
}
};
registry.registerMenuAction(
DebugVariablesWidget.WATCH_MENU,
{ commandId: ViewVariableInMemoryCommand.id, label: ViewVariableInMemoryCommand.label },
);
registry.registerMenuAction(
DebugVariablesWidget.WATCH_MENU,
{ commandId: FollowPointerDebugCommand.id, label: FollowPointerDebugCommand.label },
);
registry.registerMenuAction(
DebugVariablesWidget.WATCH_MENU,
{ commandId: ViewVariableInRegisterViewCommand.id, label: ViewVariableInRegisterViewCommand.label },
);
registry.registerMenuAction(
MemoryEditableTableWidget.CONTEXT_MENU,
{ commandId: ResetModifiedCellCommand.id, label: ResetModifiedCellCommand.label },
);
registry.registerMenuAction(
MemoryTableWidget.CONTEXT_MENU,
{ commandId: FollowPointerTableCommand.id, label: FollowPointerTableCommand.label },
);
registerMenuActions(
RegisterTableWidget.CONTEXT_MENU,
RegisterSetVariableCommand,
);
}
registerToolbarItems(toolbarRegistry: TabBarToolbarRegistry): void {
toolbarRegistry.registerItem({
id: CreateNewMemoryViewCommand.id,
command: CreateNewMemoryViewCommand.id,
tooltip: CreateNewMemoryViewCommand.label,
priority: -2,
});
toolbarRegistry.registerItem({
id: CreateNewRegisterViewCommand.id,
command: CreateNewRegisterViewCommand.id,
tooltip: CreateNewRegisterViewCommand.label,
priority: -1,
});
toolbarRegistry.registerItem({
id: ToggleDiffSelectWidgetVisibilityCommand.id,
command: ToggleDiffSelectWidgetVisibilityCommand.id,
tooltip: nls.localize('theia/memory-inspector/toggleComparisonWidgetVisibility', 'Toggle Comparison Widget Visibility'),
priority: -3,
onDidChange: this.memoryWidgetManager.onChanged,
});
}
registerColors(colorRegistry: ColorRegistry): void {
colorRegistry.register(
{
id: 'memoryDiff.removedTextBackground',
defaults: {
dark: Color.transparent('diffEditor.removedTextBackground', ONE_HALF_OPACITY),
light: Color.transparent('diffEditor.removedTextBackground', ONE_HALF_OPACITY),
},
description: 'A less opaque diff color for use in the Memory Inspector where various overlays may me in place at once.',
},
{
id: 'memoryDiff.insertedTextBackground',
defaults: {
dark: Color.transparent('diffEditor.insertedTextBackground', ONE_HALF_OPACITY),
light: Color.transparent('diffEditor.insertedTextBackground', ONE_HALF_OPACITY),
},
description: 'A less opaque diff color for use in the Memory Inspector where various overlays may me in place at once.',
},
{
id: 'memoryInspector.focusBorder',
defaults: {
dark: Color.transparent('focusBorder', ONE_HALF_OPACITY),
light: Color.transparent('focusBorder', ONE_HALF_OPACITY),
},
description: 'A less opaque focus border color for use in the Memory Inspector where several overlays may be in place at once.',
},
{
id: 'memoryInspector.foreground',
defaults: {
dark: Color.transparent('editor.foreground', ONE_HALF_OPACITY),
light: Color.transparent('editor.foreground', ONE_HALF_OPACITY),
},
description: 'A less opaque foreground text style for use in the Memory Inspector',
},
);
}
}

View File

@@ -0,0 +1,118 @@
/********************************************************************************
* Copyright (C) 2019 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 '../../src/browser/register-widget/register-widget.css';
import '../../src/browser/style/index.css';
import '../../src/browser/utils/multi-select-bar.css';
import { bindContributionProvider } from '@theia/core';
import { bindViewContribution, FrontendApplicationContribution, WidgetFactory } from '@theia/core/lib/browser';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { ContainerModule } from '@theia/core/shared/inversify';
import { DebugFrontendContribution } from './memory-inspector-frontend-contribution';
import { MemoryDiffOptionsWidget } from './diff-widget/memory-diff-options-widget';
import { MemoryDiffSelectWidget } from './diff-widget/memory-diff-select-widget';
import { MemoryDiffTableWidget, MemoryDiffWidget } from './diff-widget/memory-diff-table-widget';
import { EditableMemoryWidget, MemoryEditableTableWidget } from './editable-widget/memory-editable-table-widget';
import { DefaultMemoryProvider, MemoryProvider } from './memory-provider/memory-provider';
import { MemoryProviderService } from './memory-provider/memory-provider-service';
import { MemoryOptionsWidget } from './memory-widget/memory-options-widget';
import { MemoryTableWidget } from './memory-widget/memory-table-widget';
import { MemoryWidget } from './memory-widget/memory-widget';
import { RegisterOptionsWidget } from './register-widget/register-options-widget';
import { RegisterTableWidget } from './register-widget/register-table-widget';
import { RegisterWidget } from './register-widget/register-widget-types';
import { MemoryHoverRendererService } from './utils/memory-hover-renderer';
import { MemoryWidgetManager } from './utils/memory-widget-manager';
import { MemoryDiffWidgetData, MemoryWidgetOptions, RegisterWidgetOptions } from './utils/memory-widget-utils';
import { MemoryDockPanel } from './wrapper-widgets/memory-dock-panel';
import { MemoryDockpanelPlaceholder } from './wrapper-widgets/memory-dockpanel-placeholder-widget';
import { MemoryLayoutWidget } from './wrapper-widgets/memory-layout-widget';
import { CDTGDBMemoryProvider } from './memory-provider/cdt-gdb-memory-provider';
export default new ContainerModule(bind => {
bindViewContribution(bind, DebugFrontendContribution);
bind(ColorContribution).toService(DebugFrontendContribution);
bind(TabBarToolbarContribution).toService(DebugFrontendContribution);
bind(FrontendApplicationContribution).toService(DebugFrontendContribution);
bind(MemoryProviderService).toSelf().inSingletonScope();
bind(DefaultMemoryProvider).toSelf().inSingletonScope();
bindContributionProvider(bind, MemoryProvider);
bind(MemoryProvider).to(CDTGDBMemoryProvider).inSingletonScope();
bind(MemoryLayoutWidget).toSelf().inSingletonScope();
bind(MemoryDiffSelectWidget).toSelf().inSingletonScope();
bind(MemoryDockpanelPlaceholder).toSelf().inSingletonScope();
bind(MemoryHoverRendererService).toSelf().inSingletonScope();
bind(MemoryWidgetManager).toSelf().inSingletonScope();
bind(WidgetFactory).toDynamicValue(({ container }) => ({
id: MemoryDockPanel.ID,
createWidget: (): MemoryDockPanel => MemoryDockPanel.createWidget(container),
}));
bind(WidgetFactory).toDynamicValue(({ container }) => ({
id: MemoryLayoutWidget.ID,
createWidget: (): MemoryLayoutWidget => container.get(MemoryLayoutWidget),
})).inSingletonScope();
bind(WidgetFactory).toDynamicValue(({ container }) => ({
id: MemoryWidget.ID,
createWidget: (options: MemoryWidgetOptions): MemoryWidget => MemoryWidget.createWidget<MemoryOptionsWidget, MemoryTableWidget>(
container,
MemoryOptionsWidget,
MemoryTableWidget,
MemoryWidgetOptions,
options,
),
}));
bind(WidgetFactory).toDynamicValue(({ container }) => ({
id: EditableMemoryWidget.ID,
createWidget: (options: MemoryWidgetOptions): EditableMemoryWidget => MemoryWidget
.createWidget<MemoryOptionsWidget, MemoryEditableTableWidget>(
container,
MemoryOptionsWidget,
MemoryEditableTableWidget,
MemoryWidgetOptions,
options,
),
}));
bind(WidgetFactory).toDynamicValue(({ container }) => ({
id: MemoryDiffWidget.ID,
createWidget: (options: MemoryDiffWidgetData): MemoryDiffWidget => MemoryWidget
.createWidget<MemoryDiffOptionsWidget, MemoryDiffTableWidget>(
container,
MemoryDiffOptionsWidget,
MemoryDiffTableWidget,
MemoryDiffWidgetData,
options,
),
}));
bind(WidgetFactory).toDynamicValue(({ container }) => ({
id: RegisterWidget.ID,
createWidget: (options: RegisterWidgetOptions): RegisterWidget => RegisterWidget
.createContainer(
container,
RegisterOptionsWidget,
RegisterTableWidget,
RegisterWidgetOptions,
options,
).get<RegisterWidget>(MemoryWidget),
}));
});

View File

@@ -0,0 +1,132 @@
/********************************************************************************
* Copyright (C) 2019 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 { injectable } from '@theia/core/shared/inversify';
import { DebugScope, DebugVariable } from '@theia/debug/lib/browser/console/debug-console-items';
import { DebugSession } from '@theia/debug/lib/browser/debug-session';
import { hexStrToUnsignedLong } from '../../common/util';
import { VariableRange } from '../utils/memory-widget-variable-utils';
import { AbstractMemoryProvider } from './memory-provider';
/**
* @file this file exists to show the customizations possible for specific debug adapters. Within the confines of the DebugAdapterProtocol, different adapters can behave
* quite differently. In particular, they can differ in the kinds of expressions that they treat as references (in the `memoryReference` field of MemoryReadArguments, for example)
* and the kinds of expressions that they can evaluate (for example to assist in determining the size of variables). The `MemoryProvider` type exists to allow applications
* to enhance the base functionality of the Memory Inspector by tapping into specifics of known adapters.
*/
/**
* Read memory through the current debug session, using the cdt-gdb-adapter
* extension to read memory.
*/
@injectable()
export class CDTGDBMemoryProvider extends AbstractMemoryProvider {
canHandle(session: DebugSession): boolean {
return session.configuration.type === 'gdb';
}
override async getLocals(session: DebugSession | undefined): Promise<VariableRange[]> {
if (session === undefined) {
console.warn('No active debug session.');
return [];
}
const frame = session.currentFrame;
if (frame === undefined) {
throw new Error('No active stack frame.');
}
const ranges: VariableRange[] = [];
const scopes = await frame.getScopes();
const scopesWithoutRegisters = scopes.filter(x => x.render() !== 'Registers');
for (const scope of scopesWithoutRegisters) {
const variables = await scope.getElements();
for (const v of variables) {
if (v instanceof DebugVariable) {
const addrExp = `&${v.name}`;
const sizeExp = `sizeof(${v.name})`;
const addrResp = await session.sendRequest('evaluate', {
expression: addrExp,
context: 'watch',
frameId: frame.raw.id,
}).catch(e => { console.warn(`Failed to evaluate ${addrExp}. Corresponding variable will be omitted from Memory Inspector display.`, e); });
if (!addrResp) { continue; }
const sizeResp = await session.sendRequest('evaluate', {
expression: sizeExp,
context: 'watch',
frameId: frame.raw.id,
}).catch(e => { console.warn(`Failed to evaluate ${sizeExp}. Corresponding variable will be omitted from Memory Inspector display.`, e); });
if (!sizeResp) { continue; }
// Make sure the address is in the format we expect.
const addressPart = /0x[0-9a-f]+/i.exec(addrResp.body.result);
if (!addressPart) { continue; }
if (!/^[0-9]+$/.test(sizeResp.body.result)) { continue; }
const size = parseInt(sizeResp.body.result);
const address = hexStrToUnsignedLong(addressPart[0]);
const pastTheEndAddress = address.add(size);
ranges.push({
name: v.name,
address,
pastTheEndAddress,
type: v.type,
value: v.value,
});
}
}
}
return ranges;
}
override supportsVariableReferenceSyntax(session: DebugSession, currentLevel?: DebugVariable): boolean {
if (this.canHandle(session)) {
if (!currentLevel) {
return false;
}
while (currentLevel.parent instanceof DebugVariable) {
currentLevel = currentLevel.parent;
}
return currentLevel.parent instanceof DebugScope && currentLevel.parent['raw'].name === 'Local';
}
return false;
}
override formatVariableReference(session: DebugSession, currentLevel?: DebugVariable): string {
if (currentLevel && this.canHandle(session)) {
let { name } = currentLevel;
while (currentLevel.parent instanceof DebugVariable) {
const separator = name.startsWith('[') ? '' : '.';
currentLevel = currentLevel.parent;
if (name.startsWith(`*${currentLevel.name}.`)) { // Theia has added a layer of pointer dereferencing
name = name.replace(`*${currentLevel.name}.`, `(*${currentLevel.name})->`);
} else if (name.startsWith(`*${currentLevel.name}`)) {
// that's fine, it's what you clicked on and probably what you want to see.
} else {
name = `${currentLevel.name}${separator}${name}`;
}
}
return `&(${name})`;
}
return '';
}
}

View File

@@ -0,0 +1,86 @@
/********************************************************************************
* Copyright (C) 2021 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 { ContributionProvider } from '@theia/core';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { DebugVariable } from '@theia/debug/lib/browser/console/debug-console-items';
import { DebugSession } from '@theia/debug/lib/browser/debug-session';
import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import { DebugProtocol } from '@vscode/debugprotocol';
import { Interfaces } from '../utils/memory-widget-utils';
import { VariableRange } from '../utils/memory-widget-variable-utils';
import { DefaultMemoryProvider, MemoryProvider } from './memory-provider';
import { nls } from '@theia/core/lib/common/nls';
@injectable()
export class MemoryProviderService {
@inject(DebugSessionManager) protected readonly sessionManager: DebugSessionManager;
@inject(DefaultMemoryProvider) protected readonly defaultProvider: DefaultMemoryProvider;
@inject(ContributionProvider) @named(MemoryProvider)
protected readonly contributions: ContributionProvider<MemoryProvider>;
readMemory(readMemoryArguments: DebugProtocol.ReadMemoryArguments): Promise<Interfaces.MemoryReadResult> {
const readError = nls.localize('theia/memory-inspector/provider/readError', 'Cannot read memory. No active debug session.');
const session = this.getSession(readError);
if (!session.capabilities.supportsReadMemoryRequest) {
throw new Error('Cannot read memory. The current session does not support the request.');
}
const provider = this.getProvider(session);
return provider.readMemory(session, readMemoryArguments);
}
writeMemory(writeMemoryArguments: DebugProtocol.WriteMemoryArguments): Promise<DebugProtocol.WriteMemoryResponse> {
const writeError = nls.localize('theia/memory-inspector/provider/writeError', 'Cannot write memory. No active debug session.');
const session = this.getSession(writeError);
if (!session.capabilities.supportsWriteMemoryRequest) {
throw new Error('Cannot write memory. The current session does not support the request.');
}
const provider = this.getProvider(session, 'writeMemory');
return provider.writeMemory(session, writeMemoryArguments);
}
getLocals(): Promise<VariableRange[]> {
const localsError = nls.localize('theia/memory-inspector/provider/localsError', 'Cannot read local variables. No active debug session.');
const session = this.getSession(localsError);
const provider = this.getProvider(session, 'getLocals');
return provider.getLocals(session);
}
supportsVariableReferenceSyntax(variable?: DebugVariable): boolean {
if (!this.sessionManager.currentSession) { return false; }
const provider = this.getProvider(this.sessionManager.currentSession, 'supportsVariableReferenceSyntax');
return provider.supportsVariableReferenceSyntax(this.sessionManager.currentSession, variable);
}
formatVariableReference(variable?: DebugVariable): string {
if (!this.sessionManager.currentSession) { return ''; }
const provider = this.getProvider(this.sessionManager.currentSession, 'formatVariableReference');
return provider.formatVariableReference(this.sessionManager.currentSession, variable);
}
/** @throws with {@link message} if there is no active debug session. */
protected getSession(message: string): DebugSession {
if (this.sessionManager.currentSession) { return this.sessionManager.currentSession; }
throw new Error(message);
}
protected getProvider(session: DebugSession, ensure?: keyof MemoryProvider): Required<MemoryProvider> {
return this.contributions.getContributions()
.find((candidate): candidate is Required<MemoryProvider> => Boolean(!ensure || candidate[ensure]) && candidate.canHandle(session))
?? this.defaultProvider;
}
}

View File

@@ -0,0 +1,23 @@
/********************************************************************************
* Copyright (C) 2019 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 { expect } from 'chai';
describe('memory-inspector', () => {
it('should pass', () => {
expect(true).to.equal(true);
});
});

View File

@@ -0,0 +1,119 @@
/********************************************************************************
* Copyright (C) 2019 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 { inject, injectable } from '@theia/core/shared/inversify';
import { DebugVariable } from '@theia/debug/lib/browser/console/debug-console-items';
import { DebugSession } from '@theia/debug/lib/browser/debug-session';
import { DebugProtocol } from '@vscode/debugprotocol';
import { Interfaces } from '../utils/memory-widget-utils';
import { VariableRange } from '../utils/memory-widget-variable-utils';
import Long from 'long';
export const MemoryProvider = Symbol('MemoryProvider');
/**
* Representation of a memory provider. It is only necessary to implement a new Memory Provider if the behavior of the Debug Adapter for a given session type
* deviates from the Debug Adapter Protocol. Otherwise, the DefaultMemoryProvider should handle standard DAP requests and responses.
*
* Specific peculiarities that might require special handling include: restrictions on the formatting of memory location identifiers (only hex numbers, e.g.)
* or deviations from the DAP in the format of the response to a given request.
*/
export interface MemoryProvider {
/**
* @param session
* @return whether the given MemoryProvider can handle memory reading / writing for a session of the type submitted.
*/
canHandle(session: DebugSession): boolean;
readMemory(session: DebugSession, readMemoryArguments: DebugProtocol.ReadMemoryArguments): Promise<Interfaces.MemoryReadResult>;
writeMemory?(session: DebugSession, writeMemoryArguments: DebugProtocol.WriteMemoryArguments): Promise<DebugProtocol.WriteMemoryResponse>;
getLocals?(session: DebugSession): Promise<VariableRange[]>;
/**
* Whether the current debugger supports variable reference syntax (e.g. &a) in the `memoryReference` field of
* @link DebugProtocol.ReadMemoryArguments, ReadMemoryArguments}
*/
supportsVariableReferenceSyntax?(session: DebugSession, variable?: DebugVariable): boolean;
formatVariableReference?(session: DebugSession, variable?: DebugVariable): string;
}
/**
* Convert a base64-encoded string of bytes to the Uint8Array equivalent.
*/
export function base64ToBytes(base64: string): Interfaces.LabeledUint8Array {
return Buffer.from(base64, 'base64');
}
@injectable()
export class DefaultMemoryProvider implements Required<MemoryProvider> {
// This provider should only be used a fallback - it shouldn't volunteer to handle any session.
canHandle(): false {
return false;
}
async readMemory(session: DebugSession, readMemoryArguments: DebugProtocol.ReadMemoryArguments): Promise<Interfaces.MemoryReadResult> {
console.log('Requesting memory with the following arguments:', readMemoryArguments);
const result = await session.sendRequest('readMemory', readMemoryArguments) as DebugProtocol.ReadMemoryResponse;
if (result.body?.data) {
const { body: { data, address } } = result;
const bytes = base64ToBytes(data);
const longAddress = result.body.address.startsWith('0x') ? Long.fromString(address, true, 16) : Long.fromString(address, true, 10);
return { bytes, address: longAddress };
}
throw new Error('Received no data from debug adapter.');
}
async writeMemory(session: DebugSession, writeMemoryArguments: DebugProtocol.WriteMemoryArguments): Promise<DebugProtocol.WriteMemoryResponse> {
return session.sendRequest('writeMemory', writeMemoryArguments);
}
async getLocals(session: DebugSession): Promise<VariableRange[]> {
return [];
}
supportsVariableReferenceSyntax(session: DebugSession, variable?: DebugVariable | undefined): boolean {
return false;
}
formatVariableReference(session: DebugSession, variable?: DebugVariable | undefined): string {
return '';
}
}
@injectable()
export abstract class AbstractMemoryProvider implements Required<MemoryProvider> {
@inject(DefaultMemoryProvider) protected readonly defaultProvider: DefaultMemoryProvider;
abstract canHandle(session: DebugSession): boolean;
readMemory(session: DebugSession, readMemoryArguments: DebugProtocol.ReadMemoryArguments): Promise<Interfaces.MemoryReadResult> {
return this.defaultProvider.readMemory(session, readMemoryArguments);
}
writeMemory(session: DebugSession, writeMemoryArguments: DebugProtocol.WriteMemoryArguments): Promise<DebugProtocol.WriteMemoryResponse> {
return this.defaultProvider.writeMemory(session, writeMemoryArguments);
}
getLocals(session: DebugSession): Promise<VariableRange[]> {
return this.defaultProvider.getLocals(session);
}
supportsVariableReferenceSyntax(session: DebugSession, variable?: DebugVariable | undefined): boolean {
return this.defaultProvider.supportsVariableReferenceSyntax(session, variable);
}
formatVariableReference(session: DebugSession, variable?: DebugVariable | undefined): string {
return this.defaultProvider.formatVariableReference(session, variable);
}
}

View File

@@ -0,0 +1,738 @@
/********************************************************************************
* Copyright (C) 2021 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 { deepFreeze, Disposable, DisposableCollection, Emitter, nls } from '@theia/core';
import { Key, KeyCode, Message, ReactWidget, StatefulWidget } from '@theia/core/lib/browser';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
import { DebugSession, DebugState } from '@theia/debug/lib/browser/debug-session';
import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import Long from 'long';
import { MemoryProviderService } from '../memory-provider/memory-provider-service';
import { Recents } from '../utils/memory-recents';
import { MWInput, MWInputWithSelect, MWSelect } from '../utils/memory-widget-components';
import { Constants, Interfaces, MemoryWidgetOptions, Utils } from '../utils/memory-widget-utils';
import { VariableRange } from '../utils/memory-widget-variable-utils';
import { MWMultiSelect, SingleSelectItemProps } from '../utils/multi-select-bar';
import debounce = require('@theia/core/shared/lodash.debounce');
export const EMPTY_MEMORY: Interfaces.MemoryReadResult = deepFreeze({
bytes: new Uint8Array(),
address: new Long(0, 0, true),
});
export const LOCATION_FIELD_ID = 't-mv-location';
export const LENGTH_FIELD_ID = 't-mv-length';
export const LOCATION_OFFSET_FIELD_ID = 't-mv-location-offset';
export const BYTES_PER_ROW_FIELD_ID = 't-mv-bytesrow';
export const BYTES_PER_GROUP_FIELD_ID = 't-mv-bytesgroup';
export const ENDIAN_SELECT_ID = 't-mv-endiannesss';
export const ASCII_TOGGLE_ID = 't-mv-ascii-toggle';
export const AUTO_UPDATE_TOGGLE_ID = 't-mv-auto-update-toggle';
@injectable()
export class MemoryOptionsWidget extends ReactWidget implements StatefulWidget {
static ID = 'memory-view-options-widget';
static LABEL = nls.localize('theia/memory-inspector/memoryTitle', 'Memory');
iconClass = 'memory-view-icon';
lockIconClass = 'memory-lock-icon';
static WIDGET_H2_CLASS = 'memory-widget-header';
static WIDGET_HEADER_INPUT_CLASS = 'memory-widget-header-input';
protected additionalColumnSelectLabel = nls.localize('theia/memory-inspector/extraColumn', 'Extra Column');
protected sessionListeners = new DisposableCollection();
protected readonly onOptionsChangedEmitter = new Emitter<string | undefined>();
readonly onOptionsChanged = this.onOptionsChangedEmitter.event;
protected readonly onMemoryChangedEmitter = new Emitter<Interfaces.MemoryReadResult>();
readonly onMemoryChanged = this.onMemoryChangedEmitter.event;
protected pinnedMemoryReadResult: Deferred<Interfaces.MemoryReadResult | false> | undefined;
protected memoryReadResult: Interfaces.MemoryReadResult = EMPTY_MEMORY;
protected columnsDisplayed: Interfaces.ColumnsDisplayed = {
address: {
label: nls.localizeByDefault('Address'),
doRender: true
},
data: {
label: nls.localize('theia/memory-inspector/data', 'Data'),
doRender: true
},
variables: {
label: nls.localizeByDefault('Variables'),
doRender: true
},
ascii: {
label: nls.localize('theia/memory-inspector/ascii', 'ASCII'),
doRender: false
},
};
protected byteSize = 8;
protected bytesPerGroup = 1;
protected groupsPerRow = 4;
protected variables: VariableRange[] = [];
protected endianness: Interfaces.Endianness = Interfaces.Endianness.Little;
protected memoryReadError = nls.localize('theia/memory-inspector/memory/readError/noContents', 'No memory contents currently available.');
protected address: string | number = 0;
protected offset = 0;
protected readLength = 256;
protected doDisplaySettings = false;
protected doUpdateAutomatically = true;
protected showMemoryError = false;
protected errorTimeout: NodeJS.Timeout | undefined = undefined;
protected addressField: HTMLInputElement | undefined;
protected offsetField: HTMLInputElement | undefined;
protected readLengthField: HTMLInputElement | undefined;
protected headerInputField: HTMLInputElement | undefined;
protected recentLocations = new Recents();
protected showTitleEditIcon = false;
protected isTitleEditable = false;
@inject(MemoryProviderService) protected readonly memoryProvider: MemoryProviderService;
@inject(DebugSessionManager) protected readonly sessionManager: DebugSessionManager;
@inject(MemoryWidgetOptions) protected readonly memoryWidgetOptions: MemoryWidgetOptions;
get memory(): Interfaces.WidgetMemoryState {
return {
...this.memoryReadResult,
variables: this.variables,
};
}
get options(): Interfaces.MemoryOptions {
return this.storeState();
}
@postConstruct()
protected init(): void {
this.addClass(MemoryOptionsWidget.ID);
this.title.label = nls.localize('theia/memory-inspector/memory', 'Memory ({0})', this.memoryWidgetOptions.displayId);
this.title.caption = nls.localize('theia/memory-inspector/memory', 'Memory ({0})', this.memoryWidgetOptions.displayId);
this.title.iconClass = this.iconClass;
this.title.closable = true;
if (this.memoryWidgetOptions.dynamic !== false) {
this.toDispose.push(this.sessionManager.onDidChangeActiveDebugSession(({ current }) => {
this.setUpListeners(current);
}));
this.toDispose.push(this.sessionManager.onDidCreateDebugSession(current => {
this.setUpListeners(current);
}));
this.setUpListeners(this.sessionManager.currentSession);
}
this.toDispose.push(this.onOptionsChanged(() => this.update()));
this.update();
}
async setAddressAndGo(
newAddress: string,
newOffset?: number,
newLength?: number,
direction?: 'above' | 'below',
): Promise<Interfaces.MemoryReadResult | false | undefined> {
let doUpdate = false;
const originalValues = {
offset: '',
length: '',
};
if (this.addressField) {
this.addressField.value = newAddress;
doUpdate = true;
}
if (this.offsetField && newOffset !== undefined) {
originalValues.offset = this.offsetField.value;
this.offsetField.value = newOffset.toString();
doUpdate = true;
}
if (this.readLengthField && newLength !== undefined) {
originalValues.length = this.readLengthField.value;
this.readLengthField.value = newLength.toString();
doUpdate = true;
}
if (doUpdate && this.readLengthField && this.offsetField) {
this.pinnedMemoryReadResult = new Deferred<Interfaces.MemoryReadResult>();
this.updateMemoryView();
const result = await this.pinnedMemoryReadResult.promise;
if (result === false) {
// Memory request errored
this.readLengthField.value = originalValues.length;
this.offsetField.value = originalValues.offset;
}
if (result) {
// Memory request returned some memory
const resultLength = result.bytes.length * 8 / this.byteSize;
const lengthFieldValue = parseInt(this.readLengthField.value);
if (lengthFieldValue !== resultLength) {
this.memoryReadError = nls.localize('theia/memory-inspector/memory/readError/bounds', 'Memory bounds exceeded, result will be truncated.');
this.doShowMemoryErrors();
this.readLengthField.value = resultLength.toString();
if (direction === 'above') {
this.offsetField.value = `${parseInt(originalValues.offset) - (resultLength - parseInt(originalValues.length))}`;
}
this.update();
}
}
}
return undefined;
}
protected setUpListeners(session?: DebugSession): void {
this.sessionListeners.dispose();
this.sessionListeners = new DisposableCollection(Disposable.create(() => this.handleActiveSessionChange()));
if (session) {
this.sessionListeners.push(session.onDidChange(() => this.handleSessionChange()));
}
}
protected handleActiveSessionChange(): void {
const isDynamic = this.memoryWidgetOptions.dynamic !== false;
if (isDynamic && this.doUpdateAutomatically) {
this.memoryReadResult = EMPTY_MEMORY;
this.fireDidChangeMemory();
}
}
protected handleSessionChange(): void {
const isStopped = this.sessionManager.currentSession?.state === DebugState.Stopped;
const isReadyForQuery = !!this.sessionManager.currentSession?.currentFrame;
const isDynamic = this.memoryWidgetOptions.dynamic !== false;
if (isStopped && isReadyForQuery && isDynamic && this.doUpdateAutomatically && this.memoryReadResult !== EMPTY_MEMORY) {
this.updateMemoryView();
}
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.acceptFocus();
}
protected acceptFocus(): void {
if (this.doUpdateAutomatically) {
if (this.addressField) {
this.addressField.focus();
this.addressField.select();
}
} else {
const settingsCog = this.node.querySelector('.toggle-settings-click-zone') as HTMLDivElement;
settingsCog?.focus();
}
}
protected handleColumnSelectionChange = (columnLabel: string, doShow: boolean): void => this.doHandleColumnSelectionChange(columnLabel, doShow);
protected doHandleColumnSelectionChange(columnLabel: string, doShow: boolean): void {
if (columnLabel in this.columnsDisplayed) {
this.columnsDisplayed[columnLabel].doRender = doShow;
this.fireDidChangeOptions(ASCII_TOGGLE_ID);
}
}
protected toggleAutoUpdate = (e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>): void => {
if (e.nativeEvent.type === 'click') {
e.currentTarget.blur();
}
if ('key' in e && KeyCode.createKeyCode(e.nativeEvent).key?.keyCode === Key.TAB.keyCode) {
return;
}
this.doUpdateAutomatically = !this.doUpdateAutomatically;
if (this.doUpdateAutomatically) {
this.title.iconClass = this.iconClass;
} else {
this.title.iconClass = this.lockIconClass;
}
this.fireDidChangeOptions();
};
protected onByteSizeChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
this.byteSize = parseInt(event.target.value);
this.fireDidChangeOptions(event.target.id);
};
protected override onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
if (this.memoryWidgetOptions.dynamic !== false) {
if (this.addressField) {
this.addressField.value = this.address.toString();
}
}
}
protected toggleDoShowSettings = (e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>): void => {
if (!('key' in e) || KeyCode.createKeyCode(e.nativeEvent).key?.keyCode === Key.TAB.keyCode) {
this.doDisplaySettings = !this.doDisplaySettings;
this.update();
}
};
protected render(): React.ReactNode {
return (
<div className='t-mv-container'>
{this.renderInputContainer()}
</div>
);
}
protected renderInputContainer(): React.ReactNode {
return (
<div className='t-mv-settings-container'>
<div className='t-mv-wrapper'>
{this.renderToolbar()}
{this.renderMemoryLocationGroup()}
{this.doDisplaySettings && (
<div className='t-mv-toggle-settings-wrapper'>
{this.renderByteDisplayGroup()}
</div>
)}
</div>
</div>
);
}
protected renderByteDisplayGroup(): React.ReactNode {
return (
<div className='t-mv-group settings-group'>
<MWSelect
id='byte-size-select'
label={nls.localize('theia/memory-inspector/byteSize', 'Byte Size')}
value={this.byteSize.toString()}
onChange={this.onByteSizeChange}
options={['8', '16', '32', '64']}
/>
<MWSelect
id={BYTES_PER_GROUP_FIELD_ID}
label={nls.localize('theia/memory-inspector/bytesPerGroup', 'Bytes Per Group')}
value={this.bytesPerGroup.toString()}
onChange={this.onBytesPerGroupChange}
options={['1', '2', '4', '8', '16']}
/>
<MWSelect
id={BYTES_PER_ROW_FIELD_ID}
label={nls.localize('theia/memory-inspector/groupsPerRow', 'Groups Per Row')}
value={this.groupsPerRow.toString()}
onChange={this.onGroupsPerRowChange}
options={['1', '2', '4', '8', '16', '32']}
/>
<MWSelect
id={ENDIAN_SELECT_ID}
label={nls.localize('theia/memory-inspector/endianness', 'Endianness')}
value={this.endianness}
onChange={this.onEndiannessChange}
options={[Interfaces.Endianness.Little, Interfaces.Endianness.Big]}
/>
<MWMultiSelect
id={ASCII_TOGGLE_ID}
label={nls.localize('theia/memory-inspector/columns', 'Columns')}
items={this.getOptionalColumns()}
onSelectionChanged={this.handleColumnSelectionChange}
/>
</div>
);
}
protected getObligatoryColumnIds(): string[] {
return ['address', 'data'];
}
protected getOptionalColumns(): SingleSelectItemProps[] {
const obligatoryColumns = new Set(this.getObligatoryColumnIds());
return Object.entries(this.columnsDisplayed)
.reduce<SingleSelectItemProps[]>((accumulated, [id, { doRender, label }]) => {
if (!obligatoryColumns.has(id)) {
accumulated.push({ id, label, defaultChecked: doRender });
}
return accumulated;
}, []);
}
protected assignLocationRef: React.LegacyRef<HTMLInputElement> = location => {
this.addressField = location ?? undefined;
};
protected assignReadLengthRef: React.LegacyRef<HTMLInputElement> = readLength => {
this.readLengthField = readLength ?? undefined;
};
protected assignOffsetRef: React.LegacyRef<HTMLInputElement> = offset => {
this.offsetField = offset ?? undefined;
};
protected setAddressFromSelect = (e: React.ChangeEvent<HTMLSelectElement>): void => {
if (this.addressField) {
this.addressField.value = e.target.value;
}
};
protected renderMemoryLocationGroup(): React.ReactNode {
return (
<>
<div className='t-mv-group view-group'>
<MWInputWithSelect
id={LOCATION_FIELD_ID}
label={nls.localizeByDefault('Address')}
title={nls.localize('theia/memory-inspector/addressTooltip', 'Memory location to display, an address or expression evaluating to an address')}
defaultValue={`${this.address}`}
onSelectChange={this.setAddressFromSelect}
passRef={this.assignLocationRef}
onKeyDown={this.doRefresh}
options={[...this.recentLocations.values]}
disabled={!this.doUpdateAutomatically}
/>
<MWInput
id={LOCATION_OFFSET_FIELD_ID}
label={nls.localize('theia/memory-inspector/offset', 'Offset')}
title={nls.localize('theia/memory-inspector/offsetTooltip', 'Offset to be added to the current memory location, when navigating')}
defaultValue='0'
passRef={this.assignOffsetRef}
onKeyDown={this.doRefresh}
disabled={!this.doUpdateAutomatically}
/>
<MWInput
id={LENGTH_FIELD_ID}
label={nls.localize('theia/memory-inspector/length', 'Length')}
title={nls.localize('theia/memory-inspector/lengthTooltip', 'Number of bytes to fetch, in decimal or hexadecimal')}
defaultValue={this.readLength.toString()}
passRef={this.assignReadLengthRef}
onChange={Utils.validateNumericalInputs}
onKeyDown={this.doRefresh}
disabled={!this.doUpdateAutomatically}
/>
<button
type='button'
className='theia-button main view-group-go-button'
onClick={this.doRefresh}
disabled={!this.doUpdateAutomatically}
title={nls.localizeByDefault('Go')}
>
{nls.localizeByDefault('Go')}
</button>
</div>
<div className={`t-mv-memory-fetch-error${this.showMemoryError ? ' show' : ' hide'}`}>
{this.memoryReadError}
</div>
</>
);
}
protected activateHeaderInputField = (e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>): void => {
if (!this.isTitleEditable) {
const isMouseDown = !('key' in e);
const isActivationKey = 'key' in e && (
KeyCode.createKeyCode(e.nativeEvent).key?.keyCode === Key.SPACE.keyCode
|| KeyCode.createKeyCode(e.nativeEvent).key?.keyCode === Key.ENTER.keyCode
);
if (isMouseDown || isActivationKey) {
if (isMouseDown) {
e.currentTarget.blur();
}
this.isTitleEditable = true;
this.update();
}
}
};
protected saveHeaderInputValue = (e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>): void => {
const isMouseDown = !('key' in e);
const isSaveKey = 'key' in e && e.key === 'Enter';
const isCancelKey = 'key' in e && e.key === 'Escape';
e.stopPropagation();
if (isMouseDown || isSaveKey || isCancelKey) {
this.updateHeader(isCancelKey);
}
};
protected assignHeaderInputRef = (element: HTMLInputElement): void => {
if (element) {
this.headerInputField = element;
element.focus();
}
};
protected updateHeader(isCancelKey: boolean): void {
if (!isCancelKey && this.headerInputField) {
this.title.label = this.headerInputField.value;
this.title.caption = this.headerInputField.value;
}
this.isTitleEditable = false;
this.update();
}
protected renderToolbar(): React.ReactNode {
return (
<div className='memory-widget-toolbar'>
{this.renderLockIcon()}
{this.renderEditableTitleField()}
{this.renderSettingsContainer()}
</div>
);
}
protected renderSettingsContainer(): React.ReactNode {
return <div className='toggle-settings-container'>
<div
className='toggle-settings-click-zone no-select'
tabIndex={0}
aria-label={this.doDisplaySettings ?
nls.localize('theia/memory-inspector/memory/hideSettings', 'Hide Settings Panel') :
nls.localize('theia/memory-inspector/memory/showSettings', 'Show Settings Panel')}
role='button'
onClick={this.toggleDoShowSettings}
onKeyDown={this.toggleDoShowSettings}
title={this.doDisplaySettings ?
nls.localize('theia/memory-inspector/memory/hideSettings', 'Hide Settings Panel') :
nls.localize('theia/memory-inspector/memory/showSettings', 'Show Settings Panel')}>
<i className='codicon codicon-settings-gear' />
<span>{this.doDisplaySettings ?
nls.localize('theia/memory-inspector/closeSettings', 'Close Settings') :
nls.localizeByDefault('Settings')}
</span>
</div>
</div>;
}
protected renderLockIcon(): React.ReactNode {
return this.memoryWidgetOptions.dynamic !== false && (
<div className='memory-widget-auto-updates-container'>
<div
className={`fa fa-${this.doUpdateAutomatically ? 'unlock' : 'lock'}`}
id={AUTO_UPDATE_TOGGLE_ID}
title={this.doUpdateAutomatically ?
nls.localize('theia/memory-inspector/memory/freeze', 'Freeze Memory View') :
nls.localize('theia/memory-inspector/memory/unfreeze', 'Unfreeze Memory View')}
onClick={this.toggleAutoUpdate}
onKeyDown={this.toggleAutoUpdate}
role='button'
tabIndex={0} />
</div>
);
}
protected renderEditableTitleField(): React.ReactNode {
return (
<div
className='memory-widget-header-click-zone'
tabIndex={0}
onClick={this.activateHeaderInputField}
onKeyDown={this.activateHeaderInputField}
role='button'
>
{!this.isTitleEditable
? (
<h2 className={`${MemoryOptionsWidget.WIDGET_H2_CLASS}${!this.doUpdateAutomatically ? ' disabled' : ''} no-select`}>
{this.title.label}
</h2>
)
: <input
className='theia-input'
type='text'
defaultValue={this.title.label}
onKeyDown={this.saveHeaderInputValue}
spellCheck={false}
ref={this.assignHeaderInputRef}
/>}
{!this.isTitleEditable && (
<div className={`fa fa-pencil${this.showTitleEditIcon ? ' show' : ' hide'}`} />
)}
{this.isTitleEditable && (
<div
className='fa fa-save'
onClick={this.saveHeaderInputValue}
onKeyDown={this.saveHeaderInputValue}
role='button'
tabIndex={0}
title={nls.localizeByDefault('Save')}
/>
)}
</div>
);
}
storeState(): Interfaces.MemoryOptions {
return {
address: this.addressField?.value ?? this.address,
offset: parseInt(`${this.offsetField?.value}`) ?? this.offset,
length: parseInt(`${this.readLengthField?.value}`) ?? this.readLength,
byteSize: this.byteSize,
bytesPerGroup: this.bytesPerGroup,
groupsPerRow: this.groupsPerRow,
endianness: this.endianness,
doDisplaySettings: this.doDisplaySettings,
columnsDisplayed: this.columnsDisplayed,
recentLocationsArray: this.recentLocations.values,
isFrozen: !this.doUpdateAutomatically,
doUpdateAutomatically: this.doUpdateAutomatically,
};
}
restoreState(oldState: Interfaces.MemoryOptions): void {
this.address = oldState.address ?? this.address;
this.offset = oldState.offset ?? this.offset;
this.readLength = oldState.length ?? this.readLength;
this.byteSize = oldState.byteSize ?? this.byteSize;
this.bytesPerGroup = oldState.bytesPerGroup ?? this.bytesPerGroup;
this.groupsPerRow = oldState.groupsPerRow ?? this.groupsPerRow;
this.endianness = oldState.endianness ?? this.endianness;
this.recentLocations = new Recents(oldState.recentLocationsArray) ?? this.recentLocations;
this.doDisplaySettings = !!oldState.doDisplaySettings;
if (oldState.columnsDisplayed) {
this.columnsDisplayed = oldState.columnsDisplayed;
}
}
protected doShowMemoryErrors = (doClearError = false): void => {
if (this.errorTimeout !== undefined) {
clearTimeout(this.errorTimeout);
}
if (doClearError) {
this.showMemoryError = false;
this.update();
this.errorTimeout = undefined;
return;
}
this.showMemoryError = true;
this.update();
this.errorTimeout = setTimeout(() => {
this.showMemoryError = false;
this.update();
this.errorTimeout = undefined;
}, Constants.ERROR_TIMEOUT);
};
fetchNewMemory(): void {
this.updateMemoryView();
}
protected doRefresh = (event: React.KeyboardEvent<HTMLInputElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>): void => {
if ('key' in event && event.key !== 'Enter') {
return;
}
this.updateMemoryView();
};
protected updateMemoryView = debounce(this.doUpdateMemoryView.bind(this), Constants.DEBOUNCE_TIME, { trailing: true });
protected async doUpdateMemoryView(): Promise<void> {
if (!(this.addressField && this.readLengthField)) { return; }
if (this.addressField?.value.trim().length === 0) {
this.memoryReadError = nls.localize('theia/memory-inspector/memory/addressField/memoryReadError', 'Enter an address or expression in the Location field.');
this.doShowMemoryErrors();
return;
}
if (this.readLengthField.value.trim().length === 0) {
this.memoryReadError = nls.localize('theia/memory-inspector/memory/readLength/memoryReadError', 'Enter a length (decimal or hexadecimal number) in the Length field.');
this.doShowMemoryErrors();
return;
}
const startAddress = this.addressField.value;
const locationOffset = parseInt(`${this.offsetField?.value}`) || 0;
const readLength = parseInt(this.readLengthField.value);
try {
this.memoryReadResult = await this.getMemory(startAddress, readLength, locationOffset);
this.fireDidChangeMemory();
if (this.pinnedMemoryReadResult) {
this.pinnedMemoryReadResult.resolve(this.memoryReadResult);
}
this.doShowMemoryErrors(true);
} catch (err) {
this.memoryReadError = this.getUserError(err);
console.error('Failed to read memory', err);
this.doShowMemoryErrors();
if (this.pinnedMemoryReadResult) {
this.pinnedMemoryReadResult.resolve(this.memoryReadResult);
}
} finally {
this.pinnedMemoryReadResult = undefined;
this.update();
}
}
protected getUserError(err: unknown): string {
return err instanceof Error ? err.message : nls.localize('theia/memory-inspector/memory/userError', 'There was an error fetching memory.');
}
protected async getMemory(memoryReference: string, count: number, offset: number): Promise<Interfaces.MemoryReadResult> {
const result = await this.retrieveMemory(memoryReference, count, offset);
try {
this.variables = await this.memoryProvider.getLocals();
} catch {
this.variables = [];
}
this.recentLocations.add(memoryReference);
this.updateDefaults(memoryReference, count, offset);
return result;
}
protected async retrieveMemory(memoryReference: string, count: number, offset: number): Promise<Interfaces.MemoryReadResult> {
return this.memoryProvider.readMemory({ memoryReference, count, offset });
}
// TODO: This may not be necessary if we change how state is stored (currently in the text fields themselves.)
protected updateDefaults(address: string, readLength: number, offset: number): void {
this.address = address;
this.readLength = readLength;
this.offset = offset;
}
// Callbacks for when the various view parameters change.
/**
* Handle bytes per row changed event.
*/
protected onGroupsPerRowChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
const { value, id } = event.target;
this.groupsPerRow = parseInt(value);
this.fireDidChangeOptions(id);
};
/**
* Handle bytes per group changed event.
*/
protected onBytesPerGroupChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
const { value, id } = event.target;
this.bytesPerGroup = parseInt(value);
this.fireDidChangeOptions(id);
};
/**
* Handle endianness changed event.
*/
protected onEndiannessChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
const { value, id } = event.target;
if (value !== Interfaces.Endianness.Big && value !== Interfaces.Endianness.Little) { return; }
this.endianness = value;
this.fireDidChangeOptions(id);
};
protected fireDidChangeOptions(targetId?: string): void {
this.onOptionsChangedEmitter.fire(targetId);
}
protected fireDidChangeMemory(): void {
this.onMemoryChangedEmitter.fire(this.memoryReadResult);
}
}

View File

@@ -0,0 +1,626 @@
/********************************************************************************
* Copyright (C) 2021 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 { ContextMenuRenderer, ReactWidget, Widget } from '@theia/core/lib/browser';
import { ThemeService } from '@theia/core/lib/browser/theming';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { ThemeChangeEvent } from '@theia/core/lib/common/theme';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
import { hexStrToUnsignedLong } from '../../common/util';
import { MemoryProviderService } from '../memory-provider/memory-provider-service';
import { EasilyMappedObject, MemoryHoverRendererService } from '../utils/memory-hover-renderer';
import { MWMoreMemorySelect } from '../utils/memory-widget-components';
import {
Constants, Interfaces, Utils
} from '../utils/memory-widget-utils';
import { VariableDecoration, VariableFinder } from '../utils/memory-widget-variable-utils';
import { MemoryOptionsWidget } from './memory-options-widget';
import debounce = require('@theia/core/shared/lodash.debounce');
/* eslint-disable @typescript-eslint/no-explicit-any */
export namespace MemoryTable {
export interface WrapperHandlers {
onKeyDown?: React.KeyboardEventHandler;
onClick?: React.MouseEventHandler;
onContextMenu?: React.MouseEventHandler;
onMouseMove?: React.MouseEventHandler;
onFocus?(e: React.FocusEvent<HTMLDivElement>): any;
onBlur?(e: React.FocusEvent<HTMLDivElement>): any;
}
/* eslint-enable @typescript-eslint/no-explicit-any */
export interface StylableNodeAttributes {
className?: string;
style?: React.CSSProperties;
variable?: VariableDecoration;
title?: string;
isHighlighted?: boolean;
}
export interface GroupData {
node: React.ReactNode;
ascii: string; index: number;
variables: VariableDecoration[];
isHighlighted?: boolean;
}
export interface ByteData {
node: React.ReactNode;
ascii: string; index: number;
variables: VariableDecoration[];
isHighlighted?: boolean;
}
export interface ItemData {
node: React.ReactNode;
content: string;
variable?: VariableDecoration;
index: number;
isHighlighted?: boolean;
}
export interface RowOptions {
address: string;
groups: React.ReactNode;
ascii?: string;
variables?: VariableDecoration[];
doShowDivider?: boolean;
index: number;
isHighlighted?: boolean;
}
export const ROW_CLASS = 't-mv-view-row';
export const ROW_DIVIDER_CLASS = 't-mv-view-row-highlight';
export const ADDRESS_DATA_CLASS = 't-mv-view-address';
export const MEMORY_DATA_CLASS = 't-mv-view-data';
export const EXTRA_COLUMN_DATA_CLASS = 't-mv-view-code';
export const GROUP_SPAN_CLASS = 'byte-group';
export const BYTE_SPAN_CLASS = 'single-byte';
export const EIGHT_BIT_SPAN_CLASS = 'eight-bits';
export const HEADER_LABEL_CONTAINER_CLASS = 't-mv-header-label-container';
export const HEADER_LABEL_CLASS = 't-mv-header-label';
export const VARIABLE_LABEL_CLASS = 't-mv-variable-label';
export const HEADER_ROW_CLASS = 't-mv-header';
}
@injectable()
export class MemoryTableWidget extends ReactWidget {
static CONTEXT_MENU = ['memory.view.context.menu'];
static ID = 'memory-table-widget';
@inject(ThemeService) protected readonly themeService: ThemeService;
@inject(MemoryOptionsWidget) readonly optionsWidget: MemoryOptionsWidget;
@inject(MemoryProviderService) protected readonly memoryProvider: MemoryProviderService;
@inject(MemoryHoverRendererService) protected readonly hoverRenderer: MemoryHoverRendererService;
@inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer;
protected previousBytes: Interfaces.LabeledUint8Array | undefined;
protected memory: Interfaces.WidgetMemoryState;
protected options: Interfaces.MemoryOptions;
protected variableFinder: VariableFinder | undefined;
protected deferredScrollContainer = new Deferred<HTMLDivElement>();
@postConstruct()
protected init(): void {
this.doInit();
}
protected async doInit(): Promise<void> {
this.id = MemoryTableWidget.ID;
this.addClass(MemoryTableWidget.ID);
this.scrollOptions = { ...this.scrollOptions, suppressScrollX: false };
this.toDispose.push(this.optionsWidget.onOptionsChanged(optionId => this.handleOptionChange(optionId)));
this.toDispose.push(this.optionsWidget.onMemoryChanged(e => this.handleMemoryChange(e)));
this.toDispose.push(this.themeService.onDidColorThemeChange(e => this.handleThemeChange(e)));
this.getStateAndUpdate();
}
protected handleOptionChange(_id?: string): Promise<void> {
this.getStateAndUpdate();
return Promise.resolve();
}
override update(): void {
super.update();
this.updateColumnWidths();
}
protected override onResize(msg: Widget.ResizeMessage): void {
this.updateColumnWidths();
super.onResize(msg);
}
protected updateColumnWidths = debounce(this.doUpdateColumnWidths.bind(this), Constants.DEBOUNCE_TIME);
protected doUpdateColumnWidths(): void {
setTimeout(() => {
const firstTR = this.node.querySelector('tr');
const header = this.node.querySelector(`.${MemoryTable.HEADER_ROW_CLASS}`) as HTMLDivElement;
if (firstTR && header) {
const allTDs = Array.from(firstTR.querySelectorAll('td'));
const allSizes = allTDs.map(td => `minmax(max-content, ${td.clientWidth}px)`);
header.style.gridTemplateColumns = allSizes.join(' ');
}
});
}
protected areSameRegion(a: Interfaces.MemoryReadResult, b: Interfaces.MemoryReadResult): boolean {
return a.address.equals(b?.address) && a.bytes.length === b?.bytes.length;
}
protected handleMemoryChange(newMemory: Interfaces.MemoryReadResult): void {
if (this.areSameRegion(this.memory, newMemory)) {
this.previousBytes = this.memory.bytes;
} else {
this.previousBytes = undefined;
}
this.getStateAndUpdate();
}
protected handleThemeChange(_themeChange: ThemeChangeEvent): void {
this.getStateAndUpdate();
}
protected getState(): void {
this.options = this.optionsWidget.options;
this.memory = this.optionsWidget.memory;
const isHighContrast = this.themeService.getCurrentTheme().type === 'hc';
this.variableFinder = this.optionsWidget.options.columnsDisplayed.variables.doRender
? new VariableFinder(this.memory.variables, isHighContrast)
: undefined;
}
protected getStateAndUpdate(): void {
this.getState();
this.update();
this.scrollIntoViewIfNecessary();
}
protected scrollIntoViewIfNecessary(): Promise<void> {
return new Promise(resolve => setTimeout(() => {
this.deferredScrollContainer.promise.then(scrollContainer => {
const table = scrollContainer.querySelector('table');
if (table && scrollContainer.scrollTop > table.clientHeight) {
const valueToGetInWindow = table.clientHeight - this.node.clientHeight;
const scrollHere = Math.max(valueToGetInWindow, 0);
scrollContainer.scrollTo(scrollContainer.scrollLeft, scrollHere);
}
this.scrollBar?.update();
resolve();
});
}));
}
protected getWrapperHandlers(): MemoryTable.WrapperHandlers {
return { onMouseMove: this.handleTableMouseMove };
}
protected assignScrollContainerRef = (element: HTMLDivElement): void => {
this.deferredScrollContainer.resolve(element);
};
override async getScrollContainer(): Promise<HTMLDivElement> {
return this.deferredScrollContainer.promise;
}
render(): React.ReactNode {
const rows = this.getTableRows();
const { onClick, onContextMenu, onFocus, onBlur, onKeyDown, onMouseMove } = this.getWrapperHandlers();
const headers: Interfaces.ColumnIDs[] = Object.entries(this.options.columnsDisplayed)
.filter(([, { doRender }]) => doRender)
.map(([id, { label }]) => ({ label, id }));
return (
<div
className={this.getWrapperClass()}
onClick={onClick}
onContextMenu={onContextMenu}
onFocus={onFocus}
onBlur={onBlur}
onKeyDown={onKeyDown}
onMouseMove={onMouseMove}
role='textbox'
tabIndex={0}
>
<div
className={this.getTableHeaderClass()}
style={this.getTableHeaderStyle(headers.length)}
>
{this.getTableHeaders(headers)}
</div>
<div
className='t-mv-view-container'
style={{ position: 'relative' }}
ref={this.assignScrollContainerRef}
>
{this.getBeforeTableContent()}
<table className='t-mv-view'>
<tbody>
{rows}
</tbody>
</table>
{this.getAfterTableContent()}
</div>
{this.getTableFooter()}
</div>
);
}
protected getWrapperClass(): string {
return `t-mv-memory-container${this.options.isFrozen ? ' frozen' : ''}`;
}
protected getTableHeaderClass(): string {
return MemoryTable.HEADER_ROW_CLASS + ' no-select';
}
protected getTableHeaderStyle(numLabels: number): React.CSSProperties {
const safePercentage = Math.floor(100 / numLabels);
const gridTemplateColumns = ` ${safePercentage}% `.repeat(numLabels);
return { gridTemplateColumns };
}
protected getTableHeaders(labels: Interfaces.ColumnIDs[]): React.ReactNode {
return labels.map(label => this.getTableHeader(label));
}
protected getTableHeader({ label, id }: Interfaces.ColumnIDs): React.ReactNode {
return (
<div key={id} className={MemoryTable.HEADER_LABEL_CONTAINER_CLASS}>
<span className='t-mv-header-label'>{label}</span>
</div>
);
}
protected getBeforeTableContent(): React.ReactNode {
return (
!!this.memory.bytes.length && (<MWMoreMemorySelect
options={[128, 256, 512]}
direction='above'
handler={this.loadMoreMemory}
/>)
);
}
protected getAfterTableContent(): React.ReactNode {
return (
!!this.memory.bytes.length && (<MWMoreMemorySelect
options={[128, 256, 512]}
direction='below'
handler={this.loadMoreMemory}
/>)
);
}
protected loadMoreMemory = async (options: Interfaces.MoreMemoryOptions): Promise<void> => {
const { direction, numBytes } = options;
const { address, offset, length } = this.optionsWidget.options;
let newOffset = 0;
const newLength = length + numBytes;
if (direction === 'above') {
newOffset = offset - numBytes;
}
await this.optionsWidget.setAddressAndGo(`${address}`, newOffset, newLength, direction);
};
protected getTableFooter(): React.ReactNode {
return undefined;
}
protected getTableRows(): React.ReactNode {
return [...this.renderRows()];
}
protected *renderRows(iteratee: Interfaces.LabeledUint8Array = this.memory.bytes): IterableIterator<React.ReactNode> {
const bytesPerRow = this.options.bytesPerGroup * this.options.groupsPerRow;
let rowsYielded = 0;
let groups: React.ReactNode[] = [];
let ascii = '';
let variables: VariableDecoration[] = [];
let isRowHighlighted = false;
for (const { node, index, ascii: groupAscii, variables: groupVariables, isHighlighted = false } of this.renderGroups(iteratee)) {
groups.push(node);
ascii += groupAscii;
variables.push(...groupVariables);
isRowHighlighted = isRowHighlighted || isHighlighted;
if (groups.length === this.options.groupsPerRow || index === iteratee.length - 1) {
const rowAddress = this.memory.address.add(bytesPerRow * rowsYielded);
const options: MemoryTable.RowOptions = {
address: `0x${rowAddress.toString(16)}`,
doShowDivider: (rowsYielded % 4) === 3,
isHighlighted: isRowHighlighted,
ascii,
groups,
variables,
index,
};
yield this.renderRow(options);
ascii = '';
variables = [];
groups = [];
rowsYielded += 1;
isRowHighlighted = false;
}
}
}
protected renderRow(
options: MemoryTable.RowOptions,
getRowAttributes: Interfaces.RowDecorator = this.getRowAttributes.bind(this),
): React.ReactNode {
const { address, groups } = options;
const { className, style, title } = getRowAttributes(options);
return (
<tr
// Add a marker to help visual navigation when scrolling
className={className}
style={style}
title={title}
key={address}
>
<td className={MemoryTable.ADDRESS_DATA_CLASS}>{address}</td>
<td className={MemoryTable.MEMORY_DATA_CLASS}>{groups}</td>
{this.getExtraColumn(options)}
</tr>
);
}
protected getRowAttributes(options: Partial<MemoryTable.RowOptions>): Partial<Interfaces.StylableNodeAttributes> {
let className = MemoryTable.ROW_CLASS;
if (options.doShowDivider) {
className += ` ${MemoryTable.ROW_DIVIDER_CLASS}`;
}
return { className };
}
protected getExtraColumn(options: Pick<MemoryTable.RowOptions, 'ascii' | 'variables'>): React.ReactNode {
const { variables } = options;
const additionalColumns = [];
if (this.options.columnsDisplayed.variables.doRender) {
additionalColumns.push(
<td className={MemoryTable.EXTRA_COLUMN_DATA_CLASS} key='variables'>
{!!variables?.length && (
<span className='variable-container'>
{variables.map(({ name, color }) => (
<span
key={name}
className={MemoryTable.VARIABLE_LABEL_CLASS}
style={{ color }}
>
{name}
</span>
))}
</span>
)}
</td>,
);
}
if (this.options.columnsDisplayed.ascii.doRender) {
const asciiColumn = this.options.columnsDisplayed.ascii.doRender && <td className={MemoryTable.EXTRA_COLUMN_DATA_CLASS} key='ascii'>{options.ascii}</td>;
additionalColumns.push(asciiColumn);
}
return additionalColumns;
}
protected *renderGroups(iteratee: Interfaces.LabeledUint8Array = this.memory.bytes): IterableIterator<MemoryTable.GroupData> {
let bytesInGroup: React.ReactNode[] = [];
let ascii = '';
let variables: VariableDecoration[] = [];
let isGroupHighlighted = false;
for (const { node, index, ascii: byteAscii, variables: byteVariables, isHighlighted = false } of this.renderBytes(iteratee)) {
this.buildGroupByEndianness(bytesInGroup, node);
ascii += byteAscii;
variables.push(...byteVariables);
isGroupHighlighted = isGroupHighlighted || isHighlighted;
if (bytesInGroup.length === this.options.bytesPerGroup || index === iteratee.length - 1) {
const itemID = this.memory.address.add(index);
yield {
node: <span className='byte-group' key={itemID.toString(16)}>{bytesInGroup}</span>,
ascii,
index,
variables,
isHighlighted: isGroupHighlighted,
};
bytesInGroup = [];
ascii = '';
variables = [];
isGroupHighlighted = false;
}
}
}
protected buildGroupByEndianness(oldBytes: React.ReactNode[], newByte: React.ReactNode): void {
if (this.options.endianness === Interfaces.Endianness.Big) {
oldBytes.push(newByte);
} else {
oldBytes.unshift(newByte);
}
}
protected *renderBytes(iteratee: Interfaces.LabeledUint8Array = this.memory.bytes): IterableIterator<MemoryTable.ByteData> {
const itemsPerByte = this.options.byteSize / 8;
let currentByte = 0;
let chunksInByte: React.ReactNode[] = [];
let variables: VariableDecoration[] = [];
let isByteHighlighted = false;
for (const { node, content, index, variable, isHighlighted = false } of this.renderArrayItems(iteratee)) {
chunksInByte.push(node);
const numericalValue = parseInt(content, 16);
currentByte = (currentByte << 8) + numericalValue;
isByteHighlighted = isByteHighlighted || isHighlighted;
if (variable?.firstAppearance) {
variables.push(variable);
}
if (chunksInByte.length === itemsPerByte || index === iteratee.length - 1) {
const itemID = this.memory.address.add(index);
const ascii = this.getASCIIForSingleByte(currentByte);
yield {
node: <span className='single-byte' key={itemID.toString(16)}>{chunksInByte}</span>,
ascii,
index,
variables,
isHighlighted: isByteHighlighted,
};
currentByte = 0;
chunksInByte = [];
variables = [];
isByteHighlighted = false;
}
}
}
protected getASCIIForSingleByte(byte: number | undefined): string {
return typeof byte === 'undefined'
? ' ' : Utils.isPrintableAsAscii(byte) ? String.fromCharCode(byte) : '.';
}
protected *renderArrayItems(
iteratee: Interfaces.LabeledUint8Array = this.memory.bytes,
getBitAttributes: Interfaces.BitDecorator = this.getBitAttributes.bind(this),
): IterableIterator<MemoryTable.ItemData> {
const { address } = this.memory;
for (let i = 0; i < iteratee.length; i += 1) {
const itemID = address.add(i).toString(16);
const { content = '', className, style, variable, title, isHighlighted } = getBitAttributes(i, iteratee);
const node = (
<span
style={style}
key={itemID}
className={className}
data-id={itemID}
title={title}
>
{content}
</span>
);
yield {
node,
content,
index: i,
variable,
isHighlighted,
};
}
}
protected getBitAttributes(arrayOffset: number, iteratee: Interfaces.LabeledUint8Array): Partial<Interfaces.FullNodeAttributes> {
const itemAddress = this.memory.address.add(arrayOffset * 8 / this.options.byteSize);
const classNames = [MemoryTable.EIGHT_BIT_SPAN_CLASS];
const isChanged = this.previousBytes && iteratee[arrayOffset] !== this.previousBytes[arrayOffset];
const variable = this.variableFinder?.getVariableForAddress(itemAddress);
if (!this.options.isFrozen) {
if (isChanged) {
classNames.push('changed');
}
}
return {
className: classNames.join(' '),
variable,
style: { color: variable?.color },
content: iteratee[arrayOffset].toString(16).padStart(2, '0')
};
}
protected handleTableMouseMove = (e: React.MouseEvent): void => {
const { target } = e; // react events can't be put into the debouncer
this.debounceHandleMouseTableMove(target);
};
protected debounceHandleMouseTableMove = debounce(this.doHandleTableMouseMove.bind(this), Constants.DEBOUNCE_TIME, { trailing: true });
protected doHandleTableMouseMove(targetSpan: React.MouseEvent['target']): void {
const target = targetSpan instanceof HTMLElement && targetSpan;
if (target) {
const { x, y } = target.getBoundingClientRect();
const anchor = { x: Math.round(x), y: Math.round(y + target.clientHeight) };
if (target.classList.contains(MemoryTable.EIGHT_BIT_SPAN_CLASS)) {
const properties = this.getHoverForChunk(target);
this.hoverRenderer.render(this.node, anchor, properties);
} else if (target.classList.contains(MemoryTable.VARIABLE_LABEL_CLASS)) {
const properties = this.getHoverForVariable(target);
this.hoverRenderer.render(this.node, anchor, properties);
} else {
this.hoverRenderer.hide();
}
} else {
this.hoverRenderer.hide();
}
}
protected getHoverForChunk(span: HTMLElement): EasilyMappedObject | undefined {
if (span.classList.contains(MemoryTable.EIGHT_BIT_SPAN_CLASS)) {
const parentByteContainer = span.parentElement;
if (parentByteContainer?.textContent) {
const hex = parentByteContainer.textContent ?? '';
const decimal = parseInt(hex, 16);
const binary = this.getPaddedBinary(decimal);
const UTF8 = String.fromCodePoint(decimal);
return { hex, binary, decimal, UTF8 };
}
}
return undefined;
}
protected getPaddedBinary(decimal: number): string {
const paddedBinary = decimal.toString(2).padStart(this.options.byteSize, '0');
let paddedAndSpacedBinary = '';
for (let i = 8; i <= paddedBinary.length; i += 8) {
paddedAndSpacedBinary += ` ${paddedBinary.slice(i - 8, i)}`;
}
return paddedAndSpacedBinary.trim();
}
protected getHoverForVariable(span: HTMLElement): EasilyMappedObject | undefined {
const variable = this.variableFinder?.searchForVariable(span.textContent ?? '');
if (variable?.type) {
return { type: variable.type };
}
return undefined;
}
protected handleTableRightClick = (e: React.MouseEvent): void => this.doHandleTableRightClick(e);
protected doHandleTableRightClick(event: React.MouseEvent): void {
event.preventDefault();
const target = event.target as HTMLElement;
if (target.classList?.contains('eight-bits')) {
const { right, top } = target.getBoundingClientRect();
this.update();
event.stopPropagation();
this.contextMenuRenderer.render({
menuPath: MemoryTableWidget.CONTEXT_MENU,
anchor: { x: right, y: top },
args: this.getContextMenuArgs(event),
context: target
});
}
}
protected getContextMenuArgs(event: React.MouseEvent): unknown[] {
const args: unknown[] = [this];
const id = (event.target as HTMLElement).getAttribute('data-id');
if (id) {
const location = hexStrToUnsignedLong(id);
args.push(location);
const offset = this.memory.address.multiply(-1).add(location);
const cellAddress = this.memory.address.add(offset.multiply(8 / this.options.byteSize));
const variableAtLocation = this.variableFinder?.searchForVariable(cellAddress);
args.push(variableAtLocation);
}
return args;
}
}

View File

@@ -0,0 +1,114 @@
/********************************************************************************
* Copyright (C) 2021 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 { nls } from '@theia/core';
import { BaseWidget, PanelLayout } from '@theia/core/lib/browser';
import { Container, inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify';
import { MemoryWidgetOptions } from '../utils/memory-widget-utils';
import { MemoryOptionsWidget } from './memory-options-widget';
import { MemoryTableWidget } from './memory-table-widget';
@injectable()
export class MemoryWidget<
O extends MemoryOptionsWidget = MemoryOptionsWidget,
T extends MemoryTableWidget = MemoryTableWidget
>
extends BaseWidget {
static readonly ID = 'memory-view-wrapper';
static readonly LABEL = nls.localize('theia/memory-inspector/memoryTitle', 'Memory');
@inject(MemoryWidgetOptions) protected readonly memoryWidgetOptions: MemoryWidgetOptions;
@inject(MemoryOptionsWidget) readonly optionsWidget: O;
@inject(MemoryTableWidget) readonly tableWidget: T;
static createWidget<
Options extends MemoryOptionsWidget = MemoryOptionsWidget,
Table extends MemoryTableWidget = MemoryTableWidget
>(
parent: interfaces.Container,
optionsWidget: interfaces.ServiceIdentifier<Options>,
tableWidget: interfaces.ServiceIdentifier<Table>,
optionSymbol: interfaces.ServiceIdentifier<MemoryWidgetOptions> = MemoryWidgetOptions,
options?: MemoryWidgetOptions,
): MemoryWidget<Options, Table> {
const child = MemoryWidget.createContainer(parent, optionsWidget, tableWidget, optionSymbol, options);
return child.get<MemoryWidget<Options, Table>>(MemoryWidget);
}
static createContainer(
parent: interfaces.Container,
optionsWidget: interfaces.ServiceIdentifier<MemoryOptionsWidget>,
tableWidget: interfaces.ServiceIdentifier<MemoryTableWidget>,
optionSymbol: interfaces.ServiceIdentifier<MemoryWidgetOptions | undefined> = MemoryWidgetOptions,
options?: MemoryWidgetOptions,
): interfaces.Container {
const child = new Container({ defaultScope: 'Singleton' });
child.parent = parent;
child.bind(optionsWidget).toSelf();
child.bind(tableWidget).toSelf();
child.bind(MemoryWidgetOptions).toConstantValue(options);
if (optionsWidget !== MemoryOptionsWidget) {
child.bind(MemoryOptionsWidget).toService(optionsWidget);
}
if (tableWidget !== MemoryTableWidget) {
child.bind(MemoryTableWidget).toService(tableWidget);
}
if (optionSymbol !== MemoryWidgetOptions) {
child.bind(optionSymbol).toConstantValue(options);
}
child.bind(MemoryWidget).toSelf();
return child;
}
static getIdentifier(optionsWidgetID: string): string {
return `${MemoryWidget.ID}-${optionsWidgetID}`;
}
@postConstruct()
protected init(): void {
this.doInit();
}
protected async doInit(): Promise<void> {
this.id = MemoryWidget.getIdentifier(this.memoryWidgetOptions.identifier.toString());
this.addClass(MemoryWidget.ID);
this.title.label = this.optionsWidget.title.label;
this.title.caption = this.optionsWidget.title.caption;
this.title.iconClass = this.optionsWidget.title.iconClass;
this.title.closable = this.optionsWidget.title.closable;
const layout = this.layout = new PanelLayout();
layout.addWidget(this.optionsWidget);
layout.addWidget(this.tableWidget);
this.toDispose.pushAll([
this.layout,
this.optionsWidget,
this.tableWidget,
]);
this.optionsWidget.title.changed.connect(title => {
this.title.label = title.label;
this.title.caption = title.caption;
this.title.iconClass = title.iconClass;
});
}
protected override onActivateRequest(): void {
this.optionsWidget.activate();
}
}

View File

@@ -0,0 +1,76 @@
/********************************************************************************
* Copyright (C) 2021 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 { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
export enum AllOrCustom {
All = 'All',
Custom = 'Custom'
}
export const RegisterFilterService = Symbol('RegisterFilterService');
export interface RegisterFilterService {
currentFilterLabel: string;
filterLabels: string[];
setFilter(filterLabel: string): void;
shouldDisplayRegister(registerName: string): boolean;
currentFilterRegisters(): string[];
}
export const RegisterFilterServiceOptions = Symbol('RegisterFilterServiceOptions');
export interface RegisterFilterServiceOptions {
[key: string]: string[];
}
@injectable()
export class RegisterFilterServiceImpl implements RegisterFilterService {
@inject(RegisterFilterServiceOptions) protected readonly options: RegisterFilterServiceOptions;
protected filters: Map<string, Set<string> | undefined> = new Map();
protected currentFilter: string = AllOrCustom.All;
get filterLabels(): string[] {
return [...this.filters.keys()];
}
get currentFilterLabel(): string {
return this.currentFilter;
}
@postConstruct()
protected init(): void {
this.filters.set(AllOrCustom.All, undefined);
this.filters.set(AllOrCustom.Custom, new Set());
for (const [key, values] of Object.entries(this.options)) {
this.filters.set(key, new Set(values));
}
}
setFilter(filterLabel: string): void {
if (this.filters.has(filterLabel)) {
this.currentFilter = filterLabel;
}
}
shouldDisplayRegister(registerName: string): boolean {
const currentFilter = this.filters.get(this.currentFilter);
return !currentFilter || currentFilter.has(registerName);
}
currentFilterRegisters(): string[] {
const currentFilterRegisters = this.filters.get(this.currentFilter);
return currentFilterRegisters ? Array.from(currentFilterRegisters) : [];
}
}

View File

@@ -0,0 +1,393 @@
/********************************************************************************
* Copyright (C) 2021 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 { Disposable, DisposableCollection, Emitter, nls } from '@theia/core';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
import { DebugSession, DebugState } from '@theia/debug/lib/browser/debug-session';
import { ASCII_TOGGLE_ID, AUTO_UPDATE_TOGGLE_ID, MemoryOptionsWidget } from '../memory-widget/memory-options-widget';
import { MWInputWithSelect } from '../utils/memory-widget-components';
import { Constants, Interfaces, RegisterWidgetOptions } from '../utils/memory-widget-utils';
import { getRegisters, RegisterReadResult } from '../utils/memory-widget-variable-utils';
import { MWMultiSelect } from '../utils/multi-select-bar';
import { RegisterFilterService } from './register-filter-service';
import debounce = require('@theia/core/shared/lodash.debounce');
export const EMPTY_REGISTERS: RegisterReadResult = {
threadId: undefined,
registers: [],
};
export const REGISTER_FIELD_ID = 't-mv-register';
export const REGISTER_RADIX_ID = 't-mv-radix';
export const REGISTER_PRE_SETS_ID = 't-mv-pre-set';
export interface RegisterOptions extends Interfaces.MemoryOptions {
reg: string;
noRadixColumnDisplayed: boolean;
}
@injectable()
export class RegisterOptionsWidget extends MemoryOptionsWidget {
override iconClass = 'register-view-icon';
override lockIconClass = 'register-lock-icon';
protected readonly LABEL_PREFIX = nls.localize('theia/memory-inspector/register', 'Register');
protected readonly onRegisterChangedEmitter = new Emitter<[RegisterReadResult, boolean]>();
readonly onRegisterChanged = this.onRegisterChangedEmitter.event;
protected registerReadResult: RegisterReadResult = EMPTY_REGISTERS;
protected reg: string;
protected registerField: HTMLInputElement | undefined;
protected registerDisplaySet = new Set();
protected registerDisplayAll = true;
protected registerFilterUpdate = false;
protected registerReadError = nls.localize('theia/memory-inspector/register/readError', 'No Registers currently available.');
protected showRegisterError = false;
protected noRadixColumnDisplayed = this.noRadixDisplayed();
protected override columnsDisplayed: Interfaces.ColumnsDisplayed = {
register: {
label: nls.localize('theia/memory-inspector/register', 'Register'),
doRender: true
},
hexadecimal: {
label: nls.localize('theia/memory-inspector/hexadecimal', 'Hexadecimal'),
doRender: true
},
decimal: {
label: nls.localize('theia/memory-inspector/decimal', 'Decimal'),
doRender: false
},
octal: {
label: nls.localize('theia/memory-inspector/octal', 'Octal'),
doRender: false
},
binary: {
label: nls.localize('theia/memory-inspector/binary', 'Binary'),
doRender: false
},
};
@inject(RegisterWidgetOptions) protected override readonly memoryWidgetOptions: RegisterWidgetOptions;
@inject(RegisterFilterService) protected readonly filterService: RegisterFilterService;
get registers(): RegisterReadResult {
return {
...this.registerReadResult,
};
}
override get options(): RegisterOptions {
return this.storeState();
}
displayReg(element: string): boolean {
return this.registerDisplayAll ||
this.registerDisplaySet.has(element);
}
handleRadixRendering(regVal: string, radix: number, _regName?: string): string {
// check if too big for integer
const bInt = BigInt(regVal);
return bInt.toString(radix);
}
@postConstruct()
protected override init(): void {
this.addClass(MemoryOptionsWidget.ID);
this.addClass('reg-options-widget');
this.title.label = `${this.LABEL_PREFIX} (${this.memoryWidgetOptions.identifier})`;
this.title.caption = `${this.LABEL_PREFIX} (${this.memoryWidgetOptions.identifier})`;
this.title.iconClass = this.iconClass;
this.title.closable = true;
if (this.memoryWidgetOptions.dynamic !== false) {
this.toDispose.push(this.sessionManager.onDidChangeActiveDebugSession(({ current }) => {
this.setUpListeners(current);
}));
this.toDispose.push(this.sessionManager.onDidCreateDebugSession(current => {
this.setUpListeners(current);
}));
this.setUpListeners(this.sessionManager.currentSession);
}
this.toDispose.push(this.onOptionsChanged(() => this.update()));
this.update();
}
setRegAndUpdate(regName: string): void {
this.handleRegFromDebugWidgetSelection(regName);
}
protected override setUpListeners(session?: DebugSession): void {
this.sessionListeners.dispose();
this.sessionListeners = new DisposableCollection(Disposable.create(() => this.handleActiveSessionChange()));
if (session) {
this.sessionListeners.push(session.onDidChange(() => this.handleSessionChange()));
}
}
protected override handleActiveSessionChange(): void {
const isDynamic = this.memoryWidgetOptions.dynamic !== false;
if (isDynamic && this.doUpdateAutomatically) {
this.registerReadResult = EMPTY_REGISTERS;
this.fireDidChangeRegister();
}
}
protected override handleSessionChange(): void {
const debugState = this.sessionManager.currentSession?.state;
if (debugState === DebugState.Inactive) {
this.registerReadResult = EMPTY_REGISTERS;
this.fireDidChangeRegister();
} else if (debugState === DebugState.Stopped) {
const isReadyForQuery = !!this.sessionManager.currentSession?.currentFrame;
const isDynamic = this.memoryWidgetOptions.dynamic !== false;
if (isReadyForQuery && isDynamic && this.doUpdateAutomatically && this.registerReadResult !== EMPTY_REGISTERS) {
this.updateRegisterView();
}
}
}
protected override acceptFocus(): void {
if (this.doUpdateAutomatically) {
if (this.registerField) {
this.registerField.focus();
this.registerField.select();
}
} else {
const multiSelectBar = this.node.querySelector('.multi-select-bar') as HTMLDivElement;
multiSelectBar?.focus();
}
}
protected assignRegisterRef: React.LegacyRef<HTMLInputElement> = reg => {
this.registerField = reg ?? undefined;
};
protected setRegFilterFromSelect = (e: React.ChangeEvent<HTMLSelectElement>): void => {
if (this.registerField) {
this.registerField.value = e.target.value;
}
};
protected radixDisplayed(): boolean {
const { register, ...radices } = this.columnsDisplayed;
for (const val of Object.values(radices)) {
if (val['doRender']) {
return true;
}
}
return false;
}
protected noRadixDisplayed(): boolean {
return !this.radixDisplayed();
}
protected renderRegisterFieldGroup(): React.ReactNode {
return (
<>
<div className='t-mv-group view-group'>
<MWInputWithSelect
id={REGISTER_FIELD_ID}
label={nls.localize('theia/memory-inspector/registers', 'Registers')}
placeholder={nls.localize('theia/memory-inspector/register-widget/filter-placeholder', 'Filter (starts with)')}
onSelectChange={this.setRegFilterFromSelect}
passRef={this.assignRegisterRef}
onKeyDown={this.doRefresh}
options={[...this.recentLocations.values]}
disabled={!this.doUpdateAutomatically}
/>
<MWMultiSelect
id={ASCII_TOGGLE_ID}
label={nls.localize('theia/memory-inspector/columns', 'Columns')}
items={this.getOptionalColumns().map(column => ({ ...column, label: column.label.slice(0, 3) }))}
onSelectionChanged={this.handleColumnSelectionChange}
/>
<button
type='button'
className='theia-button main view-group-go-button'
onClick={this.doRefresh}
disabled={!this.doUpdateAutomatically}
>
{nls.localizeByDefault('Go')}
</button>
</div>
<div className={`t-mv-memory-fetch-error${this.showRegisterError ? ' show' : ' hide'}`}>
{this.registerReadError}
</div>
</>
);
}
protected override doHandleColumnSelectionChange(columnLabel: string, doShow: boolean): void {
const trueColumnLabel = Object.keys(this.columnsDisplayed).find(key => key.startsWith(columnLabel));
if (trueColumnLabel) {
super.doHandleColumnSelectionChange(trueColumnLabel, doShow);
}
}
protected override getObligatoryColumnIds(): string[] {
return ['register'];
}
protected override renderInputContainer(): React.ReactNode {
return (
<div className='t-mv-settings-container'>
<div className='t-mv-wrapper'>
{this.renderToolbar()}
{this.renderRegisterFieldGroup()}
</div>
</div>);
}
protected handleRegFromDebugWidgetSelection(regName: string): void {
this.registerDisplaySet.clear();
if (this.registerField) {
this.registerField.value = regName;
this.registerDisplayAll = false;
}
this.doUpdateRegisterView();
}
protected override renderToolbar(): React.ReactNode {
return (
<div className='memory-widget-toolbar'>
{this.memoryWidgetOptions.dynamic !== false && (
<div className='memory-widget-auto-updates-container'>
<div
className={`fa fa-${this.doUpdateAutomatically ? 'unlock' : 'lock'}`}
id={AUTO_UPDATE_TOGGLE_ID}
title={this.doUpdateAutomatically ?
nls.localize('theia/memory-inspector/register/freeze', 'Freeze memory view') :
nls.localize('theia/memory-inspector/register/unfreeze', 'Unfreeze memory view')
}
onClick={this.toggleAutoUpdate}
onKeyDown={this.toggleAutoUpdate}
role='button'
tabIndex={0}
/>
</div>
)}
{this.renderEditableTitleField()}
</div>
);
}
protected validateInputRegs(input: string): void {
// identify sequences of alphanumeric characters
const searchTexts = input.match(/\w+/g) ?? [];
if (searchTexts.length !== 0) {
this.registerDisplayAll = false;
this.registerDisplaySet.clear();
this.recentLocations.add(input);
for (const { name } of this.registerReadResult.registers) {
if (searchTexts.some(x => name.startsWith(x))) {
this.registerDisplaySet.add(name);
}
}
} else {
this.registerDisplayAll = true;
this.registerDisplaySet.clear();
}
}
protected updateRegisterView = debounce(this.doUpdateRegisterView.bind(this), Constants.DEBOUNCE_TIME, { trailing: true });
protected async doUpdateRegisterView(): Promise<void> {
try {
if (!this.registerReadResult.registers || this.registerReadResult.threadId !== this.sessionManager.currentThread?.id) {
this.registerReadResult = await this.getRegisters();
}
this.updateRegDisplayFilter();
this.fireDidChangeRegister();
this.doShowRegisterErrors(true);
} catch (err) {
this.registerReadError = nls.localize('theia/memory-inspector/registerReadError', 'There was an error fetching registers.');
console.error('Failed to read registers', err);
this.doShowRegisterErrors();
} finally {
this.registerFilterUpdate = false;
this.update();
}
}
protected updateRegDisplayFilter(): void {
if (this.registerField) {
if (this.registerField.value.length === 0) {
this.registerDisplayAll = true;
} else {
this.validateInputRegs(this.registerField.value);
}
}
}
protected override doRefresh = (event: React.KeyboardEvent<HTMLInputElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>): void => {
if ('key' in event && event.key !== 'Enter') {
return;
}
this.registerFilterUpdate = true;
this.updateRegisterView();
};
protected async getRegisters(): Promise<RegisterReadResult> {
const regResult = await getRegisters(this.sessionManager.currentSession);
const threadResult = this.sessionManager.currentSession?.currentThread?.id;
return { threadId: threadResult, registers: regResult };
}
protected fireDidChangeRegister(): void {
this.onRegisterChangedEmitter.fire([this.registerReadResult, this.registerFilterUpdate]);
}
override storeState(): RegisterOptions {
return {
...super.storeState(),
reg: this.registerField?.value ?? this.reg,
noRadixColumnDisplayed: this.noRadixDisplayed(),
};
}
override restoreState(oldState: RegisterOptions): void {
this.reg = oldState.reg ?? this.reg;
this.noRadixColumnDisplayed = oldState.noRadixColumnDisplayed;
}
protected doShowRegisterErrors = (doClearError = false): void => {
if (this.errorTimeout !== undefined) {
clearTimeout(this.errorTimeout);
}
if (doClearError) {
this.showRegisterError = false;
this.update();
this.errorTimeout = undefined;
return;
}
this.showRegisterError = true;
this.update();
this.errorTimeout = setTimeout(() => {
this.showRegisterError = false;
this.update();
this.errorTimeout = undefined;
}, Constants.ERROR_TIMEOUT);
};
}

View File

@@ -0,0 +1,277 @@
/********************************************************************************
* Copyright (C) 2021 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 { Key, KeyCode } from '@theia/core/lib/browser';
import { inject } from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
import { DebugVariable } from '@theia/debug/lib/browser/console/debug-console-items';
import { EMPTY_MEMORY } from '../memory-widget/memory-options-widget';
import { MemoryTable, MemoryTableWidget } from '../memory-widget/memory-table-widget';
import { Interfaces } from '../utils/memory-widget-utils';
import { RegisterReadResult } from '../utils/memory-widget-variable-utils';
import { RegisterOptions, RegisterOptionsWidget } from './register-options-widget';
export namespace RegisterTable {
export const ROW_CLASS = 't-mv-view-row';
export const ROW_DIVIDER_CLASS = 't-mv-view-row-highlight';
export const REGISTER_NAME_CLASS = 't-mv-view-address';
export const REGISTER_DATA_CLASS = 't-mv-view-data';
export const EXTRA_COLUMN_DATA_CLASS = 't-mv-view-code';
export const HEADER_ROW_CLASS = 't-mv-header';
export interface RowOptions {
regName: string;
regVal: string;
hexadecimal?: string;
decimal?: string;
octal?: string;
binary?: string;
doShowDivider?: boolean;
isChanged?: boolean;
}
export interface StylableNodeAttributes {
className?: string;
style?: React.CSSProperties;
title?: string;
isChanged?: boolean;
}
export interface RowDecorator {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(...args: any[]): Partial<StylableNodeAttributes>;
}
}
export class RegisterTableWidget extends MemoryTableWidget {
static override CONTEXT_MENU = ['register.view.context.menu'];
static override ID = 'register-table-widget';
@inject(RegisterOptionsWidget) override readonly optionsWidget: RegisterOptionsWidget;
protected readonly registerNotSaved = '<not saved>';
protected registers: RegisterReadResult;
protected previousRegisters: RegisterReadResult | undefined;
protected override options: RegisterOptions;
protected override memory: Interfaces.WidgetMemoryState = { ...EMPTY_MEMORY, variables: [] };
protected override async doInit(): Promise<void> {
this.id = RegisterTableWidget.ID;
this.addClass(RegisterTableWidget.ID);
this.scrollOptions = { ...this.scrollOptions, suppressScrollX: false };
this.toDispose.push(this.optionsWidget.onOptionsChanged(optionId => this.handleOptionChange(optionId)));
this.toDispose.push(this.optionsWidget.onRegisterChanged(e => this.handleRegisterChange(e)));
this.toDispose.push(this.themeService.onDidColorThemeChange(e => this.handleThemeChange(e)));
this.getStateAndUpdate();
}
handleSetValue(dVar: DebugVariable | undefined): void {
if (dVar) {
dVar.open();
}
}
protected handleRegisterChange(newRegister: [RegisterReadResult, boolean]): void {
const regResult = newRegister[0];
const updatePrevRegs = !newRegister[1];
if (this.registers.threadId !== regResult.threadId) {
// if not same thread Id, dont highlighting register changes
this.previousRegisters = undefined;
} else {
if (updatePrevRegs) {
this.previousRegisters = this.registers;
}
}
this.getStateAndUpdate();
}
protected override getState(): void {
this.options = this.optionsWidget.options;
this.registers = this.optionsWidget.registers;
}
protected override getTableRows(): React.ReactNode {
return [...this.renderRegRows()];
}
protected *renderRegRows(result: RegisterReadResult = this.registers): IterableIterator<React.ReactNode> {
let rowsYielded = 0;
// For each row...
for (const reg of result.registers) {
if (this.optionsWidget.displayReg(reg.name)) {
const notSaved = reg.value === this.registerNotSaved;
const isChanged = this.previousRegisters && reg.value !== this.getPrevRegVal(reg.name, this.previousRegisters);
const options: RegisterTable.RowOptions = {
regName: reg.name,
regVal: reg.value,
hexadecimal: notSaved ? reg.value : this.optionsWidget.handleRadixRendering(reg.value, 16, reg.name),
decimal: notSaved ? reg.value : this.optionsWidget.handleRadixRendering(reg.value, 10),
octal: notSaved ? reg.value : this.optionsWidget.handleRadixRendering(reg.value, 8),
binary: notSaved ? reg.value : this.optionsWidget.handleRadixRendering(reg.value, 2, reg.name),
doShowDivider: (rowsYielded % 4) === 3,
isChanged,
};
yield this.renderRegRow(options);
rowsYielded += 1;
}
}
}
protected getPrevRegVal(regName: string, inRegs: RegisterReadResult): string | undefined {
return inRegs.registers.find(element => element.name === regName)?.value;
}
protected renderRegRow(
options: RegisterTable.RowOptions,
getRowAttributes: RegisterTable.RowDecorator = this.getRowAttributes.bind(this),
): React.ReactNode {
const { regName } = options;
const { className, style, title } = getRowAttributes(options);
return (
<tr
// Add a marker to help visual navigation when scrolling
className={className}
style={style}
title={title}
key={regName}
data-id={regName}
data-value={options.decimal ?? 'none'}
tabIndex={0}
onKeyDown={this.handleRowKeyDown}
onContextMenu={this.options.isFrozen ? undefined : this.handleTableRightClick}
onDoubleClick={this.options.isFrozen ? undefined : this.openDebugVariableByCurrentTarget}
>
<td className={RegisterTable.REGISTER_NAME_CLASS}>{regName}</td>
{this.getExtraRegColumn(options)}
</tr>
);
}
protected override getRowAttributes(options: Partial<RegisterTable.RowOptions>): Partial<RegisterTable.StylableNodeAttributes> {
let className = RegisterTable.ROW_CLASS;
if (options.doShowDivider) {
className += ` ${RegisterTable.ROW_DIVIDER_CLASS}`;
}
if (options.isChanged) {
// use the eight-bits change CSS class
className += ' eight-bits changed';
}
return { className };
}
protected getExtraRegColumn(options: Pick<RegisterTable.RowOptions, 'hexadecimal' | 'decimal' | 'octal' | 'binary'>): React.ReactNode[] {
const additionalColumns = [];
if (this.options.columnsDisplayed.hexadecimal.doRender) {
additionalColumns.push(<td className={RegisterTable.EXTRA_COLUMN_DATA_CLASS} key='hexadecimal'>{options.hexadecimal}</td>);
}
if (this.options.columnsDisplayed.decimal.doRender) {
additionalColumns.push(<td className={RegisterTable.EXTRA_COLUMN_DATA_CLASS} key='decimal'>{options.decimal}</td>);
}
if (this.options.columnsDisplayed.octal.doRender) {
additionalColumns.push(<td className={RegisterTable.EXTRA_COLUMN_DATA_CLASS} key='octal'>{options.octal}</td>);
}
if (this.options.columnsDisplayed.binary.doRender) {
additionalColumns.push(<td className={RegisterTable.EXTRA_COLUMN_DATA_CLASS} key='binary'>{options.binary}</td>);
}
return additionalColumns;
}
protected override getWrapperHandlers(): MemoryTable.WrapperHandlers {
return this.options.isFrozen || this.options.noRadixColumnDisplayed
? super.getWrapperHandlers()
: {
onMouseMove: this.handleTableMouseMove,
onContextMenu: this.handleTableRightClick,
};
}
protected override doHandleTableMouseMove(targetElement: React.MouseEvent['target']): void {
const tempTarget = targetElement as HTMLElement;
const target = tempTarget.parentElement?.tagName === 'TR' ? tempTarget.parentElement : tempTarget;
if (target.tagName === 'TR') {
const { x, y } = target.getBoundingClientRect();
const anchor = { x: Math.round(x), y: Math.round(y + target.clientHeight) };
const value = Number(target.getAttribute('data-value'));
if (!isNaN(value)) {
const register = target.getAttribute('data-id') as string;
const properties = {
register,
hex: `0x${value.toString(16)}`,
binary: `0b${value.toString(2)}`,
decimal: value.toString(10),
octal: `0o${value.toString(8)}`,
};
return this.hoverRenderer.render(this.node, anchor, properties);
}
}
return this.hoverRenderer.hide();
}
protected handleRowKeyDown = (event: React.KeyboardEvent<HTMLElement>): void => {
const keyCode = KeyCode.createKeyCode(event.nativeEvent).key?.keyCode;
switch (keyCode) {
case Key.ENTER.keyCode:
this.openDebugVariableByCurrentTarget(event);
break;
default:
break;
}
};
protected openDebugVariableByCurrentTarget = (event: React.KeyboardEvent<HTMLElement> | React.MouseEvent<HTMLElement>): void => {
this.openDebugVariableByDataId(event.currentTarget);
};
protected openDebugVariableByDataId(element: HTMLElement): void {
const registerName = element.getAttribute('data-id');
if (registerName) {
this.openDebugVariableByName(registerName);
}
}
protected openDebugVariableByName(registerName: string): void {
const debugVariable = this.registers.registers.find(element => element.name === registerName);
this.handleSetValue(debugVariable);
}
protected override doHandleTableRightClick(event: React.MouseEvent): void {
event.preventDefault();
const curTarget = event.currentTarget as HTMLElement;
if (curTarget.tagName === 'TR') {
this.update();
event.stopPropagation();
this.contextMenuRenderer.render({
menuPath: RegisterTableWidget.CONTEXT_MENU,
anchor: event.nativeEvent,
args: this.getContextMenuArgs(event),
context: curTarget
});
}
}
protected override getContextMenuArgs(event: React.MouseEvent): unknown[] {
const args: unknown[] = [this];
const regName = (event.currentTarget as HTMLElement).getAttribute('data-id');
if (regName) {
const dVar = this.registers.registers.find(element => element.name === regName);
args.push(dVar);
}
return args;
}
}

View File

@@ -0,0 +1,45 @@
/********************************************************************************
* Copyright (C) 2021 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 { nls } from '@theia/core';
import { interfaces } from '@theia/core/shared/inversify';
import { MemoryOptionsWidget } from '../memory-widget/memory-options-widget';
import { MemoryTableWidget } from '../memory-widget/memory-table-widget';
import { MemoryWidget } from '../memory-widget/memory-widget';
import { MemoryWidgetOptions } from '../utils/memory-widget-utils';
import { RegisterFilterService, RegisterFilterServiceImpl, RegisterFilterServiceOptions } from './register-filter-service';
import { RegisterOptionsWidget } from './register-options-widget';
import { RegisterTableWidget } from './register-table-widget';
export type RegisterWidget = MemoryWidget<RegisterOptionsWidget, RegisterTableWidget>;
export namespace RegisterWidget {
export const ID = 'register-view-options-widget';
export const LABEL = nls.localize('theia/memory-inspector/register', 'Register');
export const is = (widget: MemoryWidget): boolean => widget.optionsWidget instanceof RegisterOptionsWidget;
export const createContainer = (
parent: interfaces.Container,
optionsWidget: interfaces.ServiceIdentifier<MemoryOptionsWidget>,
tableWidget: interfaces.ServiceIdentifier<MemoryTableWidget>,
optionSymbol: interfaces.ServiceIdentifier<MemoryWidgetOptions | undefined> = MemoryWidgetOptions,
options?: MemoryWidgetOptions,
): interfaces.Container => {
const child = MemoryWidget.createContainer(parent, optionsWidget, tableWidget, optionSymbol, options);
child.bind(RegisterFilterService).to(RegisterFilterServiceImpl).inSingletonScope();
child.bind(RegisterFilterServiceOptions).toConstantValue({});
return child;
};
}

View File

@@ -0,0 +1,34 @@
/********************************************************************************
* Copyright (C) 2021 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
********************************************************************************/
.register-table-widget table.t-mv-view {
table-layout: fixed;
width: 100%;
}
.register-table-widget table.t-mv-view td,
.register-table-widget table.t-mv-view th {
overflow: hidden;
text-overflow: ellipsis;
}
.reg-options-widget .t-mv-group.view-group {
grid-template-columns: 3fr 2fr 30px;
}
.reg-options-widget .multi-select-bar {
height: 100%;
}

View File

@@ -0,0 +1,748 @@
/********************************************************************************
* Copyright (C) 2021 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
********************************************************************************/
:root {
--memory-widget-disabled-opacity: 0.7;
--memory-widget-placeholder-text-line-height: 24px;
}
.memory-view-options-widget {
display: flex;
overflow: unset !important;
}
.memory-view-icon {
-webkit-mask: url("memory-view.svg");
mask: url("memory-view.svg");
}
.register-view-icon {
-webkit-mask: url("register-view.svg");
mask: url("register-view.svg");
}
.memory-view-icon.toolbar,
.register-view-icon.toolbar {
display: inline-block;
background-color: var(--theia-settings-headerForeground);
mask-size: 16px 15px;
mask-repeat: no-repeat;
mask-position: center center;
-webkit-mask-size: 16px 15px;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center center;
min-width: var(--theia-icon-size);
min-height: var(--theia-icon-size);
cursor: pointer;
}
.memory-lock-icon {
-webkit-mask: url("memory-lock.svg");
mask: url("memory-lock.svg");
}
.register-lock-icon {
-webkit-mask: url("register-lock.svg");
mask: url("register-lock.svg");
}
.t-mv-container {
display: flex;
flex-direction: column;
align-items: left;
box-sizing: border-box;
height: 100%;
width: 100%;
padding: 10px;
overflow: hidden;
}
.t-mv-container button:focus {
/* Fixes a padding issue when clicking a button */
border: none;
}
.memory-view-wrapper {
display: flex;
flex-direction: column;
}
.t-mv-memory-container {
display: flex;
flex-direction: column;
overflow: hidden;
background-color: var(--theia-editor-background);
height: 100%;
white-space: nowrap;
}
.t-mv-memory-container:focus {
outline: none;
}
.t-mv-memory-container.frozen,
.t-mv-memory-container.frozen:focus {
font-style: italic;
opacity: var(--memory-widget-disabled-opacity) !important;
}
.t-mv-memory-fetch-error {
margin: var(--theia-ui-padding);
color: var(--theia-editorWarning-foreground);
}
.t-mv-memory-fetch-error.show {
display: block;
}
.t-mv-memory-fetch-error.hide {
display: none;
}
.t-mv-header {
margin-top: var(--theia-ui-padding);
display: grid;
box-shadow: 0px 6px 5px -5px var(--theia-widget-shadow);
padding-bottom: var(--theia-ui-padding);
}
.t-mv-header-label {
background-color: var(--theia-editor-background);
border-radius: 10px;
font-weight: 400;
padding: 2px 5px 2px 5px;
}
.t-mv-view {
width: 100%;
}
.t-mv-view .eight-bits.changed {
background-color: var(--theia-memoryInspector-foreground);
color: var(--theia-editor-background);
}
.t-mv-view .eight-bits.changed:hover {
background-color: var(--theia-memoryInspector-foreground);
opacity: 0.85;
}
.t-mv-view .eight-bits {
border-bottom: 1px solid transparent;
box-sizing: border-box;
}
.t-mv-view .eight-bits:not(.changed):not(.highlight):hover {
background-color: var(--theia-editor-inactiveSelectionBackground);
}
.t-mv-memory-container:focus .eight-bits.highlight {
background-color: var(--theia-editor-selectionBackground);
border-radius: 1px;
border-bottom: solid 1px var(--theia-editorCursor-foreground);
}
.t-mv-memory-container:focus .eight-bits.changed.highlight {
background-color: var(--theia-memoryInspector-focusBorder);
}
.t-mv-memory-container:not(:focus) .eight-bits.highlight {
background-color: var(--theia-editor-inactiveSelectionBackground);
}
.t-mv-view .eight-bits.modified {
outline-width: 1px;
outline-style: solid;
outline-offset: -1px;
outline-color: var(--theia-editorWarning-foreground);
border: none;
}
.t-mv-view .byte-group:not(:first-of-type) {
display: inline-block;
padding-left: var(--theia-ui-padding);
}
.t-mv-view .data-address-col {
width: unset;
}
.t-mv-view-container {
flex: 1;
overflow: hidden;
margin-top: var(--theia-ui-padding);
height: 100%;
}
.t-mv-view-container thead {
position: absolute;
}
.t-mv-view-container .ps__rail-y {
left: unset !important;
}
.t-mv-view-container:focus {
/* Fixes a padding issue when clicking inside the container */
border: none;
}
.memory-diff-select .theia-select:focus,
.t-mv-container .theia-select:focus {
outline-width: 1px;
outline-style: solid;
outline-offset: -1px;
opacity: 1 !important;
outline-color: var(--theia-focusBorder);
}
.t-mv-settings-container {
flex: none;
padding-bottom: var(--theia-ui-padding);
}
.t-mv-settings-container .t-mv-settings-group-header {
padding-bottom: calc(var(--theia-ui-padding) / 2);
border-bottom: 1px solid hsla(0, 0%, 50%, 0.5);
}
.memory-widget-toolbar {
display: flex;
flex-flow: row nowrap;
height: 24px;
align-items: center;
max-width: 100%;
}
.memory-widget-toolbar * {
flex: none;
}
.memory-widget-toolbar .memory-widget-header-click-zone {
min-width: 0;
}
.memory-widget-header-click-zone * {
flex: none;
}
.memory-widget-auto-updates-container {
width: 16px;
margin-right: var(--theia-ui-padding);
font-size: 1.3em;
}
.toggle-settings-container {
margin-left: auto;
}
.toggle-settings-container .toggle-settings-click-zone {
display: flex;
align-items: center;
justify-content: flex-end;
margin-left: var(--theia-ui-padding);
user-select: none;
box-sizing: content-box;
border-top: 1px solid transparent;
}
.toggle-settings-container .toggle-settings-click-zone:focus {
outline: none;
border-top: 1px solid var(--theia-focusBorder);
}
.toggle-settings-container .toggle-settings-click-zone span {
white-space: nowrap;
}
.toggle-settings-container .codicon-settings-gear {
font-size: 1.1em;
margin-right: var(--theia-ui-padding);
}
.toggle-settings-container + div > .t-mv-settings-group-header:first-of-type,
.toggle-settings-container + .t-mv-settings-group-header {
margin-top: 0;
}
.t-mv-wrapper button.theia-button {
min-width: 30px;
margin: unset;
padding: unset;
}
.view-group {
grid-template-columns: 70px 1fr;
}
.settings-group {
grid-template-columns: 110px 1fr;
margin-top: calc(2 * var(--theia-ui-padding));
}
.ascii-group {
grid-template-columns: 70px 1fr;
}
.t-mv-group.view-group {
grid-template-columns: 3fr repeat(2, 1fr) 30px;
grid-template-rows: repeat(2, 24px);
grid-auto-flow: column;
border-bottom: 1px solid hsla(0, 0%, 50%, 0.5);
border-top: 1px solid hsla(0, 0%, 50%, 0.5);
padding-bottom: var(--theia-ui-padding);
}
.view-group-go-button {
grid-row: 2 / 3;
height: 100%;
}
.t-mv-group {
display: grid;
grid-column-gap: var(--theia-ui-padding);
grid-row-gap: calc(var(--theia-ui-padding) / 2);
align-items: center;
}
.t-mv-group .theia-select,
.t-mv-group .theia-input {
width: 100%;
box-sizing: border-box;
}
.t-mv-group .multi-select-bar {
min-height: 19px;
}
.t-mv-group .multi-select-label {
line-height: 17px;
}
.t-mv-input-group.button-wrapper {
margin: 2px;
width: 100%;
display: block;
text-align: center;
}
.t-mv-input-group.button-wrapper .theia-button.main {
margin: var(--theia-ui-padding) 0 0 0;
}
table.t-mv-view {
align-self: center;
border-collapse: collapse;
border: none;
}
table.t-mv-view {
font-size: var(--theia-ui-font-size1);
font-weight: 400;
text-align: left;
padding: 10px;
}
table.t-mv-view td,
table.t-mv-view th {
font-family: monospace;
padding-right: 15px;
white-space: pre;
}
table.t-mv-view .t-mv-view-row.t-mv-view-row-highlight {
border-bottom: 1px var(--theia-editorGroup-border) solid;
}
table.t-mv-view .t-mv-view-row:hover {
background-color: var(--theia-sideBar-background);
}
table.t-mv-view .t-mv-view-address {
font-weight: 700;
text-align: left;
}
.t-mv-settings-group-header.small-margin {
margin-bottom: var(--theia-ui-padding);
margin-top: var(--theia-ui-padding);
}
.t-mv-variable-label:not(:last-of-type)::after {
content: ",";
margin-right: var(--theia-ui-padding);
color: var(--theia-editor-foreground);
}
.mw-input-select {
position: relative;
height: 100%;
}
.mw-input-select .theia-input {
position: absolute;
z-index: 1;
width: calc(100% - 18px);
border: var(--theia-border-width) solid var(--theia-dropdown-border);
line-height: calc(var(--theia-content-line-height) - 2px);
}
.mw-input-select .theia-input:focus {
outline: unset;
}
.mw-input-select .theia-select {
position: absolute;
z-index: 0;
height: calc(var(--theia-content-line-height) + 2px);
}
.mw-input-select .theia-select:focus {
outline: unset;
}
.memory-layout-widget {
display: flex;
flex-direction: column;
}
.memory-dock-panel {
flex-grow: 1;
margin: var(--theia-ui-padding);
margin-top: 0;
}
.memory-edit-button-container {
padding: var(--theia-ui-padding) 0;
width: 100%;
display: flex;
justify-content: center;
box-shadow: 0px 6px 6px 6px var(--theia-widget-shadow);
}
.memory-edit-button-container .memory-edit-error {
width: 100%;
height: 100%;
bottom: 0;
box-sizing: border-box;
white-space: normal;
padding: var(--theia-ui-padding);
background-color: var(--theia-editor-background);
color: var(--theia-editorWarning-foreground);
}
.memory-diff-select {
padding: var(--theia-ui-padding);
}
.memory-diff-select-wrapper {
display: flex;
flex-flow: row wrap;
justify-content: center;
align-content: center;
align-items: center;
border-top: 1px solid hsla(0, 0%, 50%, 0.5);
padding-top: var(--theia-ui-padding);
}
.diff-select-input-wrapper {
display: flex;
justify-content: space-around;
align-content: center;
align-items: center;
flex-flow: row wrap;
}
.memory-diff-select-go {
min-width: unset;
text-align: center;
width: 30px;
margin: 0 var(--theia-ui-padding);
margin-top: var(--theia-ui-padding);
align-self: flex-end;
}
.t-mv-diff-select-widget-options-wrapper {
display: grid;
grid-template-rows: 12px 24px;
grid-auto-flow: column;
gap: calc(var(--theia-ui-padding) / 2);
max-width: 215px;
margin: 0 var(--theia-ui-padding);
align-items: center;
}
.t-mv-diff-select-widget-options-wrapper .t-mv-label,
.t-mv-diff-select-widget-options-wrapper .t-mv-select {
min-width: 0;
}
.t-mv-diff-select-widget-options-wrapper .t-mv-select {
height: 100%;
}
.theia-input:disabled {
opacity: var(--memory-widget-disabled-opacity);
}
.t-mv-label.disabled {
font-style: italic;
opacity: var(--memory-widget-disabled-opacity);
}
.memory-widget-header {
font-weight: normal;
}
.memory-widget-header-click-zone {
display: flex;
flex: 1 1 0;
align-items: center;
}
.memory-widget-header-click-zone .memory-widget-header {
margin: 0;
white-space: nowrap;
flex-shrink: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.memory-widget-header-click-zone input {
flex-grow: 1;
width: 0;
height: 18px;
}
.memory-widget-toolbar .fa.fa-pencil,
.memory-widget-toolbar .fa.fa-save {
font-size: 1.2em;
margin: 0 var(--theia-ui-padding);
transition: opacity 0.3s;
}
.memory-widget-header-click-zone:hover .fa.fa-pencil {
opacity: 1;
}
.memory-widget-header-click-zone .fa.fa-pencil {
opacity: 0;
}
.memory-widget-header-click-zone .theia-input {
font-size: 1.5em;
/* same as theia h2 */
}
.t-mv-settings-group-header.disabled,
.memory-widget-header.disabled {
font-style: italic;
opacity: var(--memory-widget-disabled-opacity);
}
.t-mv-view-container span {
display: inline-block;
}
.t-mv-diffed-ascii:not(:last-of-type) {
margin-right: var(--theia-ui-padding);
}
.t-mv-view-container span.different {
position: relative;
}
.t-mv-view-container span.different::before {
content: "";
position: absolute;
height: 100%;
width: 100%;
top: 0;
left: 0;
}
.t-mv-view-container span.before.different::before,
.t-mv-view-container .before.different {
background-color: var(--theia-memoryDiff-removedTextBackground);
}
.t-mv-view-container span.after.different::before,
.t-mv-view-container .after.different {
background-color: var(--theia-memoryDiff-insertedTextBackground);
}
.t-mv-view-container span.before.different.hc::before,
.t-mv-view-container .before.different.hc {
border-top: dotted 1px var(--theia-contrastActiveBorder);
border-bottom: dotted 1px var(--theia-contrastActiveBorder);
box-sizing: border-box;
}
.t-mv-view-container span.after.different.hc::before,
.t-mv-view-container .after.different.hc {
border-top: dashed 1px var(--theia-contrastBorder);
border-bottom: dashed 1px var(--theia-contrastBorder);
box-sizing: border-box;
}
.memory-dockpanel-placeholder {
position: absolute;
top: 10px;
padding: calc(2 * var(--theia-ui-padding));
width: 100%;
line-height: var(--memory-widget-placeholder-text-line-height);
}
.memory-dockpanel-placeholder i.toolbar {
background-color: var(--theia-errorForeground);
vertical-align: middle;
cursor: default;
}
.t-mv-hover {
position: fixed;
font-family: monospace;
box-sizing: border-box;
padding: var(--theia-ui-padding);
font-size: var(--theia-ui-font-size1);
background: var(--theia-editorHoverWidget-background);
border: 2px solid var(--theia-editorHoverWidget-border);
/* This ensures that the hover is visible over the widget even when focused */
z-index: 1000;
display: grid;
grid-template-columns: max-content max-content;
gap: calc(var(--theia-ui-padding) / 2) var(--theia-ui-padding);
}
.t-mv-hover.hidden {
display: none;
}
.t-mv-hover-key {
color: var(--theia-symbolIcon-variableForeground);
}
.t-mv-hover-value {
color: var(--theia-variable-number-variable-color);
}
.t-mv-hover-value.utf8 {
color: var(--theia-variable-string-variable-color);
}
.mw-more-memory-select {
display: flex;
justify-content: center;
align-items: center;
font-style: italic;
opacity: 0.7;
}
.mw-more-memory-select-top {
display: flex;
justify-content: center;
height: 16px;
padding-bottom: 1px;
transition: border-color 0.1s;
border-color: transparent;
}
.mw-more-memory-select-top:hover {
border-bottom: 1px solid;
padding-bottom: 0;
border-color: var(--theia-sideBar-foreground);
}
.mw-more-memory-select select {
border: none;
background: none;
border-radius: 3px;
margin: 0 2px;
position: relative;
bottom: 1px;
transition: background 0.1s;
font-style: italic;
}
.mw-more-memory-select select:hover {
background: var(--theia-dropdown-background);
}
.mw-more-memory-select button {
background-color: unset;
border: none;
padding: 2px;
color: var(--theia-sideBar-foreground);
}
.mw-bookmarks-bar {
padding: calc(var(--theia-ui-padding) / 2) 0;
}
.mw-bookmarks-bar .bookmark-container {
display: inline;
position: relative;
cursor: pointer;
padding: 0 var(--theia-ui-padding);
}
.mw-bookmarks-bar .bookmark-container .bookmark-radio-button {
height: 100%;
z-index: -1;
border-radius: 3px;
padding: 0 var(--theia-ui-padding);
background-color: var(--theia-button-secondaryBackground);
color: var(--theia-button-secondaryForeground);
box-shadow: 0 2px 0 rgba(187, 187, 187, 0.4);
}
.mw-bookmarks-bar input {
position: absolute;
left: 0;
appearance: none;
-webkit-appearance: none;
}
.mw-bookmarks-bar input:checked ~ .bookmark-radio-button {
background-color: var(--theia-button-background);
color: var(--theia-button-foreground);
}
.diff-options-widget .t-mv-group.view-group {
grid-template-columns: repeat(2, 1fr) 30px;
grid-template-rows: 18px 24px;
}
#memory-table-widget.editable .t-mv-view .eight-bits:hover {
cursor: pointer;
}
#memory-table-widget.editable
.t-mv-memory-container:focus
.eight-bits.highlight {
cursor: text;
}
.diff-options-widget .memory-widget-header-click-zone,
.diff-options-widget .toggle-settings-click-zone,
#memory-layout-widget .memory-widget-header-click-zone,
#memory-layout-widget .toggle-settings-click-zone,
#memory-layout-widget .fa-unlock,
#memory-layout-widget .fa-lock {
cursor: pointer;
}

View File

@@ -0,0 +1,21 @@
<!-- /********************************************************************************
* Copyright (C) 2021 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
********************************************************************************/ -->
<svg width="369" height="369" viewBox="0 0 369 369" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M35.7183 98.6009L184.5 12.7017L333.282 98.6008V270.399L184.5 356.298L35.7183 270.399V98.6009Z" stroke="black" stroke-width="22"/>
<rect x="94" y="153" width="181" height="114" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M184.756 61C222.756 61 253.561 91.8052 253.561 129.805L253.561 219.721H115.951V129.85L115.951 129.805C115.951 91.8052 146.756 61 184.756 61ZM228.384 129.567C228.383 129.602 228.383 129.637 228.383 129.672V186.924H140.434V129.457H140.435C140.435 105.171 160.123 85.4836 184.409 85.4836C208.696 85.4836 228.384 105.172 228.384 129.458C228.384 129.494 228.384 129.531 228.384 129.567Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,20 @@
<!-- /********************************************************************************
* Copyright (C) 2021 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
********************************************************************************/ -->
<svg width="369" height="369" viewBox="0 0 369 369" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M131.8 253C125.933 253 121.133 252.8 117.4 252.4C113.8 252 109.933 251.2 105.8 250C101.4 248.8 97.8667 247.067 95.2 244.8C92.6667 242.4 90.5333 239.067 88.8 234.8C87.0667 230.8 86.2 225.8 86.2 219.8V154.2C86.2 148.2 87.0667 142.933 88.8 138.4C90.5333 133.867 92.8 130.267 95.6 127.6C98.1333 125.333 101.467 123.533 105.6 122.2C109.867 120.733 114.133 119.8 118.4 119.4C123.2 119.133 127.667 119 131.8 119C137.667 119 142.4 119.2 146 119.6C149.733 120 153.667 120.8 157.8 122C162.2 123.333 165.733 125.2 168.4 127.6C171.2 130 173.467 133.4 175.2 137.8C176.933 142.2 177.8 147.6 177.8 154V219.8C177.8 225.267 176.933 230.133 175.2 234.4C173.467 238.667 171.2 242 168.4 244.4C165.733 246.667 162.333 248.533 158.2 250C154.067 251.333 149.867 252.133 145.6 252.4C141.867 252.8 137.267 253 131.8 253ZM132.2 241.2C139.267 241.2 144.733 240.733 148.6 239.8C152.467 238.733 155.267 236.733 157 233.8C158.733 230.867 159.6 226.533 159.6 220.8V152.8C159.6 146.533 158.667 141.867 156.8 138.8C155.067 135.6 152.267 133.467 148.4 132.4C144.667 131.333 139.2 130.8 132 130.8C124.667 130.8 119.067 131.333 115.2 132.4C111.467 133.467 108.733 135.6 107 138.8C105.267 141.867 104.4 146.533 104.4 152.8V220.8C104.4 226.533 105.267 230.867 107 233.8C108.867 236.733 111.733 238.733 115.6 239.8C119.467 240.733 125 241.2 132.2 241.2ZM228.016 201.2L194.016 154.2H213.816L237.816 189.6L262.016 154.2H281.816L247.816 201.2L283.816 251H264.016L237.816 212.8L211.816 251H192.016L228.016 201.2Z" fill="black"/>
<path d="M35.7183 98.6009L184.5 12.7017L333.282 98.6008V270.399L184.5 356.298L35.7183 270.399V98.6009Z" stroke="black" stroke-width="22"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,29 @@
<!-- /********************************************************************************
* Copyright (C) 2021 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
********************************************************************************/ -->
<svg width="369" height="369" viewBox="0 0 369 369" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="15" y="55" width="62" height="49" rx="13" fill="black"/>
<rect x="293" y="55" width="62" height="49" rx="13" fill="black"/>
<rect x="15" y="265" width="62" height="49" rx="13" fill="black"/>
<rect x="293" y="265" width="62" height="49" rx="13" fill="black"/>
<rect x="15" y="195" width="62" height="49" rx="13" fill="black"/>
<rect x="293" y="195" width="62" height="49" rx="13" fill="black"/>
<rect x="15" y="125" width="62" height="49" rx="13" fill="black"/>
<rect x="293" y="125" width="62" height="49" rx="13" fill="black"/>
<rect x="101" y="160" width="167" height="118" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M239.561 199.576V126.171C239.561 95.1486 215.259 70 185.281 70C155.302 70 131 95.1486 131 126.171V199.576H239.561ZM219.699 125.887C219.699 125.954 219.698 126.02 219.698 126.086V172.801H150.315V125.887H150.316C150.316 106.06 165.848 89.9878 185.007 89.9878C204.167 89.9878 219.699 106.061 219.699 125.887Z" fill="black"/>
<rect width="369" height="369" stroke="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,28 @@
<!-- /********************************************************************************
* Copyright (C) 2021 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
********************************************************************************/ -->
<svg width="369" height="369" viewBox="0 0 369 369" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="102" y="30" width="166" height="309" rx="27" stroke="black" stroke-width="24"/>
<rect x="15" y="55" width="62" height="49" rx="13" fill="black"/>
<rect x="293" y="55" width="62" height="49" rx="13" fill="black"/>
<rect x="15" y="265" width="62" height="49" rx="13" fill="black"/>
<rect x="293" y="265" width="62" height="49" rx="13" fill="black"/>
<rect x="15" y="195" width="62" height="49" rx="13" fill="black"/>
<rect x="293" y="195" width="62" height="49" rx="13" fill="black"/>
<rect x="15" y="125" width="62" height="49" rx="13" fill="black"/>
<rect x="293" y="125" width="62" height="49" rx="13" fill="black"/>
<rect width="369" height="369" stroke="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,76 @@
/********************************************************************************
* Copyright (C) 2021 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 { Command } from '@theia/core';
import { nls } from '@theia/core/lib/common/nls';
export const MemoryCommand: Command = { id: 'memory-inspector-command' };
export const MemoryCategory = nls.localize('theia/memory-inspector/memoryCategory', 'Memory Inspector');
export const ViewVariableInMemoryCommand: Command = {
id: 'view-variable-in-memory',
category: MemoryCategory,
label: nls.localize('theia/memory-inspector/command/viewVariable', 'Show Variable in Memory Inspector'),
};
export const ViewVariableInRegisterViewCommand: Command = {
id: 'view-variable-in-register-view',
category: MemoryCategory,
label: nls.localize('theia/memory-inspector/command/showRegister', 'Show Register in Memory Inspector'),
};
export const ResetModifiedCellCommand: Command = {
id: 'reset-modified-cell',
category: MemoryCategory,
label: nls.localize('theia/memory-inspector/command/resetValue', 'Reset Value'),
};
export const CreateNewMemoryViewCommand: Command = {
id: 'create-new-memory-view',
category: MemoryCategory,
label: nls.localize('theia/memory-inspector/command/createNewMemory', 'Create New Memory Inspector'),
iconClass: 'memory-view-icon toolbar',
};
export const FollowPointerTableCommand: Command = {
id: 'follow-pointer-table',
category: MemoryCategory,
label: nls.localize('theia/memory-inspector/command/followPointer', 'Follow Pointer'),
};
export const FollowPointerDebugCommand: Command = {
id: 'follow-pointer-debug',
category: MemoryCategory,
label: nls.localize('theia/memory-inspector/command/followPointerMemory', 'Follow Pointer in Memory Inspector'),
};
export const CreateNewRegisterViewCommand: Command = {
id: 'create-new-register-view',
category: MemoryCategory,
label: nls.localize('theia/memory-inspector/command/createNewRegisterView', 'Create New Register View'),
iconClass: 'register-view-icon toolbar',
};
export const RegisterSetVariableCommand: Command = {
id: 'register-set-variable-value',
category: MemoryCategory,
label: nls.localizeByDefault('Set Value')
};
export const ToggleDiffSelectWidgetVisibilityCommand: Command = {
id: 'toggle-diff-select-visibility',
iconClass: 'codicon codicon-git-compare',
};

View File

@@ -0,0 +1,113 @@
/********************************************************************************
* Copyright (C) 2021 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 { Disposable } from '@theia/core';
import { Anchor } from '@theia/core/lib/browser';
import { injectable } from '@theia/core/shared/inversify';
export interface EasilyMappedObject {
[key: string]: string | number;
}
@injectable()
export class MemoryHoverRendererService implements Disposable {
protected readonly container: HTMLDivElement;
protected isShown = false;
protected currentRenderContainer: HTMLElement;
constructor() {
this.container = document.createElement('div');
this.container.classList.add('t-mv-hover', 'hidden');
document.body.appendChild(this.container);
}
render(container: HTMLElement, anchor: Anchor, properties?: EasilyMappedObject): void {
this.clearAll();
if (!this.isShown) {
document.addEventListener('mousemove', this.closeIfHoverOff);
this.currentRenderContainer = container;
}
if (properties) {
for (const [key, value] of Object.entries(properties)) {
const label = key.toLowerCase().replace(/[\W]/g, '-');
const keySpan = document.createElement('span');
keySpan.classList.add('t-mv-hover-key', label);
keySpan.textContent = `${key}:`;
const valueSpan = document.createElement('span');
valueSpan.classList.add('t-mv-hover-value', label);
// stringify as decimal number by default.
valueSpan.textContent = value.toString(10);
this.container.appendChild(keySpan);
this.container.appendChild(valueSpan);
}
}
if (this.container.children.length) {
this.show(anchor);
this.isShown = true;
} else {
this.hide();
}
}
hide(): void {
if (this.isShown) {
document.removeEventListener('mousemove', this.closeIfHoverOff);
this.container.classList.add('hidden');
this.isShown = false;
}
}
show({ x, y }: Anchor): void {
this.container.classList.remove('hidden');
this.container.style.top = `${y}px`;
this.container.style.left = `${x}px`;
setTimeout(() => this.checkNotOffScreen());
}
protected checkNotOffScreen(): void {
const left = parseInt((this.container.style.left ?? '').replace('px', ''));
const width = this.container.clientWidth;
const overflow = left + width - document.body.clientWidth;
if (overflow > 0) {
const safeLeft = Math.round(left - overflow);
this.container.style.left = `${safeLeft}px`;
}
}
protected clearAll(): void {
let toRemove = this.container.lastChild;
while (toRemove) {
this.container.removeChild(toRemove);
toRemove = this.container.lastChild;
}
}
protected closeIfHoverOff = (e: MouseEvent): void => {
const { target } = e;
if (!(target instanceof HTMLElement)) {
return;
}
if (!this.currentRenderContainer.contains(target) && !this.container.contains(target)) {
this.hide();
}
};
dispose(): void {
this.container.remove();
}
}

View File

@@ -0,0 +1,58 @@
/********************************************************************************
* Copyright (C) 2021 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
********************************************************************************/
/*
* Utility for storing and sorting an array of most recently visited memory locations
*/
interface RecentsOptions {
maxValues?: number;
}
export class Recents {
protected maxValues: number;
protected _values: string[] = [];
get values(): string[] {
return this._values;
}
constructor(initialValues?: string[], opts?: RecentsOptions) {
this.maxValues = opts?.maxValues ?? 10;
if (initialValues) {
if (initialValues.length <= this.maxValues) {
this._values = initialValues;
return;
}
console.error('Initial values length is greater than allowed length, resetting to empty array');
}
this._values = [];
}
add(locationString: string): void {
const indexOf = this.has(locationString);
if (indexOf > -1) {
this._values.splice(indexOf, 1);
} else {
if (this._values.length === this.maxValues) {
this._values.shift();
}
}
this._values.push(locationString);
}
has(locationString: string): number {
return this._values.indexOf(locationString);
}
}

View File

@@ -0,0 +1,193 @@
/********************************************************************************
* Copyright (C) 2021 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 { Key, KeyCode } from '@theia/core/lib/browser';
import * as React from '@theia/core/shared/react';
import { Interfaces } from './memory-widget-utils';
export interface MWLabelProps { id: string; label: string; disabled?: boolean; classNames?: string[] }
export const MWLabel: React.FC<MWLabelProps> = ({ id, label, disabled, classNames }) => {
const additionalClassNames = classNames ? classNames.join(' ') : '';
return <label htmlFor={id} className={`t-mv-label theia-header no-select ${additionalClassNames}${disabled ? ' disabled' : ''}`}>{label}</label>;
};
export interface InputProps<T extends HTMLElement = HTMLElement> {
id: string;
label: string;
defaultValue?: string;
value?: string;
onChange?: React.EventHandler<React.ChangeEvent>;
onKeyDown?: React.EventHandler<React.KeyboardEvent<HTMLInputElement>>;
passRef?: React.ClassAttributes<T>['ref'];
title?: string;
disabled?: boolean;
placeholder?: string;
}
export const MWInput: React.FC<InputProps<HTMLInputElement>> = ({ id, label, passRef, defaultValue, onChange, title, onKeyDown, disabled }) => (
<>
<MWLabel id={id} label={label} disabled={disabled} />
<input
tabIndex={0}
type='text'
ref={passRef}
id={id}
className='theia-input t-mv-input'
defaultValue={defaultValue}
onChange={onChange}
onKeyDown={onKeyDown}
title={title}
spellCheck={false}
disabled={disabled}
/>
</>
);
export interface LabelAndSelectProps extends InputProps<HTMLSelectElement> {
options: string[];
}
export const MWSelect: React.FC<LabelAndSelectProps> = ({ id, label, options, passRef, onChange, title, value, disabled }) => (
<>
<MWLabel id={id} label={label} disabled={disabled} />
<select
tabIndex={0}
ref={passRef}
id={id}
className='theia-select t-mv-select'
value={value}
onChange={onChange}
title={title}
disabled={disabled}
>
{options.map(option => <option value={option} key={option}>{option}</option>)}
</select>
</>
);
export interface LabelAndSelectWithNameProps extends InputProps<HTMLSelectElement> {
options: Array<[string, string]>;
}
export const MWSelectWithName: React.FC<LabelAndSelectWithNameProps> = ({ id, label, options, passRef, onChange, title, value, disabled }) => (
<>
<MWLabel id={id} label={label} disabled={disabled} />
<select
tabIndex={0}
ref={passRef}
id={id}
className='theia-select'
value={value}
onChange={onChange}
title={title}
disabled={disabled}
>
{options.map(option => <option value={option[0]} key={option[0]}>{option[1]}</option>)}
</select>
</>
);
export interface InputWithSelectProps<T extends HTMLElement> extends InputProps<T> {
options: string[];
onSelectChange?(e: React.ChangeEvent): void;
onInputChange?(e: React.ChangeEvent<HTMLInputElement>): void;
}
export const MWInputWithSelect: React.FC<InputWithSelectProps<HTMLInputElement>> = (
{ id, label, passRef, onKeyDown, title, options, onSelectChange, defaultValue, disabled, placeholder },
) => (
<>
<MWLabel id={id} label={label} disabled={disabled} />
<div className='mw-input-select'>
<input
tabIndex={0}
type='text'
ref={passRef}
id={id}
className='theia-input t-mv-input'
defaultValue={defaultValue}
onKeyDown={onKeyDown}
title={title}
spellCheck={false}
disabled={disabled}
placeholder={placeholder}
/>
<select
className='theia-select t-mv-select'
onChange={onSelectChange}
disabled={disabled || (options.length === 0)}
>
{options.reverse().map(option => <option key={`'mw-input-select'-${id}-${option}`} value={option}>{option}</option>)}
</select>
</div>
</>
);
export interface MoreMemoryProps {
options: number[];
direction: 'above' | 'below';
handler(opts: Interfaces.MoreMemoryOptions): void;
}
export const MWMoreMemorySelect: React.FC<MoreMemoryProps> = ({ options, handler, direction }) => {
const [numBytes, setNumBytes] = React.useState<number>(options[0]);
const containerRef = React.createRef<HTMLDivElement>();
const onSelectChange = (e: React.ChangeEvent<HTMLSelectElement>): void => {
e.stopPropagation();
const { value } = e.currentTarget;
setNumBytes(parseInt(value));
};
const loadMoreMemory = (e: React.MouseEvent | React.KeyboardEvent): void => {
containerRef.current?.blur();
const doHandle = !('key' in e) || KeyCode.createKeyCode(e.nativeEvent).key?.keyCode === Key.ENTER.keyCode;
if (doHandle) {
handler({
numBytes,
direction,
});
}
};
return (
<div
className='mw-more-memory-select'
tabIndex={0}
role='button'
onClick={loadMoreMemory}
onKeyDown={loadMoreMemory}
ref={containerRef}
>
<div className='mw-more-memory-select-top no-select'>
Load
<select
className='theia-select'
onChange={onSelectChange}
tabIndex={0}
>
{options.map(option => (
<option
key={`mw-more-memory-select-${option}`}
value={option}
>
{option}
</option>))}
</select>
{`more bytes ${direction}`}
</div>
</div>
);
};

View File

@@ -0,0 +1,179 @@
/********************************************************************************
* Copyright (C) 2021 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 { Disposable, DisposableCollection, Emitter, MessageService } from '@theia/core';
import { ApplicationShell, OpenViewArguments, WidgetManager } from '@theia/core/lib/browser';
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { MemoryDiffTableWidget, MemoryDiffWidget } from '../diff-widget/memory-diff-table-widget';
import { MemoryWidget } from '../memory-widget/memory-widget';
import { RegisterWidget } from '../register-widget/register-widget-types';
import { MemoryDiffWidgetData, MemoryWidgetOptions } from './memory-widget-utils';
import { nls } from '@theia/core/lib/common/nls';
import { EditableMemoryWidget } from '../editable-widget/memory-editable-table-widget';
@injectable()
export class MemoryWidgetManager implements Disposable {
protected createdWidgetCount = 0;
protected widgetDisplayId = 0;
protected readonly toDispose = new DisposableCollection();
@inject(WidgetManager) protected readonly widgetManager: WidgetManager;
@inject(ApplicationShell) protected readonly shell: ApplicationShell;
@inject(MessageService) protected readonly messageService: MessageService;
protected readonly onNewWidgetCreated = new Emitter<MemoryWidget>();
readonly onDidCreateNewWidget = this.onNewWidgetCreated.event;
protected readonly onSelectedWidgetChanged = new Emitter<MemoryWidget | undefined>();
readonly onDidChangeSelectedWidget = this.onSelectedWidgetChanged.event;
protected readonly onChangedEmitter = new Emitter<void>();
readonly onChanged = this.onChangedEmitter.event;
protected readonly _availableWidgets = new Map<string, MemoryWidget>();
protected _focusedWidget: MemoryWidget | undefined;
protected _canCompare = false;
get availableWidgets(): MemoryWidget[] {
return Array.from(this._availableWidgets.values());
}
get canCompare(): boolean {
return this._canCompare;
}
@postConstruct()
protected init(): void {
this.toDispose.pushAll([
this.shell.onDidChangeActiveWidget(({ newValue }) => {
if (newValue instanceof MemoryWidget) {
this._focusedWidget = newValue;
}
}),
this.widgetManager.onDidCreateWidget(e => {
const { widget } = e;
if (widget instanceof MemoryWidget) {
this._availableWidgets.set(widget.id, widget);
this.toDispose.push(widget.onDidDispose(() => {
this._availableWidgets.delete(widget.id);
if (widget === this._focusedWidget) {
this.focusedWidget = undefined;
}
this.onChangedEmitter.fire();
}));
}
}),
this.onChanged(() => this.setCanCompare()),
this.onNewWidgetCreated,
this.onChangedEmitter,
this.onSelectedWidgetChanged,
]);
}
get focusedWidget(): MemoryWidget | undefined {
return this._focusedWidget ?? this._availableWidgets.values().next().value;
}
set focusedWidget(title: MemoryWidget | undefined) {
this._focusedWidget = title;
this.onSelectedWidgetChanged.fire(title);
}
protected setCanCompare(): void {
this._canCompare = this.availableWidgets.filter(widget => !RegisterWidget.is(widget) && !MemoryDiffWidget.is(widget)).length > 1;
}
async createNewMemoryWidget<T extends MemoryWidget>(kind: 'register' | 'memory' | 'writable' | string = 'memory'): Promise<T> {
this.widgetDisplayId = this._availableWidgets.size !== 0 ? this.widgetDisplayId + 1 : 1;
const widget = await this.getWidgetOfKind<T>(kind);
this._availableWidgets.set(widget.id, widget);
widget.title.changed.connect(() => this.onChangedEmitter.fire());
widget.activate();
this.fireNewWidget(widget);
return widget;
}
protected getWidgetOfKind<T extends MemoryWidget>(kind: string): Promise<T> {
const widgetId = this.getWidgetIdForKind(kind);
const options = this.getWidgetOptionsForId(widgetId);
return this.widgetManager.getOrCreateWidget<T>(widgetId, options);
}
protected getWidgetIdForKind(kind: string): string {
switch (kind) {
case 'register':
case RegisterWidget.ID:
return RegisterWidget.ID;
case 'writable':
case EditableMemoryWidget.ID:
return EditableMemoryWidget.ID;
default:
return MemoryWidget.ID;
}
}
protected getWidgetOptionsForId(widgetId: string): MemoryWidgetOptions {
return { identifier: this.createdWidgetCount += 1, displayId: this.widgetDisplayId };
}
dispose(): void {
this.toDispose.dispose();
}
protected fireNewWidget(widget: MemoryWidget): void {
this.onNewWidgetCreated.fire(widget);
this.onChangedEmitter.fire();
}
async doDiff(options: Omit<MemoryDiffWidgetData, 'dynamic' | 'identifier'>): Promise<MemoryDiffWidget | undefined> {
if (options.beforeBytes.length === 0) {
// eslint-disable-next-line max-len
const beforeBytesMessage = nls.localize('theia/memory-inspector/utils/bytesMessage', 'You must load memory in both widgets you would like to compare. {0} has no memory loaded.', options.titles[0]);
this.messageService.warn(beforeBytesMessage);
return undefined;
} else if (options.afterBytes.length === 0) {
// eslint-disable-next-line max-len
const afterBytesMessage = nls.localize('theia/memory-inspector/utils/afterBytes', 'You must load memory in both widgets you would like to compare. {0} has no memory loaded.', options.titles[1]);
this.messageService.warn(afterBytesMessage);
return undefined;
}
const fullOptions: MemoryDiffWidgetData = { ...options, dynamic: false, identifier: options.titles.join('-') };
const existingWidget = this._availableWidgets.get(MemoryWidget.getIdentifier(fullOptions.identifier.toString())) as MemoryDiffWidget;
if (existingWidget && existingWidget.tableWidget instanceof MemoryDiffTableWidget) {
existingWidget.tableWidget.updateDiffData(options);
}
const widget = existingWidget ?? await this.widgetManager
.getOrCreateWidget<MemoryDiffWidget>(
MemoryDiffWidget.ID,
{ ...options, dynamic: false, identifier: options.titles.join('-') },
);
const tabBar = this.shell.getTabBarFor(widget);
if (!tabBar) {
// The widget is not attached yet, so add it to the shell
const widgetArgs: OpenViewArguments = {
area: 'main',
};
await this.shell.addWidget(widget, widgetArgs);
}
await this.shell.activateWidget(widget.id);
return widget;
}
}

View File

@@ -0,0 +1,132 @@
/********************************************************************************
* Copyright (C) 2021 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 Long from 'long';
import { VariableRange, VariableDecoration } from './memory-widget-variable-utils';
export namespace Constants {
export const DEBOUNCE_TIME = 200;
export const ERROR_TIMEOUT = 5000;
}
export namespace Utils {
export const validateNumericalInputs = (e: React.ChangeEvent<HTMLInputElement>, allowNegative = true): void => {
const toReplace = allowNegative ? /[^\d-]/g : /[^\d]/g;
e.target.value = e.target.value.replace(toReplace, '');
};
export const isPrintableAsAscii = (byte: number): boolean => byte >= 32 && byte < (128 - 1);
}
export namespace Interfaces {
export interface MemoryReadResult {
bytes: LabeledUint8Array;
address: Long;
}
export interface WidgetMemoryState extends MemoryReadResult {
variables: VariableRange[];
}
export interface MemoryOptions {
address: string | number;
offset: number;
length: number;
byteSize: number;
bytesPerGroup: number;
groupsPerRow: number;
endianness: Endianness;
doDisplaySettings: boolean;
doUpdateAutomatically: boolean;
columnsDisplayed: ColumnsDisplayed;
recentLocationsArray: string[];
isFrozen: boolean;
}
export interface MoreMemoryOptions {
numBytes: number;
direction: 'above' | 'below';
}
export enum Endianness {
Little = 'Little Endian',
Big = 'Big Endian'
}
export interface LabeledUint8Array extends Uint8Array {
label?: string;
}
export interface StylableNodeAttributes {
className?: string;
style?: React.CSSProperties;
variable?: VariableDecoration;
title?: string;
isHighlighted?: boolean;
}
export interface FullNodeAttributes extends StylableNodeAttributes {
content: string;
}
export interface BitDecorator {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(...args: any[]): Partial<FullNodeAttributes>;
}
export interface RowDecorator {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(...args: any[]): Partial<StylableNodeAttributes>;
}
export interface ByteFromChunkData {
address: Long;
/**
* A single eight-bit byte
*/
value: string;
}
export interface Column {
label: string;
doRender: boolean;
}
export interface ColumnIDs {
label: string;
id: string;
}
export interface ColumnsDisplayed {
[id: string]: Column;
}
}
export const MemoryWidgetOptions = Symbol('MemoryWidgetOptions');
export interface MemoryWidgetOptions {
identifier: string | number;
displayId?: string | number;
dynamic?: boolean;
}
export const MemoryDiffWidgetData = Symbol('MemoryDiffWidgetData');
export interface MemoryDiffWidgetData extends MemoryWidgetOptions {
beforeAddress: Long;
beforeBytes: Interfaces.LabeledUint8Array;
beforeVariables: VariableRange[];
afterAddress: Long;
afterBytes: Interfaces.LabeledUint8Array;
afterVariables: VariableRange[];
dynamic: false;
titles: [string, string];
}
export const RegisterWidgetOptions = Symbol('RegisterWidgetData');
export type RegisterWidgetOptions = MemoryWidgetOptions;

View File

@@ -0,0 +1,170 @@
/********************************************************************************
* Copyright (C) 2021 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 { DebugScope, DebugVariable } from '@theia/debug/lib/browser/console/debug-console-items';
import { DebugSession } from '@theia/debug/lib/browser/debug-session';
import Long from 'long';
export interface VariableRange {
name: string;
address: Long;
pastTheEndAddress: Long;
type?: string;
value?: string;
}
export interface VariableDecoration {
name: string;
color: string;
firstAppearance?: boolean;
}
export interface RegisterReadResult {
threadId: string | undefined;
registers: DebugVariable[];
}
export class VariableFinder {
protected readonly HIGH_CONTRAST_COLORS = [
'var(--theia-contrastActiveBorder)',
'var(--theia-contrastBorder)',
];
protected readonly NON_HC_COLORS = [
'var(--theia-terminal-ansiBlue)',
'var(--theia-terminal-ansiGreen)',
'var(--theia-terminal-ansiRed)',
'var(--theia-terminal-ansiYellow)',
'var(--theia-terminal-ansiMagenta)',
];
protected readonly variables: VariableRange[];
protected currentIndex = -1;
protected currentVariable: VariableRange | undefined = undefined;
protected handledVariables = new Map<string, Long>();
protected workingColors: string[];
protected lastCall = Long.MAX_UNSIGNED_VALUE;
constructor(variables: VariableRange[], highContrast = false) {
this.variables = variables.sort((a, b) => a.address.lessThan(b.address) ? -1 : 1);
this.workingColors = highContrast ? this.HIGH_CONTRAST_COLORS : this.NON_HC_COLORS;
}
/**
* @param address the address of interest.
*
* This function should be called with a sequence of addresses in increasing order
*/
getVariableForAddress(address: Long): VariableDecoration | undefined {
if (address.lessThan(this.lastCall)) {
this.initialize(address);
}
this.lastCall = address;
if (this.currentVariable && address.greaterThanOrEqual(this.currentVariable.pastTheEndAddress)) {
this.currentIndex += 1;
this.currentVariable = this.variables[this.currentIndex];
}
if (!this.currentVariable) {
return undefined;
}
const { name } = this.currentVariable;
// const color = `hsl(${HSL_BASIS * this.currentIndex / this.variables.length}, 60%, 60%)`;
const color = this.workingColors[this.currentIndex % this.workingColors.length];
const decoration: VariableDecoration = {
name,
color,
firstAppearance: this.handledVariables.get(name) === address || !this.handledVariables.has(name),
};
if (address.greaterThanOrEqual(this.currentVariable.address) && address.lessThan(this.currentVariable.pastTheEndAddress)) {
this.handledVariables.set(name, address);
return decoration;
}
return undefined;
}
protected initialize(address: Long): void {
this.handledVariables.clear();
const firstCandidateIndex = this.variables.findIndex(variable => address.lessThan(variable.pastTheEndAddress));
if (firstCandidateIndex === -1) {
this.currentIndex = this.variables.length;
} else {
this.currentVariable = this.variables[firstCandidateIndex];
this.currentIndex = firstCandidateIndex;
}
}
searchForVariable(addressOrName: Long | string): VariableRange | undefined {
if (typeof addressOrName === 'string') {
return this.variables.find(variable => variable.name === addressOrName);
}
let upperLimit = this.variables.length - 1;
let lowerLimit = 0;
while (upperLimit >= lowerLimit) {
const target = Math.floor((lowerLimit + upperLimit) / 2);
const candidate = this.variables[target];
if (addressOrName >= candidate.address && addressOrName < candidate.pastTheEndAddress) {
return candidate;
}
if (addressOrName < candidate.address) {
upperLimit = target - 1;
}
if (addressOrName >= candidate.pastTheEndAddress) {
lowerLimit = target + 1;
}
}
return undefined;
}
}
/**
* Get the Registers of the currently selected frame.
*/
export async function getRegisters(session: DebugSession | undefined): Promise<DebugVariable[]> {
if (session === undefined) {
console.warn('No active debug session.');
return [];
}
const frame = session.currentFrame;
if (frame === undefined) {
throw new Error('No active stack frame.');
}
const registers: DebugVariable[] = [];
const scopes = await frame.getScopes();
const regScope = scopes.find(x => x.render() === 'Registers');
if (regScope !== undefined) {
const handleRegisterScope = async (scope: DebugVariable | DebugScope) => {
const variables = await scope.getElements();
for (const v of variables) {
if (v instanceof DebugVariable) {
try {
BigInt(v.value); // Make sure the value looks like a numerical value
registers.push(v);
} catch {
handleRegisterScope(v);
}
}
}
};
handleRegisterScope(regScope);
} else {
throw new Error('No Register scope in active stack frame.');
}
return registers;
}

View File

@@ -0,0 +1,61 @@
/********************************************************************************
* Copyright (C) 2021 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
********************************************************************************/
.multi-select-bar {
display: flex;
flex-flow: row nowrap;
user-select: none;
box-sizing: border-box;
}
.multi-select-checkbox-wrapper {
display: flex;
position: relative;
flex: auto;
text-align: center;
}
.multi-select-label {
height: 100%;
flex: auto;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid;
padding: 0 var(--theia-ui-padding);
background-color: var(--theia-editor-background);
border-color: var(--theia-dropdown-border);
box-sizing: border-box;
}
.multi-select-checkbox-wrapper input:checked ~ .multi-select-label {
background-color: var(--theia-input-background);
border-color: var(--theia-sideBar-foreground);
text-decoration: underline;
font-weight: bolder;
}
.multi-select-checkbox {
appearance: none;
-webkit-appearance: none;
position: absolute;
left: 0;
top: 0;
margin: 0;
height: 100%;
width: 100%;
cursor: pointer;
}

View File

@@ -0,0 +1,75 @@
/********************************************************************************
* Copyright (C) 2021 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 * as React from '@theia/core/shared/react';
import { MWLabel, MWLabelProps } from './memory-widget-components';
export interface SingleSelectItemProps {
id: string;
label: string;
defaultChecked?: boolean;
}
interface MultiSelectBarProps {
items: SingleSelectItemProps[];
id?: string;
onSelectionChanged: (labelSelected: string, newSelectionState: boolean) => unknown;
}
export const MultiSelectBar: React.FC<MultiSelectBarProps> = ({ items, onSelectionChanged, id }) => {
const changeHandler: React.ChangeEventHandler<HTMLInputElement> = React.useCallback(e => {
onSelectionChanged(e.target.id, e.target.checked);
}, [onSelectionChanged]);
return (
<div className='multi-select-bar' id={id}>
{items.map(({ label, id: itemId, defaultChecked }) => (<LabeledCheckbox
label={label}
onChange={changeHandler}
defaultChecked={!!defaultChecked}
id={itemId}
key={`${label}-${id}-checkbox`}
/>))}
</div>
);
};
interface LabeledCheckboxProps {
label: string;
id: string;
onChange: React.ChangeEventHandler;
defaultChecked: boolean;
}
const LabeledCheckbox: React.FC<LabeledCheckboxProps> = ({ defaultChecked, label, onChange, id }) => (
<div className='multi-select-checkbox-wrapper'>
<input
tabIndex={0}
type='checkbox'
id={id}
className='multi-select-checkbox'
defaultChecked={defaultChecked}
onChange={onChange}
/>
<MWLabel id={id} label={label} classNames={['multi-select-label']} />
</div>
);
export const MWMultiSelect: React.FC<MWLabelProps & MultiSelectBarProps> = ({ id, label, disabled, items, onSelectionChanged }) => (
<>
<MWLabel id={id} label={label} disabled={disabled} />
<MultiSelectBar id={id} items={items} onSelectionChanged={onSelectionChanged} />
</>
);

View File

@@ -0,0 +1,51 @@
/********************************************************************************
* Copyright (C) 2019 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 { DockPanelRendererFactory } from '@theia/core/lib/browser';
import { TheiaDockPanel } from '@theia/core/lib/browser/shell/theia-dock-panel';
import { interfaces } from '@theia/core/shared/inversify';
export class MemoryDockPanel extends TheiaDockPanel {
override toggleMaximized(): void { /* don't */ }
}
export namespace MemoryDockPanel {
export const ID = 'memory-dock-panel-widget';
const DOCK_PANEL_ID = 'theia-main-content-panel';
const THEIA_TABBAR_CLASSES = ['theia-app-centers', 'theia-app-main'];
const MEMORY_INSPECTOR_TABBAR_CLASS = 'memory-dock-tabbar';
const DOCK_PANEL_CLASS = 'memory-dock-panel';
const createDockPanel = (factory: DockPanelRendererFactory): MemoryDockPanel => {
const renderer = factory();
renderer.tabBarClasses.push(...THEIA_TABBAR_CLASSES, MEMORY_INSPECTOR_TABBAR_CLASS);
const dockPanel = new MemoryDockPanel({
mode: 'multiple-document',
renderer,
spacing: 0,
});
dockPanel.addClass(DOCK_PANEL_CLASS);
dockPanel.id = DOCK_PANEL_ID;
return dockPanel;
};
export const createWidget = (parent: interfaces.Container): MemoryDockPanel => {
const dockFactory = parent.get<DockPanelRendererFactory>(DockPanelRendererFactory);
const dockPanel = createDockPanel(dockFactory);
return dockPanel;
};
}

View File

@@ -0,0 +1,38 @@
/********************************************************************************
* Copyright (C) 2021 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 { ReactWidget } from '@theia/core/lib/browser';
import { injectable, postConstruct } from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
@injectable()
export class MemoryDockpanelPlaceholder extends ReactWidget {
static ID = 'memory-dockpanel-placeholder';
@postConstruct()
init(): void {
this.id = MemoryDockpanelPlaceholder.ID;
this.addClass(MemoryDockpanelPlaceholder.ID);
this.update();
}
render(): React.ReactNode {
return (
<div className='t-mv-memory-fetch-error'>
Click the <i className='memory-view-icon toolbar' /> icon to add a new memory view or the <i className='register-view-icon toolbar' /> icon to add a register view.
</div>
);
}
}

View File

@@ -0,0 +1,161 @@
/********************************************************************************
* Copyright (C) 2021 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 { Disposable, DisposableCollection, Emitter, nls } from '@theia/core';
import { ApplicationShell, Message, Panel, Widget, WidgetManager } from '@theia/core/lib/browser';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { MemoryDiffSelectWidget } from '../diff-widget/memory-diff-select-widget';
import { MemoryWidget } from '../memory-widget/memory-widget';
import { MemoryWidgetManager } from '../utils/memory-widget-manager';
import { MemoryDockPanel } from './memory-dock-panel';
import { MemoryDockpanelPlaceholder } from './memory-dockpanel-placeholder-widget';
@injectable()
export class MemoryLayoutWidget extends Panel implements Disposable, ApplicationShell.TrackableWidgetProvider {
static readonly ID = 'memory-layout-widget';
static readonly LABEL = nls.localize('theia/memory-inspector/memoryInspector', 'Memory Inspector');
// Necessary to inherit theia's tabbar styling
static readonly DOCK_PANEL_ID = 'theia-main-content-panel';
static readonly THEIA_TABBAR_CLASSES = ['theia-app-centers', 'theia-app-main'];
static readonly MEMORY_INSPECTOR_TABBAR_CLASS = 'memory-dock-tabbar';
static readonly DOCK_PANEL_CLASS = 'memory-dock-panel';
@inject(WidgetManager) protected readonly widgetManager: WidgetManager;
@inject(MemoryWidgetManager) protected readonly memoryWidgetManager: MemoryWidgetManager;
@inject(MemoryDiffSelectWidget) protected readonly diffSelectWidget: MemoryDiffSelectWidget;
@inject(MemoryDockpanelPlaceholder) protected readonly placeholderWidget: MemoryDockpanelPlaceholder;
@inject(ApplicationShell) protected readonly shell: ApplicationShell;
protected readonly onDidChangeTrackableWidgetsEmitter = new Emitter<Widget[]>();
readonly onDidChangeTrackableWidgets = this.onDidChangeTrackableWidgetsEmitter.event;
protected readonly toDispose = new DisposableCollection();
protected dockPanel: MemoryDockPanel;
protected hasGeneratedWidgetAutomatically = false;
@postConstruct()
protected init(): void {
this.doInit();
}
protected async doInit(): Promise<void> {
this.id = MemoryLayoutWidget.ID;
this.addClass(MemoryLayoutWidget.ID);
this.title.label = MemoryLayoutWidget.LABEL;
this.title.caption = MemoryLayoutWidget.LABEL;
this.title.closable = true;
this.title.iconClass = 'memory-view-icon';
this.dockPanel = await this.widgetManager.getOrCreateWidget<MemoryDockPanel>(MemoryDockPanel.ID);
this.addWidget(this.dockPanel);
this.addWidget(this.diffSelectWidget);
this.addWidget(this.placeholderWidget);
this.toDispose.push(this.memoryWidgetManager.onDidCreateNewWidget(widget => {
this.dockPanel.addWidget(widget);
this.dockPanel.activateWidget(widget);
this.onDidChangeTrackableWidgetsEmitter.fire([widget]);
}));
this.toDispose.push(this.memoryWidgetManager.onChanged(() => {
if (!this.memoryWidgetManager.canCompare) {
this.diffSelectWidget.hide();
}
}));
this.dockPanel.widgetRemoved.connect(this.handleWidgetRemoved.bind(this), this);
this.dockPanel.widgetAdded.connect(this.handleWidgetsChanged.bind(this), this);
this.toDispose.push(this.onDidChangeTrackableWidgetsEmitter);
this.diffSelectWidget.hide();
this.update();
}
toggleComparisonVisibility(): void {
if (this.diffSelectWidget.isHidden) {
this.diffSelectWidget.show();
} else {
this.diffSelectWidget.hide();
}
this.update();
}
override dispose(): void {
this.toDispose.dispose();
super.dispose();
}
protected dockPanelHoldsWidgets(): boolean {
const iter = this.dockPanel.tabBars();
let tabBar = iter.next();
while (!tabBar.done) {
if (tabBar.value.titles.length) {
return true;
}
tabBar = iter.next();
}
return false;
}
protected handleWidgetsChanged(): void {
if (this.dockPanelHoldsWidgets()) {
this.placeholderWidget.hide();
} else {
this.placeholderWidget.show();
}
}
protected handleWidgetRemoved(_sender: Widget, widgetRemoved: Widget): void {
if (widgetRemoved instanceof MemoryWidget) { // Sometimes it's the tabbar.
this.handleWidgetsChanged();
this.shell.activateWidget(this.id);
}
}
protected async createAndFocusWidget(): Promise<void> {
const widget = await this.memoryWidgetManager.createNewMemoryWidget();
widget.activate();
}
protected override async onAfterShow(msg: Message): Promise<void> {
if (!this.hasGeneratedWidgetAutomatically && !this.dockPanelHoldsWidgets()) {
await this.createAndFocusWidget();
this.hasGeneratedWidgetAutomatically = true;
}
super.onAfterShow(msg);
}
getTrackableWidgets(): Widget[] {
const childIterator = this.dockPanel.children();
return Array.from(childIterator);
}
activateWidget(id: string): Widget | undefined {
const widget = this.getTrackableWidgets().find(candidateWidget => candidateWidget.id === id);
if (widget) {
this.dockPanel.activateWidget(widget);
}
return widget;
}
override onActivateRequest(msg: Message): void {
const displayedWidget = this.dockPanel.currentTabBar?.currentTitle?.owner;
if (displayedWidget) {
displayedWidget.activate();
} else {
// Only happens if you remove all widgets, then close the view.
this.node.tabIndex = -1;
this.node.focus();
}
super.onActivateRequest(msg);
}
}

View File

@@ -0,0 +1,28 @@
/********************************************************************************
* Copyright (C) 2019 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 Long from 'long';
/**
* Parse `hexStr` as an hexadecimal string (with or without the leading 0x)
* and return the value as a Long.
*/
export function hexStrToUnsignedLong(hexStr: string): Long {
if (hexStr.trim().length === 0) {
return new Long(0, 0, true);
}
return Long.fromString(hexStr, true, 16);
}

View File

@@ -0,0 +1,52 @@
/********************************************************************************
* Copyright (C) 2019 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 { hexStrToUnsignedLong } from './util';
import { expect } from 'chai';
import Long from 'long';
describe('utils', function (): void {
it('#hexStrToUnsignedLong', function (): void {
let val = hexStrToUnsignedLong('');
expect(val).eql(new Long(0, 0, true));
val = hexStrToUnsignedLong('0x');
expect(val).eql(new Long(0, 0, true));
val = hexStrToUnsignedLong('0x0');
expect(val).eql(new Long(0, 0, true));
val = hexStrToUnsignedLong('0x1');
expect(val).eql(new Long(0x1, 0, true));
val = hexStrToUnsignedLong('0x12345678abcd');
expect(val).eql(new Long(0x5678abcd, 0x1234, true));
val = hexStrToUnsignedLong('0x12345678abcd1234');
expect(val).eql(new Long(0xabcd1234, 0x12345678, true));
});
it('should handle -1 correctly', () => {
const val = hexStrToUnsignedLong('-0x1');
expect(val).eql(Long.fromInt(-1, true));
});
it('should handle long decimal numbers (up to 2^64-1)', () => {
const input = '18446744073709551615';
const val = Long.fromString(input, true, 10);
expect(val.toString(10)).eql(input);
});
});

View File

@@ -0,0 +1,20 @@
{
"extends": "../../configs/base.tsconfig",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib",
"esModuleInterop": true
},
"include": [
"src"
],
"references": [
{
"path": "../core"
},
{
"path": "../debug"
}
]
}