Skip to content
100 changes: 100 additions & 0 deletions apps/web/src/components/ChatMarkdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { getSyntaxHighlighterPromise } from "../lib/syntaxHighlighting";
import { GitHubIcon } from "./Icons";
import { Button } from "./ui/button";
import { setMarkdownTaskChecked } from "./files/filePreviewMode";
import { readLocalApi } from "../localApi";

vi.mock("../hooks/useFileMetadata", () => ({ useFileMetadata: () => null }));
vi.mock("@effect/atom-react", () => ({ useAtomValue: () => null }));
Expand Down Expand Up @@ -1046,6 +1047,105 @@ describe("ChatMarkdown Windows file links", () => {
});
});

describe.each(["Win32", "MacIntel"])(
"ChatMarkdown file path clipboard actions on %s",
(platform) => {
it.each([
{
name: "full Windows drive path with spaces",
cwd: "C:/Users/me/project",
href: "C:/Users/me/project/reports/Device Users.csv",
action: "copy-full",
expected: String.raw`C:\Users\me\project\reports\Device Users.csv`,
},
{
name: "relative Windows drive path",
cwd: "C:/Users/me/project",
href: "C:/Users/me/project/reports/Device Users.csv",
action: "copy-relative",
expected: String.raw`project\reports\Device Users.csv`,
},
{
name: "relative link in a Windows workspace",
cwd: String.raw`C:\Users\me\project`,
href: "reports/Device Users.csv",
action: "copy-full",
expected: String.raw`C:\Users\me\project\reports\Device Users.csv`,
},
{
name: "full UNC path",
cwd: String.raw`\\server\share\project`,
href: "file://server/share/project/reports/Device%20Users.csv",
action: "copy-full",
expected: String.raw`\\server\share\project\reports\Device Users.csv`,
},
{
name: "relative UNC path",
cwd: String.raw`\\server\share\project`,
href: "file://server/share/project/reports/Device%20Users.csv",
action: "copy-relative",
expected: String.raw`project\reports\Device Users.csv`,
},
{
name: "full POSIX path",
cwd: "/home/me/project",
href: "/home/me/project/reports/Device Users.csv",
action: "copy-full",
expected: "/home/me/project/reports/Device Users.csv",
},
{
name: "relative POSIX path",
cwd: "/home/me/project",
href: "/home/me/project/reports/Device Users.csv",
action: "copy-relative",
expected: "project/reports/Device Users.csv",
},
{
name: "Windows path with line and column",
cwd: "C:/Users/me/project",
href: "C:/Users/me/project/src/main.ts:12:3",
action: "copy-full",
expected: String.raw`C:\Users\me\project\src\main.ts:12:3`,
},
])("copies $name", async ({ cwd, href, action, expected }) => {
const writeText = vi.fn(async (_text: string) => {});
const container = document.createElement("div");
document.body.append(container);
const root = createRoot(container);

try {
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
vi.stubGlobal("navigator", { platform, clipboard: { writeText } });
const api = readLocalApi();
if (!api) throw new Error("Local API unavailable");
vi.spyOn(api.contextMenu, "show").mockResolvedValue(action);
await act(async () => {
root.render(
<ChatMarkdown
cwd={cwd}
environmentId={EnvironmentId.make("env-file-copy")}
text={`[File](<${href}>)`}
/>,
);
});
const link = container.querySelector(".chat-markdown-file-link");
if (!link) throw new Error("File link unavailable");
await act(async () => {
link.dispatchEvent(
new MouseEvent("contextmenu", { bubbles: true, clientX: 10, clientY: 10 }),
);
});
expect(writeText).toHaveBeenCalledWith(expected);
} finally {
await act(async () => root.unmount());
container.remove();
vi.unstubAllGlobals();
vi.restoreAllMocks();
}
});
},
);

it("opens a disclosure only when find selects a match inside it", async () => {
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
vi.stubGlobal(
Expand Down
16 changes: 9 additions & 7 deletions apps/web/src/components/ChatMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2077,8 +2077,8 @@ const MarkdownFileLink = memo(function MarkdownFileLink({
})();
}, [onReveal, targetPath]);

const handleCopy = useCallback(
(value: string, title: string) => {
const handleCopyFilePath = useCallback(
(path: string, title: string) => {
if (typeof window === "undefined" || !navigator.clipboard?.writeText) {
toastManager.add(
stackedThreadToast({
Expand All @@ -2090,12 +2090,14 @@ const MarkdownFileLink = memo(function MarkdownFileLink({
return;
}

void navigator.clipboard.writeText(value).then(
// A remote file's host OS can differ from the browser's OS.
const clipboardPath = isWindowsAbsolutePath(targetPath) ? path.replaceAll("/", "\\") : path;
Comment thread
tonyjosephsebastians marked this conversation as resolved.
Outdated
void navigator.clipboard.writeText(clipboardPath).then(
() => {
toastManager.add({
type: "success",
title: `${title} copied`,
description: value,
description: clipboardPath,
});
},
(error) => {
Expand Down Expand Up @@ -2153,11 +2155,11 @@ const MarkdownFileLink = memo(function MarkdownFileLink({
return;
}
if (clicked === "copy-relative") {
handleCopy(displayPath, "Relative path");
handleCopyFilePath(displayPath, "Relative path");
return;
}
if (clicked === "copy-full") {
handleCopy(targetPath, "Full path");
handleCopyFilePath(targetPath, "Full path");
}
} catch (cause) {
reportMarkdownActionFailure(
Expand All @@ -2168,7 +2170,7 @@ const MarkdownFileLink = memo(function MarkdownFileLink({
},
[
displayPath,
handleCopy,
handleCopyFilePath,
handleOpenInBrowser,
handleOpenInEditor,
handleRevealInFileManager,
Expand Down
Loading