|
1 | 1 | import { describe, expect, it } from "vitest"; |
2 | 2 |
|
3 | 3 | import { |
| 4 | + collectWrappedTerminalLinkLine, |
4 | 5 | extractTerminalLinks, |
5 | 6 | isTerminalLinkActivation, |
6 | 7 | resolvePathLinkTarget, |
| 8 | + resolveWrappedTerminalLinkRange, |
| 9 | + wrappedTerminalLinkRangeIntersectsBufferLine, |
| 10 | + type TerminalBufferLineLike, |
7 | 11 | } from "./terminal-links"; |
8 | 12 |
|
| 13 | +function createBufferLine(text: string, isWrapped = false): TerminalBufferLineLike { |
| 14 | + return { |
| 15 | + isWrapped, |
| 16 | + translateToString: (trimRight = false) => (trimRight ? text.replace(/\s+$/u, "") : text), |
| 17 | + }; |
| 18 | +} |
| 19 | + |
9 | 20 | describe("extractTerminalLinks", () => { |
10 | 21 | it("finds http urls and path tokens", () => { |
11 | 22 | const line = |
@@ -71,6 +82,99 @@ describe("extractTerminalLinks", () => { |
71 | 82 | }); |
72 | 83 | }); |
73 | 84 |
|
| 85 | +describe("collectWrappedTerminalLinkLine", () => { |
| 86 | + it("reconstructs a wrapped line from any physical row", () => { |
| 87 | + const firstSegment = "see https://example.com/a"; |
| 88 | + const secondSegment = "/bc?x=1"; |
| 89 | + const lines = [ |
| 90 | + createBufferLine("prompt> "), |
| 91 | + createBufferLine(firstSegment), |
| 92 | + createBufferLine(secondSegment, true), |
| 93 | + createBufferLine("done"), |
| 94 | + ]; |
| 95 | + |
| 96 | + const fromFirstRow = collectWrappedTerminalLinkLine(2, (index) => lines[index]); |
| 97 | + const fromWrappedRow = collectWrappedTerminalLinkLine(3, (index) => lines[index]); |
| 98 | + |
| 99 | + expect(fromFirstRow).toEqual({ |
| 100 | + text: `${firstSegment}${secondSegment}`, |
| 101 | + segments: [ |
| 102 | + { |
| 103 | + bufferLineNumber: 2, |
| 104 | + text: firstSegment, |
| 105 | + startIndex: 0, |
| 106 | + endIndex: firstSegment.length, |
| 107 | + }, |
| 108 | + { |
| 109 | + bufferLineNumber: 3, |
| 110 | + text: secondSegment, |
| 111 | + startIndex: firstSegment.length, |
| 112 | + endIndex: firstSegment.length + secondSegment.length, |
| 113 | + }, |
| 114 | + ], |
| 115 | + }); |
| 116 | + expect(fromWrappedRow).toEqual(fromFirstRow); |
| 117 | + }); |
| 118 | + |
| 119 | + it("preserves trailing spaces on continued segments for downstream offsets", () => { |
| 120 | + const firstSegment = "prefix "; |
| 121 | + const secondSegment = "https://example.com/path"; |
| 122 | + const lines = [createBufferLine(firstSegment), createBufferLine(secondSegment, true)]; |
| 123 | + |
| 124 | + const wrappedLine = collectWrappedTerminalLinkLine(2, (index) => lines[index]); |
| 125 | + |
| 126 | + expect(wrappedLine?.text).toBe(`${firstSegment}${secondSegment}`); |
| 127 | + expect(extractTerminalLinks(wrappedLine?.text ?? "")).toEqual([ |
| 128 | + { |
| 129 | + kind: "url", |
| 130 | + text: secondSegment, |
| 131 | + start: firstSegment.length, |
| 132 | + end: firstSegment.length + secondSegment.length, |
| 133 | + }, |
| 134 | + ]); |
| 135 | + }); |
| 136 | +}); |
| 137 | + |
| 138 | +describe("resolveWrappedTerminalLinkRange", () => { |
| 139 | + it("maps wrapped URL matches back to the correct buffer rows", () => { |
| 140 | + const prefix = "see "; |
| 141 | + const firstSegment = `${prefix}https://example.com/a`; |
| 142 | + const secondSegment = "/bc?x=1"; |
| 143 | + const lines = [ |
| 144 | + createBufferLine("prompt> "), |
| 145 | + createBufferLine(firstSegment), |
| 146 | + createBufferLine(secondSegment, true), |
| 147 | + ]; |
| 148 | + const wrappedLine = collectWrappedTerminalLinkLine(2, (index) => lines[index]); |
| 149 | + |
| 150 | + expect(wrappedLine).not.toBeNull(); |
| 151 | + if (!wrappedLine) { |
| 152 | + throw new Error("Expected wrapped terminal line to be present."); |
| 153 | + } |
| 154 | + |
| 155 | + const [match] = extractTerminalLinks(wrappedLine.text); |
| 156 | + expect(match).toEqual({ |
| 157 | + kind: "url", |
| 158 | + text: "https://example.com/a/bc?x=1", |
| 159 | + start: prefix.length, |
| 160 | + end: firstSegment.length + secondSegment.length, |
| 161 | + }); |
| 162 | + if (!match) { |
| 163 | + throw new Error("Expected wrapped URL match to be present."); |
| 164 | + } |
| 165 | + |
| 166 | + const range = resolveWrappedTerminalLinkRange(wrappedLine, match); |
| 167 | + |
| 168 | + expect(range).toEqual({ |
| 169 | + start: { x: prefix.length + 1, y: 2 }, |
| 170 | + end: { x: secondSegment.length, y: 3 }, |
| 171 | + }); |
| 172 | + expect(wrappedTerminalLinkRangeIntersectsBufferLine(range, 2)).toBe(true); |
| 173 | + expect(wrappedTerminalLinkRangeIntersectsBufferLine(range, 3)).toBe(true); |
| 174 | + expect(wrappedTerminalLinkRangeIntersectsBufferLine(range, 4)).toBe(false); |
| 175 | + }); |
| 176 | +}); |
| 177 | + |
74 | 178 | describe("resolvePathLinkTarget", () => { |
75 | 179 | it("resolves relative paths against cwd", () => { |
76 | 180 | expect( |
|
0 commit comments