You've already forked objdiff-web
mirror of
https://github.com/encounter/objdiff-web.git
synced 2026-07-10 12:18:37 -07:00
Add standalone web build & decomp.me integration
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
.instruction-list {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.instruction-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
height: var(--list-row-height);
|
||||
}
|
||||
|
||||
.instruction-cell {
|
||||
flex: 1 0 0;
|
||||
font-family: var(--code-font-family);
|
||||
font-weight: var(--code-font-weight);
|
||||
font-size: var(--code-font-size);
|
||||
text-wrap: nowrap;
|
||||
white-space: pre;
|
||||
overflow: hidden;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.highlightable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.highlighted {
|
||||
color: white;
|
||||
background-color: #aa8b00;
|
||||
}
|
||||
|
||||
.diff-any {
|
||||
background-color: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.segment-dim {
|
||||
color: var(--line-number-foreground);
|
||||
}
|
||||
.segment-bright {
|
||||
color: var(--color-bright);
|
||||
}
|
||||
.segment-replace {
|
||||
color: var(--color-blue);
|
||||
}
|
||||
.segment-delete {
|
||||
color: var(--color-red);
|
||||
}
|
||||
.segment-insert {
|
||||
color: var(--color-green);
|
||||
}
|
||||
|
||||
.rotation0 {
|
||||
color: light-dark(
|
||||
/* hsv(0° 60% 80%) */
|
||||
rgb(205, 82, 82),
|
||||
magenta
|
||||
);
|
||||
}
|
||||
.rotation1 {
|
||||
color: light-dark(
|
||||
/* hsv(40° 60% 80%) */
|
||||
rgb(205, 164, 82),
|
||||
cyan
|
||||
);
|
||||
}
|
||||
.rotation2 {
|
||||
color: light-dark(
|
||||
/* hsv(80° 60% 80%) */
|
||||
rgb(164, 205, 82),
|
||||
rgb(0, 212, 0)
|
||||
);
|
||||
}
|
||||
.rotation3 {
|
||||
color: light-dark(
|
||||
/* hsv(120° 60% 80%) */
|
||||
rgb(82, 205, 82),
|
||||
red
|
||||
);
|
||||
}
|
||||
.rotation4 {
|
||||
color: light-dark(
|
||||
/* hsv(160° 60% 80%) */
|
||||
rgb(82, 205, 164),
|
||||
rgb(103, 106, 255)
|
||||
);
|
||||
}
|
||||
.rotation5 {
|
||||
color: light-dark(
|
||||
/* hsv(200° 60% 80%) */
|
||||
rgb(82, 164, 205),
|
||||
lightpink
|
||||
);
|
||||
}
|
||||
.rotation6 {
|
||||
color: light-dark(
|
||||
/* hsv(240° 60% 80%) */
|
||||
rgb(82, 82, 205),
|
||||
lightcyan
|
||||
);
|
||||
}
|
||||
.rotation7 {
|
||||
color: light-dark(
|
||||
/* hsv(280° 60% 80%) */
|
||||
rgb(164, 82, 205),
|
||||
lightgreen
|
||||
);
|
||||
}
|
||||
.rotation8 {
|
||||
color: light-dark(
|
||||
/* hsv(320° 60% 80%) */
|
||||
rgb(205, 82, 164),
|
||||
grey
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
import headerStyles from '../common/Header.module.css';
|
||||
import styles from './FunctionView.module.css';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { type diff, display } from 'objdiff-wasm';
|
||||
import { memo, useMemo } from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { FixedSizeList, areEqual } from 'react-window';
|
||||
import type { ListChildComponentProps } from 'react-window';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import TooltipShared from '../common/TooltipShared';
|
||||
import {
|
||||
buildDiffConfig,
|
||||
runBuild,
|
||||
useAppStore,
|
||||
useExtensionStore,
|
||||
} from '../state';
|
||||
import {
|
||||
type HighlightState,
|
||||
highlightColumn,
|
||||
highlightMatches,
|
||||
updateHighlight,
|
||||
} from '../util/highlight';
|
||||
import { percentClass, useFontSize } from '../util/util';
|
||||
|
||||
const ROTATION_CLASSES = [
|
||||
styles.rotation0,
|
||||
styles.rotation1,
|
||||
styles.rotation2,
|
||||
styles.rotation3,
|
||||
styles.rotation4,
|
||||
styles.rotation5,
|
||||
styles.rotation6,
|
||||
styles.rotation7,
|
||||
styles.rotation8,
|
||||
];
|
||||
|
||||
const AsmCell = ({
|
||||
obj,
|
||||
config,
|
||||
symbol,
|
||||
row,
|
||||
column,
|
||||
highlight: highlightState,
|
||||
setHighlight,
|
||||
}: {
|
||||
obj: diff.ObjectDiff | undefined;
|
||||
config: diff.DiffConfig;
|
||||
symbol: display.SectionDisplaySymbol | null;
|
||||
row: number;
|
||||
column: number;
|
||||
highlight: HighlightState;
|
||||
setHighlight: (highlight: HighlightState) => void;
|
||||
}) => {
|
||||
if (!obj || !symbol) {
|
||||
return <div className={styles.instructionCell} />;
|
||||
}
|
||||
|
||||
const highlight = highlightColumn(highlightState, column);
|
||||
const out: React.ReactNode[] = [];
|
||||
|
||||
const insRow = display.displayInstructionRow(obj, symbol, row, config);
|
||||
let index = 0;
|
||||
for (const segment of insRow.segments) {
|
||||
let className: string | undefined;
|
||||
switch (segment.color.tag) {
|
||||
case 'normal':
|
||||
break;
|
||||
case 'dim':
|
||||
className = styles.segmentDim;
|
||||
break;
|
||||
case 'bright':
|
||||
className = styles.segmentBright;
|
||||
break;
|
||||
case 'replace':
|
||||
className = styles.segmentReplace;
|
||||
break;
|
||||
case 'delete':
|
||||
className = styles.segmentDelete;
|
||||
break;
|
||||
case 'insert':
|
||||
className = styles.segmentInsert;
|
||||
break;
|
||||
case 'rotating':
|
||||
className =
|
||||
ROTATION_CLASSES[segment.color.val % ROTATION_CLASSES.length];
|
||||
break;
|
||||
default:
|
||||
console.warn('Unknown color type', segment.color);
|
||||
break;
|
||||
}
|
||||
const t = segment.text;
|
||||
let text = '';
|
||||
let postText = ''; // unhighlightable text after the token
|
||||
let isToken = false;
|
||||
switch (t.tag) {
|
||||
case 'basic':
|
||||
text = t.val;
|
||||
break;
|
||||
case 'line':
|
||||
text = t.val.toString(10);
|
||||
break;
|
||||
case 'address':
|
||||
text = t.val.toString(16);
|
||||
postText = ':';
|
||||
isToken = true;
|
||||
break;
|
||||
case 'opcode':
|
||||
text = t.val.mnemonic;
|
||||
isToken = true;
|
||||
break;
|
||||
case 'signed':
|
||||
if (t.val < 0) {
|
||||
text = `-0x${(-t.val).toString(16)}`;
|
||||
} else {
|
||||
text = `0x${t.val.toString(16)}`;
|
||||
}
|
||||
isToken = true;
|
||||
break;
|
||||
case 'unsigned':
|
||||
text = `0x${t.val.toString(16)}`;
|
||||
isToken = true;
|
||||
break;
|
||||
case 'opaque':
|
||||
text = t.val;
|
||||
isToken = true;
|
||||
break;
|
||||
case 'branch-dest':
|
||||
text = t.val.toString(16);
|
||||
isToken = true;
|
||||
break;
|
||||
case 'symbol':
|
||||
text = t.val.demangledName || t.val.name;
|
||||
isToken = true;
|
||||
break;
|
||||
case 'addend':
|
||||
if (t.val < 0) {
|
||||
text = `-0x${(-t.val).toString(16)}`;
|
||||
} else {
|
||||
text = `+0x${t.val.toString(16)}`;
|
||||
}
|
||||
break;
|
||||
case 'spacing':
|
||||
text = ' '.repeat(t.val);
|
||||
break;
|
||||
case 'eol':
|
||||
continue;
|
||||
default:
|
||||
console.warn('Unknown text type', t);
|
||||
break;
|
||||
}
|
||||
out.push(
|
||||
<span
|
||||
key={index}
|
||||
className={clsx(className, {
|
||||
[styles.highlightable]: isToken,
|
||||
[styles.highlighted]: highlightMatches(highlight, t),
|
||||
})}
|
||||
onClick={(e) => {
|
||||
if (isToken) {
|
||||
setHighlight(updateHighlight(highlightState, t, column));
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</span>,
|
||||
);
|
||||
index++;
|
||||
if (postText) {
|
||||
out.push(
|
||||
<span key={index} className={className}>
|
||||
{postText}
|
||||
</span>,
|
||||
);
|
||||
index++;
|
||||
}
|
||||
if (segment.padTo > text.length + postText.length) {
|
||||
const spacing = ' '.repeat(segment.padTo - text.length - postText.length);
|
||||
out.push(<span key={index}>{spacing}</span>);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
const classes = [styles.instructionCell];
|
||||
if (insRow.diffKind !== 'none') {
|
||||
classes.push(styles.diffAny);
|
||||
}
|
||||
if (!out.length) {
|
||||
return <div className={clsx(classes)} />;
|
||||
}
|
||||
|
||||
const tooltipContent: InstructionTooltipContent = { column, row };
|
||||
return (
|
||||
<div
|
||||
className={clsx(classes)}
|
||||
data-tooltip-id="instruction-tooltip"
|
||||
data-tooltip-content={JSON.stringify(tooltipContent)}
|
||||
>
|
||||
{out}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type InstructionTooltipContent = {
|
||||
column: number;
|
||||
row: number;
|
||||
};
|
||||
|
||||
type ItemData = {
|
||||
itemCount: number;
|
||||
symbolName: string;
|
||||
result: diff.DiffResult;
|
||||
config: diff.DiffConfig;
|
||||
matchPercent?: number;
|
||||
left: display.SectionDisplaySymbol | null;
|
||||
leftSymbol: display.SymbolDisplay | null;
|
||||
right: display.SectionDisplaySymbol | null;
|
||||
rightSymbol: display.SymbolDisplay | null;
|
||||
highlight: HighlightState;
|
||||
setHighlight: (highlight: HighlightState) => void;
|
||||
};
|
||||
|
||||
const AsmRow = memo(
|
||||
({
|
||||
index,
|
||||
style,
|
||||
data: { result, config, left, right, highlight, setHighlight },
|
||||
}: ListChildComponentProps<ItemData>) => {
|
||||
return (
|
||||
<div
|
||||
className={styles.instructionRow}
|
||||
style={style}
|
||||
onClick={() => {
|
||||
// Clear highlight on background click
|
||||
setHighlight({ left: null, right: null });
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
// Prevent double click text selection
|
||||
if (e.detail > 1) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AsmCell
|
||||
obj={result.left}
|
||||
config={config}
|
||||
symbol={left}
|
||||
row={index}
|
||||
column={0}
|
||||
highlight={highlight}
|
||||
setHighlight={setHighlight}
|
||||
/>
|
||||
<AsmCell
|
||||
obj={result.right}
|
||||
config={config}
|
||||
symbol={right}
|
||||
row={index}
|
||||
column={1}
|
||||
highlight={highlight}
|
||||
setHighlight={setHighlight}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
areEqual,
|
||||
);
|
||||
|
||||
const createItemData = memoizeOne(
|
||||
(
|
||||
result: diff.DiffResult,
|
||||
left: display.SectionDisplaySymbol | null,
|
||||
right: display.SectionDisplaySymbol | null,
|
||||
highlight: HighlightState,
|
||||
setHighlight: (highlight: HighlightState) => void,
|
||||
): ItemData => {
|
||||
const leftSymbol = left ? display.displaySymbol(result.left!, left) : null;
|
||||
const rightSymbol = right
|
||||
? display.displaySymbol(result.right!, right)
|
||||
: null;
|
||||
const itemCount = Math.max(
|
||||
leftSymbol?.rowCount || 0,
|
||||
rightSymbol?.rowCount || 0,
|
||||
);
|
||||
const symbolName = leftSymbol?.name || rightSymbol?.name || '';
|
||||
const config = buildDiffConfig(null);
|
||||
const matchPercent = rightSymbol?.matchPercent;
|
||||
return {
|
||||
itemCount,
|
||||
symbolName,
|
||||
result,
|
||||
config,
|
||||
matchPercent,
|
||||
left,
|
||||
leftSymbol,
|
||||
right,
|
||||
rightSymbol,
|
||||
highlight,
|
||||
setHighlight,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const SymbolLabel = ({
|
||||
symbol,
|
||||
}: {
|
||||
symbol: display.SymbolDisplay | null;
|
||||
}) => {
|
||||
if (!symbol) {
|
||||
return (
|
||||
<span className={clsx(headerStyles.label, headerStyles.missing)}>
|
||||
Missing
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const displayName = symbol.demangledName || symbol.name;
|
||||
return (
|
||||
<span
|
||||
className={clsx(headerStyles.label, headerStyles.emphasized)}
|
||||
title={displayName}
|
||||
>
|
||||
{displayName}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const FunctionView = ({
|
||||
diff,
|
||||
left,
|
||||
right,
|
||||
}: {
|
||||
diff: diff.DiffResult;
|
||||
left: display.SectionDisplaySymbol | null;
|
||||
right: display.SectionDisplaySymbol | null;
|
||||
}) => {
|
||||
const { buildRunning, currentUnit, lastBuilt, hasProjectConfig } =
|
||||
useExtensionStore(
|
||||
useShallow((state) => ({
|
||||
buildRunning: state.buildRunning,
|
||||
currentUnit: state.currentUnit,
|
||||
lastBuilt: state.lastBuilt,
|
||||
hasProjectConfig: state.projectConfig != null,
|
||||
})),
|
||||
);
|
||||
const currentUnitName = currentUnit?.name || '';
|
||||
const { highlight, setSelectedSymbol, setSymbolScrollOffset, setHighlight } =
|
||||
useAppStore(
|
||||
useShallow((state) => ({
|
||||
highlight: state.highlight,
|
||||
setSelectedSymbol: state.setSelectedSymbol,
|
||||
setSymbolScrollOffset: state.setSymbolScrollOffset,
|
||||
setHighlight: state.setHighlight,
|
||||
})),
|
||||
);
|
||||
|
||||
const itemData = createItemData(diff, left, right, highlight, setHighlight);
|
||||
const initialScrollOffset = useMemo(
|
||||
() =>
|
||||
useAppStore.getState().getUnitState(currentUnitName).symbolScrollOffsets[
|
||||
itemData.symbolName
|
||||
] || 0,
|
||||
[currentUnitName, itemData.symbolName],
|
||||
);
|
||||
|
||||
const itemSize = useFontSize() * 1.33;
|
||||
return (
|
||||
<>
|
||||
<div className={headerStyles.header}>
|
||||
<div className={headerStyles.column}>
|
||||
<div className={headerStyles.row}>
|
||||
<button title="Back" onClick={() => setSelectedSymbol(null, null)}>
|
||||
<span className="codicon codicon-chevron-left" />
|
||||
</button>
|
||||
</div>
|
||||
<div className={headerStyles.row}>
|
||||
<SymbolLabel symbol={itemData.leftSymbol} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={headerStyles.column}>
|
||||
<div className={headerStyles.row}>
|
||||
{hasProjectConfig && (
|
||||
<button
|
||||
title="Build"
|
||||
onClick={() => runBuild()}
|
||||
disabled={buildRunning}
|
||||
>
|
||||
<span className="codicon codicon-refresh" />
|
||||
</button>
|
||||
)}
|
||||
{lastBuilt && (
|
||||
<span className={headerStyles.label}>
|
||||
Last built: {new Date(lastBuilt).toLocaleTimeString('en-US')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={headerStyles.row}>
|
||||
{itemData.matchPercent !== undefined && (
|
||||
<>
|
||||
<span
|
||||
className={clsx(
|
||||
headerStyles.label,
|
||||
percentClass(itemData.matchPercent),
|
||||
)}
|
||||
>
|
||||
{Math.floor(itemData.matchPercent).toFixed(0)}%
|
||||
</span>
|
||||
{' | '}
|
||||
</>
|
||||
)}
|
||||
<SymbolLabel symbol={itemData.rightSymbol} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.instructionList}>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<FixedSizeList
|
||||
height={height}
|
||||
itemCount={itemData.itemCount}
|
||||
itemSize={itemSize}
|
||||
width={width}
|
||||
itemData={itemData}
|
||||
overscanCount={20}
|
||||
onScroll={(e) => {
|
||||
setSymbolScrollOffset(
|
||||
currentUnitName,
|
||||
itemData.symbolName,
|
||||
e.scrollOffset,
|
||||
);
|
||||
}}
|
||||
initialScrollOffset={initialScrollOffset}
|
||||
>
|
||||
{AsmRow}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
<TooltipShared
|
||||
id="instruction-tooltip"
|
||||
callback={(content) => {
|
||||
const data: InstructionTooltipContent = JSON.parse(content);
|
||||
let obj: diff.ObjectDiff | undefined;
|
||||
let symbol: display.SectionDisplaySymbol | undefined;
|
||||
switch (data.column) {
|
||||
case 0:
|
||||
obj = diff.left;
|
||||
symbol = itemData.left ?? undefined;
|
||||
break;
|
||||
case 1:
|
||||
obj = diff.right;
|
||||
symbol = itemData.right ?? undefined;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (!obj || !symbol) {
|
||||
return null;
|
||||
}
|
||||
return display.instructionHover(
|
||||
obj,
|
||||
symbol,
|
||||
data.row,
|
||||
itemData.config,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FunctionView;
|
||||
@@ -0,0 +1,20 @@
|
||||
.container {
|
||||
flex: 1 1 0;
|
||||
padding: 0 1em;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.category-header {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
.property {
|
||||
margin: 0.5em 1em;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import headerStyles from '../common/Header.module.css';
|
||||
import styles from './SettingsView.module.css';
|
||||
|
||||
import { version as wasmVersion } from 'objdiff-wasm';
|
||||
import {
|
||||
CONFIG_SCHEMA,
|
||||
type ConfigPropertyBoolean,
|
||||
type ConfigPropertyChoice,
|
||||
} from '../../shared/config';
|
||||
import {
|
||||
inVsCode,
|
||||
openSettings,
|
||||
setConfigProperty,
|
||||
useAppStore,
|
||||
useExtensionStore,
|
||||
} from '../state';
|
||||
|
||||
const BooleanProperty = ({
|
||||
property,
|
||||
value,
|
||||
}: { property: ConfigPropertyBoolean; value: boolean }) => (
|
||||
<div className={styles.property} title={property.description}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={property.id}
|
||||
checked={value}
|
||||
onChange={(e) => {
|
||||
const value = e.target.checked;
|
||||
if (value === property.default) {
|
||||
setConfigProperty(property.id, undefined);
|
||||
} else {
|
||||
setConfigProperty(property.id, value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label htmlFor={property.id}>{property.name}</label>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ChoiceProperty = ({
|
||||
property,
|
||||
value,
|
||||
}: { property: ConfigPropertyChoice; value: string }) => (
|
||||
<div className={styles.property} title={property.description}>
|
||||
<label htmlFor={property.id}>{property.name}</label>
|
||||
<select
|
||||
id={property.id}
|
||||
defaultValue={value}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === property.default) {
|
||||
setConfigProperty(property.id, undefined);
|
||||
} else {
|
||||
setConfigProperty(property.id, value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{property.items.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
|
||||
const SettingsView = () => {
|
||||
const setCurrentView = useAppStore((state) => state.setCurrentView);
|
||||
const configProperties = useExtensionStore((state) => state.configProperties);
|
||||
|
||||
const items = [];
|
||||
for (const group of CONFIG_SCHEMA.groups) {
|
||||
items.push(
|
||||
<h2 className={styles.categoryHeader} key={group.id}>
|
||||
{group.name}
|
||||
</h2>,
|
||||
);
|
||||
for (const id of group.properties) {
|
||||
const property = CONFIG_SCHEMA.properties.find((p) => p.id === id);
|
||||
if (!property) {
|
||||
continue;
|
||||
}
|
||||
const value = configProperties[property.id] ?? property.default;
|
||||
switch (property.type) {
|
||||
case 'boolean':
|
||||
items.push(
|
||||
<BooleanProperty
|
||||
key={property.id}
|
||||
property={property}
|
||||
value={value as boolean}
|
||||
/>,
|
||||
);
|
||||
break;
|
||||
case 'choice':
|
||||
items.push(
|
||||
<ChoiceProperty
|
||||
key={property.id}
|
||||
property={property}
|
||||
value={value as string}
|
||||
/>,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={headerStyles.header}>
|
||||
<button title="Back" onClick={() => setCurrentView('main')}>
|
||||
<span className="codicon codicon-chevron-left" />
|
||||
</button>
|
||||
{inVsCode && (
|
||||
<button onClick={() => openSettings()}>Open in Editor</button>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.container}>
|
||||
<h1 className={styles.header}>About</h1>
|
||||
{window.webviewProps?.extensionVersion && (
|
||||
<p>
|
||||
<strong>Extension version:</strong>{' '}
|
||||
{window.webviewProps.extensionVersion}
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
<strong>objdiff version:</strong> {wasmVersion()}
|
||||
</p>
|
||||
<h1 className={styles.header}>Settings</h1>
|
||||
{items}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsView;
|
||||
@@ -0,0 +1,76 @@
|
||||
.symbols {
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.symbol-list {
|
||||
flex: 1 1 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.symbol-list-row {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
height: var(--list-row-height);
|
||||
|
||||
font-family: var(--code-font-family);
|
||||
font-weight: var(--code-font-weight);
|
||||
font-size: var(--code-font-size);
|
||||
text-wrap: nowrap;
|
||||
white-space: pre;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--list-row-hover-background);
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
padding-left: 0.5em;
|
||||
|
||||
&::before {
|
||||
content: "▼ ";
|
||||
}
|
||||
&.collapsed {
|
||||
opacity: 0.75;
|
||||
&::before {
|
||||
content: "▶ ";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.symbol {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.flag-local {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.flag-global {
|
||||
color: var(--color-green);
|
||||
}
|
||||
|
||||
.flag-weak {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.flag-common {
|
||||
color: var(--color-blue);
|
||||
}
|
||||
|
||||
.symbol-name {
|
||||
color: var(--color-bright);
|
||||
}
|
||||
|
||||
.no-object {
|
||||
color: var(--color-blue);
|
||||
|
||||
font-family: var(--code-font-family);
|
||||
font-weight: var(--code-font-weight);
|
||||
font-size: var(--code-font-size);
|
||||
text-wrap: nowrap;
|
||||
white-space: pre;
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
import headerStyles from '../common/Header.module.css';
|
||||
import styles from './SymbolsView.module.css';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { type diff, display } from 'objdiff-wasm';
|
||||
import { memo, useMemo } from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import {
|
||||
FixedSizeList,
|
||||
type ListChildComponentProps,
|
||||
areEqual,
|
||||
} from 'react-window';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import TooltipShared from '../common/TooltipShared';
|
||||
import {
|
||||
type UnitScrollOffsets,
|
||||
runBuild,
|
||||
setCurrentUnit,
|
||||
useAppStore,
|
||||
useExtensionStore,
|
||||
} from '../state';
|
||||
import { percentClass, useFontSize } from '../util/util';
|
||||
|
||||
const SectionRow = ({
|
||||
section,
|
||||
style,
|
||||
onClick,
|
||||
}: {
|
||||
section: SectionData;
|
||||
style?: React.CSSProperties;
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||
}) => {
|
||||
let percentElem = null;
|
||||
if (section.matchPercent != null) {
|
||||
percentElem = (
|
||||
<>
|
||||
{' ('}
|
||||
<span className={percentClass(section.matchPercent)}>
|
||||
{Math.floor(section.matchPercent).toFixed(0)}%
|
||||
</span>
|
||||
{')'}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.symbolListRow, styles.section, {
|
||||
[styles.collapsed]: section.collapsed,
|
||||
})}
|
||||
style={style}
|
||||
onClick={onClick}
|
||||
>
|
||||
{section.name} ({section.size.toString(16)}){percentElem}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SymbolRow = ({
|
||||
obj,
|
||||
section,
|
||||
symbolRef,
|
||||
side,
|
||||
style,
|
||||
}: {
|
||||
obj: diff.ObjectDiff;
|
||||
section: SectionData;
|
||||
symbolRef: display.SectionDisplaySymbol;
|
||||
side: keyof UnitScrollOffsets;
|
||||
style?: React.CSSProperties;
|
||||
}) => {
|
||||
const setSelectedSymbol = useAppStore((state) => state.setSelectedSymbol);
|
||||
const symbol = display.displaySymbol(obj, symbolRef);
|
||||
const flags = [];
|
||||
if (symbol.flags.global) {
|
||||
flags.push(
|
||||
<span key="g" className={styles.flagGlobal}>
|
||||
g
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
if (symbol.flags.weak) {
|
||||
flags.push(
|
||||
<span key="w" className={styles.flagWeak}>
|
||||
w
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
if (symbol.flags.local) {
|
||||
flags.push(
|
||||
<span key="l" className={styles.flagLocal}>
|
||||
l
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
if (symbol.flags.common) {
|
||||
flags.push(
|
||||
<span key="c" className={styles.flagCommon}>
|
||||
c
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
let flagsElem = null;
|
||||
if (flags.length > 0) {
|
||||
flagsElem = <>[{flags}] </>;
|
||||
}
|
||||
let percentElem = null;
|
||||
if (symbol.matchPercent != null) {
|
||||
percentElem = (
|
||||
<>
|
||||
{'('}
|
||||
<span className={percentClass(symbol.matchPercent)}>
|
||||
{Math.floor(symbol.matchPercent).toFixed(0)}%
|
||||
</span>
|
||||
{') '}
|
||||
</>
|
||||
);
|
||||
}
|
||||
const tooltipContent: SymbolTooltipContent = {
|
||||
symbolRef,
|
||||
side,
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.symbolListRow, styles.symbol)}
|
||||
style={style}
|
||||
onClick={() => {
|
||||
setSelectedSymbol(
|
||||
{
|
||||
symbolName: symbol.name,
|
||||
sectionName: section.name,
|
||||
},
|
||||
{
|
||||
symbolName: symbol.name,
|
||||
sectionName: section.name,
|
||||
},
|
||||
);
|
||||
}}
|
||||
data-vscode-context={JSON.stringify({
|
||||
contextType: 'symbol',
|
||||
preventDefaultContextMenuItems: true,
|
||||
symbolName: symbol.name,
|
||||
symbolDemangledName: symbol.demangledName,
|
||||
})}
|
||||
data-tooltip-id="symbol-tooltip"
|
||||
data-tooltip-content={JSON.stringify(tooltipContent)}
|
||||
>
|
||||
{flagsElem}
|
||||
{percentElem}
|
||||
<span className={styles.symbolName}>
|
||||
{symbol.demangledName || symbol.name}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type SymbolTooltipContent = {
|
||||
symbolRef: display.SectionDisplaySymbol;
|
||||
side: keyof UnitScrollOffsets;
|
||||
};
|
||||
|
||||
type SectionData = display.SectionDisplay & { collapsed: boolean };
|
||||
|
||||
type ItemData = {
|
||||
obj: diff.ObjectDiff | undefined;
|
||||
itemCount: number;
|
||||
sections: SectionData[];
|
||||
side: keyof UnitScrollOffsets;
|
||||
setSectionCollapsed: (section: string, collapsed: boolean) => void;
|
||||
};
|
||||
|
||||
const SymbolListRow = memo(
|
||||
({
|
||||
index,
|
||||
style,
|
||||
data: { obj, sections, side, setSectionCollapsed },
|
||||
}: ListChildComponentProps<ItemData>) => {
|
||||
if (!obj) {
|
||||
return null;
|
||||
}
|
||||
let currentIndex = 0;
|
||||
for (const section of sections) {
|
||||
if (currentIndex === index) {
|
||||
return (
|
||||
<SectionRow
|
||||
section={section}
|
||||
style={style}
|
||||
onClick={() => setSectionCollapsed(section.id, !section.collapsed)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
currentIndex++;
|
||||
if (section.collapsed) {
|
||||
continue;
|
||||
}
|
||||
if (index < currentIndex + section.symbols.length) {
|
||||
const symbolRef = section.symbols[index - currentIndex];
|
||||
return (
|
||||
<SymbolRow
|
||||
obj={obj}
|
||||
section={section}
|
||||
symbolRef={symbolRef}
|
||||
side={side}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
currentIndex += section.symbols.length;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
areEqual,
|
||||
);
|
||||
|
||||
const createItemDataFn = (
|
||||
obj: diff.ObjectDiff | undefined,
|
||||
collapsedSections: Record<string, boolean>,
|
||||
search: string | null,
|
||||
side: keyof UnitScrollOffsets,
|
||||
setSectionCollapsed: (section: string, collapsed: boolean) => void,
|
||||
): ItemData => {
|
||||
if (!obj) {
|
||||
return {
|
||||
obj,
|
||||
itemCount: 0,
|
||||
sections: [],
|
||||
side,
|
||||
setSectionCollapsed,
|
||||
};
|
||||
}
|
||||
const displaySections = display.displaySections(
|
||||
obj,
|
||||
{
|
||||
mapping: undefined,
|
||||
regex: search ?? undefined,
|
||||
},
|
||||
{
|
||||
showHiddenSymbols: false,
|
||||
showMappedSymbols: false,
|
||||
reverseFnOrder: false,
|
||||
},
|
||||
);
|
||||
let itemCount = 0;
|
||||
const sections: SectionData[] = [];
|
||||
for (const section of displaySections) {
|
||||
itemCount++;
|
||||
if (search !== null && section.symbols.length === 0) {
|
||||
continue;
|
||||
}
|
||||
if (collapsedSections[section.id]) {
|
||||
sections.push({
|
||||
...section,
|
||||
symbols: [],
|
||||
collapsed: true,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
itemCount += section.symbols.length;
|
||||
sections.push({
|
||||
...section,
|
||||
collapsed: false,
|
||||
});
|
||||
}
|
||||
return {
|
||||
obj,
|
||||
itemCount,
|
||||
sections,
|
||||
side,
|
||||
setSectionCollapsed,
|
||||
};
|
||||
};
|
||||
const createItemDataLeft = memoizeOne(createItemDataFn);
|
||||
const createItemDataRight = memoizeOne(createItemDataFn);
|
||||
|
||||
const SymbolsView = ({ diff }: { diff: diff.DiffResult }) => {
|
||||
const { buildRunning, currentUnit, hasProjectConfig } = useExtensionStore(
|
||||
useShallow((state) => ({
|
||||
buildRunning: state.buildRunning,
|
||||
currentUnit: state.currentUnit,
|
||||
hasProjectConfig: state.projectConfig != null,
|
||||
})),
|
||||
);
|
||||
const currentUnitName = currentUnit?.name || '';
|
||||
const {
|
||||
collapsedSections,
|
||||
search,
|
||||
setUnitSectionCollapsed,
|
||||
setUnitScrollOffset,
|
||||
setUnitSearch,
|
||||
setCurrentView,
|
||||
} = useAppStore(
|
||||
useShallow((state) => {
|
||||
const unit = state.getUnitState(currentUnitName);
|
||||
return {
|
||||
collapsedSections: unit.collapsedSections,
|
||||
search: unit.search,
|
||||
setUnitSectionCollapsed: state.setUnitSectionCollapsed,
|
||||
setUnitScrollOffset: state.setUnitScrollOffset,
|
||||
setUnitSearch: state.setUnitSearch,
|
||||
setCurrentView: state.setCurrentView,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const initialScrollOffsets = useMemo(
|
||||
() => useAppStore.getState().getUnitState(currentUnitName).scrollOffsets,
|
||||
[currentUnitName],
|
||||
);
|
||||
const setLeftSectionCollapsed = useMemo(
|
||||
() => (section: string, collapsed: boolean) => {
|
||||
setUnitSectionCollapsed(currentUnitName, section, 'left', collapsed);
|
||||
},
|
||||
[currentUnitName, setUnitSectionCollapsed],
|
||||
);
|
||||
const setRightSectionCollapsed = useMemo(
|
||||
() => (section: string, collapsed: boolean) => {
|
||||
setUnitSectionCollapsed(currentUnitName, section, 'right', collapsed);
|
||||
},
|
||||
[currentUnitName, setUnitSectionCollapsed],
|
||||
);
|
||||
|
||||
const renderList = (
|
||||
height: number,
|
||||
width: number,
|
||||
itemData: ItemData,
|
||||
side: keyof UnitScrollOffsets,
|
||||
) => {
|
||||
if (!itemData.obj) {
|
||||
return (
|
||||
<div className={clsx(styles.symbolList, styles.noObject)}>
|
||||
No object configured
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FixedSizeList
|
||||
key={`${side}-${currentUnitName}`}
|
||||
className={styles.symbolList}
|
||||
height={height - 1}
|
||||
itemCount={itemData.itemCount}
|
||||
itemSize={itemSize}
|
||||
width={width / 2}
|
||||
itemData={itemData}
|
||||
overscanCount={20}
|
||||
onScroll={(e) => {
|
||||
if (currentUnitName) {
|
||||
setUnitScrollOffset(currentUnitName, side, e.scrollOffset);
|
||||
}
|
||||
}}
|
||||
initialScrollOffset={initialScrollOffsets[side]}
|
||||
>
|
||||
{SymbolListRow}
|
||||
</FixedSizeList>
|
||||
);
|
||||
};
|
||||
|
||||
const itemSize = useFontSize() * 1.33;
|
||||
const leftItemData = createItemDataLeft(
|
||||
diff.left,
|
||||
collapsedSections.left,
|
||||
search,
|
||||
'left',
|
||||
setLeftSectionCollapsed,
|
||||
);
|
||||
const rightItemData = createItemDataRight(
|
||||
diff.right,
|
||||
collapsedSections.right,
|
||||
search,
|
||||
'right',
|
||||
setRightSectionCollapsed,
|
||||
);
|
||||
|
||||
const unitNameRow = (
|
||||
<div className={headerStyles.row}>
|
||||
<span className={clsx(headerStyles.label, headerStyles.emphasized)}>
|
||||
{currentUnitName}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const filterRow = (
|
||||
<div className={headerStyles.row}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter symbols"
|
||||
value={search || ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setUnitSearch(currentUnitName, value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const settingsRow = (
|
||||
<div className={headerStyles.row}>
|
||||
<button title="Settings" onClick={() => setCurrentView('settings')}>
|
||||
<span className="codicon codicon-settings-gear" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={headerStyles.header}>
|
||||
<div className={headerStyles.column}>
|
||||
<div className={headerStyles.row}>
|
||||
{hasProjectConfig ? (
|
||||
<button
|
||||
title="Back"
|
||||
onClick={() => setCurrentUnit(null)}
|
||||
disabled={buildRunning}
|
||||
>
|
||||
<span className="codicon codicon-chevron-left" />
|
||||
</button>
|
||||
) : null}
|
||||
<span className={headerStyles.label}>Target object</span>
|
||||
</div>
|
||||
{currentUnitName ? unitNameRow : filterRow}
|
||||
</div>
|
||||
<div className={headerStyles.column}>
|
||||
<div className={headerStyles.row}>
|
||||
{hasProjectConfig && (
|
||||
<button
|
||||
title="Build"
|
||||
onClick={() => runBuild()}
|
||||
disabled={buildRunning}
|
||||
>
|
||||
<span className="codicon codicon-refresh" />
|
||||
</button>
|
||||
)}
|
||||
<span className={headerStyles.label}>Base object</span>
|
||||
</div>
|
||||
{currentUnitName ? filterRow : settingsRow}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.symbols}>
|
||||
<AutoSizer className={styles.symbols}>
|
||||
{({ height, width }) => (
|
||||
<>
|
||||
{renderList(height, width, leftItemData, 'left')}
|
||||
{renderList(height, width, rightItemData, 'right')}
|
||||
</>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
<TooltipShared
|
||||
id="symbol-tooltip"
|
||||
callback={(content) => {
|
||||
const data: SymbolTooltipContent = JSON.parse(content);
|
||||
let obj: diff.ObjectDiff | undefined;
|
||||
switch (data.side) {
|
||||
case 'left':
|
||||
obj = diff.left;
|
||||
break;
|
||||
case 'right':
|
||||
obj = diff.right;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (!obj) {
|
||||
return null;
|
||||
}
|
||||
return display.symbolHover(obj, data.symbolRef);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SymbolsView;
|
||||
@@ -0,0 +1,58 @@
|
||||
.units {
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
height: var(--list-row-height);
|
||||
padding-left: 0.5em;
|
||||
|
||||
font-family: var(--code-font-family, monospace);
|
||||
font-weight: var(--code-font-weight, normal);
|
||||
font-size: var(--code-font-size);
|
||||
text-wrap: nowrap;
|
||||
white-space: pre;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--list-row-hover-background);
|
||||
}
|
||||
}
|
||||
|
||||
.indent {
|
||||
display: inline-block;
|
||||
height: var(--list-row-height);
|
||||
width: 0;
|
||||
border-right: 1px solid var(--panel-separator);
|
||||
margin-left: 0.5em;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: inline-block;
|
||||
height: var(--list-row-height);
|
||||
width: 1em;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.unit {
|
||||
.label {
|
||||
margin-left: 0.4em;
|
||||
}
|
||||
}
|
||||
|
||||
.complete {
|
||||
color: var(--color-green);
|
||||
}
|
||||
|
||||
.incomplete {
|
||||
color: var(--color-red);
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
opacity: 0.75;
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import styles from './UnitsView.module.css';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { memo, useMemo } from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import {
|
||||
FixedSizeList,
|
||||
type ListChildComponentProps,
|
||||
areEqual,
|
||||
} from 'react-window';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import type { ProjectConfig, Unit } from '../../shared/config';
|
||||
import headerStyles from '../common/Header.module.css';
|
||||
import {
|
||||
quickPickUnit,
|
||||
setCurrentUnit,
|
||||
useAppStore,
|
||||
useExtensionStore,
|
||||
} from '../state';
|
||||
import { useFontSize } from '../util/util';
|
||||
|
||||
type DirectoryItem = {
|
||||
type: 'directory';
|
||||
indent: number;
|
||||
label: string;
|
||||
id: string;
|
||||
collapsed: boolean;
|
||||
};
|
||||
|
||||
type UnitItem = {
|
||||
type: 'unit';
|
||||
indent: number;
|
||||
label: string;
|
||||
id: string;
|
||||
unit: Unit;
|
||||
};
|
||||
|
||||
type Item = DirectoryItem | UnitItem;
|
||||
|
||||
type ItemData = {
|
||||
unitsCount: number;
|
||||
items: Item[];
|
||||
};
|
||||
|
||||
const UnitRow = memo(
|
||||
({ index, style, data: { items } }: ListChildComponentProps<ItemData>) => {
|
||||
const setCollapsedUnit = useAppStore((state) => state.setCollapsedUnit);
|
||||
const item = items[index];
|
||||
const classes = [styles.row];
|
||||
if (item.type === 'unit') {
|
||||
classes.push(styles.unit);
|
||||
if (item.unit.metadata?.complete !== undefined) {
|
||||
if (item.unit.metadata.complete) {
|
||||
classes.push(styles.complete);
|
||||
} else {
|
||||
classes.push(styles.incomplete);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// classes.push(styles.directory);
|
||||
if (item.collapsed) {
|
||||
classes.push(styles.collapsed);
|
||||
}
|
||||
}
|
||||
const indentItems = [];
|
||||
for (let i = 0; i < item.indent; i++) {
|
||||
indentItems.push(<span key={i} className={styles.indent} />);
|
||||
}
|
||||
if (item.type === 'directory') {
|
||||
indentItems.push(
|
||||
<span
|
||||
key="toggle"
|
||||
className={clsx(
|
||||
styles.toggle,
|
||||
'codicon',
|
||||
item.collapsed ? 'codicon-chevron-right' : 'codicon-chevron-down',
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={clsx(classes)}
|
||||
style={style}
|
||||
onClick={() => {
|
||||
if (item.type === 'unit') setCurrentUnit(item.unit);
|
||||
else setCollapsedUnit(item.id, !item.collapsed);
|
||||
}}
|
||||
>
|
||||
{indentItems}
|
||||
<span className={styles.label}>{item.label}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
areEqual,
|
||||
);
|
||||
|
||||
type TreeItem = DirectoryItem & { children: (TreeItem | UnitItem)[] };
|
||||
|
||||
function pushTreeItems(item: TreeItem | UnitItem, out: Item[]) {
|
||||
if (item.type === 'directory') {
|
||||
out.push(item);
|
||||
if (!item.collapsed) {
|
||||
for (const child of item.children) {
|
||||
pushTreeItems(child, out);
|
||||
}
|
||||
}
|
||||
} else if (item.type === 'unit') {
|
||||
out.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
const createItemData = memoizeOne(
|
||||
(
|
||||
config: ProjectConfig | null,
|
||||
collapsedUnits: Record<string, boolean>,
|
||||
): ItemData => {
|
||||
const units = config?.units ?? [];
|
||||
const map = new Map<string, TreeItem>();
|
||||
const rootItems = [];
|
||||
for (const unit of units) {
|
||||
const path = unit.name || '';
|
||||
const split = path.split('/');
|
||||
let parent: TreeItem | null = null;
|
||||
for (let i = 0; i < split.length - 1; i++) {
|
||||
const name = split[i];
|
||||
const key = split.slice(0, i + 1).join('/');
|
||||
let item = map.get(key);
|
||||
if (!item) {
|
||||
item = {
|
||||
type: 'directory',
|
||||
indent: i,
|
||||
label: name,
|
||||
id: key,
|
||||
collapsed: !!collapsedUnits[key],
|
||||
children: [],
|
||||
};
|
||||
if (parent) {
|
||||
parent.children.push(item);
|
||||
} else {
|
||||
rootItems.push(item);
|
||||
}
|
||||
map.set(key, item);
|
||||
}
|
||||
parent = item;
|
||||
}
|
||||
const unitItem: UnitItem = {
|
||||
type: 'unit',
|
||||
indent: split.length - 1,
|
||||
label: split[split.length - 1],
|
||||
id: path,
|
||||
unit,
|
||||
};
|
||||
if (parent) {
|
||||
parent.children.push(unitItem);
|
||||
} else {
|
||||
rootItems.push(unitItem);
|
||||
}
|
||||
}
|
||||
const items: Item[] = [];
|
||||
for (const item of rootItems) {
|
||||
pushTreeItems(item, items);
|
||||
}
|
||||
return { unitsCount: units.length, items };
|
||||
},
|
||||
);
|
||||
|
||||
const UnitsView = () => {
|
||||
const config = useExtensionStore((state) => state.projectConfig);
|
||||
const { collapsedUnits, setCurrentView, setUnitsScrollOffset } = useAppStore(
|
||||
useShallow((state) => ({
|
||||
collapsedUnits: state.collapsedUnits,
|
||||
setCurrentView: state.setCurrentView,
|
||||
setUnitsScrollOffset: state.setUnitsScrollOffset,
|
||||
})),
|
||||
);
|
||||
const initialScrollOffset = useMemo(
|
||||
() => useAppStore.getState().unitsScrollOffset,
|
||||
[],
|
||||
);
|
||||
const itemSize = useFontSize() * 1.33;
|
||||
const itemData = createItemData(config, collapsedUnits);
|
||||
return (
|
||||
<>
|
||||
<div className={headerStyles.header}>
|
||||
<div className={headerStyles.column}>
|
||||
<div className={headerStyles.row}>
|
||||
<button onClick={() => setCurrentUnit('source')}>
|
||||
Current File
|
||||
</button>
|
||||
<button onClick={() => quickPickUnit()}>Quick Pick</button>
|
||||
<button title="Settings" onClick={() => setCurrentView('settings')}>
|
||||
<span className="codicon codicon-settings-gear" />
|
||||
</button>
|
||||
</div>
|
||||
<div className={headerStyles.row}>
|
||||
<span className={headerStyles.label}>
|
||||
{itemData.unitsCount} unit{itemData.unitsCount === 1 ? '' : 's'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.units}>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<FixedSizeList
|
||||
height={height}
|
||||
itemCount={itemData.items.length}
|
||||
itemSize={itemSize}
|
||||
width={width}
|
||||
itemData={itemData}
|
||||
overscanCount={20}
|
||||
onScroll={(e) => {
|
||||
setUnitsScrollOffset(e.scrollOffset);
|
||||
}}
|
||||
initialScrollOffset={initialScrollOffset}
|
||||
>
|
||||
{UnitRow}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnitsView;
|
||||
Reference in New Issue
Block a user