diff --git a/apps/web/src/terminal-links.test.ts b/apps/web/src/terminal-links.test.ts index 242df9ddcccd3..8d30ef3eb93b3 100644 --- a/apps/web/src/terminal-links.test.ts +++ b/apps/web/src/terminal-links.test.ts @@ -124,6 +124,20 @@ describe("extractTerminalLinks", () => { ]); }); + it.each([ + ["https://example.com/a(b)c)", "https://example.com/a(b)c"], + ["https://example.com/(docs)", "https://example.com/(docs)"], + ["https://example.com/docs)}])", "https://example.com/docs)"], + ["https://example.com/docs):", "https://example.com/docs):"], + ["/tmp/a[b]c]", "/tmp/a[b]c"], + ["/tmp/docs)}])", "/tmp/docs)"], + ["/tmp/docs):", "/tmp/docs"], + ])("trims only unbalanced closing delimiters from %s", (raw, text) => { + expect(extractTerminalLinks(`see ${raw}`)).toEqual([ + { kind: isTerminalUrl(text) ? "url" : "path", text, start: 4, end: 4 + text.length }, + ]); + }); + it.each([ "src/żółć.ts:12:7", "żółć/main.ts:12", diff --git a/apps/web/src/terminal-links.ts b/apps/web/src/terminal-links.ts index 1d4366cc42748..9f9cbf69c99b5 100644 --- a/apps/web/src/terminal-links.ts +++ b/apps/web/src/terminal-links.ts @@ -39,7 +39,6 @@ const URL_PATTERN = /https?:\/\/[^\s"'`<>]+/giu; const POWERSHELL_PROMPT_PATH_PATTERN = /^PS ((?:[A-Za-z]:[\\/]|\\\\|\/)[^>\r\n]*)>/u; // Bare paths start only at a segment boundary: hover detection runs this on // every frame, so separator-free output must not be rescanned at every suffix. -// For the same reason `]` needs its `[`: trimming a long run of `]` is quadratic. // Parenthesized path segments need a following slash to distinguish compiler locations. const FILE_PATH_PATTERN = /(?:~[\\/]|\.{1,2}[\\/]|\/|[A-Za-z]:[\\/]|\\\\)[^\s"'`<>]+|(?]+/gu; function trimClosingDelimiters(value: string, kind: TerminalLinkKind): string { - let output = value.replace( + const output = value.replace( kind === "path" ? TRAILING_PATH_PUNCTUATION_PATTERN : TRAILING_PUNCTUATION_PATTERN, "", ); if (output.length === 0) return output; + let end = output.length; const trimUnbalanced = (open: string, close: string) => { - while (output.endsWith(close)) { - const opens = output.split(open).length - 1; - const closes = output.split(close).length - 1; - if (opens >= closes) return; - output = output.slice(0, -1); + if (output[end - 1] !== close) return; + let excess = output.split(close).length - output.split(open).length; + while (excess > 0 && output[end - 1] === close) { + end -= 1; + excess -= 1; } }; trimUnbalanced("(", ")"); trimUnbalanced("[", "]"); trimUnbalanced("{", "}"); - return output; + return output.slice(0, end); } function overlaps(a: { start: number; end: number }, b: { start: number; end: number }): boolean {