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

119 lines
2.6 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;
match = re.exec(text);
while (match !== 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;
match = re.exec(text);
}
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) => {
if (
part.type === "ref" &&
part.path !== undefined &&
part.line !== undefined
) {
return (
<CodeRefLink
key={`ref-${part.path}:${part.line}`}
path={part.path}
line={part.line}
>
{part.value}
</CodeRefLink>
);
}
return <span key={`text-${part.value}`}>{part.value}</span>;
})}
</>
);
}