Skip to content

Commit

Permalink
Display test out in editor to preserve formatting
Browse files Browse the repository at this point in the history
  • Loading branch information
netalondon committed Jan 15, 2024
1 parent 32a45e6 commit 11210af
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 19 deletions.
164 changes: 152 additions & 12 deletions components/src/compare.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
import { isErr, Ok } from "@davidsouther/jiffies/lib/esm/result.js";
import { CMP } from "@nand2tetris/simulator/languages/cmp.js";
import { Span } from "@nand2tetris/simulator/languages/base";
import { Cmp, CMP } from "@nand2tetris/simulator/languages/cmp.js";

export const compare = (cmp: string, out: string) => {
const cmpResult = CMP.parse(cmp);
const outResult = CMP.parse(out);
interface Diff {
row: number;
col: number;
expected: string;
given: string;
}

if (isErr(cmpResult) || isErr(outResult)) {
return false;
}
interface DiffLineDisplay {
expectedLine: string;
givenLine: string;
correctCellSpans: Span[];
incorrectCellSpans: Span[];
}

const cmpData = Ok(cmpResult);
const outData = Ok(outResult);
export interface DiffDisplay {
text: string;
failureNum: number;
correctCellSpans: Span[];
incorrectCellSpans: Span[];
}

function getDiffs(cmpData: Cmp, outData: Cmp): Diff[] {
const diffs: Diff[] = [];

for (let i = 0; i < Math.min(cmpData.length, outData.length); i++) {
const cmpI = cmpData[i] ?? [];
Expand All @@ -22,9 +36,135 @@ export const compare = (cmp: string, out: string) => {
if (
!(cmpJ?.trim().match(/^\*+$/) !== null || outJ?.trim() === cmpJ?.trim())
) {
return false;
diffs.push({ row: i, col: j, expected: cmpJ, given: outJ });
}
}
}
return true;
};
return diffs;
}

export function compare(cmp: string, out: string) {
const cmpResult = CMP.parse(cmp);
const outResult = CMP.parse(out);

if (isErr(cmpResult) || isErr(outResult)) {
return false;
}

const cmpData = Ok(cmpResult);
const outData = Ok(outResult);

return getDiffs(cmpData, outData).length == 0;
}

export function generateDiffs(cmp: string, out: string): DiffDisplay {
const cmpResult = CMP.parse(cmp);
const outResult = CMP.parse(out);

if (isErr(cmpResult) || isErr(outResult)) {
return {
text: "",
failureNum: 0,
correctCellSpans: [],
incorrectCellSpans: [],
};
}

const cmpData = Ok(cmpResult);
const outData = Ok(outResult);

const diffs = getDiffs(cmpData, outData);

const diffsByLine: Map<number, Diff[]> = new Map();
for (const diff of diffs) {
const lineDiffs = diffsByLine.get(diff.row);
if (lineDiffs) {
lineDiffs.push(diff);
} else {
diffsByLine.set(diff.row, [diff]);
}
}

const lines = out.split("\n");
const diffLines: Map<number, DiffLineDisplay> = new Map();
for (const [row, diffs] of diffsByLine) {
diffLines.set(row, generateDiffLine(lines[row], diffs));
}

const finalLines: string[] = [];
let lineStart = 0;
const correctCellSpans: Span[] = [];
const incorrectCellSpans: Span[] = [];

for (let i = 0; i < lines.length; i++) {
const diffLine = diffLines.get(i);
if (diffLine) {
finalLines.push(diffLine.expectedLine);
correctCellSpans.push(
...diffLine.correctCellSpans.map((span) => {
return {
start: span.start + lineStart,
end: span.end + lineStart,
line: span.line,
};
})
);
lineStart += diffLine.expectedLine.length + 1; // +1 for the newline character
finalLines.push(diffLine.givenLine);
incorrectCellSpans.push(
...diffLine.correctCellSpans.map((span) => {
return {
start: span.start + lineStart,
end: span.end + lineStart,
line: span.line,
};
})
);
lineStart += diffLine.givenLine.length + 1;
} else {
finalLines.push(lines[i]);
lineStart += lines[i].length + 1;
}
}

return {
text: finalLines.join("\n"),
failureNum: diffs.length,
correctCellSpans,
incorrectCellSpans,
};
}

