Skip to content

Commit

Permalink
feat: add inline data inspector
Browse files Browse the repository at this point in the history
The old data inspector is preserved, but the new one is the default. It
functions (and looks) like the standard widget in the editor, and
basically just has information parity with the old sidebar inspector.

![](https://memes.peet.io/img/22-02-9bf5c4a2-4915-4e57-abda-dc9864bacdaa.png)
  • Loading branch information
connor4312 committed Feb 14, 2022
1 parent e30d0bd commit 91d32d0
Show file tree
Hide file tree
Showing 12 changed files with 333 additions and 83 deletions.
3 changes: 2 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionDevelopmentPath=${workspaceFolder}/../vscode-js-debug/out",
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
Expand Down
27 changes: 17 additions & 10 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
// Place your settings in this file to overwrite default and user settings.
{
"editor.defaultFormatter": "vscode.typescript-language-features",
"files.exclude": {
"out": false // set this to true to hide the "out" folder with the compiled JS files
},
"search.exclude": {
"out": true // set this to false to include "out" folder in search results
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off",
"typescript.tsdk": "node_modules\\typescript\\lib",
"files.eol": "\n",
"files.exclude": {
"out": false // set this to true to hide the "out" folder with the compiled JS files
},
"search.exclude": {
"out": true // set this to false to include "out" folder in search results
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off",
"typescript.tsdk": "node_modules\\typescript\\lib",
"files.eol": "\n",
"typescript.preferences.quoteStyle": "double",
"editor.formatOnSave": true,
"[typescriptreact]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
}
22 changes: 7 additions & 15 deletions media/editor/dataDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useRecoilValue, useSetRecoilState } from "recoil";
import { EditRangeOp, HexDocumentEditOp } from "../../shared/hexDocumentModel";
import { MessageType } from "../../shared/protocol";
import { PastePopup } from "./copyPaste";
import { FocusedElement, useDisplayContext, useIsFocused, useIsHovered, useIsSelected, useIsUnsaved } from "./dataDisplayContext";
import { dataCellCls, FocusedElement, useDisplayContext, useIsFocused, useIsHovered, useIsSelected, useIsUnsaved } from "./dataDisplayContext";
import { useGlobalHandler, useLastAsyncRecoilValue } from "./hooks";
import * as select from "./state";
import { clamp, clsx, getAsciiCharacter, Range, RangeDirection } from "./util";
Expand All @@ -25,20 +25,6 @@ const Address = styled.div`
line-height: var(--cell-size);
`;

export const dataCellCls = css`
font-family: var(--vscode-editor-font-family);
width: var(--cell-size);
height: var(--cell-size);
line-height: var(--cell-size);
text-align: center;
display: inline-block;
&:focus {
outline-offset: 1px;
outline: var(--vscode-focusBorder) 2px solid;
}
`;

const DataCellGroup = styled.div`
padding: 0 calc(var(--cell-size) / 4);
display: inline-flex;
Expand Down Expand Up @@ -348,6 +334,10 @@ for (const [key, value] of keysToOctets) {
keysToOctets.set(key.toUpperCase(), value);
}

const dataCellCharCls = css`
width: calc(var(--cell-size) * 0.7) !important;
`;

const DataCell: React.FC<{
byte: number;
value: number;
Expand Down Expand Up @@ -479,6 +469,7 @@ const DataCell: React.FC<{
onFocus={onFocus}
onBlur={onBlur}
className={clsx(
isChar && dataCellCharCls,
dataCellCls,
className,
useIsHovered(focusedElement) && dataCellHoveredCls,
Expand All @@ -489,6 +480,7 @@ const DataCell: React.FC<{
onMouseDown={onMouseDown}
onMouseLeave={onMouseLeave}
onKeyDown={onKeyDown}
data-key={focusedElement.key}
>{firstOctetOfEdit !== undefined
? firstOctetOfEdit.toString(16).toUpperCase()
: children}</span>
Expand Down
40 changes: 36 additions & 4 deletions media/editor/dataDisplayContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { SetterOrUpdater } from "recoil";
import { HexDocumentEdit } from "../../shared/hexDocumentModel";
import { MessageType } from "../../shared/protocol";
import { messageHandler, registerHandler } from "./state";
import { css } from "@linaria/core";
import { Range } from "./util";

export class FocusedElement {
Expand All @@ -15,7 +16,7 @@ export class FocusedElement {
public readonly char: boolean,
/** Focused byte index */
public readonly byte: number,
) {}
) { }

/** Gets the other element at this byte (the character or non-character) */
public other(): FocusedElement {
Expand All @@ -40,8 +41,10 @@ export class DisplayContext {
private _focusedByte?: FocusedElement;
private _unsavedRanges: readonly Range[] = [];
private readonly unsavedRangesEmitter = new EventEmitter<readonly Range[]>();
private readonly selectionChangeEmitter = new EventEmitter<{ range:Range; isSingleSwap: boolean }>();
private readonly selectionChangeEmitter = new EventEmitter<{ range: Range; isSingleSwap: boolean }>();
private readonly hoverChangeEmitter = new EventEmitter<FocusedElement | undefined>();
private readonly hoverChangeHandlers = new Map<bigint, (isSelected: boolean) => void>();
private readonly focusChangeEmitter = new EventEmitter<FocusedElement | undefined>();
private readonly focusChangeHandlers = new Map<bigint, (isSelected: boolean) => void>();
private readonly focusChangeGenericHandler = new EventEmitter<number | undefined>();

Expand All @@ -50,6 +53,16 @@ export class DisplayContext {
*/
public isSelecting = false;

/**
* Handler for when any focus changes.
*/
public readonly onDidFocus = this.focusChangeEmitter.addListener;

/**
* Handler for when any hover changes.
*/
public readonly onDidHover = this.hoverChangeEmitter.addListener;

/**
* Emitter that fires when a selection for a single byte changes.
*/
Expand Down Expand Up @@ -77,7 +90,7 @@ export class DisplayContext {
}

/**
* Emitter that fires when the given byte is focused or unfocused.
* Emitter that fires when the given byte is focused or unfocused.
*/
public onDidChangeFocus(element: FocusedElement, listener: (isFocused: boolean) => void): IDisposable {
if (this.focusChangeHandlers.has(element.key)) {
Expand Down Expand Up @@ -115,7 +128,7 @@ export class DisplayContext {
/**
* Updates the currently focused byte.
*/
public set focusedElement(element: FocusedElement | undefined ) {
public set focusedElement(element: FocusedElement | undefined) {
if (this._focusedByte?.key === element?.key) {
return;
}
Expand All @@ -125,6 +138,7 @@ export class DisplayContext {
}

this._focusedByte = element;
this.focusChangeEmitter.emit(element);

if (this._focusedByte !== undefined) {
this.focusChangeHandlers.get(this._focusedByte.key)?.(true);
Expand Down Expand Up @@ -172,6 +186,7 @@ export class DisplayContext {
}

this._hoveredByte = byte;
this.hoverChangeEmitter.emit(byte);

if (this._hoveredByte !== undefined) {
this.hoverChangeHandlers.get(this._hoveredByte.key)?.(true);
Expand Down Expand Up @@ -347,3 +362,20 @@ export const useIsUnsaved = (byte: number): boolean => {

return unsaved;
};

export const dataCellCls = css`
font-family: var(--vscode-editor-font-family);
width: var(--cell-size);
height: var(--cell-size);
line-height: var(--cell-size);
text-align: center;
display: inline-block;
&:focus {
outline-offset: 1px;
outline: var(--vscode-focusBorder) 2px solid;
}
`;

export const getDataCellElement = (element: FocusedElement) =>
document.querySelector(`.${dataCellCls}[data-key="${element.key}"]`);
176 changes: 176 additions & 0 deletions media/editor/dataInspector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { styled } from "@linaria/react";
import React, { Suspense, useEffect, useMemo, useState } from "react";
import { useRecoilValue } from "recoil";
import { Endianness } from "../../shared/protocol";
import { FocusedElement, getDataCellElement, useDisplayContext } from "./dataDisplayContext";
import { usePersistedState } from "./hooks";
import * as select from "./state";
import { reverseInPlace } from "./util";
import { VsTooltipPopover } from "./vscodeUi";

export const DataInspector: React.FC = () => {
const ctx = useDisplayContext();
const defaultEndianness = useRecoilValue(select.editorSettings).defaultEndianness;
const [endianness, setEndianness] = usePersistedState("endianness", defaultEndianness);
const [inspected, setInspected] = useState<FocusedElement>();
const anchor = useMemo(() => inspected && getDataCellElement(inspected), [inspected]);

useEffect(() => {
let hoverTimeout: NodeJS.Timeout | undefined;

const disposable = ctx.onDidHover(target => {
if (hoverTimeout) {
clearTimeout(hoverTimeout);
hoverTimeout = undefined;
}
if (target && !ctx.isSelecting) {
setInspected(undefined);
hoverTimeout = setTimeout(() => setInspected(target), 500);
}
});

return () => disposable.dispose();
}, []);


if (!inspected || !anchor) {
return null;
}

return <VsTooltipPopover
anchor={anchor}
hide={() => setInspected(undefined)} visible={true}>
<Suspense fallback="Loading...">
<InspectorContents offset={inspected.byte} endianness={endianness} setEndianness={setEndianness} />
</Suspense>
</VsTooltipPopover>;
};

const lookahead = 8;

const TypesList = styled.dl`
display: grid;
grid-template-columns: max-content max-content max-content max-content;
gap: 0.3rem 1rem;
align-items: center;
margin: 0;
dd, dl {
margin: 0;
}
dd {
font-family: var(--vscode-editor-font-family);
}
`;

const InspectorContents: React.FC<{
offset: number;
endianness: Endianness;
setEndianness: (e: Endianness) => void;
}> = ({ offset, endianness, setEndianness }) => {
const dataPageSize = useRecoilValue(select.dataPageSize);
const startPageNo = Math.floor(offset / dataPageSize);
const startPageStartsAt = startPageNo * dataPageSize;
const endPageNo = Math.floor((offset + lookahead) / dataPageSize);
const endPageStartsAt = endPageNo * dataPageSize;

const startPage = useRecoilValue(select.editedDataPages(startPageNo));
const endPage = useRecoilValue(select.editedDataPages(endPageNo));

const target = new Uint8Array(lookahead);
for (let i = 0; i < lookahead; i++) {
if (offset + i >= endPageStartsAt) {
target[i] = endPage[offset + i - endPageStartsAt];
} else {
target[i] = startPage[offset + i - startPageStartsAt];
}
}

const utf8 = getUtf8(target);
const utf16 = getUTF16(target);
if (endianness === Endianness.Little) {
reverseInPlace(target);
}

const dv = new DataView(target.buffer);

return <>
<TypesList>
<dt>uint8</dt>
<dd>{dv.getUint8(0)}</dd>
<dt>int8</dt>
<dd>{dv.getInt8(0)}</dd>

<dt>uint16</dt>
<dd>{dv.getUint16(0)}</dd>
<dt>int16</dt>
<dd>{dv.getInt16(0)}</dd>

<dt>uint32</dt>
<dd>{dv.getUint32(0)}</dd>
<dt>int32</dt>
<dd>{dv.getInt32(0)}</dd>

<dt>int64</dt>
<dd>{dv.getBigInt64(0).toString()}</dd>
<dt>uint64</dt>
<dd>{dv.getBigUint64(0).toString()}</dd>

<dt>float32</dt>
<dd>{dv.getFloat32(0)}</dd>
<dt>float64</dt>
<dd>{dv.getFloat64(0)}</dd>

<dt>UTF-8</dt>
<dd>{utf8}</dd>
<dt>UTF-16</dt>
<dd>{utf16}</dd>
</TypesList>
<EndiannessToggle endianness={endianness} setEndianness={setEndianness} />
</>;
};

const EndiannessToggleContainer = styled.div`
display: flex;
justify-content: flex-end;
`;

const EndiannessToggle: React.FC<{
endianness: Endianness;
setEndianness: (e: Endianness) => void;
}> = ({ endianness, setEndianness }) => (
<EndiannessToggleContainer>
<label htmlFor="endian-checkbox">Little Endian</label>
<input
type="checkbox"
id="endian-checkbox"
checked={endianness === Endianness.Little}
onChange={evt => setEndianness(evt.target.checked ? Endianness.Little : Endianness.Big)}
/>
</EndiannessToggleContainer>
);

/**
* @description Converts the byte data to a utf-8 character
* @param {boolean} littleEndian Whether or not it's represented in little endian
* @returns {string} The utf-8 character
*/
const getUtf8 = (buf: Uint8Array): string => {
const utf8 = new TextDecoder("utf-8").decode(buf);
// We iterate through the string and immediately reutrn the first character
for (const char of utf8) return char;
return utf8;
};

/**
* @description Converts the byte data to a utf-16 character
* @param {boolean} littleEndian Whether or not it's represented in little endian
* @returns {string} The utf-16 character
*/
const getUTF16 = (buf: Uint8Array): string => {
const utf16 = new TextDecoder("utf-16").decode(buf);
// We iterate through the string and immediately reutrn the first character
for (const char of utf16) return char;
return utf16;
};
Loading

0 comments on commit 91d32d0

Please sign in to comment.