Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
41 changes: 41 additions & 0 deletions apps/web/src/terminal-links.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,47 @@ describe("extractTerminalLinks", () => {
{ kind: "path", text, start, end: start + text.length },
]);
});

it.each([
" GET /api/trpc/post.list,user.me?batch=1&input=%7B%220%22%3A%7B%22json%22%3Anull%7D%7D 200 in 35ms",
"GET /api/users?id=42 200",
"GET /api/users?=42 200",
"GET /api/search?&q=term 200",
"proxying api/trpc/post.list?batch=1 to the backend",
])("skips request routes carrying a query string in %s", (line) => {
expect(extractTerminalLinks(line)).toEqual([]);
});

it("keeps a URL with a query string as a URL link", () => {
const line = "open https://example.com/api/trpc/post.list?batch=1";
expect(extractTerminalLinks(line)).toEqual([
{ kind: "url", text: "https://example.com/api/trpc/post.list?batch=1", start: 5, end: 51 },
]);
});

it("still treats a bare route without a query string as a path", () => {
// Without a query string a route is indistinguishable from a file such as `post.list`.
expect(extractTerminalLinks("GET /api/trpc/post.list 200")).toEqual([
{ kind: "path", text: "/api/trpc/post.list", start: 4, end: 23 },
]);
});

it.each([
["src/main.ts:12", "src/main.ts:12"],
["/Users/me/project/file.ts", "/Users/me/project/file.ts"],
["~/project/file", "~/project/file"],
["./a/b", "./a/b"],
["C:\\repo\\file.ts", "C:\\repo\\file.ts"],
["tail -f /tmp/foo.log", "/tmp/foo.log"],
["see /var/log/x", "/var/log/x"],
["/app/bin/server:3:7", "/app/bin/server:3:7"],
["did you mean src/main.ts?", "src/main.ts"],
])("keeps detecting the real path in %s", (line, text) => {
const start = line.indexOf(text);
expect(extractTerminalLinks(line)).toEqual([
{ kind: "path", text, start, end: start + text.length },
]);
});
});

describe("collectWrappedTerminalLinkLine", () => {
Expand Down
15 changes: 14 additions & 1 deletion apps/web/src/terminal-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ const FILE_PATH_PATTERN =
const TRAILING_PUNCTUATION_PATTERN = /[.,;!?]+$/;
// Paths also drop a trailing colon: compilers end `file:line:col:` with one.
const TRAILING_PATH_PUNCTUATION_PATTERN = /[.,;:!?]+$/;
// The whitespace-delimited token a path match sits in, and a query string inside it.
// Request logs print routes like `/api/trpc/post.list?batch=1`; files don't carry
// queries, while prose like `src/main.ts?` still trims to a path.
const TOKEN_PATTERN = /[^\s"'`<>]*/y;
const QUERY_STRING_PATTERN = /\?[A-Za-z0-9_%&=-]/;
Comment thread
lnieuwenhuis marked this conversation as resolved.

function trimClosingDelimiters(value: string, kind: TerminalLinkKind): string {
let output = value.replace(
Expand All @@ -60,6 +65,12 @@ function trimClosingDelimiters(value: string, kind: TerminalLinkKind): string {
return output;
}

function hasQueryString(line: string, start: number): boolean {
TOKEN_PATTERN.lastIndex = start;
const token = TOKEN_PATTERN.exec(line)?.[0] ?? "";
return QUERY_STRING_PATTERN.test(token);
}

function overlaps(a: { start: number; end: number }, b: { start: number; end: number }): boolean {
return a.start < b.end && b.start < a.end;
}
Expand All @@ -80,7 +91,9 @@ function collectMatches(

const trimmed = trimClosingDelimiters(raw, kind);
if (trimmed.length === 0) continue;
if (kind === "path" && isTerminalUrl(trimmed)) continue;
if (kind === "path" && (isTerminalUrl(trimmed) || hasQueryString(line, start))) {
continue;
}

const candidate: TerminalLinkMatch = {
kind,
Expand Down
Loading