function generateDiffLine(original: string, diffs: Diff[]): DiffLineDisplay {
const cells = original.split("|").filter((cell) => cell != "");
const newCells = cells.map((cell) => " ".repeat(cell.length));

const cellStarts: number[] = [];
let sum = 0;
for (let i = 0; i < cells.length; i++) {
cellStarts.push(sum + 1);
sum += cells[i].length + 1;
}

const correctCellSpans: Span[] = [];
const incorrectCellSpans: Span[] = [];

for (const diff of diffs) {
cells[diff.col] = diff.expected;
newCells[diff.col] = diff.given;

const span = {
start: cellStarts[diff.col],
end: cellStarts[diff.col] + diff.expected.length,
line: 0, // not used
};
correctCellSpans.push(span);
incorrectCellSpans.push(span);
}
return {
expectedLine: `|${cells.join("|")}|`,
givenLine: `|${newCells.join("|")}|`,
correctCellSpans,
incorrectCellSpans,
};
}
8 changes: 8 additions & 0 deletions web/src/shell/editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@
background-color: var(--mark-background-color);
}

.red {
color: rgb(190, 16, 16);
}

.green {
color: green;
}

textarea {
flex: 1;
}
Expand Down
40 changes: 35 additions & 5 deletions web/src/shell/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,16 @@ const Textarea = ({
const MONACO_LIGHT_THEME = "vs";
const MONACO_DARK_THEME = "vs-dark";

const makeHighlight = (
export interface Decoration {
span: Span;
cssClass: string;
}

const makeDecorations = (
monaco: typeof monacoT | null,
editor: monacoT.editor.IStandaloneCodeEditor | undefined,
highlight: Span | undefined,
additionalDecorations: Decoration[],
decorations: string[]
): string[] => {
if (!(editor && highlight)) return decorations;
Expand All @@ -83,6 +89,18 @@ const makeHighlight = (
editor.revealRangeInCenter(range);
}
}
for (const decoration of additionalDecorations) {
const range = monaco?.Range.fromPositions(
model.getPositionAt(decoration.span.start),
model.getPositionAt(decoration.span.end)
);
if (range) {
nextDecoration.push({
range,
options: { inlineClassName: decoration.cssClass },
});
}
}
return editor.deltaDecorations(decorations, nextDecoration);
};

Expand All @@ -94,6 +112,7 @@ const Monaco = ({
error,
disabled = false,
highlight: currentHighlight,
customDecorations: currentCustomDecorations = [],
dynamicHeight = false,
lineNumberTransform,
}: {
Expand All @@ -104,6 +123,7 @@ const Monaco = ({
error?: CompilationError;
disabled?: boolean;
highlight?: Span;
customDecorations?: Decoration[];
dynamicHeight?: boolean;
lineNumberTransform?: (n: number) => string;
}) => {
Expand All @@ -114,6 +134,7 @@ const Monaco = ({
const editor = useRef<monacoT.editor.IStandaloneCodeEditor>();
const decorations = useRef<string[]>([]);
const highlight = useRef<Span | undefined>(undefined);
const customDecorations = useRef<Decoration[]>([]);

const codeTheme = useCallback(() => {
const isDark =
Expand All @@ -123,8 +144,8 @@ const Monaco = ({
return isDark ? MONACO_DARK_THEME : MONACO_LIGHT_THEME;
}, [theme]);

const doHighlight = useCallback(() => {
decorations.current = makeHighlight(
const doDecorations = useCallback(() => {
decorations.current = makeDecorations(
monaco,
editor.current,
// I'm not sure why this makes things work, but it is load bearing.
Expand All @@ -133,6 +154,7 @@ const Monaco = ({
// cause a 1-character highlight in the editor view, so don't do that
// either.
highlight.current ?? { start: 0, end: 0, line: 0 },
customDecorations.current,
decorations.current
);
}, [decorations, monaco, editor, highlight]);
Expand All @@ -149,9 +171,14 @@ const Monaco = ({
// Mark and center highlighted spans
useEffect(() => {
highlight.current = currentHighlight;
doHighlight();
doDecorations();
}, [currentHighlight]);

useEffect(() => {
customDecorations.current = currentCustomDecorations;
doDecorations();
}, [currentCustomDecorations]);

// Set options when mounting
const onMount: OnMount = useCallback(
(ed) => {
Expand All @@ -171,7 +198,7 @@ const Monaco = ({
lineNumbers: lineNumberTransform ?? "on",
folding: false,
});
doHighlight();
doDecorations();
calculateHeight();
editor.current?.onDidChangeCursorPosition((e) => {
const index = editor.current?.getModel()?.getOffsetAt(e.position);
Expand Down Expand Up @@ -255,6 +282,7 @@ export const Editor = ({
grammar,
language,
highlight,
customDecorations = [],
dynamicHeight = false,
lineNumberTransform,
}: {
Expand All @@ -268,6 +296,7 @@ export const Editor = ({
grammar?: ohm.Grammar;
language: string;
highlight?: Span;
customDecorations?: Decoration[];
dynamicHeight?: boolean;
lineNumberTransform?: (n: number) => string;
}) => {
Expand All @@ -287,6 +316,7 @@ export const Editor = ({
error={error}
disabled={disabled}
highlight={highlight}
customDecorations={customDecorations}
dynamicHeight={dynamicHeight}
lineNumberTransform={lineNumberTransform}
/>
Expand Down
36 changes: 34 additions & 2 deletions web/src/shell/test_panel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Trans } from "@lingui/macro";
import { DiffTable } from "@nand2tetris/components/difftable.js";
import { DiffDisplay, generateDiffs } from "@nand2tetris/components/compare.js";
import { Runbar } from "@nand2tetris/components/runbar.js";
import { BaseContext } from "@nand2tetris/components/stores/base.context.js";
import { Span } from "@nand2tetris/simulator/languages/base";
Expand All @@ -12,6 +12,7 @@ import {
RefObject,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import { AppContext } from "../App.context";
Expand Down Expand Up @@ -68,6 +69,12 @@ export const TestPanel = ({
}
}, [filePicker, setStatus, fs]);

const [diffDisplay, setDiffDisplay] = useState<DiffDisplay>();

useEffect(() => {
setDiffDisplay(generateDiffs(cmp, out));
}, [out, cmp]);

return (
<Panel
className="_test_panel"
Expand Down Expand Up @@ -152,6 +159,7 @@ export const TestPanel = ({
grammar={CMP.parser}
language={"cmp"}
disabled={disabled}
lineNumberTransform={(_) => ""}
/>
</div>
<div
Expand All @@ -177,7 +185,31 @@ export const TestPanel = ({
id="test-tabpanel-out"
aria-labelledby="test-tab-out"
>
<DiffTable cmp={cmp} out={out} />
{out == "" && <p>Execute test script to compare output.</p>}
{(diffDisplay?.failureNum ?? 0) > 0 && (
<p>
{diffDisplay?.failureNum} failure
{diffDisplay?.failureNum === 1 ? "" : "s"}
</p>
)}
<Editor
value={diffDisplay?.text ?? ""}
onChange={() => {
return;
}}
language={""}
disabled={true}
lineNumberTransform={(_) => ""}
customDecorations={diffDisplay?.correctCellSpans
.map((span) => {
return { span, cssClass: "green" };
})
.concat(
diffDisplay?.incorrectCellSpans.map((span) => {
return { span, cssClass: "red" };
})
)}
/>
</div>
</div>
</Panel>
Expand Down

0 comments on commit 11210af

Please sign in to comment.