authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-05 01:29:35-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-05 01:29:35-07:00
log6615d3d70ba399c4b274d17f19487d94afac17ed
treeac7376784672263c040a70119beab6b02b5e0cf5
parent5a4287cd63aefef93c0fd15fe79f63a2f2657211
signature Signed by PGP key B5690EEEBB952194

refactor(web): test live Ghostty link resolution directly (#10041)


2 files changed, 15 insertions(+), 27 deletions(-)

apps/web/src/terminal/ghostty/surface.test.ts+15-15
...@@ -23,8 +23,6 @@ import {...@@ -23,8 +23,6 @@ import {
23 terminalGridCellAt,23 terminalGridCellAt,
24 terminalScrollbarGeometry,24 terminalScrollbarGeometry,
25 terminalScrollbarOffsetAtPointer,25 terminalScrollbarOffsetAtPointer,
26 terminalLinkAtColumn,
27 terminalLinkAtPosition,
28 terminalLinkAtPositionWithRange,26 terminalLinkAtPositionWithRange,
29 terminalContentOriginY,27 terminalContentOriginY,
30 terminalFontFamily,28 terminalFontFamily,
...@@ -433,7 +431,7 @@ describe("shouldBlinkTerminalCursor", () => {...@@ -433,7 +431,7 @@ describe("shouldBlinkTerminalCursor", () => {
433 });431 });
434});432});
435433
436describe("terminalLinkAtColumn", () => {434describe("terminalLinkAtPositionWithRange", () => {
437 it("maps terminal cells to UTF-16 offsets after a wide emoji", () => {435 it("maps terminal cells to UTF-16 offsets after a wide emoji", () => {
438 const cells = [436 const cells = [
439 cell("🙂"),437 cell("🙂"),
...@@ -450,9 +448,11 @@ describe("terminalLinkAtColumn", () => {...@@ -450,9 +448,11 @@ describe("terminalLinkAtColumn", () => {
450 wrapsToNext: false,448 wrapsToNext: false,
451 };449 };
452450
453 expect(terminalLinkAtColumn(row, 2)).toBe("https://t3.codes");451 expect(terminalLinkAtPositionWithRange([row], 0, 2)?.text).toBe("https://t3.codes");
454 expect(terminalLinkAtColumn(row, cells.length - 1)).toBe("https://t3.codes");452 expect(terminalLinkAtPositionWithRange([row], 0, cells.length - 1)?.text).toBe(
455 expect(terminalLinkAtColumn(row, 0)).toBeNull();453 "https://t3.codes",
454 );
455 expect(terminalLinkAtPositionWithRange([row], 0, 0)).toBeNull();
456 expect(terminalLinkAtPositionWithRange([row], 0, 8)?.range).toEqual({456 expect(terminalLinkAtPositionWithRange([row], 0, 8)?.range).toEqual({
457 start: { x: 2, y: 0 },457 start: { x: 2, y: 0 },
458 end: { x: cells.length - 1, y: 0 },458 end: { x: cells.length - 1, y: 0 },
...@@ -473,10 +473,10 @@ describe("terminalLinkAtColumn", () => {...@@ -473,10 +473,10 @@ describe("terminalLinkAtColumn", () => {
473 row("C:\\repo\\file.ts", false),473 row("C:\\repo\\file.ts", false),
474 ];474 ];
475475
476 expect(terminalLinkAtPosition(rows, 0, 8)).toBe("https://example.com/reference");476 expect(terminalLinkAtPositionWithRange(rows, 0, 8)?.text).toBe("https://example.com/reference");
477 expect(terminalLinkAtPosition(rows, 1, 4)).toBe("https://example.com/reference");477 expect(terminalLinkAtPositionWithRange(rows, 1, 4)?.text).toBe("https://example.com/reference");
478 expect(terminalLinkAtPosition(rows, 2, 2)).toBe("~/project/file");478 expect(terminalLinkAtPositionWithRange(rows, 2, 2)?.text).toBe("~/project/file");
479 expect(terminalLinkAtPosition(rows, 3, 4)).toBe("C:\\repo\\file.ts");479 expect(terminalLinkAtPositionWithRange(rows, 3, 4)?.text).toBe("C:\\repo\\file.ts");
480 expect(terminalLinkAtPositionWithRange(rows, 1, 4)).toEqual({480 expect(terminalLinkAtPositionWithRange(rows, 1, 4)).toEqual({
481 text: "https://example.com/reference",481 text: "https://example.com/reference",
482 range: {482 range: {
...@@ -495,13 +495,13 @@ describe("terminalLinkAtColumn", () => {...@@ -495,13 +495,13 @@ describe("terminalLinkAtColumn", () => {
495 });495 });
496 // The head of the wrapped line scrolled above the viewport.496 // The head of the wrapped line scrolled above the viewport.
497 const headCut = [row("ple.com/missing", true), row("head", true)];497 const headCut = [row("ple.com/missing", true), row("head", true)];
498 expect(terminalLinkAtPosition(headCut, 0, 4)).toBeNull();498 expect(terminalLinkAtPositionWithRange(headCut, 0, 4)).toBeNull();
499 // The bottom row soft-wraps on below the viewport.499 // The bottom row soft-wraps on below the viewport.
500 const tailCut = [row("https://t3.codes", false, true)];500 const tailCut = [row("https://t3.codes", false, true)];
501 expect(terminalLinkAtPosition(tailCut, 0, 8)).toBeNull();501 expect(terminalLinkAtPositionWithRange(tailCut, 0, 8)).toBeNull();
502 // A partial bottom row is provably complete and still resolves.502 // A partial bottom row is provably complete and still resolves.
503 const complete = [row("https://t3.codes", false), row("", false)];503 const complete = [row("https://t3.codes", false), row("", false)];
504 expect(terminalLinkAtPosition(complete, 0, 8)).toBe("https://t3.codes");504 expect(terminalLinkAtPositionWithRange(complete, 0, 8)?.text).toBe("https://t3.codes");
505 // A wide grapheme earlier in the row must not break truncation detection:505 // A wide grapheme earlier in the row must not break truncation detection:
506 // the soft-wrap flag decides, not string-length-versus-cell-count.506 // the soft-wrap flag decides, not string-length-versus-cell-count.
507 const wideFull: GhosttyRow = {507 const wideFull: GhosttyRow = {
...@@ -514,7 +514,7 @@ describe("terminalLinkAtColumn", () => {...@@ -514,7 +514,7 @@ describe("terminalLinkAtColumn", () => {
514 isWrapContinuation: false,514 isWrapContinuation: false,
515 wrapsToNext: true,515 wrapsToNext: true,
516 };516 };
517 expect(terminalLinkAtPosition([wideFull], 0, 8)).toBeNull();517 expect(terminalLinkAtPositionWithRange([wideFull], 0, 8)).toBeNull();
518 // Unwritten trailing cells prove the bottom row is complete.518 // Unwritten trailing cells prove the bottom row is complete.
519 const unwrittenTail: GhosttyRow = {519 const unwrittenTail: GhosttyRow = {
520 cells: [520 cells: [
...@@ -526,7 +526,7 @@ describe("terminalLinkAtColumn", () => {...@@ -526,7 +526,7 @@ describe("terminalLinkAtColumn", () => {
526 isWrapContinuation: false,526 isWrapContinuation: false,
527 wrapsToNext: false,527 wrapsToNext: false,
528 };528 };
529 expect(terminalLinkAtPosition([unwrittenTail], 0, 8)).toBe("https://t3.codes");529 expect(terminalLinkAtPositionWithRange([unwrittenTail], 0, 8)?.text).toBe("https://t3.codes");
530 });530 });
531});531});
532532
apps/web/src/terminal/ghostty/surface.ts-12
...@@ -244,14 +244,6 @@ function terminalColumnOffset(row: GhosttySnapshot["rowData"][number], column: n...@@ -244,14 +244,6 @@ function terminalColumnOffset(row: GhosttySnapshot["rowData"][number], column: n
244 return offset;244 return offset;
245}245}
246246
247export function terminalLinkAtPosition(
248 rows: GhosttySnapshot["rowData"],
249 rowIndex: number,
250 column: number,
251): string | null {
252 return terminalLinkAtPositionWithRange(rows, rowIndex, column)?.text ?? null;
253}
254
255export interface TerminalLinkWithRange {247export interface TerminalLinkWithRange {
256 readonly text: string;248 readonly text: string;
257 readonly range: GhosttyCellRange;249 readonly range: GhosttyCellRange;
...@@ -325,10 +317,6 @@ export function terminalLinkAtPositionWithRange(...@@ -325,10 +317,6 @@ export function terminalLinkAtPositionWithRange(
325 return null;317 return null;
326}318}
327319
328export function terminalLinkAtColumn(row: GhosttySnapshot["rowData"][number], column: number) {
329 return terminalLinkAtPosition([row], 0, column);
330}
331
332export function isTerminalCopyShortcut(320export function isTerminalCopyShortcut(
333 event: Pick<KeyboardEvent, "ctrlKey" | "key" | "metaKey" | "shiftKey">,321 event: Pick<KeyboardEvent, "ctrlKey" | "key" | "metaKey" | "shiftKey">,
334 platform = navigator.platform,322 platform = navigator.platform,