@theia/memory-inspector 1.53.0-next.55 → 1.53.0-next.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +71 -71
  2. package/package.json +4 -4
  3. package/src/browser/diff-widget/memory-diff-options-widget.tsx +152 -152
  4. package/src/browser/diff-widget/memory-diff-select-widget.tsx +163 -163
  5. package/src/browser/diff-widget/memory-diff-table-widget.tsx +366 -366
  6. package/src/browser/diff-widget/memory-diff-widget-types.ts +45 -45
  7. package/src/browser/editable-widget/memory-editable-table-widget.tsx +359 -359
  8. package/src/browser/memory-inspector-frontend-contribution.ts +301 -301
  9. package/src/browser/memory-inspector-frontend-module.ts +118 -118
  10. package/src/browser/memory-provider/cdt-gdb-memory-provider.ts +132 -132
  11. package/src/browser/memory-provider/memory-provider-service.ts +86 -86
  12. package/src/browser/memory-provider/memory-provider.spec.ts +23 -23
  13. package/src/browser/memory-provider/memory-provider.ts +119 -119
  14. package/src/browser/memory-widget/memory-options-widget.tsx +738 -738
  15. package/src/browser/memory-widget/memory-table-widget.tsx +625 -625
  16. package/src/browser/memory-widget/memory-widget.ts +114 -114
  17. package/src/browser/register-widget/register-filter-service.ts +76 -76
  18. package/src/browser/register-widget/register-options-widget.tsx +393 -393
  19. package/src/browser/register-widget/register-table-widget.tsx +276 -276
  20. package/src/browser/register-widget/register-widget-types.ts +45 -45
  21. package/src/browser/register-widget/register-widget.css +34 -34
  22. package/src/browser/style/index.css +746 -746
  23. package/src/browser/style/memory-lock.svg +21 -21
  24. package/src/browser/style/memory-view.svg +20 -20
  25. package/src/browser/style/register-lock.svg +29 -29
  26. package/src/browser/style/register-view.svg +28 -28
  27. package/src/browser/utils/memory-commands.ts +76 -76
  28. package/src/browser/utils/memory-hover-renderer.ts +113 -113
  29. package/src/browser/utils/memory-recents.ts +58 -58
  30. package/src/browser/utils/memory-widget-components.tsx +193 -193
  31. package/src/browser/utils/memory-widget-manager.ts +179 -179
  32. package/src/browser/utils/memory-widget-utils.tsx +132 -132
  33. package/src/browser/utils/memory-widget-variable-utils.ts +170 -170
  34. package/src/browser/utils/multi-select-bar.css +61 -61
  35. package/src/browser/utils/multi-select-bar.tsx +75 -75
  36. package/src/browser/wrapper-widgets/memory-dock-panel.ts +51 -51
  37. package/src/browser/wrapper-widgets/memory-dockpanel-placeholder-widget.tsx +38 -38
  38. package/src/browser/wrapper-widgets/memory-layout-widget.tsx +167 -167
  39. package/src/common/util.ts +28 -28
  40. package/src/common/utils.spec.ts +52 -52
