authorgravatar for 48602572+ValeraZSD@users.noreply.github.comValeraZSD <48602572+ValeraZSD@users.noreply.github.com> 2026-09-26 09:12:03+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-26 02:12:03-05:00
log8bc9b78f827072a89707f6e4b577710e27263697
tree94fded5f10f0152d6e5584bdc8dfa13b3736ae20
parenta21b42cec478b093cdc50cc2105b5368ea8b3546
signature Signed by PGP key B5690EEEBB952194

fix(web): terminal links drop a trailing colon (#13408)


2 files changed, 34 insertions(+), 4 deletions(-)

apps/web/src/terminal-links.test.ts+25
...@@ -97,6 +97,24 @@ describe("extractTerminalLinks", () => {...@@ -97,6 +97,24 @@ describe("extractTerminalLinks", () => {
97 },97 },
98 ]);98 ]);
99 });99 });
100
101 it("keeps a trailing colon on URLs", () => {
102 expect(extractTerminalLinks("GET https://example.test/items/foo:")).toEqual([
103 { kind: "url", text: "https://example.test/items/foo:", start: 4, end: 35 },
104 ]);
105 });
106
107 it.each([
108 ["./main.go:10:5: undefined: x", "./main.go:10:5"],
109 ["/home/dev/app/src/main.c:10:5: error: expected ';'", "/home/dev/app/src/main.c:10:5"],
110 ["C:\\dev\\app\\src\\main.c:10:5: error: expected ';'", "C:\\dev\\app\\src\\main.c:10:5"],
111 ["wrote ./out/report.txt:", "./out/report.txt"],
112 ])("drops the colon that ends a compiler diagnostic location in %s", (line, text) => {
113 const start = line.indexOf(text);
114 expect(extractTerminalLinks(line)).toEqual([
115 { kind: "path", text, start, end: start + text.length },
116 ]);
117 });
100});118});
101119
102describe("collectWrappedTerminalLinkLine", () => {120describe("collectWrappedTerminalLinkLine", () => {
...@@ -173,6 +191,13 @@ describe("resolvePathLinkTarget", () => {...@@ -173,6 +191,13 @@ describe("resolvePathLinkTarget", () => {
173 resolvePathLinkTarget("C:/Users/julius/project/src/main.ts:12", "C:\\Users\\julius\\project"),191 resolvePathLinkTarget("C:/Users/julius/project/src/main.ts:12", "C:\\Users\\julius\\project"),
174 ).toBe("C:/Users/julius/project/src/main.ts:12");192 ).toBe("C:/Users/julius/project/src/main.ts:12");
175 });193 });
194
195 it("keeps the line and column of a compiler diagnostic", () => {
196 const [link] = extractTerminalLinks("/Users/julius/project/main.c:10:5: error: expected ';'");
197 expect(resolvePathLinkTarget(link?.text ?? "", "/Users/julius/project")).toBe(
198 "/Users/julius/project/main.c:10:5",
199 );
200 });
176});201});
177202
178describe("isTerminalLinkActivation", () => {203describe("isTerminalLinkActivation", () => {
apps/web/src/terminal-links.ts+9-4
...@@ -35,9 +35,14 @@ const URL_PATTERN = /https?:\/\/[^\s"'`<>]+/giu;...@@ -35,9 +35,14 @@ const URL_PATTERN = /https?:\/\/[^\s"'`<>]+/giu;
35const FILE_PATH_PATTERN =35const FILE_PATH_PATTERN =
36 /(?:~\/|\.{1,2}\/|\/|[A-Za-z]:[\\/]|\\\\)[^\s"'`<>]+|[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+(?::\d+){0,2}/g;36 /(?:~\/|\.{1,2}\/|\/|[A-Za-z]:[\\/]|\\\\)[^\s"'`<>]+|[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+(?::\d+){0,2}/g;
37const TRAILING_PUNCTUATION_PATTERN = /[.,;!?]+$/;37const TRAILING_PUNCTUATION_PATTERN = /[.,;!?]+$/;
3838// Paths also drop a trailing colon: compilers end `file:line:col:` with one.
39function trimClosingDelimiters(value: string): string {39const TRAILING_PATH_PUNCTUATION_PATTERN = /[.,;:!?]+$/;
40 let output = value.replace(TRAILING_PUNCTUATION_PATTERN, "");40
41function trimClosingDelimiters(value: string, kind: TerminalLinkKind): string {
42 let output = value.replace(
43 kind === "path" ? TRAILING_PATH_PUNCTUATION_PATTERN : TRAILING_PUNCTUATION_PATTERN,
44 "",
45 );
41 if (output.length === 0) return output;46 if (output.length === 0) return output;
4247
43 const trimUnbalanced = (open: string, close: string) => {48 const trimUnbalanced = (open: string, close: string) => {
...@@ -73,7 +78,7 @@ function collectMatches(...@@ -73,7 +78,7 @@ function collectMatches(
73 const start = rawMatch.index ?? -1;78 const start = rawMatch.index ?? -1;
74 if (start < 0 || raw.length === 0) continue;79 if (start < 0 || raw.length === 0) continue;
7580
76 const trimmed = trimClosingDelimiters(raw);81 const trimmed = trimClosingDelimiters(raw, kind);
77 if (trimmed.length === 0) continue;82 if (trimmed.length === 0) continue;
78 if (kind === "path" && isTerminalUrl(trimmed)) continue;83 if (kind === "path" && isTerminalUrl(trimmed)) continue;
7984