-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve naming and allow renaming of memory view instances #69
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,6 +30,7 @@ import { | |
getVariables, | ||
setMemoryViewSettingsType, | ||
resetMemoryViewSettingsType, | ||
setTitleType, | ||
} from '../common/messaging'; | ||
import { MemoryProvider } from './memory-provider'; | ||
import { outputChannelLogger } from './logger'; | ||
|
@@ -62,6 +63,8 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { | |
protected messenger: Messenger; | ||
protected refreshOnStop: RefreshEnum; | ||
|
||
protected panelIndices: number = 1; | ||
|
||
public constructor(protected extensionUri: vscode.Uri, protected memoryProvider: MemoryProvider) { | ||
this.messenger = new Messenger(); | ||
|
||
|
@@ -125,7 +128,7 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { | |
}; | ||
|
||
if (!panel) { | ||
panel = vscode.window.createWebviewPanel(MemoryWebview.ViewType, 'Memory Inspector', vscode.ViewColumn.Active, options); | ||
panel = vscode.window.createWebviewPanel(MemoryWebview.ViewType, `Memory ${this.panelIndices++}`, vscode.ViewColumn.Active, options); | ||
} else { | ||
panel.webview.options = options; | ||
} | ||
|
@@ -137,7 +140,8 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { | |
// and executes code based on the message that is received | ||
const webviewParticipant = this.setWebviewMessageListener(panel, initialMemory); | ||
|
||
// initialize with configuration | ||
// initialize web view content | ||
this.setTitle(webviewParticipant, panel.title); | ||
this.setInitialSettings(webviewParticipant); | ||
} | ||
|
||
|
@@ -191,6 +195,7 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { | |
this.messenger.onRequest(writeMemoryType, request => this.writeMemory(request), { sender: participant }), | ||
this.messenger.onRequest(getVariables, request => this.getVariables(request), { sender: participant }), | ||
this.messenger.onNotification(resetMemoryViewSettingsType, () => this.setInitialSettings(participant), { sender: participant }), | ||
this.messenger.onNotification(setTitleType, title => { panel.title = title; }, { sender: participant }), | ||
|
||
this.memoryProvider.onDidStopDebug(() => { | ||
if (this.refreshOnStop === RefreshEnum.on) { | ||
|
@@ -208,6 +213,10 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { | |
return participant; | ||
} | ||
|
||
protected setTitle(webviewParticipant: WebviewIdMessageParticipant, title: string): void { | ||
this.messenger.sendNotification(setTitleType, webviewParticipant, title); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The machinery to set title from plugin -> webview feels a little heavy: at the moment, it's only used on panel initialization (and may not be working correctly, since in my test, the panel showed the default On the other hand I don't actually have a suggestion here - see my next comment. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, it indeed is a bit heavy-weight. But I'm not aware of a mechanism to store state across webview instances other than in the extension "main". However, as you've suggested, I've now put updating the title into the |
||
|
||
protected setInitialSettings(webviewParticipant: WebviewIdMessageParticipant): void { | ||
this.messenger.sendNotification(setMemoryViewSettingsType, webviewParticipant, this.getMemoryViewSettings()); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,6 +23,7 @@ import { OptionsWidget } from './options-widget'; | |
|
||
interface MemoryWidgetProps extends MemoryDisplayConfiguration { | ||
memory?: Memory; | ||
initialTitle: string; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This name is a little confusing - it's really just It might be possible to avoid prop drilling here and set up the logic for handling the title in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've renamed Regarding the prop drilling... I didn't find a great approach to avoid that though. I personally have a slight preference for keeping the handling of One way of avoiding prop drilling might be to pull up the header into its own component and import it in the |
||
decorations: Decoration[]; | ||
columns: ColumnStatus[]; | ||
memoryReference: string; | ||
|
@@ -34,6 +35,7 @@ interface MemoryWidgetProps extends MemoryDisplayConfiguration { | |
toggleColumn(id: string, active: boolean): void; | ||
updateMemoryDisplayConfiguration: (memoryArguments: Partial<MemoryDisplayConfiguration>) => void; | ||
resetMemoryDisplayConfiguration: () => void; | ||
updateTitle: (title: string) => void; | ||
fetchMemory(partialOptions?: Partial<DebugProtocol.ReadMemoryArguments>): Promise<void> | ||
} | ||
|
||
|
@@ -64,9 +66,11 @@ export class MemoryWidget extends React.Component<MemoryWidgetProps, MemoryWidge | |
wordSize={this.state.wordSize} | ||
wordsPerGroup={this.props.wordsPerGroup} | ||
groupsPerRow={this.props.groupsPerRow} | ||
initialTitle={this.props.initialTitle} | ||
updateMemoryArguments={this.props.updateMemoryArguments} | ||
updateRenderOptions={this.props.updateMemoryDisplayConfiguration} | ||
resetRenderOptions={this.props.resetMemoryDisplayConfiguration} | ||
updateTitle={this.props.updateTitle} | ||
refreshMemory={this.props.refreshMemory} | ||
toggleColumn={this.props.toggleColumn} | ||
/> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,7 +21,7 @@ import { Dropdown, DropdownChangeEvent } from 'primereact/dropdown'; | |
import { InputText } from 'primereact/inputtext'; | ||
import { OverlayPanel } from 'primereact/overlaypanel'; | ||
import { classNames } from 'primereact/utils'; | ||
import React, { KeyboardEvent, MouseEventHandler } from 'react'; | ||
import React, { ChangeEventHandler, FocusEventHandler, KeyboardEvent, KeyboardEventHandler, MouseEventHandler } from 'react'; | ||
import { TableRenderOptions } from '../columns/column-contribution-service'; | ||
import { | ||
SerializedTableRenderOptions, | ||
|
@@ -31,15 +31,23 @@ import { MultiSelectWithLabel } from './multi-select'; | |
export interface OptionsWidgetProps | ||
extends Omit<TableRenderOptions, 'scrollingBehavior'>, | ||
Required<DebugProtocol.ReadMemoryArguments> { | ||
initialTitle: string; | ||
updateRenderOptions: (options: Partial<SerializedTableRenderOptions>) => void; | ||
resetRenderOptions: () => void; | ||
updateTitle: (title: string) => void; | ||
updateMemoryArguments: ( | ||
memoryArguments: Partial<DebugProtocol.ReadMemoryArguments> | ||
) => void; | ||
refreshMemory: () => void; | ||
toggleColumn(id: string, isVisible: boolean): void; | ||
} | ||
|
||
interface OptionsWidgetState { | ||
title: string; | ||
previousTitle?: string; | ||
isTitleEditing: boolean; | ||
} | ||
|
||
const enum InputId { | ||
Address = 'address', | ||
Offset = 'offset', | ||
|
@@ -57,9 +65,10 @@ interface OptionsForm { | |
const allowedBytesPerGroup = [1, 2, 4, 8, 16]; | ||
const allowedGroupsPerRow = [1, 2, 4, 8, 16, 32]; | ||
|
||
export class OptionsWidget extends React.Component<OptionsWidgetProps, {}> { | ||
export class OptionsWidget extends React.Component<OptionsWidgetProps, OptionsWidgetState> { | ||
protected formConfig: FormikConfig<OptionsForm>; | ||
protected extendedOptions = React.createRef<OverlayPanel>(); | ||
protected labelEditInput = React.createRef<HTMLInputElement>(); | ||
|
||
protected get optionsFormValues(): OptionsForm { | ||
return { | ||
|
@@ -80,6 +89,10 @@ export class OptionsWidget extends React.Component<OptionsWidgetProps, {}> { | |
this.props.refreshMemory(); | ||
}, | ||
}; | ||
this.state = { | ||
title: this.props.initialTitle, | ||
isTitleEditing: false, | ||
}; | ||
} | ||
|
||
protected validate = (values: OptionsForm) => { | ||
|
@@ -117,11 +130,48 @@ export class OptionsWidget extends React.Component<OptionsWidgetProps, {}> { | |
return errors; | ||
}; | ||
|
||
componentDidUpdate(prevProps: Readonly<OptionsWidgetProps>, prevState: Readonly<OptionsWidgetState>): void { | ||
if (this.props.initialTitle !== prevProps.initialTitle) { | ||
this.setState({ title: this.props.initialTitle }); | ||
} | ||
if (!prevState.isTitleEditing && this.state.isTitleEditing) { | ||
this.labelEditInput.current?.focus(); | ||
this.labelEditInput.current?.select(); | ||
} | ||
} | ||
|
||
override render(): React.ReactNode { | ||
this.formConfig.initialValues = this.optionsFormValues; | ||
const isLabelEditing = this.state.isTitleEditing; | ||
|
||
return ( | ||
<div className='memory-options-widget px-4'> | ||
<div className='title-container'> | ||
<InputText | ||
ref={this.labelEditInput} | ||
type='text' | ||
value={this.state.title} | ||
onChange={this.handleTitleEdit} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be possible to simplify the state handling here (and avoid storing Then There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Excellent suggestion! Done! 👍 |
||
onKeyDown={this.handleTitleEditingKeyDown} | ||
onBlur={this.confirmEditedTitle} | ||
style={{ display: isLabelEditing ? 'block' : 'none' }} | ||
/> | ||
{!isLabelEditing && ( | ||
<h1 onDoubleClick={this.enableTitleEditing}>{this.state.title}</h1> | ||
)} | ||
{!isLabelEditing && ( | ||
<Button | ||
type='button' | ||
className='edit-label-toggle' | ||
icon='codicon codicon-edit' | ||
onClick={this.enableTitleEditing} | ||
title='Edit view title' | ||
aria-label='Edit view title' | ||
rounded | ||
aria-haspopup | ||
/> | ||
)} | ||
</div> | ||
<div className='core-options py-2'> | ||
<Formik {...this.formConfig}> | ||
{formik => ( | ||
|
@@ -327,4 +377,41 @@ export class OptionsWidget extends React.Component<OptionsWidgetProps, {}> { | |
|
||
protected handleResetAdvancedOptions: MouseEventHandler<HTMLButtonElement> | undefined = () => this.props.resetRenderOptions(); | ||
|
||
protected enableTitleEditing = () => this.doEnableTitleEditing(); | ||
protected doEnableTitleEditing(): void { | ||
this.setState({ isTitleEditing: true, previousTitle: this.state.title }); | ||
} | ||
|
||
protected disableTitleEditing = () => this.doDisableTitleEditing(); | ||
protected doDisableTitleEditing(): void { | ||
this.setState({ isTitleEditing: false }); | ||
} | ||
|
||
protected handleTitleEdit: ChangeEventHandler<HTMLInputElement> | undefined = () => this.doHandleTitleEdit(); | ||
protected doHandleTitleEdit(): void { | ||
if (this.labelEditInput.current) { | ||
this.setState({ title: this.labelEditInput.current?.value }); | ||
} | ||
} | ||
|
||
protected handleTitleEditingKeyDown: KeyboardEventHandler<HTMLInputElement> | undefined = event => this.doHandleTitleEditingKeyDown(event); | ||
protected doHandleTitleEditingKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void { | ||
if (event.key === 'Enter' && this.labelEditInput.current) { | ||
this.doConfirmEditedTitle(); | ||
} else if (event.key === 'Escape') { | ||
if (this.state.previousTitle) { | ||
this.setState({ title: this.state.previousTitle }); | ||
} | ||
this.disableTitleEditing(); | ||
} | ||
} | ||
|
||
protected confirmEditedTitle: FocusEventHandler<HTMLInputElement> | undefined = () => this.doConfirmEditedTitle(); | ||
protected doConfirmEditedTitle(): void { | ||
if (this.state.isTitleEditing && this.state.title) { | ||
this.props.updateTitle(this.state.title); | ||
this.disableTitleEditing(); | ||
} | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that both of these lines should be moved into the handler of the
readyType
notification handler insetWebviewMessageListener
. Otherwise, there's no guarantee that the code that sets up the listeners to handle these requests will have run inside the webview - I suspect that that's why I'm seeing justMemory
in the webview I created rather than the intended title. This also suggests that we should probably keep all of the message handling centralized in theApp
class, even if it's a bit inconvenient: that at least provides the guarantee that the messaging is synchronized.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent suggestion! Done! 👍