refactor(mobile): drop the dead URL-tap constant and two stale reflow guards

Round 1 fixes, all three folded here.

1. `URL_TAP_WEBVIEW_JS` is gone from terminal-webview-url-tap.ts, with
   `document/url-tap.test.ts` deleted alongside it. The document is generated
   from its modules now, so that constant was a second copy of the URL-tap group
   with no consumer but its own tests. terminal-webview-url-tap.test.ts's
   resolver harness reads the document's own text instead, the path-tap,
   url-tap, osc-link-tap and surface-tap modules in document order through
   `generatedDocumentModule`, which refuses unless the document carries each
   verbatim. Its 33 expects all stay. One mechanism-only assertion went with the
   file: `document/url-tap.test.ts`'s single `compareTerminalDocumentScripts`
   pin of the three emissions against the constant, which the flip test's
   whole-document pin already covers. The file's other exports stay.

   The deletion surfaced a third reader. terminal-webview-scroll-routing.test.ts
   concatenated terminal-webview-url-tap.ts into its `source`, and its
   `notify({ type: 'terminal-tap' });` assertion was matching the constant's
   single-quoted text, not the document. The read is dropped, since nothing else
   in that file needed it, and the assertion is the document's form:

     notify({ type: 'terminal-tap' });  ->  notify({ type: "terminal-tap" });

   Its 95 expects stay. Leaving the read in place would let a document assertion
   pass against a module source, which is the hazard this lane exists to remove.

2. terminal-webview-reflow.test.ts guarded a template placeholder that no longer
   exists, so it could not fail:

     expect(XTERM_HTML).not.toContain('TERMINAL_REFLOW_JS}')
       ->  expect(XTERM_HTML.split(reflowSource).length - 1).toBe(1)

   Same intent against the generated document: the reflow module's emitted text
   is in the document exactly once. The case is renamed to say so and the
   comment above it describes the generator, not the deleted template.

3. Same file, the routine assertion still passed as a substring of the qualified
   call; qualified as line 30 already was:

     term.resize(nextCols, nextRows);  ->  scope.term.resize(nextCols, nextRows);

   Its 22 expects stay.

