Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 61 additions & 40 deletions apps/web/src/components/ChatMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ import {
rewriteMarkdownFileUriHref,
shouldOpenMarkdownFileLinkInBrowserByDefault,
shouldOpenMarkdownFileLinkInEditor,
withMarkdownFileLinkPath,
type MarkdownFileLinkMeta,
} from "../markdown-links";
import { isMarkdownFileLinkLabel } from "@t3tools/shared/markdownLinks";
Expand All @@ -191,8 +192,7 @@ import { useAtomQueryRunner } from "../state/use-atom-query-runner";
import { projectEnvironment } from "../state/projects";
import {
claimWorkspaceBasenameLookup,
needsWorkspaceBasenameLookup,
pickWorkspaceBasenameMatch,
findWorkspaceFileMatch,
WORKSPACE_BASENAME_LOOKUP_LIMIT,
} from "../workspaceBasenameLookup";
import {
Expand Down Expand Up @@ -1202,7 +1202,6 @@ interface MarkdownFileLinkProps {
href: string;
targetPath: string;
iconPath: string;
displayPath: string;
/** What the files panel opens: workspace-relative inside the workspace, the
absolute host path outside it, null when the panel cannot show the file. */
panelPath: string | null;
Expand All @@ -1211,6 +1210,8 @@ interface MarkdownFileLinkProps {
copyMarkdown: string;
theme: "light" | "dark";
threadRef?: ScopedThreadRef | undefined;
/** The file the chip's actions target, which can differ from its literal path. */
resolveLink: () => Promise<Pick<MarkdownFileLinkMeta, "displayPath" | "targetPath">>;
onOpen?: ((targetPath: string) => Promise<AtomCommandResult<unknown, unknown>>) | undefined;
onOpenInPanel: (panelPath: string, line: number | undefined) => void;
openInEditorMenuLabel: string;
Expand Down Expand Up @@ -1953,13 +1954,13 @@ const MarkdownFileLink = memo(function MarkdownFileLink({
href,
targetPath,
iconPath,
displayPath,
panelPath,
line,
label,
copyMarkdown,
theme,
threadRef,
resolveLink,
onOpen,
onOpenInPanel,
openInEditorMenuLabel,
Expand Down Expand Up @@ -2170,11 +2171,11 @@ const MarkdownFileLink = memo(function MarkdownFileLink({
return;
}
if (clicked === "copy-relative") {
handleCopy(displayPath, "Relative path");
handleCopy((await resolveLink()).displayPath, "Relative path");
return;
}
if (clicked === "copy-full") {
handleCopy(targetPath, "Full path");
handleCopy((await resolveLink()).targetPath, "Full path");
}
} catch (cause) {
reportMarkdownActionFailure(
Comment thread
mikeohuo marked this conversation as resolved.
Outdated
Expand All @@ -2184,7 +2185,6 @@ const MarkdownFileLink = memo(function MarkdownFileLink({
}
},
[
displayPath,
handleCopy,
handleOpenInBrowser,
handleOpenInEditor,
Expand All @@ -2194,6 +2194,7 @@ const MarkdownFileLink = memo(function MarkdownFileLink({
onOpen,
onReveal,
openInEditorMenuLabel,
resolveLink,
revealLabel,
targetPath,
],
Expand Down Expand Up @@ -2304,13 +2305,13 @@ function areMarkdownFileLinkPropsEqual(
previous.targetPath === next.targetPath &&
previous.iconPath === next.iconPath &&
previous.environmentId === next.environmentId &&
previous.displayPath === next.displayPath &&
previous.panelPath === next.panelPath &&
previous.line === next.line &&
previous.label === next.label &&
previous.copyMarkdown === next.copyMarkdown &&
previous.theme === next.theme &&
previous.threadRef === next.threadRef &&
previous.resolveLink === next.resolveLink &&
previous.onOpen === next.onOpen &&
previous.onOpenInPanel === next.onOpenInPanel &&
previous.openInEditorMenuLabel === next.openInEditorMenuLabel &&
Expand Down Expand Up @@ -2356,6 +2357,9 @@ function useChatMarkdownState({
const searchProjectEntries = useAtomQueryRunner(projectEnvironment.searchEntries, {
reportFailure: false,
});
const readFileMetadata = useAtomQueryRunner(projectEnvironment.fileMetadata, {
reportFailure: false,
});
const openPreview = useAtomCommand(previewEnvironment.open, {
reportFailure: false,
});
Expand Down Expand Up @@ -2601,33 +2605,47 @@ function useChatMarkdownState({
},
[canOperatePreview, createAssetUrl, cwd, openPreview, preparedConnection, threadRef],
);
const findWorkspaceBasenameMatch = useCallback(
const findWorkspaceMatch = useCallback(
async (workspaceRelativePath: string) => {
if (
!cwd ||
environmentId === null ||
!readEnvironmentScope(environmentId, AuthFilesystemReadScope) ||
!needsWorkspaceBasenameLookup(workspaceRelativePath)
!readEnvironmentScope(environmentId, AuthFilesystemReadScope)
) {
return null;
}
const result = await searchProjectEntries({
environmentId,
input: {
cwd,
query: workspaceRelativePath,
limit: WORKSPACE_BASENAME_LOOKUP_LIMIT,
kind: "file",
return findWorkspaceFileMatch(workspaceRelativePath, {
literalExists: async () => {
const result = await readFileMetadata({
environmentId,
input: { path: resolvePathLinkTarget(workspaceRelativePath, cwd) },
});
return result._tag !== "Success" || result.value !== null;
},
searchFiles: async (query) => {
const result = await searchProjectEntries({
environmentId,
input: { cwd, query, limit: WORKSPACE_BASENAME_LOOKUP_LIMIT, kind: "file" },
});
return result._tag === "Success" ? result.value.entries : null;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
},
});
return result._tag === "Success"
? pickWorkspaceBasenameMatch(workspaceRelativePath, result.value.entries)
: null;
},
[cwd, environmentId, searchProjectEntries],
[cwd, environmentId, readFileMetadata, searchProjectEntries],
);
// A bare filename resolves to the workspace root, which is rarely where the
// file is, so ask the index before opening. Absolute host paths open as-is.
// Every chip action opens the same file: the literal target, or the
// workspace file that findWorkspaceFileMatch picks in its place.
const resolveMarkdownFileLink = useCallback(
async (fileLinkMeta: MarkdownFileLinkMeta) => {
const workspaceRelativePath = fileLinkMeta.workspaceRelativePath;
const match = workspaceRelativePath ? await findWorkspaceMatch(workspaceRelativePath) : null;
return match && cwd
? withMarkdownFileLinkPath(fileLinkMeta, resolvePathLinkTarget(match, cwd), cwd)
: fileLinkMeta;
},
[cwd, findWorkspaceMatch],
);
// Absolute host paths open as-is.
const openFileInPanel = useCallback(
(panelPath: string, line: number | undefined) => {
if (!threadRef) return;
Expand All @@ -2636,28 +2654,22 @@ function useChatMarkdownState({
const isLatestLookup = claimWorkspaceBasenameLookup();
const openAt = (path: string) =>
useRightPanelStore.getState().openFile(threadRef, path, line);
if (!cwd || !needsWorkspaceBasenameLookup(panelPath)) {
if (!cwd || isAbsolutePath(panelPath)) {
openAt(panelPath);
return;
}
void (async () => {
const match = await findWorkspaceBasenameMatch(panelPath);
const match = await findWorkspaceMatch(panelPath);
if (!isLatestLookup()) return;
openAt(match ?? panelPath);
})();
},
[cwd, findWorkspaceBasenameMatch, threadRef],
[cwd, findWorkspaceMatch, threadRef],
);
const revealMarkdownFileInFileManager = useCallback(
async (fileLinkMeta: MarkdownFileLinkMeta) => {
const workspaceRelativePath = fileLinkMeta.workspaceRelativePath;
const match = workspaceRelativePath
? await findWorkspaceBasenameMatch(workspaceRelativePath)
: null;
const filePath = match && cwd ? resolvePathLinkTarget(match, cwd) : fileLinkMeta.filePath;
return revealFileInFileManager(filePath);
},
[cwd, findWorkspaceBasenameMatch, revealFileInFileManager],
async (fileLinkMeta: MarkdownFileLinkMeta) =>
revealFileInFileManager((await resolveMarkdownFileLink(fileLinkMeta)).filePath),
[resolveMarkdownFileLink, revealFileInFileManager],
);
const fileLinkChip = useCallback(
(fileLinkMeta: MarkdownFileLinkMeta, copyMarkdown: string, mediaSource?: string) => {
Expand All @@ -2671,14 +2683,14 @@ function useChatMarkdownState({
const panelPath =
fileLinkMeta.workspaceRelativePath ??
(!canPreviewMedia && isAbsolutePath(fileLinkMeta.filePath) ? fileLinkMeta.filePath : null);
const resolveLink = () => resolveMarkdownFileLink(fileLinkMeta);

return (
<MarkdownFileLink
environmentId={environmentId}
href={fileLinkMeta.targetPath}
targetPath={fileLinkMeta.targetPath}
iconPath={fileLinkMeta.filePath}
displayPath={fileLinkMeta.displayPath}
panelPath={panelPath}
line={fileLinkMeta.line}
label={fileLinkLabel(
Expand All @@ -2692,11 +2704,19 @@ function useChatMarkdownState({
copyMarkdown={copyMarkdown}
theme={resolvedTheme}
threadRef={threadRef}
{...(canUseShellActions ? { onOpen: openInPreferredEditor } : {})}
resolveLink={resolveLink}
{...(canUseShellActions
? {
onOpen: async () => openInPreferredEditor((await resolveLink()).targetPath),
}
: {})}
onOpenInPanel={openFileInPanel}
onOpenMedia={
threadRef && canPreviewMedia
? () => openMarkdownMedia(mediaPath, fileLinkMeta.filePath)
? () =>
void resolveLink().then((resolved) =>
Comment thread
mikeohuo marked this conversation as resolved.
Outdated
openMarkdownMedia(mediaPath, resolved.filePath),
)
: undefined
}
openInEditorMenuLabel={preferredEditorMenuLabel}
Expand All @@ -2711,7 +2731,7 @@ function useChatMarkdownState({
canOperatePreview &&
isPreviewAvailableFor(threadRef.environmentId) &&
isBrowserPreviewFile(fileLinkMeta.filePath)
? () => openMarkdownFileInPreview(fileLinkMeta.filePath)
? async () => openMarkdownFileInPreview((await resolveLink()).filePath)
: undefined
}
/>
Expand All @@ -2727,6 +2747,7 @@ function useChatMarkdownState({
openMarkdownFileInPreview,
openMarkdownMedia,
preferredEditorMenuLabel,
resolveMarkdownFileLink,
resolvedTheme,
revealInFileManagerLabel,
revealMarkdownFileInFileManager,
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/markdown-links.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
rewriteMarkdownFileUriHref,
shouldOpenMarkdownFileLinkInBrowserByDefault,
shouldOpenMarkdownFileLinkInEditor,
withMarkdownFileLinkPath,
} from "./markdown-links";

describe("shouldOpenMarkdownFileLinkInEditor", () => {
Expand Down Expand Up @@ -267,6 +268,16 @@ describe("resolveInlineCodeFileLinkMeta", () => {
});
});

describe("withMarkdownFileLinkPath", () => {
it("points every path field at the new file and keeps the position", () => {
const meta = resolveInlineCodeFileLinkMeta("docs/plan.md:12:3", "/repo");
expect(meta).not.toBeNull();
expect(withMarkdownFileLinkPath(meta!, "/repo/BudgetLens/docs/plan.md", "/repo")).toEqual(
resolveMarkdownFileLinkMeta("/repo/BudgetLens/docs/plan.md:12:3", "/repo"),
);
});
});

describe("directory paths with a trailing separator", () => {
it("keeps the final segment for a POSIX directory path", () => {
expect(resolveMarkdownFileLinkMeta("/tmp/favicons/", "/repo/project")).toMatchObject({
Expand Down
22 changes: 21 additions & 1 deletion apps/web/src/markdown-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import {
normalizeMarkdownLinkDestination,
resolveMarkdownFileLinkTarget,
} from "@t3tools/shared/markdownLinks";
import { parseFileUrlHref, splitFilePathPosition } from "@t3tools/shared/fileLinks";
import {
formatFilePathPosition,
parseFileUrlHref,
splitFilePathPosition,
} from "@t3tools/shared/fileLinks";

import { formatWorkspaceRelativePath } from "./filePathDisplay";
import { isTerminalLinkActivation } from "./terminal-links";
Expand Down Expand Up @@ -84,6 +88,22 @@ export function resolveMarkdownFileLinkMeta(
return buildFileLinkMetaFromTarget(targetPath, cwd);
}

/** The same link pointed at another file, keeping its line and column. */
export function withMarkdownFileLinkPath(
meta: MarkdownFileLinkMeta,
filePath: string,
cwd?: string,
): MarkdownFileLinkMeta {
return buildFileLinkMetaFromTarget(
formatFilePathPosition({
path: filePath,
...(meta.line !== undefined ? { line: meta.line } : {}),
...(meta.column !== undefined ? { column: meta.column } : {}),
}),
cwd,
);
}

function buildFileLinkMetaFromTarget(targetPath: string, cwd?: string): MarkdownFileLinkMeta {
const { path, line, column } = splitFilePathPosition(targetPath);
return {
Expand Down
Loading
Loading