diff --git a/apps/web/src/terminal-links.test.ts b/apps/web/src/terminal-links.test.ts index 3c466378ba8bbc2fd519282312e2ca8e29f5aa2c..5cb2ad915827e46178987dff7ffce51227600d3a 100644 --- a/apps/web/src/terminal-links.test.ts +++ b/apps/web/src/terminal-links.test.ts @@ -97,6 +97,24 @@ describe("extractTerminalLinks", () => { }, ]); }); + + it("keeps a trailing colon on URLs", () => { + expect(extractTerminalLinks("GET https://example.test/items/foo:")).toEqual([ + { kind: "url", text: "https://example.test/items/foo:", start: 4, end: 35 }, + ]); + }); + + it.each([ + ["./main.go:10:5: undefined: x", "./main.go:10:5"], + ["/home/dev/app/src/main.c:10:5: error: expected ';'", "/home/dev/app/src/main.c:10:5"], + ["C:\\dev\\app\\src\\main.c:10:5: error: expected ';'", "C:\\dev\\app\\src\\main.c:10:5"], + ["wrote ./out/report.txt:", "./out/report.txt"], + ])("drops the colon that ends a compiler diagnostic location in %s", (line, text) => { + const start = line.indexOf(text); + expect(extractTerminalLinks(line)).toEqual([ + { kind: "path", text, start, end: start + text.length }, + ]); + }); }); describe("collectWrappedTerminalLinkLine", () => { @@ -173,6 +191,13 @@ describe("resolvePathLinkTarget", () => { resolvePathLinkTarget("C:/Users/julius/project/src/main.ts:12", "C:\\Users\\julius\\project"), ).toBe("C:/Users/julius/project/src/main.ts:12"); }); + + it("keeps the line and column of a compiler diagnostic", () => { + const [link] = extractTerminalLinks("/Users/julius/project/main.c:10:5: error: expected ';'"); + expect(resolvePathLinkTarget(link?.text ?? "", "/Users/julius/project")).toBe( + "/Users/julius/project/main.c:10:5", + ); + }); }); describe("isTerminalLinkActivation", () => { diff --git a/apps/web/src/terminal-links.ts b/apps/web/src/terminal-links.ts index 59e2082a735981af2264141c2287d4bbb0abb0dd..0f253588b700a91d8432e5191e1ab50a1af5a695 100644 --- a/apps/web/src/terminal-links.ts +++ b/apps/web/src/terminal-links.ts @@ -35,9 +35,14 @@ const URL_PATTERN = /https?:\/\/[^\s"'`<>]+/giu; const FILE_PATH_PATTERN = /(?:~\/|\.{1,2}\/|\/|[A-Za-z]:[\\/]|\\\\)[^\s"'`<>]+|[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+(?::\d+){0,2}/g; const TRAILING_PUNCTUATION_PATTERN = /[.,;!?]+$/; +// Paths also drop a trailing colon: compilers end `file:line:col:` with one. +const TRAILING_PATH_PUNCTUATION_PATTERN = /[.,;:!?]+$/; -function trimClosingDelimiters(value: string): string { - let output = value.replace(TRAILING_PUNCTUATION_PATTERN, ""); +function trimClosingDelimiters(value: string, kind: TerminalLinkKind): string { + let output = value.replace( + kind === "path" ? TRAILING_PATH_PUNCTUATION_PATTERN : TRAILING_PUNCTUATION_PATTERN, + "", + ); if (output.length === 0) return output; const trimUnbalanced = (open: string, close: string) => { @@ -73,7 +78,7 @@ function collectMatches( const start = rawMatch.index ?? -1; if (start < 0 || raw.length === 0) continue; - const trimmed = trimClosingDelimiters(raw); + const trimmed = trimClosingDelimiters(raw, kind); if (trimmed.length === 0) continue; if (kind === "path" && isTerminalUrl(trimmed)) continue;