Controls, each verified to have changed the file first, all red, tree green
after restore:

  osc-link-tap  return parsePathLineCol(value)        -> url-tap test, 3 failed
  surface-tap   notify({ type: 'terminal-tap' })      -> scroll-routing, 1 failed
  reflow        scope.term.resize(nextCols, nextRows) -> reflow test, 2 failed
  module order  'reflow' listed twice                 -> reflow test, expected 2 to be 1

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo-H
2026-09-20 11:35:10 -04:00
parent d914af2e1c
commit 8da7680c9b
5 changed files with 24 additions and 272 deletions
@@ -1,37 +0,0 @@
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { emitTerminalDocumentModule } from '../../../scripts/build-terminal-document-script.mjs'
import { URL_TAP_WEBVIEW_JS } from '../terminal-webview-url-tap'
import { compareTerminalDocumentScripts } from './terminal-document-equivalence.test-support'
/**
* The URL-tap group is three modules, not one: at 303 lines it was over the file cap, and the
* document's own order interleaves the OSC 8 lookup with the file-URL parsing. The split follows
* that order, so the group's text is the three emissions joined.
*/
const modulePaths = ['./url-tap.ts', './osc-link-tap.ts', './surface-tap.ts'].map((relative) =>
fileURLToPath(new URL(relative, import.meta.url))
)
describe('the url-tap group', () => {
it('emits the script the document carries, modulo the five normalisations', async () => {
const emitted = (await Promise.all(modulePaths.map(emitTerminalDocumentModule))).join('\n')
expect(compareTerminalDocumentScripts(URL_TAP_WEBVIEW_JS, emitted, 'scope')).toEqual({
equivalent: true,
normalisations: {
// The terminal twice through its internals, and the captured OSC 8 links with their row
// offset; the two patterns and the length bound are build-time constants, not state.
qualifiedReferences: 10,
scopeFieldDeclarations: 0,
rebindings: 41,
bracedBodies: 25,
// Every read of xterm's internals, the two URL parses and the text capture.
unboundCatches: 6,
// All four take a digit run a capture group already matched.
numberProperties: 4,
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
})
@@ -55,19 +55,15 @@ describe('terminal WebView reflow', () => {
expect(htmlSource).toContain('notify({ type: "measure-result", cols: null, rows: null });')
})
// Why: the raw-source assertions above pass even if the reflow module is
// dropped from the XTERM_HTML concatenation (a broken/removed import or an
// emptied TERMINAL_REFLOW_JS leaves the `${...}` placeholder in the template
// but never injects the routine). That was the regression class reported when
// a sibling refactor extracted the tap dispatcher next to the reflow inject.
// Guard the *assembled* document so the routine and its dispatch are really
// present in what the WebView runs.
// Why: the assertions above read the reflow module's own emission, which still reads whole if
// the generator drops the module from the document or emits it twice. That was the regression
// class reported when a sibling refactor extracted the tap dispatcher next to reflow. Guard the
// assembled document so the routine, once, and its dispatch are really in what the WebView runs.
describe('assembled XTERM_HTML', () => {
it('still injects the reflow routine (placeholder fully expanded)', () => {
it('carries the reflow routine exactly once', () => {
expect(XTERM_HTML).toContain('function reflow(cols, rows) {')
expect(XTERM_HTML).toContain('term.resize(nextCols, nextRows);')
// No unexpanded template placeholder for the injected reflow JS.
expect(XTERM_HTML).not.toContain('TERMINAL_REFLOW_JS}')
expect(XTERM_HTML).toContain('scope.term.resize(nextCols, nextRows);')
expect(XTERM_HTML.split(reflowSource).length - 1).toBe(1)
})
it('still routes the reflow message to the injected routine', () => {
@@ -2,12 +2,11 @@ import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
import { XTERM_HTML } from './terminal-webview-html'
// The in-WebView JS lives in terminal-webview-html.ts; the RN wrapper in
// TerminalWebView.tsx. Concatenate both so assertions resolve regardless of file.
// The RN wrapper and the pending-message queue are TypeScript; everything the WebView runs is the
// generated document. Concatenated so assertions resolve regardless of file.
const source =
readFileSync(new URL('./TerminalWebView.tsx', import.meta.url), 'utf8') +
readFileSync(new URL('./terminal-webview-pending-messages.ts', import.meta.url), 'utf8') +
readFileSync(new URL('./terminal-webview-url-tap.ts', import.meta.url), 'utf8') +
XTERM_HTML
const sessionSource = readFileSync(
new URL('../session/use-mobile-session-terminal-input.ts', import.meta.url),
@@ -242,7 +241,7 @@ describe('TerminalWebView scroll routing', () => {
expect(tapHandlerBlock).toContain(
'if (focusKeyboard || !isClickMouseTrackingMode(getMouseTrackingMode()))'
)
expect(tapHandlerBlock).toContain("notify({ type: 'terminal-tap' });")
expect(tapHandlerBlock).toContain('notify({ type: "terminal-tap" });')
})
it('allows x10 mouse gesture reports through the mobile session gate', () => {
@@ -1,11 +1,13 @@
import { createContext, Script } from 'node:vm'
import { describe, expect, it } from 'vitest'
import type { TappedFilePath } from './terminal-path-tap'
import { generatedDocumentModule } from './document/generated-document-region.test-support'
import {
documentDeclaredFunction,
generatedDocumentModule
} from './document/generated-document-region.test-support'
import {
TERMINAL_HTTP_URL_MAX_LENGTH,
TERMINAL_HTTP_URL_REGEX_SOURCE,
URL_TAP_WEBVIEW_JS,
findFileUrlAtColumn,
findUrlAtColumn,
resolveTerminalOscFileTap,
@@ -13,7 +15,12 @@ import {
} from './terminal-webview-url-tap'
import { XTERM_HTML } from './terminal-webview-html'
const pathTapSource = await generatedDocumentModule('path-tap')
// The three modules the document carries the URL-tap group as, in its own order.
const urlTapGroupSource = (
await Promise.all(
['path-tap', 'url-tap', 'osc-link-tap', 'surface-tap'].map(generatedDocumentModule)
)
).join('\n')
type FileTapResolverCase = {
name: string
@@ -101,19 +108,15 @@ function createInjectedFileTapResolvers(): {
resolveTerminalFileUrlTap: InjectedFileTapResolver
resolveTerminalOscFileTap: InjectedFileTapResolver
} {
const context = createContext({ URL })
const context: Record<string, unknown> = createContext({ URL })
new Script(
`${pathTapSource}\n${URL_TAP_WEBVIEW_JS}\n` +
`${urlTapGroupSource}\n` +
'this.__resolveTerminalFileUrlTap = resolveTerminalFileUrlTap;\n' +
'this.__resolveTerminalOscFileTap = resolveTerminalOscFileTap;'
).runInContext(context)
const injected = context as {
__resolveTerminalFileUrlTap: InjectedFileTapResolver
__resolveTerminalOscFileTap: InjectedFileTapResolver
}
return {
resolveTerminalFileUrlTap: injected.__resolveTerminalFileUrlTap,
resolveTerminalOscFileTap: injected.__resolveTerminalOscFileTap
resolveTerminalFileUrlTap: documentDeclaredFunction(context, '__resolveTerminalFileUrlTap'),
resolveTerminalOscFileTap: documentDeclaredFunction(context, '__resolveTerminalOscFileTap')
}
}
@@ -43,212 +43,3 @@ function findTerminalUrlAtColumn(lineText: string, col: number, source: string):
}
return null
}
export const URL_TAP_WEBVIEW_JS = `
var URL_TAP_RE_SOURCE = ${JSON.stringify(TERMINAL_HTTP_URL_REGEX_SOURCE)};
var FILE_URL_TAP_RE_SOURCE = ${JSON.stringify(TERMINAL_FILE_URL_REGEX_SOURCE)};
var URL_TAP_MAX_LENGTH = ${TERMINAL_HTTP_URL_MAX_LENGTH};
function findUrlAtColumn(lineText, col) {
return findTerminalUrlAtColumn(lineText, col, URL_TAP_RE_SOURCE);
}
function findFileUrlAtColumn(lineText, col) {
return findTerminalUrlAtColumn(lineText, col, FILE_URL_TAP_RE_SOURCE);
}
function findTerminalUrlAtColumn(lineText, col, source) {
if (typeof lineText !== 'string' || lineText.length === 0) return null;
var re = new RegExp(source, 'gi');
var match;
while ((match = re.exec(lineText)) !== null) {
var end = match.index + match[0].length;
if (match[0].length <= URL_TAP_MAX_LENGTH && col >= match.index && col < end) return match[0];
if (match[0].length === 0) re.lastIndex++;
}
return null;
}
function fileUrlAtViewportPoint(clientX, clientY) {
var cell = viewportToCell(clientX, clientY);
if (!cell) return null;
return findFileUrlAtColumn(getLineText(cell.row), cellColToStringIndex(cell.row, cell.col));
}
function urlAtViewportPoint(clientX, clientY) {
var cell = viewportToCell(clientX, clientY);
if (!cell) return null;
// Map the cell column to a string index so wide chars earlier on the line
// don't shift the match column off the tapped URL.
return findUrlAtColumn(getLineText(cell.row), cellColToStringIndex(cell.row, cell.col));
}
// Why: OSC 8 links can render as labels like "#1234"; the URI lives in
// xterm's internal link service, so every access is guarded and falls through.
function oscLinkService() {
try {
var core = term && term._core;
if (!core) return null;
return core._oscLinkService
|| (core._inputHandler && core._inputHandler._oscLinkService)
|| null;
} catch (e) { return null; }
}
function oscLinkAtViewportPoint(clientX, clientY) {
try {
var cell = viewportToCell(clientX, clientY);
if (!cell) return null;
var line = term.buffer.active.getLine(cell.row);
if (!line) return null;
var urlId = oscLinkIdAtCell(line, cell.col);
if (!urlId) return initialOscLinkAtCell(cell.row, cell.col);
var svc = oscLinkService();
if (!svc || !svc.getLinkData) return initialOscLinkAtCell(cell.row, cell.col);
var data = svc.getLinkData(urlId);
var uri = data && data.uri;
return terminalOscLinkTarget(uri);
} catch (e) { return null; }
}
function initialOscLinkAtCell(row, col) {
for (var i = 0; i < initialOscLinks.length; i++) {
var link = initialOscLinks[i];
if (!link || typeof link.uri !== 'string') continue;
if (link.row < initialOscLinkRowOffset) continue;
var shiftedRow = link.row - initialOscLinkRowOffset;
if (shiftedRow === row && col >= link.startCol && col < link.endCol && initialOscLinkTextStillMatches(link, shiftedRow)) return terminalOscLinkTarget(link.uri);
}
return null;
}
function terminalOscLinkTarget(uri) {
if (typeof uri !== 'string') return null;
if (/^https?:/i.test(uri)) return { kind: 'url', url: uri };
var fileTap = resolveTerminalOscFileTap(uri);
return fileTap ? { kind: 'file', fileTap: fileTap } : null;
}
function resolveTerminalOscFileTap(uri) {
return resolveTerminalFileUrlTap(uri) || parseOscPathLikeTarget(uri);
}
function resolveTerminalFileUrlTap(uri) {
var parsed;
try {
parsed = new URL(uri);
} catch (e) {
return null;
}
if (parsed.protocol !== 'file:') return null;
var filePath;
try {
filePath = decodeURIComponent(parsed.pathname || '');
} catch (e) {
return null;
}
if (parsed.hostname && !isLocalFileUriHostname(parsed.hostname)) {
filePath = '//' + parsed.hostname + filePath;
} else if (/^\\/[A-Za-z]:\\//.test(filePath)) {
filePath = filePath.slice(1);
}
if (!filePath) return null;
var hashTarget = parseFileUrlLineHash(parsed.hash || '');
if (hashTarget) {
return { pathText: filePath, line: hashTarget.line, column: hashTarget.column };
}
if (/%3a/i.test(parsed.pathname || '')) {
return { pathText: filePath, line: null, column: null };
}
return parseFilePathTrailingLineTarget(filePath) || { pathText: filePath, line: null, column: null };
}
function isLocalFileUriHostname(hostname) {
var normalized = String(hostname).toLowerCase();
return normalized === 'localhost' || normalized === '127.0.0.1' || normalized === '::1' || normalized === '[::1]';
}
function parseOscPathLikeTarget(value) {
if (!/^(?:~[\\\\/]|[\\\\/]|\\.{1,2}[\\\\/]|[A-Za-z]:[\\\\/]|[A-Za-z0-9._-]+[\\\\/]|(?=[A-Za-z0-9._-]*\\.[A-Za-z0-9]))/.test(value)) return null;
return parsePathLineCol(value);
}
function parseFileUrlLineHash(hash) {
var match = /^#?L(\\d+)(?:C(\\d+))?$/i.exec(hash);
if (!match) return null;
var line = parseInt(match[1], 10);
var column = match[2] ? parseInt(match[2], 10) : null;
if (line < 1 || (column !== null && column < 1)) return null;
return { line: line, column: column };
}
function parseFilePathTrailingLineTarget(filePath) {
var match = /^(.*?)(?::(\\d+))(?::(\\d+))?$/.exec(filePath);
if (!match || !match[1] || match[1].charAt(match[1].length - 1) === '/' || match[1].charAt(match[1].length - 1) === '\\\\') return null;
var line = parseInt(match[2], 10);
var column = match[3] ? parseInt(match[3], 10) : null;
if (line < 1 || (column !== null && column < 1)) return null;
return { pathText: match[1], line: line, column: column };
}
function captureInitialOscLinkTexts() {
if (!Array.isArray(initialOscLinks)) return;
for (var i = 0; i < initialOscLinks.length; i++) {
var link = initialOscLinks[i];
if (!link || typeof link.text === 'string') continue;
link.text = initialOscLinkTextAtRow(link, link.row);
}
}
function initialOscLinkTextStillMatches(link, row) {
if (typeof link.text !== 'string') return false;
return link.text.length > 0 && initialOscLinkTextAtRow(link, row) === link.text;
}
function initialOscLinkTextAtRow(link, row) {
try {
var lineText = getLineText(row);
var start = cellColToStringIndex(row, link.startCol);
var end = cellColToStringIndex(row, link.endCol);
return lineText.slice(start, end);
} catch (e) {
return '';
}
}
function oscLinkIdAtCell(line, col) {
try {
var bufCell = line.getCell(col);
return bufCell && bufCell.extended && bufCell.extended.urlId ? bufCell.extended.urlId : 0;
} catch (e) { return 0; }
}
function notifyTerminalSurfaceTap(originX, originY, focusKeyboard) {
var tappedOscLink = oscLinkAtViewportPoint(originX, originY);
if (tappedOscLink && tappedOscLink.kind === 'file') {
notify({
type: 'terminal-file-tap',
pathText: tappedOscLink.fileTap.pathText,
line: tappedOscLink.fileTap.line,
column: tappedOscLink.fileTap.column
});
return;
}
var tappedFileUrl = fileUrlAtViewportPoint(originX, originY);
var tappedFileUrlPath = tappedFileUrl ? resolveTerminalFileUrlTap(tappedFileUrl) : null;
if (tappedFileUrlPath) {
notify({
type: 'terminal-file-tap',
pathText: tappedFileUrlPath.pathText,
line: tappedFileUrlPath.line,
column: tappedFileUrlPath.column
});
return;
}
var tappedUrl = tappedOscLink && tappedOscLink.kind === 'url' ? tappedOscLink.url : urlAtViewportPoint(originX, originY);
if (tappedUrl) {
notify({ type: 'open-url', url: tappedUrl });
return;
}
var tappedPath = filePathAtViewportPoint(originX, originY);
if (tappedPath) {
notify({
type: 'terminal-file-tap',
pathText: tappedPath.pathText,
line: tappedPath.line,
column: tappedPath.column
});
return;
}
var clickInput = buildMouseClickInput(originX, originY);
if (clickInput) {
notify({ type: 'terminal-input', bytes: clickInput });
}
// Touch still needs native input focus after the TUI consumes its mouse click.
if (focusKeyboard || !isClickMouseTrackingMode(getMouseTrackingMode())) {
notify({ type: 'terminal-tap' });
}
}
`