diff --git a/media/memory-table.css b/media/memory-table.css index 788f971..124340f 100644 --- a/media/memory-table.css +++ b/media/memory-table.css @@ -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 { diff --git a/src/plugin/memory-provider.ts b/src/plugin/memory-provider.ts index 8777b6e..0f122d6 100644 --- a/src/plugin/memory-provider.ts +++ b/src/plugin/memory-provider.ts @@ -42,6 +42,7 @@ export class MemoryProvider { protected readonly sessionDebugCapabilities = new Map(); protected readonly sessionClientCapabilities = new Map(); + protected scheduledOnDidMemoryWriteEvents: { [memoryReference: string]: ((response: WriteMemoryResult) => void) | undefined } = {}; constructor(protected adapterRegistry: AdapterRegistry) { } @@ -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); @@ -147,14 +149,18 @@ export class MemoryProvider { public async writeMemory(args: DebugProtocol.WriteMemoryArguments): Promise { 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; }); } diff --git a/src/webview/columns/data-column.tsx b/src/webview/columns/data-column.tsx index 6e5185d..d3499f4 100644 --- a/src/webview/columns/data-column.tsx +++ b/src/webview/columns/data-column.tsx @@ -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 { @@ -31,32 +35,72 @@ 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 ; } +} + +export interface EditableDataColumnRowProps { + range: BigIntMemoryRange; + memory: Memory; + options: TableRenderOptions; +} + +export interface EditableDataColumnRowState { + editedRange?: BigIntMemoryRange; +} + +export class EditableDataColumnRow extends React.Component { + state: EditableDataColumnRowState = {}; + protected inputText = React.createRef(); + 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({words}); + 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({words}); } + 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 + {words} + ; + } + 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[] = []; @@ -64,12 +108,7 @@ export class DataColumn implements ColumnContribution { bytes.push(this.renderEightBits(memory, currentAddress, i)); } this.applyEndianness(bytes, options); - return {bytes}; - } - - protected applyEndianness(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 {bytes}; } protected renderEightBits(memory: Memory, currentAddress: bigint, offset: number): React.ReactNode { @@ -92,11 +131,111 @@ export class DataColumn implements ColumnContribution { content: (memory.bytes[offset] ?? 0).toString(16).padStart(2, '0') }; } + + protected applyEndianness(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 ; + } + + 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 = () => { + this.submitChanges(); + }; + + protected onKeyDown: React.KeyboardEventHandler = event => { + switch (event.key) { + case 'Escape': { + this.disableEdit(); + break; + } + case 'Enter': { + this.submitChanges(); + } + } + event.stopPropagation(); + }; + + protected setGroupEdit: React.MouseEventHandler = 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 { + 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` + }; } /**