diff --git a/apps/web/src/markdown-links.ts b/apps/web/src/markdown-links.ts index de5b7d3efad25..87667726fe3c0 100644 --- a/apps/web/src/markdown-links.ts +++ b/apps/web/src/markdown-links.ts @@ -44,7 +44,7 @@ export function resolveInlineCodeFileLinkMeta( cwd?: string, baseDir: string | undefined = cwd, ): MarkdownFileLinkMeta | null { - const candidate = inlineCodeFilePathCandidate(codeText); + const candidate = inlineCodeFilePathCandidate(codeText, cwd); if (candidate === null) return null; return resolveMarkdownFileLinkMeta( diff --git a/packages/shared/src/markdownLinks.test.ts b/packages/shared/src/markdownLinks.test.ts index 5337700871637..aed637cdeeaac 100644 --- a/packages/shared/src/markdownLinks.test.ts +++ b/packages/shared/src/markdownLinks.test.ts @@ -26,11 +26,30 @@ describe("inlineCodeFilePathCandidate", () => { ["origin/main", null], ["z-ai/glm-5.3", null], ["share/man/ls.1", "share/man/ls.1"], + ["usr/lib/libfoo.so.1", "usr/lib/libfoo.so.1"], + ["vendor/jquery-3.6.0.min.js", "vendor/jquery-3.6.0.min.js"], + ["./models/glm-5.3", "./models/glm-5.3"], + ["/D:/Programme/t3code/file.ts", "/D:/Programme/t3code/file.ts"], + ["/D%3A/Programme/other/file.ts", "/D%3A/Programme/other/file.ts"], + ["/%74mp/image.png", "/%74mp/image.png"], + ["/tmp/image.png", "/tmp/image.png"], + ["/images/ZOOM/provider/te115_2.jpg", null], + ["/api/v1/users.json", null], ["src/main.ts/", null], ["./patches/", "./patches/"], ])("distinguishes file paths from code and hostnames in %s", (source, candidate) => { expect(inlineCodeFilePathCandidate(source)).toBe(candidate); }); + + it("keeps absolute paths inside a workspace outside the usual roots", () => { + expect(inlineCodeFilePathCandidate("/code/app/src/index.ts", "/code/app")).toBe( + "/code/app/src/index.ts", + ); + expect(inlineCodeFilePathCandidate("/images/logo.png", "/code/app")).toBeNull(); + expect(inlineCodeFilePathCandidate("/code/my%20app/src/main.ts", "/code/my app")).toBe( + "/code/my%20app/src/main.ts", + ); + }); }); describe("parseMarkdownFileLink", () => { diff --git a/packages/shared/src/markdownLinks.ts b/packages/shared/src/markdownLinks.ts index 177e6fcbf5008..7106b64bcc5ea 100644 --- a/packages/shared/src/markdownLinks.ts +++ b/packages/shared/src/markdownLinks.ts @@ -170,7 +170,7 @@ function looksLikeHostname(segment: string, hasPosition: boolean): boolean { return !hasPosition && COUNTRY_HOSTNAME_TLDS.has(lastLabel); } -export function inlineCodeFilePathCandidate(codeText: string): string | null { +export function inlineCodeFilePathCandidate(codeText: string, cwd?: string): string | null { const trimmed = codeText.trim(); if (trimmed.length === 0 || INLINE_CODE_DISQUALIFIER_PATTERN.test(trimmed)) return null; @@ -197,6 +197,16 @@ export function inlineCodeFilePathCandidate(codeText: string): string | null { if (VERSION_SUFFIX_PATTERN.test(basename)) return null; if (!hasPosition && !FILE_EXTENSION_PATTERN.test(basename)) return null; } + // `/images/logo.jpg` in code is usually a URL path, not a file. + const decodedCandidate = safeDecodeURIComponent(candidate); + if ( + candidate.startsWith("/") && + !isWindowsAbsolutePath(stripSlashPrefixedWindowsDrive(decodedCandidate)) && + !POSIX_FILE_ROOT_PREFIXES.some((prefix) => decodedCandidate.startsWith(prefix)) && + workspaceRelativeFilePath(decodedCandidate, cwd) === null + ) { + return null; + } return candidate; } diff --git a/packages/shared/src/threadFindText.ts b/packages/shared/src/threadFindText.ts index 3afec6e04729e..8e9d87ed11cec 100644 --- a/packages/shared/src/threadFindText.ts +++ b/packages/shared/src/threadFindText.ts @@ -120,7 +120,9 @@ function markdownThreadFindText( const tree = processor.runSync(processor.parse(markdown)); const paths = [ ...extractMarkdownLinkHrefs(renderCodexInlineDirectivesAsMarkdown(markdown)), - ...extractInlineCodeSpans(markdown).flatMap((span) => inlineCodeFilePathCandidate(span) ?? []), + ...extractInlineCodeSpans(markdown).flatMap( + (span) => inlineCodeFilePathCandidate(span, cwd) ?? [], + ), ].flatMap((href) => { const target = resolveMarkdownFileLinkTarget(href, cwd); return target ? [splitFilePathPosition(target).path] : []; @@ -155,6 +157,7 @@ function markdownThreadFindText( ) { candidate = inlineCodeFilePathCandidate( (node.children ?? []).map((child) => child.value ?? "").join(""), + cwd, ); } const target = candidate ? resolveMarkdownFileLinkTarget(candidate, cwd) : null;