@@ -1,738 +1,738 @@
1
- /********************************************************************************
2
- * Copyright (C) 2021 Ericsson and others.
3
- *
4
- * This program and the accompanying materials are made available under the
5
- * terms of the Eclipse Public License v. 2.0 which is available at
6
- * http://www.eclipse.org/legal/epl-2.0.
7
- *
8
- * This Source Code may also be made available under the following Secondary
9
- * Licenses when the conditions for such availability set forth in the Eclipse
10
- * Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
- * with the GNU Classpath Exception which is available at
12
- * https://www.gnu.org/software/classpath/license.html.
13
- *
14
- * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
- ********************************************************************************/
16
-
17
- import { deepFreeze, Disposable, DisposableCollection, Emitter, nls } from '@theia/core';
18
- import { Key, KeyCode, Message, ReactWidget, StatefulWidget } from '@theia/core/lib/browser';
19
- import { Deferred } from '@theia/core/lib/common/promise-util';
20
- import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
21
- import * as React from '@theia/core/shared/react';
22
- import { DebugSession, DebugState } from '@theia/debug/lib/browser/debug-session';
23
- import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
24
- import * as Long from 'long';
25
- import { MemoryProviderService } from '../memory-provider/memory-provider-service';
26
- import { Recents } from '../utils/memory-recents';
27
- import { MWInput, MWInputWithSelect, MWSelect } from '../utils/memory-widget-components';
28
- import { Constants, Interfaces, MemoryWidgetOptions, Utils } from '../utils/memory-widget-utils';
29
- import { VariableRange } from '../utils/memory-widget-variable-utils';
30
- import { MWMultiSelect, SingleSelectItemProps } from '../utils/multi-select-bar';
31
- import debounce = require('@theia/core/shared/lodash.debounce');
32
-
33
- export const EMPTY_MEMORY: Interfaces.MemoryReadResult = deepFreeze({
34
- bytes: new Uint8Array(),
35
- address: new Long(0, 0, true),
36
- });
37
-
38
- export const LOCATION_FIELD_ID = 't-mv-location';
39
- export const LENGTH_FIELD_ID = 't-mv-length';
40
- export const LOCATION_OFFSET_FIELD_ID = 't-mv-location-offset';
41
- export const BYTES_PER_ROW_FIELD_ID = 't-mv-bytesrow';
42
- export const BYTES_PER_GROUP_FIELD_ID = 't-mv-bytesgroup';
43
- export const ENDIAN_SELECT_ID = 't-mv-endiannesss';
44
- export const ASCII_TOGGLE_ID = 't-mv-ascii-toggle';
45
- export const AUTO_UPDATE_TOGGLE_ID = 't-mv-auto-update-toggle';
46
-
47
- @injectable()
48
- export class MemoryOptionsWidget extends ReactWidget implements StatefulWidget {
49
- static ID = 'memory-view-options-widget';
50
- static LABEL = nls.localize('theia/memory-inspector/memoryTitle', 'Memory');
51
- iconClass = 'memory-view-icon';
52
- lockIconClass = 'memory-lock-icon';
53
-
54
- static WIDGET_H2_CLASS = 'memory-widget-header';
55
- static WIDGET_HEADER_INPUT_CLASS = 'memory-widget-header-input';
56
-
57
- protected additionalColumnSelectLabel = nls.localize('theia/memory-inspector/extraColumn', 'Extra Column');
58
-
59
- protected sessionListeners = new DisposableCollection();
60
-
61
- protected readonly onOptionsChangedEmitter = new Emitter<string | undefined>();
62
- readonly onOptionsChanged = this.onOptionsChangedEmitter.event;
63
- protected readonly onMemoryChangedEmitter = new Emitter<Interfaces.MemoryReadResult>();
64
- readonly onMemoryChanged = this.onMemoryChangedEmitter.event;
65
- protected pinnedMemoryReadResult: Deferred<Interfaces.MemoryReadResult | false> | undefined;
66
-
67
- protected memoryReadResult: Interfaces.MemoryReadResult = EMPTY_MEMORY;
68
- protected columnsDisplayed: Interfaces.ColumnsDisplayed = {
69
- address: {
70
- label: nls.localizeByDefault('Address'),
71
- doRender: true
72
- },
73
- data: {
74
- label: nls.localize('theia/memory-inspector/data', 'Data'),
75
- doRender: true
76
- },
77
- variables: {
78
- label: nls.localizeByDefault('Variables'),
79
- doRender: true
80
- },
81
- ascii: {
82
- label: nls.localize('theia/memory-inspector/ascii', 'ASCII'),
83
- doRender: false
84
- },
85
- };
86
-
87
- protected byteSize = 8;
88
-
89
- protected bytesPerGroup = 1;
90
- protected groupsPerRow = 4;
91
- protected variables: VariableRange[] = [];
92
- protected endianness: Interfaces.Endianness = Interfaces.Endianness.Little;
93
-
94
- protected memoryReadError = nls.localize('theia/memory-inspector/memory/readError/noContents', 'No memory contents currently available.');
95
-
96
- protected address: string | number = 0;
97
- protected offset = 0;
98
- protected readLength = 256;
99
- protected doDisplaySettings = false;
100
- protected doUpdateAutomatically = true;
101
- protected showMemoryError = false;
102
- protected errorTimeout: NodeJS.Timeout | undefined = undefined;
103
- protected addressField: HTMLInputElement | undefined;
104
- protected offsetField: HTMLInputElement | undefined;
105
- protected readLengthField: HTMLInputElement | undefined;
106
- protected headerInputField: HTMLInputElement | undefined;
107
- protected recentLocations = new Recents();
108
- protected showTitleEditIcon = false;
109
- protected isTitleEditable = false;
110
-
111
- @inject(MemoryProviderService) protected readonly memoryProvider: MemoryProviderService;
112
- @inject(DebugSessionManager) protected readonly sessionManager: DebugSessionManager;
113
- @inject(MemoryWidgetOptions) protected readonly memoryWidgetOptions: MemoryWidgetOptions;
114
-
115
- get memory(): Interfaces.WidgetMemoryState {
116
- return {
117
- ...this.memoryReadResult,
118
- variables: this.variables,
119
- };
120
- }
121
-
122
- get options(): Interfaces.MemoryOptions {
123
- return this.storeState();
124
- }
125
-
126
- @postConstruct()
127
- protected init(): void {
128
- this.addClass(MemoryOptionsWidget.ID);
129
-
130
- this.title.label = nls.localize('theia/memory-inspector/memory', 'Memory ({0})', this.memoryWidgetOptions.displayId);
131
- this.title.caption = nls.localize('theia/memory-inspector/memory', 'Memory ({0})', this.memoryWidgetOptions.displayId);
132
- this.title.iconClass = this.iconClass;
133
- this.title.closable = true;
134
-
135
- if (this.memoryWidgetOptions.dynamic !== false) {
136
- this.toDispose.push(this.sessionManager.onDidChangeActiveDebugSession(({ current }) => {
137
- this.setUpListeners(current);
138
- }));
139
-
140
- this.toDispose.push(this.sessionManager.onDidCreateDebugSession(current => {
141
- this.setUpListeners(current);
142
- }));
143
- this.setUpListeners(this.sessionManager.currentSession);
144
- }
145
- this.toDispose.push(this.onOptionsChanged(() => this.update()));
146
-
147
- this.update();
148
- }
149
-
150
- async setAddressAndGo(
151
- newAddress: string,
152
- newOffset?: number,
153
- newLength?: number,
154
- direction?: 'above' | 'below',
155
- ): Promise<Interfaces.MemoryReadResult | false | undefined> {
156
- let doUpdate = false;
157
- const originalValues = {
158
- offset: '',
159
- length: '',
160
- };
161
- if (this.addressField) {
162
- this.addressField.value = newAddress;
163
- doUpdate = true;
164
- }
165
- if (this.offsetField && newOffset !== undefined) {
166
- originalValues.offset = this.offsetField.value;
167
- this.offsetField.value = newOffset.toString();
168
- doUpdate = true;
169
- }
170
- if (this.readLengthField && newLength !== undefined) {
171
- originalValues.length = this.readLengthField.value;
172
- this.readLengthField.value = newLength.toString();
173
- doUpdate = true;
174
- }
175
- if (doUpdate && this.readLengthField && this.offsetField) {
176
- this.pinnedMemoryReadResult = new Deferred<Interfaces.MemoryReadResult>();
177
- this.updateMemoryView();
178
- const result = await this.pinnedMemoryReadResult.promise;
179
- if (result === false) {
180
- // Memory request errored
181
- this.readLengthField.value = originalValues.length;
182
- this.offsetField.value = originalValues.offset;
183
- }
184
- if (result) {
185
- // Memory request returned some memory
186
- const resultLength = result.bytes.length * 8 / this.byteSize;
187
- const lengthFieldValue = parseInt(this.readLengthField.value);
188
- if (lengthFieldValue !== resultLength) {
189
- this.memoryReadError = nls.localize('theia/memory-inspector/memory/readError/bounds', 'Memory bounds exceeded, result will be truncated.');
190
- this.doShowMemoryErrors();
191
- this.readLengthField.value = resultLength.toString();
192
- if (direction === 'above') {
193
- this.offsetField.value = `${parseInt(originalValues.offset) - (resultLength - parseInt(originalValues.length))}`;
194
- }
195
- this.update();
196
- }
197
- }
198
- }
199
- return undefined;
200
- }
201
-
202
- protected setUpListeners(session?: DebugSession): void {
203
- this.sessionListeners.dispose();
204
- this.sessionListeners = new DisposableCollection(Disposable.create(() => this.handleActiveSessionChange()));
205
- if (session) {
206
- this.sessionListeners.push(session.onDidChange(() => this.handleSessionChange()));
207
- }
208
- }
209
-
210
- protected handleActiveSessionChange(): void {
211
- const isDynamic = this.memoryWidgetOptions.dynamic !== false;
212
- if (isDynamic && this.doUpdateAutomatically) {
213
- this.memoryReadResult = EMPTY_MEMORY;
214
- this.fireDidChangeMemory();
215
- }
216
- }
217
-
218
- protected handleSessionChange(): void {
219
- const isStopped = this.sessionManager.currentSession?.state === DebugState.Stopped;
220
- const isReadyForQuery = !!this.sessionManager.currentSession?.currentFrame;
221
- const isDynamic = this.memoryWidgetOptions.dynamic !== false;
222
- if (isStopped && isReadyForQuery && isDynamic && this.doUpdateAutomatically && this.memoryReadResult !== EMPTY_MEMORY) {
223
- this.updateMemoryView();
224
- }
225
- }
226
-
227
- protected override onActivateRequest(msg: Message): void {
228
- super.onActivateRequest(msg);
229
- this.acceptFocus();
230
- }
231
-
232
- protected acceptFocus(): void {
233
- if (this.doUpdateAutomatically) {
234
- if (this.addressField) {
235
- this.addressField.focus();
236
- this.addressField.select();
237
- }
238
- } else {
239
- const settingsCog = this.node.querySelector('.toggle-settings-click-zone') as HTMLDivElement;
240
- settingsCog?.focus();
241
- }
242
- }
243
-
244
- protected handleColumnSelectionChange = (columnLabel: string, doShow: boolean): void => this.doHandleColumnSelectionChange(columnLabel, doShow);
245
-
246
- protected doHandleColumnSelectionChange(columnLabel: string, doShow: boolean): void {
247
- if (columnLabel in this.columnsDisplayed) {
248
- this.columnsDisplayed[columnLabel].doRender = doShow;
249
- this.fireDidChangeOptions(ASCII_TOGGLE_ID);
250
- }
251
- }
252
-
253
- protected toggleAutoUpdate = (e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>): void => {
254
- if (e.nativeEvent.type === 'click') {
255
- e.currentTarget.blur();
256
- }
257
- if ('key' in e && KeyCode.createKeyCode(e.nativeEvent).key?.keyCode === Key.TAB.keyCode) {
258
- return;
259
- }
260
- this.doUpdateAutomatically = !this.doUpdateAutomatically;
261
- if (this.doUpdateAutomatically) {
262
- this.title.iconClass = this.iconClass;
263
- } else {
264
- this.title.iconClass = this.lockIconClass;
265
- }
266
- this.fireDidChangeOptions();
267
- };
268
-
269
- protected onByteSizeChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
270
- this.byteSize = parseInt(event.target.value);
271
- this.fireDidChangeOptions(event.target.id);
272
- };
273
-
274
- protected override onAfterAttach(msg: Message): void {
275
- super.onAfterAttach(msg);
276
- if (this.memoryWidgetOptions.dynamic !== false) {
277
- if (this.addressField) {
278
- this.addressField.value = this.address.toString();
279
- }
280
- }
281
- }
282
-
283
- protected toggleDoShowSettings = (e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>): void => {
284
- if (!('key' in e) || KeyCode.createKeyCode(e.nativeEvent).key?.keyCode === Key.TAB.keyCode) {
285
- this.doDisplaySettings = !this.doDisplaySettings;
286
- this.update();
287
- }
288
- };
289
-
290
- protected render(): React.ReactNode {
291
- return (
292
- <div className='t-mv-container'>
293
- {this.renderInputContainer()}
294
- </div>
295
- );
296
- }
297
-
298
- protected renderInputContainer(): React.ReactNode {
299
- return (
300
- <div className='t-mv-settings-container'>
301
- <div className='t-mv-wrapper'>
302
- {this.renderToolbar()}
303
- {this.renderMemoryLocationGroup()}
304
- {this.doDisplaySettings && (
305
- <div className='t-mv-toggle-settings-wrapper'>
306
- {this.renderByteDisplayGroup()}
307
- </div>
308
- )}
309
- </div>
310
- </div>
311
- );
312
- }
313
-
314
- protected renderByteDisplayGroup(): React.ReactNode {
315
- return (
316
- <div className='t-mv-group settings-group'>
317
- <MWSelect
318
- id='byte-size-select'
319
- label={nls.localize('theia/memory-inspector/byteSize', 'Byte Size')}
320
- value={this.byteSize.toString()}
321
- onChange={this.onByteSizeChange}
322
- options={['8', '16', '32', '64']}
323
- />
324
- <MWSelect
325
- id={BYTES_PER_GROUP_FIELD_ID}
326
- label={nls.localize('theia/memory-inspector/bytesPerGroup', 'Bytes Per Group')}
327
- value={this.bytesPerGroup.toString()}
328
- onChange={this.onBytesPerGroupChange}
329
- options={['1', '2', '4', '8', '16']}
330
- />
331
- <MWSelect
332
- id={BYTES_PER_ROW_FIELD_ID}
333
- label={nls.localize('theia/memory-inspector/groupsPerRow', 'Groups Per Row')}
334
- value={this.groupsPerRow.toString()}
335
- onChange={this.onGroupsPerRowChange}
336
- options={['1', '2', '4', '8', '16', '32']}
337
- />
338
- <MWSelect
339
- id={ENDIAN_SELECT_ID}
340
- label={nls.localize('theia/memory-inspector/endianness', 'Endianness')}
341
- value={this.endianness}
342
- onChange={this.onEndiannessChange}
343
- options={[Interfaces.Endianness.Little, Interfaces.Endianness.Big]}
344
- />
345
- <MWMultiSelect
346
- id={ASCII_TOGGLE_ID}
347
- label={nls.localize('theia/memory-inspector/columns', 'Columns')}
348
- items={this.getOptionalColumns()}
349
- onSelectionChanged={this.handleColumnSelectionChange}
350
- />
351
- </div>
352
- );
353
- }
354
-
355
- protected getObligatoryColumnIds(): string[] {
356
- return ['address', 'data'];
357
- }
358
-
359
- protected getOptionalColumns(): SingleSelectItemProps[] {
360
- const obligatoryColumns = new Set(this.getObligatoryColumnIds());
361
- return Object.entries(this.columnsDisplayed)
362
- .reduce<SingleSelectItemProps[]>((accumulated, [id, { doRender, label }]) => {
363
- if (!obligatoryColumns.has(id)) {
364
- accumulated.push({ id, label, defaultChecked: doRender });
365
- }
366
- return accumulated;
367
- }, []);
368
- }
369
-
370
- protected assignLocationRef: React.LegacyRef<HTMLInputElement> = location => {
371
- this.addressField = location ?? undefined;
372
- };
373
-
374
- protected assignReadLengthRef: React.LegacyRef<HTMLInputElement> = readLength => {
375
- this.readLengthField = readLength ?? undefined;
376
- };
377
-
378
- protected assignOffsetRef: React.LegacyRef<HTMLInputElement> = offset => {
379
- this.offsetField = offset ?? undefined;
380
- };
381
-
382
- protected setAddressFromSelect = (e: React.ChangeEvent<HTMLSelectElement>): void => {
383
- if (this.addressField) {
384
- this.addressField.value = e.target.value;
385
- }
386
- };
387
-
388
- protected renderMemoryLocationGroup(): React.ReactNode {
389
- return (
390
- <>
391
- <div className='t-mv-group view-group'>
392
- <MWInputWithSelect
393
- id={LOCATION_FIELD_ID}
394
- label={nls.localizeByDefault('Address')}
395
- title={nls.localize('theia/memory-inspector/addressTooltip', 'Memory location to display, an address or expression evaluating to an address')}
396
- defaultValue={`${this.address}`}
397
- onSelectChange={this.setAddressFromSelect}
398
- passRef={this.assignLocationRef}
399
- onKeyDown={this.doRefresh}
400
- options={[...this.recentLocations.values]}
401
- disabled={!this.doUpdateAutomatically}
402
- />
403
- <MWInput
404
- id={LOCATION_OFFSET_FIELD_ID}
405
- label={nls.localize('theia/memory-inspector/offset', 'Offset')}
406
- title={nls.localize('theia/memory-inspector/offsetTooltip', 'Offset to be added to the current memory location, when navigating')}
407
- defaultValue='0'
408
- passRef={this.assignOffsetRef}
409
- onKeyDown={this.doRefresh}
410
- disabled={!this.doUpdateAutomatically}
411
- />
412
- <MWInput
413
- id={LENGTH_FIELD_ID}
414
- label={nls.localize('theia/memory-inspector/length', 'Length')}
415
- title={nls.localize('theia/memory-inspector/lengthTooltip', 'Number of bytes to fetch, in decimal or hexadecimal')}
416
- defaultValue={this.readLength.toString()}
417
- passRef={this.assignReadLengthRef}
418
- onChange={Utils.validateNumericalInputs}
419
- onKeyDown={this.doRefresh}
420
- disabled={!this.doUpdateAutomatically}
421
- />
422
- <button
423
- type='button'
424
- className='theia-button main view-group-go-button'
425
- onClick={this.doRefresh}
426
- disabled={!this.doUpdateAutomatically}
427
- title={nls.localizeByDefault('Go')}
428
- >
429
- {nls.localizeByDefault('Go')}
430
- </button>
431
- </div>
432
- <div className={`t-mv-memory-fetch-error${this.showMemoryError ? ' show' : ' hide'}`}>
433
- {this.memoryReadError}
434
- </div>
435
- </>
436
- );
437
- }
438
-
439
- protected activateHeaderInputField = (e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>): void => {
440
- if (!this.isTitleEditable) {
441
- const isMouseDown = !('key' in e);
442
- const isActivationKey = 'key' in e && (
443
- KeyCode.createKeyCode(e.nativeEvent).key?.keyCode === Key.SPACE.keyCode
444
- || KeyCode.createKeyCode(e.nativeEvent).key?.keyCode === Key.ENTER.keyCode
445
- );
446
- if (isMouseDown || isActivationKey) {
447
- if (isMouseDown) {
448
- e.currentTarget.blur();
449
- }
450
- this.isTitleEditable = true;
451
- this.update();
452
- }
453
- }
454
- };
455
-
456
- protected saveHeaderInputValue = (e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>): void => {
457
- const isMouseDown = !('key' in e);
458
- const isSaveKey = 'key' in e && e.key === 'Enter';
459
- const isCancelKey = 'key' in e && e.key === 'Escape';
460
- e.stopPropagation();
461
- if (isMouseDown || isSaveKey || isCancelKey) {
462
- this.updateHeader(isCancelKey);
463
- }
464
- };
465
-
466
- protected assignHeaderInputRef = (element: HTMLInputElement): void => {
467
- if (element) {
468
- this.headerInputField = element;
469
- element.focus();
470
- }
471
- };
472
-
473
- protected updateHeader(isCancelKey: boolean): void {
474
- if (!isCancelKey && this.headerInputField) {
475
- this.title.label = this.headerInputField.value;
476
- this.title.caption = this.headerInputField.value;
477
- }
478
-
479
- this.isTitleEditable = false;
480
- this.update();
481
- }
482
-
483
- protected renderToolbar(): React.ReactNode {
484
- return (
485
- <div className='memory-widget-toolbar'>
486
- {this.renderLockIcon()}
487
- {this.renderEditableTitleField()}
488
- {this.renderSettingsContainer()}
489
- </div>
490
- );
491
- }
492
-
493
- protected renderSettingsContainer(): React.ReactNode {
494
- return <div className='toggle-settings-container'>
495
- <div
496
- className='toggle-settings-click-zone no-select'
497
- tabIndex={0}
498
- aria-label={this.doDisplaySettings ?
499
- nls.localize('theia/memory-inspector/memory/hideSettings', 'Hide Settings Panel') :
500
- nls.localize('theia/memory-inspector/memory/showSettings', 'Show Settings Panel')}
501
- role='button'
502
- onClick={this.toggleDoShowSettings}
503
- onKeyDown={this.toggleDoShowSettings}
504
- title={this.doDisplaySettings ?
505
- nls.localize('theia/memory-inspector/memory/hideSettings', 'Hide Settings Panel') :
506
- nls.localize('theia/memory-inspector/memory/showSettings', 'Show Settings Panel')}>
507
- <i className='codicon codicon-settings-gear' />
508
- <span>{this.doDisplaySettings ?
509
- nls.localize('theia/memory-inspector/closeSettings', 'Close Settings') :
510
- nls.localizeByDefault('Settings')}
511
- </span>
512
- </div>
513
- </div>;
514
- }
515
-
516
- protected renderLockIcon(): React.ReactNode {
517
- return this.memoryWidgetOptions.dynamic !== false && (
518
- <div className='memory-widget-auto-updates-container'>
519
- <div
520
- className={`fa fa-${this.doUpdateAutomatically ? 'unlock' : 'lock'}`}
521
- id={AUTO_UPDATE_TOGGLE_ID}
522
- title={this.doUpdateAutomatically ?
523
- nls.localize('theia/memory-inspector/memory/freeze', 'Freeze Memory View') :
524
- nls.localize('theia/memory-inspector/memory/unfreeze', 'Unfreeze Memory View')}
525
- onClick={this.toggleAutoUpdate}
526
- onKeyDown={this.toggleAutoUpdate}
527
- role='button'
528
- tabIndex={0} />
529
- </div>
530
- );
531
- }
532
-
533
- protected renderEditableTitleField(): React.ReactNode {
534
- return (
535
- <div
536
- className='memory-widget-header-click-zone'
537
- tabIndex={0}
538
- onClick={this.activateHeaderInputField}
539
- onKeyDown={this.activateHeaderInputField}
540
- role='button'
541
- >
542
- {!this.isTitleEditable
543
- ? (
544
- <h2 className={`${MemoryOptionsWidget.WIDGET_H2_CLASS}${!this.doUpdateAutomatically ? ' disabled' : ''} no-select`}>
545
- {this.title.label}
546
- </h2>
547
- )
548
- : <input
549
- className='theia-input'
550
- type='text'
551
- defaultValue={this.title.label}
552
- onKeyDown={this.saveHeaderInputValue}
553
- spellCheck={false}
554
- ref={this.assignHeaderInputRef}
555
- />}
556
- {!this.isTitleEditable && (
557
- <div className={`fa fa-pencil${this.showTitleEditIcon ? ' show' : ' hide'}`} />
558
- )}
559
- {this.isTitleEditable && (
560
- <div
561
- className='fa fa-save'
562
- onClick={this.saveHeaderInputValue}
563
- onKeyDown={this.saveHeaderInputValue}
564
- role='button'
565
- tabIndex={0}
566
- title={nls.localizeByDefault('Save')}
567
- />
568
- )}
569
- </div>
570
- );
571
- }
572
-
573
- storeState(): Interfaces.MemoryOptions {
574
- return {
575
- address: this.addressField?.value ?? this.address,
576
- offset: parseInt(`${this.offsetField?.value}`) ?? this.offset,
577
- length: parseInt(`${this.readLengthField?.value}`) ?? this.readLength,
578
- byteSize: this.byteSize,
579
- bytesPerGroup: this.bytesPerGroup,
580
- groupsPerRow: this.groupsPerRow,
581
- endianness: this.endianness,
582
- doDisplaySettings: this.doDisplaySettings,
583
- columnsDisplayed: this.columnsDisplayed,
584
- recentLocationsArray: this.recentLocations.values,
585
- isFrozen: !this.doUpdateAutomatically,
586
- doUpdateAutomatically: this.doUpdateAutomatically,
587
- };
588
- }
589
-
590
- restoreState(oldState: Interfaces.MemoryOptions): void {
591
- this.address = oldState.address ?? this.address;
592
- this.offset = oldState.offset ?? this.offset;
593
- this.readLength = oldState.length ?? this.readLength;
594
- this.byteSize = oldState.byteSize ?? this.byteSize;
595
- this.bytesPerGroup = oldState.bytesPerGroup ?? this.bytesPerGroup;
596
- this.groupsPerRow = oldState.groupsPerRow ?? this.groupsPerRow;
597
- this.endianness = oldState.endianness ?? this.endianness;
598
- this.recentLocations = new Recents(oldState.recentLocationsArray) ?? this.recentLocations;
599
- this.doDisplaySettings = !!oldState.doDisplaySettings;
600
- if (oldState.columnsDisplayed) {
601
- this.columnsDisplayed = oldState.columnsDisplayed;
602
- }
603
- }
604
-
605
- protected doShowMemoryErrors = (doClearError = false): void => {
606
- if (this.errorTimeout !== undefined) {
607
- clearTimeout(this.errorTimeout);
608
- }
609
- if (doClearError) {
610
- this.showMemoryError = false;
611
- this.update();
612
- this.errorTimeout = undefined;
613
- return;
614
- }
615
- this.showMemoryError = true;
616
- this.update();
617
- this.errorTimeout = setTimeout(() => {
618
- this.showMemoryError = false;
619
- this.update();
620
- this.errorTimeout = undefined;
621
- }, Constants.ERROR_TIMEOUT);
622
- };
623
-
624
- fetchNewMemory(): void {
625
- this.updateMemoryView();
626
- }
627
-
628
- protected doRefresh = (event: React.KeyboardEvent<HTMLInputElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>): void => {
629
- if ('key' in event && event.key !== 'Enter') {
630
- return;
631
- }
632
- this.updateMemoryView();
633
- };
634
-
635
- protected updateMemoryView = debounce(this.doUpdateMemoryView.bind(this), Constants.DEBOUNCE_TIME, { trailing: true });
636
-
637
- protected async doUpdateMemoryView(): Promise<void> {
638
- if (!(this.addressField && this.readLengthField)) { return; }
639
-
640
- if (this.addressField?.value.trim().length === 0) {
641
- this.memoryReadError = nls.localize('theia/memory-inspector/memory/addressField/memoryReadError', 'Enter an address or expression in the Location field.');
642
- this.doShowMemoryErrors();
643
- return;
644
- }
645
- if (this.readLengthField.value.trim().length === 0) {
646
- this.memoryReadError = nls.localize('theia/memory-inspector/memory/readLength/memoryReadError', 'Enter a length (decimal or hexadecimal number) in the Length field.');
647
- this.doShowMemoryErrors();
648
- return;
649
- }
650
-
651
- const startAddress = this.addressField.value;
652
- const locationOffset = parseInt(`${this.offsetField?.value}`) || 0;
653
- const readLength = parseInt(this.readLengthField.value);
654
-
655
- try {
656
- this.memoryReadResult = await this.getMemory(startAddress, readLength, locationOffset);
657
- this.fireDidChangeMemory();
658
- if (this.pinnedMemoryReadResult) {
659
- this.pinnedMemoryReadResult.resolve(this.memoryReadResult);
660
- }
661
- this.doShowMemoryErrors(true);
662
- } catch (err) {
663
- this.memoryReadError = this.getUserError(err);
664
- console.error('Failed to read memory', err);
665
- this.doShowMemoryErrors();
666
- if (this.pinnedMemoryReadResult) {
667
- this.pinnedMemoryReadResult.resolve(this.memoryReadResult);
668
- }
669
- } finally {
670
- this.pinnedMemoryReadResult = undefined;
671
- this.update();
672
- }
673
- }
674
-
675
- protected getUserError(err: unknown): string {
676
- return err instanceof Error ? err.message : nls.localize('theia/memory-inspector/memory/userError', 'There was an error fetching memory.');
677
- }
678
-
679
- protected async getMemory(memoryReference: string, count: number, offset: number): Promise<Interfaces.MemoryReadResult> {
680
- const result = await this.retrieveMemory(memoryReference, count, offset);
681
- try {
682
- this.variables = await this.memoryProvider.getLocals();
683
- } catch {
684
- this.variables = [];
685
- }
686
- this.recentLocations.add(memoryReference);
687
- this.updateDefaults(memoryReference, count, offset);
688
- return result;
689
- }
690
-
691
- protected async retrieveMemory(memoryReference: string, count: number, offset: number): Promise<Interfaces.MemoryReadResult> {
692
- return this.memoryProvider.readMemory({ memoryReference, count, offset });
693
- }
694
-
695
- // TODO: This may not be necessary if we change how state is stored (currently in the text fields themselves.)
696
- protected updateDefaults(address: string, readLength: number, offset: number): void {
697
- this.address = address;
698
- this.readLength = readLength;
699
- this.offset = offset;
700
- }
701
-
702
- // Callbacks for when the various view parameters change.
703
- /**
704
- * Handle bytes per row changed event.
705
- */
706
- protected onGroupsPerRowChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
707
- const { value, id } = event.target;
708
- this.groupsPerRow = parseInt(value);
709
- this.fireDidChangeOptions(id);
710
- };
711
-
712
- /**
713
- * Handle bytes per group changed event.
714
- */
715
- protected onBytesPerGroupChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
716
- const { value, id } = event.target;
717
- this.bytesPerGroup = parseInt(value);
718
- this.fireDidChangeOptions(id);
719
- };
720
-
721
- /**
722
- * Handle endianness changed event.
723
- */
724
- protected onEndiannessChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
725
- const { value, id } = event.target;
726
- if (value !== Interfaces.Endianness.Big && value !== Interfaces.Endianness.Little) { return; }
727
- this.endianness = value;
728
- this.fireDidChangeOptions(id);
729
- };
730
-
731
- protected fireDidChangeOptions(targetId?: string): void {
732
- this.onOptionsChangedEmitter.fire(targetId);
733
- }
734
-
735
- protected fireDidChangeMemory(): void {
736
- this.onMemoryChangedEmitter.fire(this.memoryReadResult);
737
- }
738
- }
1
+ /********************************************************************************
2
+ * Copyright (C) 2021 Ericsson and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the Eclipse Public License v. 2.0 which is available at
6
+ * http://www.eclipse.org/legal/epl-2.0.
7
+ *
8
+ * This Source Code may also be made available under the following Secondary
9
+ * Licenses when the conditions for such availability set forth in the Eclipse
10
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ * with the GNU Classpath Exception which is available at
12
+ * https://www.gnu.org/software/classpath/license.html.
13
+ *
14
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ ********************************************************************************/
16
+
17
+ import { deepFreeze, Disposable, DisposableCollection, Emitter, nls } from '@theia/core';
18
+ import { Key, KeyCode, Message, ReactWidget, StatefulWidget } from '@theia/core/lib/browser';
19
+ import { Deferred } from '@theia/core/lib/common/promise-util';
20
+ import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
21
+ import * as React from '@theia/core/shared/react';
22
+ import { DebugSession, DebugState } from '@theia/debug/lib/browser/debug-session';
23
+ import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
24
+ import * as Long from 'long';
25
+ import { MemoryProviderService } from '../memory-provider/memory-provider-service';
26
+ import { Recents } from '../utils/memory-recents';
27
+ import { MWInput, MWInputWithSelect, MWSelect } from '../utils/memory-widget-components';
28
+ import { Constants, Interfaces, MemoryWidgetOptions, Utils } from '../utils/memory-widget-utils';
29
+ import { VariableRange } from '../utils/memory-widget-variable-utils';
30
+ import { MWMultiSelect, SingleSelectItemProps } from '../utils/multi-select-bar';
31
+ import debounce = require('@theia/core/shared/lodash.debounce');
32
+
33
+ export const EMPTY_MEMORY: Interfaces.MemoryReadResult = deepFreeze({
34
+ bytes: new Uint8Array(),
35
+ address: new Long(0, 0, true),
36
+ });
37
+
38
+ export const LOCATION_FIELD_ID = 't-mv-location';
39
+ export const LENGTH_FIELD_ID = 't-mv-length';
40
+ export const LOCATION_OFFSET_FIELD_ID = 't-mv-location-offset';
41
+ export const BYTES_PER_ROW_FIELD_ID = 't-mv-bytesrow';
42
+ export const BYTES_PER_GROUP_FIELD_ID = 't-mv-bytesgroup';
43
+ export const ENDIAN_SELECT_ID = 't-mv-endiannesss';
44
+ export const ASCII_TOGGLE_ID = 't-mv-ascii-toggle';
45
+ export const AUTO_UPDATE_TOGGLE_ID = 't-mv-auto-update-toggle';
46
+
47
+ @injectable()
48
+ export class MemoryOptionsWidget extends ReactWidget implements StatefulWidget {
49
+ static ID = 'memory-view-options-widget';
50
+ static LABEL = nls.localize('theia/memory-inspector/memoryTitle', 'Memory');
51
+ iconClass = 'memory-view-icon';
52
+ lockIconClass = 'memory-lock-icon';
53
+
54
+ static WIDGET_H2_CLASS = 'memory-widget-header';
55
+ static WIDGET_HEADER_INPUT_CLASS = 'memory-widget-header-input';
56
+
57
+ protected additionalColumnSelectLabel = nls.localize('theia/memory-inspector/extraColumn', 'Extra Column');
58
+
59
+ protected sessionListeners = new DisposableCollection();
60
+
61
+ protected readonly onOptionsChangedEmitter = new Emitter<string | undefined>();
62
+ readonly onOptionsChanged = this.onOptionsChangedEmitter.event;
63
+ protected readonly onMemoryChangedEmitter = new Emitter<Interfaces.MemoryReadResult>();
64
+ readonly onMemoryChanged = this.onMemoryChangedEmitter.event;
65
+ protected pinnedMemoryReadResult: Deferred<Interfaces.MemoryReadResult | false> | undefined;
66
+
67
+ protected memoryReadResult: Interfaces.MemoryReadResult = EMPTY_MEMORY;
68
+ protected columnsDisplayed: Interfaces.ColumnsDisplayed = {
69
+ address: {
70
+ label: nls.localizeByDefault('Address'),
71
+ doRender: true
72
+ },
73
+ data: {
74
+ label: nls.localize('theia/memory-inspector/data', 'Data'),
75
+ doRender: true
76
+ },
77
+ variables: {
78
+ label: nls.localizeByDefault('Variables'),
79
+ doRender: true
80
+ },
81
+ ascii: {
82
+ label: nls.localize('theia/memory-inspector/ascii', 'ASCII'),
83
+ doRender: false
84
+ },
85
+ };
86
+
87
+ protected byteSize = 8;
88
+
89
+ protected bytesPerGroup = 1;
90
+ protected groupsPerRow = 4;
91
+ protected variables: VariableRange[] = [];
92
+ protected endianness: Interfaces.Endianness = Interfaces.Endianness.Little;
93
+
94
+ protected memoryReadError = nls.localize('theia/memory-inspector/memory/readError/noContents', 'No memory contents currently available.');
95
+
96
+ protected address: string | number = 0;
97
+ protected offset = 0;
98
+ protected readLength = 256;
99
+ protected doDisplaySettings = false;
100
+ protected doUpdateAutomatically = true;
101
+ protected showMemoryError = false;
102
+ protected errorTimeout: NodeJS.Timeout | undefined = undefined;
103
+ protected addressField: HTMLInputElement | undefined;
104
+ protected offsetField: HTMLInputElement | undefined;
105
+ protected readLengthField: HTMLInputElement | undefined;
106
+ protected headerInputField: HTMLInputElement | undefined;
107
+ protected recentLocations = new Recents();
108
+ protected showTitleEditIcon = false;
109
+ protected isTitleEditable = false;
110
+
111
+ @inject(MemoryProviderService) protected readonly memoryProvider: MemoryProviderService;
112
+ @inject(DebugSessionManager) protected readonly sessionManager: DebugSessionManager;
113
+ @inject(MemoryWidgetOptions) protected readonly memoryWidgetOptions: MemoryWidgetOptions;
114
+
115
+ get memory(): Interfaces.WidgetMemoryState {
116
+ return {
117
+ ...this.memoryReadResult,
118
+ variables: this.variables,
119
+ };
120
+ }
121
+
122
+ get options(): Interfaces.MemoryOptions {
123
+ return this.storeState();
124
+ }
125
+
126
+ @postConstruct()
127
+ protected init(): void {
128
+ this.addClass(MemoryOptionsWidget.ID);
129
+
130
+ this.title.label = nls.localize('theia/memory-inspector/memory', 'Memory ({0})', this.memoryWidgetOptions.displayId);
131
+ this.title.caption = nls.localize('theia/memory-inspector/memory', 'Memory ({0})', this.memoryWidgetOptions.displayId);
132
+ this.title.iconClass = this.iconClass;
133
+ this.title.closable = true;
134
+
135
+ if (this.memoryWidgetOptions.dynamic !== false) {
136
+ this.toDispose.push(this.sessionManager.onDidChangeActiveDebugSession(({ current }) => {
137
+ this.setUpListeners(current);
138
+ }));
139
+
140
+ this.toDispose.push(this.sessionManager.onDidCreateDebugSession(current => {
141
+ this.setUpListeners(current);
142
+ }));
143
+ this.setUpListeners(this.sessionManager.currentSession);
144
+ }
145
+ this.toDispose.push(this.onOptionsChanged(() => this.update()));
146
+
147
+ this.update();
148
+ }
149
+
150
+ async setAddressAndGo(
151
+ newAddress: string,
152
+ newOffset?: number,
153
+ newLength?: number,
154
+ direction?: 'above' | 'below',
155
+ ): Promise<Interfaces.MemoryReadResult | false | undefined> {
156
+ let doUpdate = false;
157
+ const originalValues = {
158
+ offset: '',
159
+ length: '',
160
+ };
161
+ if (this.addressField) {
162
+ this.addressField.value = newAddress;
163
+ doUpdate = true;
164
+ }
165
+ if (this.offsetField && newOffset !== undefined) {
166
+ originalValues.offset = this.offsetField.value;
167
+ this.offsetField.value = newOffset.toString();
168
+ doUpdate = true;
169
+ }
170
+ if (this.readLengthField && newLength !== undefined) {
171
+ originalValues.length = this.readLengthField.value;
172
+ this.readLengthField.value = newLength.toString();
173
+ doUpdate = true;
174
+ }
175
+ if (doUpdate && this.readLengthField && this.offsetField) {
176
+ this.pinnedMemoryReadResult = new Deferred<Interfaces.MemoryReadResult>();
177
+ this.updateMemoryView();
178
+ const result = await this.pinnedMemoryReadResult.promise;
179
+ if (result === false) {
180
+ // Memory request errored
181
+ this.readLengthField.value = originalValues.length;
182
+ this.offsetField.value = originalValues.offset;
183
+ }
184
+ if (result) {
185
+ // Memory request returned some memory
186
+ const resultLength = result.bytes.length * 8 / this.byteSize;
187
+ const lengthFieldValue = parseInt(this.readLengthField.value);
188
+ if (lengthFieldValue !== resultLength) {
189
+ this.memoryReadError = nls.localize('theia/memory-inspector/memory/readError/bounds', 'Memory bounds exceeded, result will be truncated.');
190
+ this.doShowMemoryErrors();
191
+ this.readLengthField.value = resultLength.toString();
192
+ if (direction === 'above') {
193
+ this.offsetField.value = `${parseInt(originalValues.offset) - (resultLength - parseInt(originalValues.length))}`;
194
+ }
195
+ this.update();
196
+ }
197
+ }
198
+ }
199
+ return undefined;
200
+ }
201
+
202
+ protected setUpListeners(session?: DebugSession): void {
203
+ this.sessionListeners.dispose();
204
+ this.sessionListeners = new DisposableCollection(Disposable.create(() => this.handleActiveSessionChange()));
205
+ if (session) {
206
+ this.sessionListeners.push(session.onDidChange(() => this.handleSessionChange()));
207
+ }
208
+ }
209
+
210
+ protected handleActiveSessionChange(): void {
211
+ const isDynamic = this.memoryWidgetOptions.dynamic !== false;
212
+ if (isDynamic && this.doUpdateAutomatically) {
213
+ this.memoryReadResult = EMPTY_MEMORY;
214
+ this.fireDidChangeMemory();
215
+ }
216
+ }
217
+
218
+ protected handleSessionChange(): void {
219
+ const isStopped = this.sessionManager.currentSession?.state === DebugState.Stopped;
220
+ const isReadyForQuery = !!this.sessionManager.currentSession?.currentFrame;
221
+ const isDynamic = this.memoryWidgetOptions.dynamic !== false;
222
+ if (isStopped && isReadyForQuery && isDynamic && this.doUpdateAutomatically && this.memoryReadResult !== EMPTY_MEMORY) {
223
+ this.updateMemoryView();
224
+ }
225
+ }
226
+
227
+ protected override onActivateRequest(msg: Message): void {
228
+ super.onActivateRequest(msg);
229
+ this.acceptFocus();
230
+ }
231
+
232
+ protected acceptFocus(): void {
233
+ if (this.doUpdateAutomatically) {
234
+ if (this.addressField) {
235
+ this.addressField.focus();
236
+ this.addressField.select();
237
+ }
238
+ } else {
239
+ const settingsCog = this.node.querySelector('.toggle-settings-click-zone') as HTMLDivElement;
240
+ settingsCog?.focus();
241
+ }
242
+ }
243
+
244
+ protected handleColumnSelectionChange = (columnLabel: string, doShow: boolean): void => this.doHandleColumnSelectionChange(columnLabel, doShow);
245
+
246
+ protected doHandleColumnSelectionChange(columnLabel: string, doShow: boolean): void {
247
+ if (columnLabel in this.columnsDisplayed) {
248
+ this.columnsDisplayed[columnLabel].doRender = doShow;
249
+ this.fireDidChangeOptions(ASCII_TOGGLE_ID);
250
+ }
251
+ }
252
+
253
+ protected toggleAutoUpdate = (e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>): void => {
254
+ if (e.nativeEvent.type === 'click') {
255
+ e.currentTarget.blur();
256
+ }
257
+ if ('key' in e && KeyCode.createKeyCode(e.nativeEvent).key?.keyCode === Key.TAB.keyCode) {
258
+ return;
259
+ }
260
+ this.doUpdateAutomatically = !this.doUpdateAutomatically;
261
+ if (this.doUpdateAutomatically) {
262
+ this.title.iconClass = this.iconClass;
263
+ } else {
264
+ this.title.iconClass = this.lockIconClass;
265
+ }
266
+ this.fireDidChangeOptions();
267
+ };
268
+
269
+ protected onByteSizeChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
270
+ this.byteSize = parseInt(event.target.value);
271
+ this.fireDidChangeOptions(event.target.id);
272
+ };
273
+
274
+ protected override onAfterAttach(msg: Message): void {
275
+ super.onAfterAttach(msg);
276
+ if (this.memoryWidgetOptions.dynamic !== false) {
277
+ if (this.addressField) {
278
+ this.addressField.value = this.address.toString();
279
+ }
280
+ }
281
+ }
282
+
283
+ protected toggleDoShowSettings = (e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>): void => {
284
+ if (!('key' in e) || KeyCode.createKeyCode(e.nativeEvent).key?.keyCode === Key.TAB.keyCode) {
285
+ this.doDisplaySettings = !this.doDisplaySettings;
286
+ this.update();
287
+ }
288
+ };
289
+
290
+ protected render(): React.ReactNode {
291
+ return (
292
+ <div className='t-mv-container'>
293
+ {this.renderInputContainer()}
294
+ </div>
295
+ );
296
+ }
297
+
298
+ protected renderInputContainer(): React.ReactNode {
299
+ return (
300
+ <div className='t-mv-settings-container'>
301
+ <div className='t-mv-wrapper'>
302
+ {this.renderToolbar()}
303
+ {this.renderMemoryLocationGroup()}
304
+ {this.doDisplaySettings && (
305
+ <div className='t-mv-toggle-settings-wrapper'>
306
+ {this.renderByteDisplayGroup()}
307
+ </div>
308
+ )}
309
+ </div>
310
+ </div>
311
+ );
312
+ }
313
+
314
+ protected renderByteDisplayGroup(): React.ReactNode {
315
+ return (
316
+ <div className='t-mv-group settings-group'>
317
+ <MWSelect
318
+ id='byte-size-select'
319
+ label={nls.localize('theia/memory-inspector/byteSize', 'Byte Size')}
320
+ value={this.byteSize.toString()}
321
+ onChange={this.onByteSizeChange}
322
+ options={['8', '16', '32', '64']}
323
+ />
324
+ <MWSelect
325
+ id={BYTES_PER_GROUP_FIELD_ID}
326
+ label={nls.localize('theia/memory-inspector/bytesPerGroup', 'Bytes Per Group')}
327
+ value={this.bytesPerGroup.toString()}
328
+ onChange={this.onBytesPerGroupChange}
329
+ options={['1', '2', '4', '8', '16']}
330
+ />
331
+ <MWSelect
332
+ id={BYTES_PER_ROW_FIELD_ID}
333
+ label={nls.localize('theia/memory-inspector/groupsPerRow', 'Groups Per Row')}
334
+ value={this.groupsPerRow.toString()}
335
+ onChange={this.onGroupsPerRowChange}
336
+ options={['1', '2', '4', '8', '16', '32']}
337
+ />
338
+ <MWSelect
339
+ id={ENDIAN_SELECT_ID}
340
+ label={nls.localize('theia/memory-inspector/endianness', 'Endianness')}
341
+ value={this.endianness}
342
+ onChange={this.onEndiannessChange}
343
+ options={[Interfaces.Endianness.Little, Interfaces.Endianness.Big]}
344
+ />
345
+ <MWMultiSelect
346
+ id={ASCII_TOGGLE_ID}
347
+ label={nls.localize('theia/memory-inspector/columns', 'Columns')}
348
+ items={this.getOptionalColumns()}
349
+ onSelectionChanged={this.handleColumnSelectionChange}
350
+ />
351
+ </div>
352
+ );
353
+ }
354
+
355
+ protected getObligatoryColumnIds(): string[] {
356
+ return ['address', 'data'];
357
+ }
358
+
359
+ protected getOptionalColumns(): SingleSelectItemProps[] {
360
+ const obligatoryColumns = new Set(this.getObligatoryColumnIds());
361
+ return Object.entries(this.columnsDisplayed)
362
+ .reduce<SingleSelectItemProps[]>((accumulated, [id, { doRender, label }]) => {
363
+ if (!obligatoryColumns.has(id)) {
364
+ accumulated.push({ id, label, defaultChecked: doRender });
365
+ }
366
+ return accumulated;
367
+ }, []);
368
+ }
369
+
370
+ protected assignLocationRef: React.LegacyRef<HTMLInputElement> = location => {
371
+ this.addressField = location ?? undefined;
372
+ };
373
+
374
+ protected assignReadLengthRef: React.LegacyRef<HTMLInputElement> = readLength => {
375
+ this.readLengthField = readLength ?? undefined;
376
+ };
377
+
378
+ protected assignOffsetRef: React.LegacyRef<HTMLInputElement> = offset => {
379
+ this.offsetField = offset ?? undefined;
380
+ };
381
+
382
+ protected setAddressFromSelect = (e: React.ChangeEvent<HTMLSelectElement>): void => {
383
+ if (this.addressField) {
384
+ this.addressField.value = e.target.value;
385
+ }
386
+ };
387
+
388
+ protected renderMemoryLocationGroup(): React.ReactNode {
389
+ return (
390
+ <>
391
+ <div className='t-mv-group view-group'>
392
+ <MWInputWithSelect
393
+ id={LOCATION_FIELD_ID}
394
+ label={nls.localizeByDefault('Address')}
395
+ title={nls.localize('theia/memory-inspector/addressTooltip', 'Memory location to display, an address or expression evaluating to an address')}
396
+ defaultValue={`${this.address}`}
397
+ onSelectChange={this.setAddressFromSelect}
398
+ passRef={this.assignLocationRef}
399
+ onKeyDown={this.doRefresh}
400
+ options={[...this.recentLocations.values]}
401
+ disabled={!this.doUpdateAutomatically}
402
+ />
403
+ <MWInput
404
+ id={LOCATION_OFFSET_FIELD_ID}
405
+ label={nls.localize('theia/memory-inspector/offset', 'Offset')}
406
+ title={nls.localize('theia/memory-inspector/offsetTooltip', 'Offset to be added to the current memory location, when navigating')}
407
+ defaultValue='0'
408
+ passRef={this.assignOffsetRef}
409
+ onKeyDown={this.doRefresh}
410
+ disabled={!this.doUpdateAutomatically}
411
+ />
412
+ <MWInput
413
+ id={LENGTH_FIELD_ID}
414
+ label={nls.localize('theia/memory-inspector/length', 'Length')}
415
+ title={nls.localize('theia/memory-inspector/lengthTooltip', 'Number of bytes to fetch, in decimal or hexadecimal')}
416
+ defaultValue={this.readLength.toString()}
417
+ passRef={this.assignReadLengthRef}
418
+ onChange={Utils.validateNumericalInputs}
419
+ onKeyDown={this.doRefresh}
420
+ disabled={!this.doUpdateAutomatically}
421
+ />
422
+ <button
423
+ type='button'
424
+ className='theia-button main view-group-go-button'
425
+ onClick={this.doRefresh}
426
+ disabled={!this.doUpdateAutomatically}
427
+ title={nls.localizeByDefault('Go')}
428
+ >
429
+ {nls.localizeByDefault('Go')}
430
+ </button>
431
+ </div>
432
+ <div className={`t-mv-memory-fetch-error${this.showMemoryError ? ' show' : ' hide'}`}>
433
+ {this.memoryReadError}
434
+ </div>
435
+ </>
436
+ );
437
+ }
438
+
439
+ protected activateHeaderInputField = (e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>): void => {
440
+ if (!this.isTitleEditable) {
441
+ const isMouseDown = !('key' in e);
442
+ const isActivationKey = 'key' in e && (
443
+ KeyCode.createKeyCode(e.nativeEvent).key?.keyCode === Key.SPACE.keyCode
444
+ || KeyCode.createKeyCode(e.nativeEvent).key?.keyCode === Key.ENTER.keyCode
445
+ );
446
+ if (isMouseDown || isActivationKey) {
447
+ if (isMouseDown) {
448
+ e.currentTarget.blur();
449
+ }
450
+ this.isTitleEditable = true;
451
+ this.update();
452
+ }
453
+ }
454
+ };
455
+
456
+ protected saveHeaderInputValue = (e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>): void => {
457
+ const isMouseDown = !('key' in e);
458
+ const isSaveKey = 'key' in e && e.key === 'Enter';
459
+ const isCancelKey = 'key' in e && e.key === 'Escape';
460
+ e.stopPropagation();
461
+ if (isMouseDown || isSaveKey || isCancelKey) {
462
+ this.updateHeader(isCancelKey);
463
+ }
464
+ };
465
+
466
+ protected assignHeaderInputRef = (element: HTMLInputElement): void => {
467
+ if (element) {
468
+ this.headerInputField = element;
469
+ element.focus();
470
+ }
471
+ };
472
+
473
+ protected updateHeader(isCancelKey: boolean): void {
474
+ if (!isCancelKey && this.headerInputField) {
475
+ this.title.label = this.headerInputField.value;
476
+ this.title.caption = this.headerInputField.value;
477
+ }
478
+
479
+ this.isTitleEditable = false;
480
+ this.update();
481
+ }
482
+
483
+ protected renderToolbar(): React.ReactNode {
484
+ return (
485
+ <div className='memory-widget-toolbar'>
486
+ {this.renderLockIcon()}
487
+ {this.renderEditableTitleField()}
488
+ {this.renderSettingsContainer()}
489
+ </div>
490
+ );
491
+ }
492
+
493
+ protected renderSettingsContainer(): React.ReactNode {
494
+ return <div className='toggle-settings-container'>
495
+ <div
496
+ className='toggle-settings-click-zone no-select'
497
+ tabIndex={0}
498
+ aria-label={this.doDisplaySettings ?
499
+ nls.localize('theia/memory-inspector/memory/hideSettings', 'Hide Settings Panel') :
500
+ nls.localize('theia/memory-inspector/memory/showSettings', 'Show Settings Panel')}
501
+ role='button'
502
+ onClick={this.toggleDoShowSettings}
503
+ onKeyDown={this.toggleDoShowSettings}
504
+ title={this.doDisplaySettings ?
505
+ nls.localize('theia/memory-inspector/memory/hideSettings', 'Hide Settings Panel') :
506
+ nls.localize('theia/memory-inspector/memory/showSettings', 'Show Settings Panel')}>
507
+ <i className='codicon codicon-settings-gear' />
508
+ <span>{this.doDisplaySettings ?
509
+ nls.localize('theia/memory-inspector/closeSettings', 'Close Settings') :
510
+ nls.localizeByDefault('Settings')}
511
+ </span>
512
+ </div>
513
+ </div>;
514
+ }
515
+
516
+ protected renderLockIcon(): React.ReactNode {
517
+ return this.memoryWidgetOptions.dynamic !== false && (
518
+ <div className='memory-widget-auto-updates-container'>
519
+ <div
520
+ className={`fa fa-${this.doUpdateAutomatically ? 'unlock' : 'lock'}`}
521
+ id={AUTO_UPDATE_TOGGLE_ID}
522
+ title={this.doUpdateAutomatically ?
523
+ nls.localize('theia/memory-inspector/memory/freeze', 'Freeze Memory View') :
524
+ nls.localize('theia/memory-inspector/memory/unfreeze', 'Unfreeze Memory View')}
525
+ onClick={this.toggleAutoUpdate}
526
+ onKeyDown={this.toggleAutoUpdate}
527
+ role='button'
528
+ tabIndex={0} />
529
+ </div>
530
+ );
531
+ }
532
+
533
+ protected renderEditableTitleField(): React.ReactNode {
534
+ return (
535
+ <div
536
+ className='memory-widget-header-click-zone'
537
+ tabIndex={0}
538
+ onClick={this.activateHeaderInputField}
539
+ onKeyDown={this.activateHeaderInputField}
540
+ role='button'
541
+ >
542
+ {!this.isTitleEditable
543
+ ? (
544
+ <h2 className={`${MemoryOptionsWidget.WIDGET_H2_CLASS}${!this.doUpdateAutomatically ? ' disabled' : ''} no-select`}>
545
+ {this.title.label}
546
+ </h2>
547
+ )
548
+ : <input
549
+ className='theia-input'
550
+ type='text'
551
+ defaultValue={this.title.label}
552
+ onKeyDown={this.saveHeaderInputValue}
553
+ spellCheck={false}
554
+ ref={this.assignHeaderInputRef}
555
+ />}
556
+ {!this.isTitleEditable && (
557
+ <div className={`fa fa-pencil${this.showTitleEditIcon ? ' show' : ' hide'}`} />
558
+ )}
559
+ {this.isTitleEditable && (
560
+ <div
561
+ className='fa fa-save'
562
+ onClick={this.saveHeaderInputValue}
563
+ onKeyDown={this.saveHeaderInputValue}
564
+ role='button'
565
+ tabIndex={0}
566
+ title={nls.localizeByDefault('Save')}
567
+ />
568
+ )}
569
+ </div>
570
+ );
571
+ }
572
+
573
+ storeState(): Interfaces.MemoryOptions {
574
+ return {
575
+ address: this.addressField?.value ?? this.address,
576
+ offset: parseInt(`${this.offsetField?.value}`) ?? this.offset,
577
+ length: parseInt(`${this.readLengthField?.value}`) ?? this.readLength,
578
+ byteSize: this.byteSize,
579
+ bytesPerGroup: this.bytesPerGroup,
580
+ groupsPerRow: this.groupsPerRow,
581
+ endianness: this.endianness,
582
+ doDisplaySettings: this.doDisplaySettings,
583
+ columnsDisplayed: this.columnsDisplayed,
584
+ recentLocationsArray: this.recentLocations.values,
585
+ isFrozen: !this.doUpdateAutomatically,
586
+ doUpdateAutomatically: this.doUpdateAutomatically,
587
+ };
588
+ }
589
+
590
+ restoreState(oldState: Interfaces.MemoryOptions): void {
591
+ this.address = oldState.address ?? this.address;
592
+ this.offset = oldState.offset ?? this.offset;
593
+ this.readLength = oldState.length ?? this.readLength;
594
+ this.byteSize = oldState.byteSize ?? this.byteSize;
595
+ this.bytesPerGroup = oldState.bytesPerGroup ?? this.bytesPerGroup;
596
+ this.groupsPerRow = oldState.groupsPerRow ?? this.groupsPerRow;
597
+ this.endianness = oldState.endianness ?? this.endianness;
598
+ this.recentLocations = new Recents(oldState.recentLocationsArray) ?? this.recentLocations;
599
+ this.doDisplaySettings = !!oldState.doDisplaySettings;
600
+ if (oldState.columnsDisplayed) {
601
+ this.columnsDisplayed = oldState.columnsDisplayed;
602
+ }
603
+ }
604
+
605
+ protected doShowMemoryErrors = (doClearError = false): void => {
606
+ if (this.errorTimeout !== undefined) {
607
+ clearTimeout(this.errorTimeout);
608
+ }
609
+ if (doClearError) {
610
+ this.showMemoryError = false;
611
+ this.update();
612
+ this.errorTimeout = undefined;
613
+ return;
614
+ }
615
+ this.showMemoryError = true;
616
+ this.update();
617
+ this.errorTimeout = setTimeout(() => {
618
+ this.showMemoryError = false;
619
+ this.update();
620
+ this.errorTimeout = undefined;
621
+ }, Constants.ERROR_TIMEOUT);
622
+ };
623
+
624
+ fetchNewMemory(): void {
625
+ this.updateMemoryView();
626
+ }
627
+
628
+ protected doRefresh = (event: React.KeyboardEvent<HTMLInputElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>): void => {
629
+ if ('key' in event && event.key !== 'Enter') {
630
+ return;
631
+ }
632
+ this.updateMemoryView();
633
+ };
634
+
635
+ protected updateMemoryView = debounce(this.doUpdateMemoryView.bind(this), Constants.DEBOUNCE_TIME, { trailing: true });
636
+
637
+ protected async doUpdateMemoryView(): Promise<void> {
638
+ if (!(this.addressField && this.readLengthField)) { return; }
639
+
640
+ if (this.addressField?.value.trim().length === 0) {
641
+ this.memoryReadError = nls.localize('theia/memory-inspector/memory/addressField/memoryReadError', 'Enter an address or expression in the Location field.');
642
+ this.doShowMemoryErrors();
643
+ return;
644
+ }
645
+ if (this.readLengthField.value.trim().length === 0) {
646
+ this.memoryReadError = nls.localize('theia/memory-inspector/memory/readLength/memoryReadError', 'Enter a length (decimal or hexadecimal number) in the Length field.');
647
+ this.doShowMemoryErrors();
648
+ return;
649
+ }
650
+
651
+ const startAddress = this.addressField.value;
652
+ const locationOffset = parseInt(`${this.offsetField?.value}`) || 0;
653
+ const readLength = parseInt(this.readLengthField.value);
654
+
655
+ try {
656
+ this.memoryReadResult = await this.getMemory(startAddress, readLength, locationOffset);
657
+ this.fireDidChangeMemory();
658
+ if (this.pinnedMemoryReadResult) {
659
+ this.pinnedMemoryReadResult.resolve(this.memoryReadResult);
660
+ }
661
+ this.doShowMemoryErrors(true);
662
+ } catch (err) {
663
+ this.memoryReadError = this.getUserError(err);
664
+ console.error('Failed to read memory', err);
665
+ this.doShowMemoryErrors();
666
+ if (this.pinnedMemoryReadResult) {
667
+ this.pinnedMemoryReadResult.resolve(this.memoryReadResult);
668
+ }
669
+ } finally {
670
+ this.pinnedMemoryReadResult = undefined;
671
+ this.update();
672
+ }
673
+ }
674
+
675
+ protected getUserError(err: unknown): string {
676
+ return err instanceof Error ? err.message : nls.localize('theia/memory-inspector/memory/userError', 'There was an error fetching memory.');
677
+ }
678
+
679
+ protected async getMemory(memoryReference: string, count: number, offset: number): Promise<Interfaces.MemoryReadResult> {
680
+ const result = await this.retrieveMemory(memoryReference, count, offset);
681
+ try {
682
+ this.variables = await this.memoryProvider.getLocals();
683
+ } catch {
684
+ this.variables = [];
685
+ }
686
+ this.recentLocations.add(memoryReference);
687
+ this.updateDefaults(memoryReference, count, offset);
688
+ return result;
689
+ }
690
+
691
+ protected async retrieveMemory(memoryReference: string, count: number, offset: number): Promise<Interfaces.MemoryReadResult> {
692
+ return this.memoryProvider.readMemory({ memoryReference, count, offset });
693
+ }
694
+
695
+ // TODO: This may not be necessary if we change how state is stored (currently in the text fields themselves.)
696
+ protected updateDefaults(address: string, readLength: number, offset: number): void {
697
+ this.address = address;
698
+ this.readLength = readLength;
699
+ this.offset = offset;
700
+ }
701
+
702
+ // Callbacks for when the various view parameters change.
703
+ /**
704
+ * Handle bytes per row changed event.
705
+ */
706
+ protected onGroupsPerRowChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
707
+ const { value, id } = event.target;
708
+ this.groupsPerRow = parseInt(value);
709
+ this.fireDidChangeOptions(id);
710
+ };
711
+
712
+ /**
713
+ * Handle bytes per group changed event.
714
+ */
715
+ protected onBytesPerGroupChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
716
+ const { value, id } = event.target;
717
+ this.bytesPerGroup = parseInt(value);
718
+ this.fireDidChangeOptions(id);
719
+ };
720
+
721
+ /**
722
+ * Handle endianness changed event.
723
+ */
724
+ protected onEndiannessChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
725
+ const { value, id } = event.target;
726
+ if (value !== Interfaces.Endianness.Big && value !== Interfaces.Endianness.Little) { return; }
727
+ this.endianness = value;
728
+ this.fireDidChangeOptions(id);
729
+ };
730
+
731
+ protected fireDidChangeOptions(targetId?: string): void {
732
+ this.onOptionsChangedEmitter.fire(targetId);
733
+ }
734
+
735
+ protected fireDidChangeMemory(): void {
736
+ this.onMemoryChangedEmitter.fire(this.memoryReadResult);
737
+ }
738
+ }