mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 02:36:01 -10:00
fix(studio): a code block wraps long lines instead of scrolling sideways; each source line is its own row carrying its number, so the number stays with the first row of a wrapped line
This commit is contained in:
1 parent
2bfb7de07a
commit
ce51afd2dc
4 files changed
+84
-13
No files matched your search
@@ -3509,14 +3509,16 @@
|
||||
.code-act:hover, .code-act:focus-visible { color: var(--text); background: var(--overlay) }
|
||||
.code-act .studio-icon { width: 14px; height: 14px }
|
||||
.code-act[data-state="done"] { color: var(--ok) }
|
||||
.code-body { display: flex; align-items: flex-start }
|
||||
.code-gutter {
|
||||
flex: none; padding: 11px 10px 12px 12px; text-align: end;
|
||||
.code-body { display: block }
|
||||
.code-body > pre.term { margin: 0; border-radius: 0; background: none; tab-size: 4; overflow-x: hidden; padding: 11px 0 12px }
|
||||
.code-ln { display: flex; align-items: flex-start }
|
||||
.code-no {
|
||||
flex: none; box-sizing: content-box; width: var(--ln-w); padding: 0 10px 0 12px; text-align: end; font-style: normal;
|
||||
font: var(--w-reg) var(--fs-ui)/var(--fs-title) var(--mono); color: var(--ghost); user-select: none;
|
||||
border-inline-end: 1px solid var(--hair);
|
||||
border-inline-end: 1px solid var(--hair); align-self: stretch;
|
||||
}
|
||||
.code-gutter > span { display: block }
|
||||
.code-body > pre.term { flex: 1; min-width: 0; margin: 0; border-radius: 0; background: none; tab-size: 4 }
|
||||
.code-tx { flex: 1; min-width: 0; padding-inline: 14px 12px; white-space: pre-wrap; overflow-wrap: anywhere }
|
||||
.md .code-body pre.term .code-tx code { white-space: inherit; overflow-wrap: inherit }
|
||||
.code-wrap[data-folded] .code-body {
|
||||
max-height: calc(19px * 12 + 11px); overflow: hidden;
|
||||
mask-image: linear-gradient(to bottom, #000 72%, transparent);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import "./testkit";
|
||||
import { CodeBlock, extensionFor } from "./CodeBlock";
|
||||
import css from "../styles/app.css?raw";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
@@ -18,7 +19,7 @@ describe("a code block", () => {
|
||||
const box = draw(lines(3));
|
||||
expect(box.querySelector(".code-lang")?.textContent).toBe("go");
|
||||
expect(box.querySelector(".code-lines")?.textContent).toContain("3");
|
||||
expect([...box.querySelectorAll(".code-gutter > span")].map((li) => li.textContent)).toEqual(["1", "2", "3"]);
|
||||
expect([...box.querySelectorAll(".code-no")].map((li) => li.textContent)).toEqual(["1", "2", "3"]);
|
||||
});
|
||||
|
||||
it("folds a long settled block and opens it on request", async () => {
|
||||
@@ -40,3 +41,39 @@ describe("a code block", () => {
|
||||
expect(extensionFor("brainfuck")).toBe("txt");
|
||||
});
|
||||
});
|
||||
|
||||
// A long line wraps inside the block instead of scrolling it sideways, so its
|
||||
// number has to stay with the first row of the line it names.
|
||||
describe("a code block's rows", () => {
|
||||
const rows = (box: HTMLElement) => [...box.querySelectorAll(".code-ln")];
|
||||
|
||||
it("is one row per source line, blank lines included, with no row for the closing newline", () => {
|
||||
const box = draw("a\n\nb\n");
|
||||
expect(rows(box).map((r) => r.querySelector(".code-no")?.textContent)).toEqual(["1", "2", "3"]);
|
||||
expect(rows(box).map((r) => r.querySelector(".code-tx")?.textContent?.replace(/\n$/, ""))).toEqual(["a", "", "b"]);
|
||||
});
|
||||
|
||||
it("splits a highlighted element that spans a newline into one piece per row", () => {
|
||||
const source = "/* one\n two */ x\n";
|
||||
const box = render(
|
||||
<CodeBlock lang="go" source={source}>
|
||||
<code className="hljs"><span className="hljs-comment">{"/* one\n two */"}</span>{" x\n"}</code>
|
||||
</CodeBlock>,
|
||||
).container;
|
||||
const [first, second] = rows(box);
|
||||
expect(first.querySelector(".hljs-comment")?.textContent).toBe("/* one");
|
||||
expect(second.querySelector(".hljs-comment")?.textContent).toBe(" two */");
|
||||
expect(second.querySelector(".code-tx")?.textContent).toContain(" x");
|
||||
});
|
||||
|
||||
it("keeps the numbers out of what a reader selects and out of the accessibility tree", () => {
|
||||
const box = draw(lines(2));
|
||||
for (const no of box.querySelectorAll(".code-no")) expect(no.getAttribute("aria-hidden")).toBe("true");
|
||||
});
|
||||
|
||||
it("wraps long lines in the stylesheet rather than scrolling the block", () => {
|
||||
const rule = css.match(/\.code-tx\s*\{([^}]*)\}/)?.[1] ?? "";
|
||||
expect(rule).toMatch(/white-space:\s*pre-wrap/);
|
||||
expect(rule).toMatch(/overflow-wrap:\s*anywhere/);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { useState, type CSSProperties, type ReactNode } from "react";
|
||||
import { t } from "../i18n";
|
||||
import { CopyButton } from "./CopyButton";
|
||||
import { StudioIcon } from "./StudioIcon";
|
||||
import { splitLines } from "./codelines";
|
||||
|
||||
// Past this many lines a settled block opens folded to FOLDED_LINES, so one long
|
||||
// file in a reply does not push the answer around it off the screen.
|
||||
@@ -41,6 +42,8 @@ export function CodeBlock({ lang, source, live, children }: { lang: string; sour
|
||||
const foldable = !live && lines > FOLD_OVER;
|
||||
const [open, setOpen] = useState(false);
|
||||
const folded = foldable && !open;
|
||||
const rows = splitLines(children);
|
||||
while (rows.length > lines && rows[rows.length - 1].length === 0) rows.pop();
|
||||
return (
|
||||
<div className="code-wrap" data-lang={lang || undefined} data-folded={folded ? "" : undefined}>
|
||||
<div className="code-hd">
|
||||
@@ -55,11 +58,15 @@ export function CodeBlock({ lang, source, live, children }: { lang: string; sour
|
||||
{!live && <CopyButton text={source} iconOnly className="code-act" label={t("chat.copy_this_code")} />}
|
||||
</span>
|
||||
</div>
|
||||
<div className="code-body">
|
||||
<div className="code-gutter" aria-hidden="true">
|
||||
{Array.from({ length: lines }, (_, i) => <span key={i}>{i + 1}</span>)}
|
||||
</div>
|
||||
<pre className="term">{children}</pre>
|
||||
<div className="code-body" style={{ "--ln-w": `${String(lines).length + 1}ch` } as CSSProperties}>
|
||||
<pre className="term">
|
||||
{rows.map((row, i) => (
|
||||
<span className="code-ln" key={i}>
|
||||
<i className="code-no" aria-hidden="true">{i + 1}</i>
|
||||
<span className="code-tx">{row.length ? row : "\n"}</span>
|
||||
</span>
|
||||
))}
|
||||
</pre>
|
||||
</div>
|
||||
{foldable && (
|
||||
<button type="button" className="code-more" data-action="code.fold" aria-expanded={open} onClick={() => setOpen(!open)}>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { cloneElement, isValidElement, type ReactElement, type ReactNode } from "react";
|
||||
|
||||
type Row = ReactNode[];
|
||||
|
||||
function join(head: Row[], tail: Row[]): Row[] {
|
||||
return [...head.slice(0, -1), [...head[head.length - 1], ...tail[0]], ...tail.slice(1)];
|
||||
}
|
||||
|
||||
function rowsOf(node: ReactNode): Row[] {
|
||||
if (node == null || typeof node === "boolean") return [[]];
|
||||
if (typeof node === "number") return [[String(node)]];
|
||||
if (typeof node === "string") return node.split("\n").map((piece) => (piece === "" ? [] : [piece]));
|
||||
if (Array.isArray(node)) return node.reduce<Row[]>((all, child) => join(all, rowsOf(child)), [[]]);
|
||||
if (!isValidElement(node)) return [[]];
|
||||
const element = node as ReactElement<{ children?: ReactNode }>;
|
||||
const inner = rowsOf(element.props.children);
|
||||
if (inner.length === 1) return [[element]];
|
||||
return inner.map((row, i) => (row.length === 0 ? [] : [cloneElement(element, { key: `${element.key ?? "n"}-${i}` }, ...row)]));
|
||||
}
|
||||
|
||||
/** The rows of a highlighted tree, one per source line. An element that spans a
|
||||
* newline is repeated in every row it touches, so each row stands on its own. */
|
||||
export function splitLines(tree: ReactNode): ReactNode[][] {
|
||||
return rowsOf(tree);
|
||||
}
|
||||
Reference in new issue
Block a user