Skip to content

Commit

Permalink
Allow to write memories within the memory-table (eclipse-cdt-cloud#108)
Browse files Browse the repository at this point in the history
* Allow to write memories within the memory-table

Co-authored-by: colin-grant-work <colin.grant@ericsson.com>
  • Loading branch information
haydar-metin and colin-grant-work committed Mar 22, 2024
1 parent 4af1bfd commit d1c825c
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 26 deletions.
4 changes: 4 additions & 0 deletions media/memory-table.css
Expand Up @@ -27,6 +27,10 @@
white-space: nowrap;
}

.memory-inspector-table .column-data .byte-group.editable:hover {
border-bottom: 1px dotted var(--vscode-editorHoverWidget-border);
}

/* == MoreMemorySelect == */

.bytes-select {
Expand Down
20 changes: 13 additions & 7 deletions src/plugin/memory-provider.ts
Expand Up @@ -42,6 +42,7 @@ export class MemoryProvider {

protected readonly sessionDebugCapabilities = new Map<string, DebugProtocol.Capabilities | undefined>();
protected readonly sessionClientCapabilities = new Map<string, DebugProtocol.InitializeRequestArguments | undefined>();
protected scheduledOnDidMemoryWriteEvents: { [memoryReference: string]: ((response: WriteMemoryResult) => void) | undefined } = {};

constructor(protected adapterRegistry: AdapterRegistry) {
}
Expand Down Expand Up @@ -70,6 +71,7 @@ export class MemoryProvider {
} else if (isDebugEvent('stopped', message)) {
this._onDidStopDebug.fire(session);
} else if (isDebugEvent('memory', message)) {
delete this.scheduledOnDidMemoryWriteEvents[message.body.memoryReference];
this._onDidWriteMemory.fire(message.body);
}
contributedTracker?.onDidSendMessage?.(message);
Expand Down Expand Up @@ -147,14 +149,18 @@ export class MemoryProvider {

public async writeMemory(args: DebugProtocol.WriteMemoryArguments): Promise<WriteMemoryResult> {
const session = this.assertCapability('supportsWriteMemoryRequest', 'write memory');
// Schedule a emit in case we don't retrieve a memory event
this.scheduledOnDidMemoryWriteEvents[args.memoryReference] = response => {
// We only send out a custom event if we don't expect the client to handle the memory event
// since our client is VS Code we can assume that they will always support this but better to be safe
const offset = response?.offset ? (args.offset ?? 0) + response.offset : args.offset;
const count = response?.bytesWritten ?? stringToBytesMemory(args.data).length;
this._onDidWriteMemory.fire({ memoryReference: args.memoryReference, offset, count });
};

return sendRequest(session, 'writeMemory', args).then(response => {
if (!this.hasClientCapabilitiy(session, 'supportsMemoryEvent')) {
// we only send out a custom event if we don't expect the client to handle the memory event
// since our client is VS Code we can assume that they will always support this but better to be safe
const offset = response?.offset ? (args.offset ?? 0) + response.offset : args.offset;
const count = response?.bytesWritten ?? stringToBytesMemory(args.data).length;
this._onDidWriteMemory.fire({ memoryReference: args.memoryReference, offset, count });
}
// The memory event is handled before we got here, if the scheduled event still exists, we need to handle it
this.scheduledOnDidMemoryWriteEvents[args.memoryReference]?.(response);
return response;
});
}
Expand Down
177 changes: 158 additions & 19 deletions src/webview/columns/data-column.tsx
Expand Up @@ -14,13 +14,17 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { InputText } from 'primereact/inputtext';
import * as React from 'react';
import { HOST_EXTENSION } from 'vscode-messenger-common';
import { Memory } from '../../common/memory';
import { BigIntMemoryRange, Endianness, toOffset } from '../../common/memory-range';
import { BigIntMemoryRange, Endianness, isWithin, toHexStringWithRadixMarker, toOffset } from '../../common/memory-range';
import { writeMemoryType } from '../../common/messaging';
import type { MemorySizeOptions } from '../components/memory-table';
import { decorationService } from '../decorations/decoration-service';
import { FullNodeAttributes } from '../utils/view-types';
import { Disposable, FullNodeAttributes } from '../utils/view-types';
import { characterWidthInContainer, elementInnerWidth } from '../utils/window';
import { messenger } from '../view-messenger';
import { ColumnContribution, TableRenderOptions } from './column-contribution-service';

export class DataColumn implements ColumnContribution {
Expand All @@ -31,45 +35,80 @@ export class DataColumn implements ColumnContribution {
readonly label = 'Data';
readonly priority = 1;

protected byteGroupStyle: React.CSSProperties = {
marginRight: `${DataColumn.Styles.MARGIN_RIGHT_PX}px`
};

render(range: BigIntMemoryRange, memory: Memory, options: TableRenderOptions): React.ReactNode {
return this.renderGroups(range, memory, options);
return <EditableDataColumnRow range={range} memory={memory} options={options} />;
}
}

export interface EditableDataColumnRowProps {
range: BigIntMemoryRange;
memory: Memory;
options: TableRenderOptions;
}

export interface EditableDataColumnRowState {
editedRange?: BigIntMemoryRange;
}

export class EditableDataColumnRow extends React.Component<EditableDataColumnRowProps, EditableDataColumnRowState> {
state: EditableDataColumnRowState = {};
protected inputText = React.createRef<HTMLInputElement>();
protected toDisposeOnUnmount?: Disposable;

protected renderGroups(range: BigIntMemoryRange, memory: Memory, options: TableRenderOptions): React.ReactNode {
render(): React.ReactNode {
return this.renderGroups();
}

protected renderGroups(): React.ReactNode {
const { range, options, memory } = this.props;
const groups = [];
let words: React.ReactNode[] = [];
for (let address = range.startAddress; address < range.endAddress; address++) {
let address = range.startAddress;
let groupStartAddress = address;
while (address < range.endAddress) {
words.push(this.renderWord(memory, options, address));
const next = address + 1n;
if (words.length % options.wordsPerGroup === 0) {
this.applyEndianness(words, options);
const isLast = address + 1n >= range.endAddress;
const style: React.CSSProperties | undefined = isLast ? undefined : this.byteGroupStyle;
groups.push(<span className='byte-group hoverable' data-column='data' style={style} key={address.toString(16)}>{words}</span>);
const isLast = next >= range.endAddress;
const style: React.CSSProperties | undefined = isLast ? undefined : DataColumn.Styles.byteGroupStyle;
groups.push(this.renderGroup(words, groupStartAddress, next, style));
groupStartAddress = next;
words = [];
}
address = next;
}
if (words.length) { groups.push(<span className='byte-group hoverable' data-column='data' key={(range.endAddress - BigInt(words.length)).toString(16)}>{words}</span>); }
if (words.length) { groups.push(this.renderGroup(words, groupStartAddress, range.endAddress)); }
return groups;
}

protected renderGroup(words: React.ReactNode, startAddress: bigint, endAddress: bigint, style?: React.CSSProperties): React.ReactNode {
return <span
className='byte-group hoverable'
data-column='data'
data-range={`${startAddress}-${endAddress}`}
style={style}
key={startAddress.toString(16)}
onDoubleClick={this.setGroupEdit}
>
{words}
</span>;
}

protected renderWord(memory: Memory, options: TableRenderOptions, currentAddress: bigint): React.ReactNode {
if (currentAddress === this.state.editedRange?.startAddress) {
return this.renderEditingGroup(this.state.editedRange);
} else if (this.state.editedRange && isWithin(currentAddress, this.state.editedRange)) {
return;
}
const initialOffset = toOffset(memory.address, currentAddress, options.bytesPerWord * 8);
const finalOffset = initialOffset + options.bytesPerWord;
const bytes: React.ReactNode[] = [];
for (let i = initialOffset; i < finalOffset; i++) {
bytes.push(this.renderEightBits(memory, currentAddress, i));
}
this.applyEndianness(bytes, options);
return <span className='single-word' key={currentAddress.toString(16)}>{bytes}</span>;
}

protected applyEndianness<T>(group: T[], options: TableRenderOptions): T[] {
// Assume data from the DAP comes in Big Endian so we need to revert the order if we use Little Endian
return options.endianness === Endianness.Big ? group : group.reverse();
return <span className='single-word' data-address={currentAddress.toString()} key={currentAddress.toString(16)}>{bytes}</span>;
}

protected renderEightBits(memory: Memory, currentAddress: bigint, offset: number): React.ReactNode {
Expand All @@ -92,11 +131,111 @@ export class DataColumn implements ColumnContribution {
content: (memory.bytes[offset] ?? 0).toString(16).padStart(2, '0')
};
}

protected applyEndianness<T>(group: T[], options: TableRenderOptions): T[] {
// Assume data from the DAP comes in Big Endian so we need to revert the order if we use Little Endian
return options.endianness === Endianness.Big ? group : group.reverse();
}

protected renderEditingGroup(editedRange: BigIntMemoryRange): React.ReactNode {
const isLast = editedRange.endAddress === this.props.range.endAddress;
const defaultValue = this.createEditingGroupDefaultValue(editedRange);

const style: React.CSSProperties = {
...decorationService.getDecoration(editedRange.startAddress)?.style,
width: `calc(${defaultValue.length}ch + 10px)`,
padding: '0 4px',
marginRight: isLast ? undefined : DataColumn.Styles.byteGroupStyle.marginRight,
minHeight: 'unset',
border: '1px solid var(--vscode-inputOption-activeBorder)',
background: 'unset'
};

return <InputText key={editedRange.startAddress.toString(16)}
ref={this.inputText}
maxLength={defaultValue.length}
defaultValue={defaultValue}
onBlur={this.onBlur}
onKeyDown={this.onKeyDown}
autoFocus
style={style}
/>;
}

protected createEditingGroupDefaultValue(editedRange: BigIntMemoryRange): string {
const bitsPerWord = this.props.options.bytesPerWord * 8;
const startOffset = toOffset(this.props.memory.address, editedRange.startAddress, bitsPerWord);
const numBytes = toOffset(editedRange.startAddress, editedRange.endAddress, bitsPerWord);

const area = Array.from(this.props.memory.bytes.slice(startOffset, startOffset + numBytes));
this.applyEndianness(area, this.props.options);

return area.map(byte => byte.toString(16).padStart(2, '0')).join('');
}

protected onBlur: React.FocusEventHandler<HTMLInputElement> = () => {
this.submitChanges();
};

protected onKeyDown: React.KeyboardEventHandler<HTMLInputElement> = event => {
switch (event.key) {
case 'Escape': {
this.disableEdit();
break;
}
case 'Enter': {
this.submitChanges();
}
}
event.stopPropagation();
};

protected setGroupEdit: React.MouseEventHandler<HTMLSpanElement> = event => {
event.stopPropagation();
const range = event.currentTarget.dataset.range;
if (!range) { return; }
const [startAddress, endAddress] = range.split('-').map(BigInt);
this.setState({ editedRange: { startAddress, endAddress } });
};

protected disableEdit(): void {
this.setState({ editedRange: undefined });
}

protected async submitChanges(): Promise<void> {
if (!this.inputText.current || !this.state.editedRange) { return; }

const originalData = this.createEditingGroupDefaultValue(this.state.editedRange);
if (originalData !== this.inputText.current.value) {
const newMemoryValue = this.processData(this.inputText.current.value, this.state.editedRange);
const converted = Buffer.from(newMemoryValue, 'hex').toString('base64');
await messenger.sendRequest(writeMemoryType, HOST_EXTENSION, {
memoryReference: toHexStringWithRadixMarker(this.state.editedRange.startAddress),
data: converted
}).catch(() => { });
}

this.disableEdit();
}

protected processData(data: string, editedRange: BigIntMemoryRange): string {
const characters = toOffset(editedRange.startAddress, editedRange.endAddress, this.props.options.bytesPerWord * 8) * 2;
// Revert Endianness
if (this.props.options.endianness === Endianness.Little) {
const chunks = data.padStart(characters, '0').match(/.{2}/g) || [];
return chunks.reverse().join('');
}

return data.padStart(characters, '0');
}
}

export namespace DataColumn {
export namespace Styles {
export const MARGIN_RIGHT_PX = 2;
export const byteGroupStyle: React.CSSProperties = {
marginRight: `${DataColumn.Styles.MARGIN_RIGHT_PX}px`
};
}

/**
Expand Down

0 comments on commit d1c825c

Please sign in to comment.