Files
storkit/frontend/src/components/CodeRef.tsx

109 lines
2.5 KiB
TypeScript
Raw Normal View History

import * as React from "react";
import { settingsApi } from "../api/settings";
// Matches patterns like `src/main.rs:42` or `path/to/file.tsx:123`
// Path must contain at least one dot (file extension) and a colon followed by digits.
const CODE_REF_PATTERN = /\b([\w.\-/]+\.\w+):(\d+)\b/g;
export interface CodeRefPart {
type: "text" | "ref";
value: string;
path?: string;
line?: number;
}
/**
* Parse a string into text and code-reference parts.
* Code references have the format `path/to/file.ext:line`.
*/
export function parseCodeRefs(text: string): CodeRefPart[] {
const parts: CodeRefPart[] = [];
let lastIndex = 0;
const re = new RegExp(CODE_REF_PATTERN.source, "g");
let match: RegExpExecArray | null;
while ((match = re.exec(text)) !== null) {
if (match.index > lastIndex) {
parts.push({ type: "text", value: text.slice(lastIndex, match.index) });
}
parts.push({
type: "ref",
value: match[0],
path: match[1],
line: Number(match[2]),
});
lastIndex = re.lastIndex;
}
if (lastIndex < text.length) {
parts.push({ type: "text", value: text.slice(lastIndex) });
}
return parts;
}
interface CodeRefLinkProps {
path: string;
line: number;
children: React.ReactNode;
}
function CodeRefLink({ path, line, children }: CodeRefLinkProps) {
const handleClick = React.useCallback(() => {
settingsApi.openFile(path, line).catch(() => {
// Silently ignore errors (e.g. no editor configured)
});
}, [path, line]);
return (
<button
type="button"
onClick={handleClick}
title={`Open ${path}:${line} in editor`}
style={{
background: "none",
border: "none",
padding: 0,
cursor: "pointer",
color: "#7ec8e3",
fontFamily: "monospace",
fontSize: "inherit",
textDecoration: "underline",
textDecorationStyle: "dotted",
}}
>
{children}
</button>
);
}
interface InlineCodeWithRefsProps {
text: string;
}
/**
* Renders inline text with code references converted to clickable links.
*/
export function InlineCodeWithRefs({ text }: InlineCodeWithRefsProps) {
const parts = parseCodeRefs(text);
if (parts.length === 1 && parts[0].type === "text") {
return <>{text}</>;
}
return (
<>
{parts.map((part, i) => {
if (part.type === "ref" && part.path !== undefined && part.line !== undefined) {
return (
<CodeRefLink key={`ref-${i}`} path={part.path} line={part.line}>
{part.value}
</CodeRefLink>
);
}
return <span key={`text-${i}`}>{part.value}</span>;
})}
</>
);
}