mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
Move file autolinks out of MarkdownBody
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
e8b709e744
commit
5b6fc549a9
5 files changed
+87
-46
No files matched your search
@@ -89,6 +89,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { MarkdownBody } from "./MarkdownBody";
|
||||
import { WorkspaceFileMarkdownBody } from "./WorkspaceFileMarkdownBody";
|
||||
import { MarkdownEditor, type MentionOption, type MarkdownEditorRef } from "./MarkdownEditor";
|
||||
import { Identity } from "./Identity";
|
||||
import { InlineEntitySelector, type InlineEntityOption } from "./InlineEntitySelector";
|
||||
@@ -669,15 +670,14 @@ const IssueChatTextPart = memo(function IssueChatTextPart({ text, recessed }: {
|
||||
return <SuccessfulRunHandoffCommentCallout text={text} recessed={recessed} onImageClick={onImageClick} />;
|
||||
}
|
||||
return (
|
||||
<MarkdownBody
|
||||
<WorkspaceFileMarkdownBody
|
||||
className="text-sm leading-6"
|
||||
style={recessed ? { opacity: 0.55 } : undefined}
|
||||
softBreaks
|
||||
linkWorkspaceFileReferences
|
||||
onImageClick={onImageClick}
|
||||
>
|
||||
{text}
|
||||
</MarkdownBody>
|
||||
</WorkspaceFileMarkdownBody>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ vi.mock("@/lib/router", () => ({
|
||||
}: { children: ReactNode; to: string } & React.ComponentProps<"a">) => (
|
||||
<a href={to} {...props}>{children}</a>
|
||||
),
|
||||
useLocation: () => ({ pathname: "/", search: "", hash: "" }),
|
||||
}));
|
||||
|
||||
vi.mock("../api/issues", () => ({
|
||||
@@ -534,22 +533,4 @@ describe("MarkdownBody", () => {
|
||||
expect(html).toContain('href="/issues/ACME-1"');
|
||||
});
|
||||
|
||||
it("renders workspace file refs in inline code as workspace file links when enabled", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<ThemeProvider>
|
||||
<MarkdownBody linkWorkspaceFileReferences>
|
||||
{"Check `ui/src/pages/IssueDetail.tsx:42` please."}
|
||||
</MarkdownBody>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
expect(html).toContain('data-workspace-file-link="true"');
|
||||
expect(html).toContain("ui/src/pages/IssueDetail.tsx:42");
|
||||
});
|
||||
|
||||
it("does not create workspace file links when the flag is off", () => {
|
||||
const html = renderMarkdown("See `ui/src/pages/IssueDetail.tsx:42` for details.");
|
||||
expect(html).not.toContain("data-workspace-file-link");
|
||||
});
|
||||
});
|
||||
@@ -12,13 +12,7 @@ import { issuesApi } from "../api/issues";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { parseIssueReferenceFromHref, remarkLinkIssueReferences } from "../lib/issue-reference";
|
||||
import { remarkSoftBreaks } from "../lib/remark-soft-breaks";
|
||||
import {
|
||||
parseWorkspaceFileHref,
|
||||
remarkWorkspaceFileRefs,
|
||||
WORKSPACE_FILE_HREF_PREFIX,
|
||||
} from "../lib/remark-workspace-file-refs";
|
||||
import { StatusIcon } from "./StatusIcon";
|
||||
import { WorkspaceFileLink } from "./WorkspaceFileLink";
|
||||
|
||||
interface MarkdownBodyProps {
|
||||
children: string;
|
||||
@@ -32,12 +26,6 @@ interface MarkdownBodyProps {
|
||||
wikiLinkRoot?: string;
|
||||
/** Optional href resolver for wikilinks. Return null to leave a token as plain text. */
|
||||
resolveWikiLinkHref?: (target: string, label: string) => string | null | undefined;
|
||||
/**
|
||||
* When true, auto-link workspace file references inside inline code spans
|
||||
* (e.g. `ui/src/pages/IssueDetail.tsx:42`). Requires a FileViewerProvider
|
||||
* ancestor so that clicking a link opens the viewer.
|
||||
*/
|
||||
linkWorkspaceFileReferences?: boolean;
|
||||
/** Optional resolver for relative image paths (e.g. within export packages) */
|
||||
resolveImageSrc?: (src: string) => string | null;
|
||||
/** Called when a user clicks an inline image */
|
||||
@@ -172,7 +160,6 @@ function extractMermaidSource(children: ReactNode): string | null {
|
||||
}
|
||||
|
||||
function safeMarkdownUrlTransform(url: string): string {
|
||||
if (url.startsWith(WORKSPACE_FILE_HREF_PREFIX)) return url;
|
||||
return parseMentionChipHref(url) ? url : defaultUrlTransform(url);
|
||||
}
|
||||
|
||||
@@ -580,7 +567,6 @@ export function MarkdownBody({
|
||||
enableWikiLinks = false,
|
||||
wikiLinkRoot,
|
||||
resolveWikiLinkHref,
|
||||
linkWorkspaceFileReferences = false,
|
||||
resolveImageSrc,
|
||||
onImageClick,
|
||||
}: MarkdownBodyProps) {
|
||||
@@ -599,9 +585,6 @@ export function MarkdownBody({
|
||||
if (linkIssueReferences) {
|
||||
remarkPlugins.push([remarkLinkIssueReferences, { knownPrefixes }]);
|
||||
}
|
||||
if (linkWorkspaceFileReferences) {
|
||||
remarkPlugins.push(remarkWorkspaceFileRefs);
|
||||
}
|
||||
if (softBreaks) {
|
||||
remarkPlugins.push(remarkSoftBreaks);
|
||||
}
|
||||
@@ -653,13 +636,6 @@ export function MarkdownBody({
|
||||
a: ({ node: _node, href, style: linkStyle, children: linkChildren, ...anchorProps }) => {
|
||||
const dataProps = anchorProps as Record<string, unknown>;
|
||||
const isWikiLink = dataProps["data-paperclip-wiki-link"] === "true";
|
||||
if (linkWorkspaceFileReferences) {
|
||||
const workspaceFileRef = parseWorkspaceFileHref(href);
|
||||
if (workspaceFileRef) {
|
||||
return <WorkspaceFileLink workspaceFileRef={workspaceFileRef} />;
|
||||
}
|
||||
}
|
||||
|
||||
if (isWikiLink && href && !/^[a-z][a-z\d+.-]*:/i.test(href) && !href.startsWith("//")) {
|
||||
return (
|
||||
<Link
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { linkWorkspaceFileInlineCode } from "./WorkspaceFileMarkdownBody";
|
||||
|
||||
describe("linkWorkspaceFileInlineCode", () => {
|
||||
it("links workspace file refs in inline code to the current issue file viewer", () => {
|
||||
const markdown = linkWorkspaceFileInlineCode(
|
||||
"Check `ui/src/pages/IssueDetail.tsx:42` please.",
|
||||
"/issues/PAP-1",
|
||||
"?tab=chat",
|
||||
"#comment-1",
|
||||
);
|
||||
|
||||
expect(markdown).toContain("[`ui/src/pages/IssueDetail.tsx:42`](");
|
||||
expect(markdown).toContain("/issues/PAP-1?tab=chat&file=ui%2Fsrc%2Fpages%2FIssueDetail.tsx&line=42#comment-1");
|
||||
});
|
||||
|
||||
it("leaves non-file inline code unchanged", () => {
|
||||
expect(linkWorkspaceFileInlineCode("Run `pnpm test`.", "/issues/PAP-1", "", "")).toBe("Run `pnpm test`.");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { MouseEvent } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useLocation } from "@/lib/router";
|
||||
import { readFileViewerStateFromSearch, useFileViewer, writeFileViewerStateToSearch } from "@/context/FileViewerContext";
|
||||
import { parseWorkspaceFileRef } from "@/lib/workspace-file-parser";
|
||||
import { MarkdownBody } from "./MarkdownBody";
|
||||
|
||||
type MarkdownBodyProps = Parameters<typeof MarkdownBody>[0];
|
||||
|
||||
const INLINE_CODE_RE = /`([^`\r\n]+)`/g;
|
||||
|
||||
function escapeMarkdownLinkLabel(value: string) {
|
||||
return value.replace(/([\\\]])/g, "\\$1");
|
||||
}
|
||||
|
||||
export function linkWorkspaceFileInlineCode(markdown: string, currentPathname: string, currentSearch: string, currentHash: string) {
|
||||
return markdown.replace(INLINE_CODE_RE, (token, rawCode: string) => {
|
||||
const ref = parseWorkspaceFileRef(rawCode);
|
||||
if (!ref) return token;
|
||||
const nextSearch = writeFileViewerStateToSearch(currentSearch, {
|
||||
path: ref.path,
|
||||
line: ref.line,
|
||||
column: ref.column,
|
||||
workspace: "auto",
|
||||
});
|
||||
const href = `${currentPathname}${nextSearch}${currentHash}`;
|
||||
return `[\`${escapeMarkdownLinkLabel(ref.raw)}\`](${href})`;
|
||||
});
|
||||
}
|
||||
|
||||
export function WorkspaceFileMarkdownBody({
|
||||
children,
|
||||
...props
|
||||
}: MarkdownBodyProps) {
|
||||
const location = useLocation();
|
||||
const viewer = useFileViewer();
|
||||
const linkedMarkdown = useMemo(
|
||||
() => linkWorkspaceFileInlineCode(children, location.pathname, location.search, location.hash),
|
||||
[children, location.hash, location.pathname, location.search],
|
||||
);
|
||||
|
||||
const handleClick = (event: MouseEvent<HTMLDivElement>) => {
|
||||
if (!viewer) return;
|
||||
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
||||
const anchor = (event.target as HTMLElement | null)?.closest("a");
|
||||
if (!anchor) return;
|
||||
|
||||
const url = new URL(anchor.href, window.location.href);
|
||||
if (url.origin !== window.location.origin || url.pathname !== location.pathname) return;
|
||||
const next = readFileViewerStateFromSearch(url.search);
|
||||
if (!next) return;
|
||||
|
||||
event.preventDefault();
|
||||
viewer.open(next);
|
||||
};
|
||||
|
||||
return (
|
||||
<div onClick={handleClick}>
|
||||
<MarkdownBody {...props}>{linkedMarkdown}</MarkdownBody>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user