perf(terminal): stop HTTP link backward scans at rows that cannot continue a URL (#35767)

* perf(terminal): stop HTTP link backward scans at rows that cannot continue a URL

Framed (hard-wrapped) detection walked back up to 2048 rows per hovered line,
building column maps for every URL row; edge-wrapped walked ~54 rows at 80 cols.
Stop once a later row cannot be a continuation (borders, prose, blank rows).
Table-heavy buffer: provideLinks 1.3-2.1ms -> 36-79us per call.

* test(terminal): pin the edge-wrapped HTTP link backward-scan bound
This commit is contained in:
Neil
2026-10-29 00:39:39 -07:00
committed by GitHub
parent 57ff56f02a
commit 6de2007cb7
4 changed files with 60 additions and 0 deletions
@@ -98,6 +98,15 @@ describe('edge-wrapped terminal HTTP links', () => {
}
)
it('stops scanning back at a prose row that cannot continue a URL', () => {
const prose = makeBufferLine('see https://example.com and more words')
const hovered = makeBufferLine('tail')
const getLine = vi.fn((y: number) => (y === 199 ? hovered : prose))
buildEdgeWrappedHttpLogicalLineCandidates({ getLine }, 200)
expect(new Set(getLine.mock.calls.map(([y]) => y)).size).toBeLessThan(5)
})
it('opens a URL when a wide glyph starts the continuation row', () => {
const prefix = 'https://example.com/wide/'
const firstRow = `${prefix}${'c'.repeat(COLS - 1 - prefix.length)}`
@@ -8,6 +8,8 @@ const HTTP_STATUS_ROW_PATTERN = /^HTTP\/\d(?:\.\d)?:\d{3}(?:\s|$)/i
const VERTICAL_LAYOUT_FRAME_PATTERN = /[│┃║╎╏┆┇┊┋|]/
const EMPTY_LABEL_ROW_PATTERN = /^[^\s:][^:]*:$/
const LABEL_WITH_SPACING_ROW_PATTERN = /^[^\s:][^:]*:\s/
const NON_BLANK_ROW_PATTERN = /\S/
const SINGLE_TOKEN_ROW_PATTERN = /^ *\S+\s*$/
function maxEdgeWrappedHttpRows(lineLength: number, padding: number): number {
// Width-2 cells give at least floor(columns / 2) characters per continued row; add two fragment rows to bound scanning by URL length.
@@ -147,8 +149,21 @@ export function buildEdgeWrappedHttpLogicalLineCandidates(
return translated
}
const minY = Math.max(0, currentY - maxRows + 1)
let laterRowText: string | undefined
for (let startY = currentY; startY >= minY; startY--) {
// Earlier starts must continue through the later row: frame-free, and one token unless it is the hovered row.
if (
startY < currentY &&
(!laterRowText ||
VERTICAL_LAYOUT_FRAME_PATTERN.test(laterRowText) ||
!(startY + 1 === currentY ? NON_BLANK_ROW_PATTERN : SINGLE_TOKEN_ROW_PATTERN).test(
laterRowText
))
) {
break
}
const startText = buffer.getLine(startY)?.translateToString(false)
laterRowText = startText
const schemeIndex = startText ? findHttpSchemeIndex(startText) : -1
if (!startText || schemeIndex === -1 || VERTICAL_LAYOUT_FRAME_PATTERN.test(startText)) {
continue
@@ -81,6 +81,19 @@ describe('hard-wrapped terminal HTTP candidate bounds', () => {
expect(onTranslateWithColumns).not.toHaveBeenCalled()
})
it('stops scanning back at a table border instead of walking every framed row', () => {
const rows = Array.from({ length: TERMINAL_HTTP_URL_MAX_HARD_WRAPPED_ROWS }, (_value, index) =>
bufferLine(
index % 10 === 0 ? '├──────────────────────┤' : `│ https://example.com/${index} │`,
() => undefined
)
)
const getLine = vi.fn((y: number) => rows[y])
buildHardWrappedHttpLogicalLineCandidates({ getLine }, TERMINAL_HTTP_URL_MAX_HARD_WRAPPED_ROWS)
expect(new Set(getLine.mock.calls.map(([y]) => y)).size).toBeLessThan(20)
})
it('reconstructs framed URL rows aligned by cells when a fragment has wide characters', () => {
const rowTexts = ['|http://a/ab|', '|a你你你你你|', '|tailzzzzzzz|']
const rows = rowTexts.map(bufferLineWithCellColumns)
@@ -11,11 +11,28 @@ const HTTP_FRAGMENT_PATTERN = /^[^\s"'!*(){}|\\^<>`│┃║╎╏┆┇┊┋]*
const VERTICAL_LAYOUT_FRAME_PATTERN = /[│┃║╎╏┆┇┊┋|]/
const NON_LAYOUT_SUFFIX_PATTERN = /[^\s│┃║╎╏┆┇┊┋|]/
const HARD_WRAP_CONTINUATION_SUFFIX_PATTERN = /[/?&=#%+:-]$/
const LAYOUT_SUFFIX_CHAR_PATTERN = /[\s│┃║╎╏┆┇┊┋|]/
const MIN_HARD_WRAPPED_HTTP_ROWS = 3
const MIN_HARD_WRAP_FILL_RATIO = 0.8
type TranslatedLine = ReturnType<typeof translateLineWithColumns>
// Continuation rows end in frame + prefix + URL fragment + layout-only suffix with a right frame.
function canBeFramedContinuationRow(text: string): boolean {
let contentEnd = text.length
let hasRightFrame = false
while (contentEnd > 0 && LAYOUT_SUFFIX_CHAR_PATTERN.test(text[contentEnd - 1]!)) {
hasRightFrame ||= VERTICAL_LAYOUT_FRAME_PATTERN.test(text[contentEnd - 1]!)
contentEnd--
}
return (
hasRightFrame &&
contentEnd > 0 &&
Boolean(HTTP_FRAGMENT_PATTERN.exec(text[contentEnd - 1]!)?.[0]) &&
VERTICAL_LAYOUT_FRAME_PATTERN.test(text.slice(0, contentEnd))
)
}
function findFrameIndexBefore(text: string, beforeIndex: number): number {
for (let index = beforeIndex - 1; index >= 0; index--) {
if (VERTICAL_LAYOUT_FRAME_PATTERN.test(text[index]!)) {
@@ -189,6 +206,12 @@ export function buildHardWrappedHttpLogicalLineCandidates(
if (candidate) {
candidates.push(candidate)
}
// Earlier starts must continue through this row; stop at borders, prose, and blank rows.
const rowText =
translatedLines.get(startY)?.text ?? buffer.getLine(startY)?.translateToString(false)
if (rowText === undefined || !canBeFramedContinuationRow(rowText)) {
break
}
}
return candidates.sort(
(left, right) => right.rows.length - left.rows.length || right.text.length - left.text.length