tui: add Tree component (collapsible device/POU listing)
<Tree project cursorPath expanded/> renders the project as a two-level collapsible list: device headers with POU count, and (when expanded) POU rows with name + kind + LOC count. Cursor row marked with a leading triangle. devicePath()/pouPath() are exported keypath helpers used by the tree state machine to address rows uniformly.
This commit is contained in:
parent
357c1d11c5
commit
9902cd6dcf
2 changed files with 102 additions and 0 deletions
44
src/tui/browser/Tree.tsx
Normal file
44
src/tui/browser/Tree.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { Project, POU } from '../shared/types.js';
|
||||
|
||||
export interface TreeProps {
|
||||
project: Project;
|
||||
cursorPath: string;
|
||||
expanded: Set<string>;
|
||||
}
|
||||
|
||||
export function devicePath(deviceName: string): string {
|
||||
return `device:${deviceName}`;
|
||||
}
|
||||
export function pouPath(deviceName: string, relPath: string): string {
|
||||
return `pou:${deviceName}:${relPath}`;
|
||||
}
|
||||
|
||||
export function Tree({ project, cursorPath, expanded }: TreeProps): React.ReactElement {
|
||||
const rows: React.ReactElement[] = [];
|
||||
for (const dev of project.devices) {
|
||||
const dPath = devicePath(dev.name);
|
||||
const isExpanded = expanded.has(dPath);
|
||||
const isCursor = cursorPath === dPath;
|
||||
rows.push(
|
||||
<Text key={dPath}>
|
||||
{isCursor ? '▶ ' : ' '}
|
||||
{isExpanded ? '▾ ' : '▸ '}
|
||||
{dev.name} {dev.pous.length} POUs
|
||||
</Text>
|
||||
);
|
||||
if (!isExpanded) continue;
|
||||
for (const p of dev.pous) {
|
||||
const pPath = pouPath(dev.name, p.relPath);
|
||||
const isPCursor = cursorPath === pPath;
|
||||
rows.push(
|
||||
<Text key={pPath}>
|
||||
{isPCursor ? '▶ ' : ' '}
|
||||
{p.name.padEnd(18)} {p.kind.padEnd(6)} {String(p.loc).padStart(4)} L
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
}
|
||||
return <Box flexDirection="column">{rows}</Box>;
|
||||
}
|
||||
58
tests/tui/Tree.test.tsx
Normal file
58
tests/tui/Tree.test.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import React from 'react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { Tree } from '../../src/tui/browser/Tree.tsx';
|
||||
import { Project } from '../../src/tui/shared/types.ts';
|
||||
|
||||
const project: Project = {
|
||||
rootDir: '/p',
|
||||
mirrorMtimeMs: 0,
|
||||
devices: [
|
||||
{
|
||||
name: 'CodesysRpi',
|
||||
pous: [
|
||||
{ name: 'PLC_PRG', kind: 'PRG', relPath: 'a/PLC_PRG.st', absPath: '/abs/a/PLC_PRG.st', loc: 5, mtimeMs: 0 },
|
||||
{ name: 'FB_Test', kind: 'FB', relPath: 'a/FB_Test.st', absPath: '/abs/a/FB_Test.st', loc: 87, mtimeMs: 0 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('<Tree>', () => {
|
||||
it('renders devices and POU rows when expanded', () => {
|
||||
const { lastFrame } = render(
|
||||
<Tree
|
||||
project={project}
|
||||
cursorPath="device:CodesysRpi"
|
||||
expanded={new Set(['device:CodesysRpi'])}
|
||||
/>
|
||||
);
|
||||
const out = lastFrame()!;
|
||||
expect(out).toContain('CodesysRpi');
|
||||
expect(out).toContain('PLC_PRG');
|
||||
expect(out).toContain('FB_Test');
|
||||
expect(out).toContain('PRG');
|
||||
expect(out).toContain('FB');
|
||||
});
|
||||
|
||||
it('does not show POU rows when device is collapsed', () => {
|
||||
const { lastFrame } = render(
|
||||
<Tree project={project} cursorPath="device:CodesysRpi" expanded={new Set()} />
|
||||
);
|
||||
const out = lastFrame()!;
|
||||
expect(out).toContain('CodesysRpi');
|
||||
expect(out).not.toContain('PLC_PRG');
|
||||
});
|
||||
|
||||
it('marks the cursor row', () => {
|
||||
const { lastFrame } = render(
|
||||
<Tree
|
||||
project={project}
|
||||
cursorPath="pou:CodesysRpi:a/FB_Test.st"
|
||||
expanded={new Set(['device:CodesysRpi'])}
|
||||
/>
|
||||
);
|
||||
const out = lastFrame()!;
|
||||
expect(out).toMatch(/▶ FB_Test/);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue