diff --git a/mobile/scripts/terminal-document-module-order.mjs b/mobile/scripts/terminal-document-module-order.mjs new file mode 100644 index 00000000000..4f6d7539df0 --- /dev/null +++ b/mobile/scripts/terminal-document-module-order.mjs @@ -0,0 +1,45 @@ +/** + * The order the document's modules are spliced back into the script, which is the order the + * hand-written document had. It is data, not a dependency graph: the document is one function + * scope, so declarations must land where they landed before. + * + * Both the generator and the equivalence test read this, so neither can drift from the other. + */ +export const TERMINAL_DOCUMENT_MODULE_ORDER = [ + 'runtime-constants', + 'terminal-handle', + 'query-reply', + 'surface-swap', + 'text-scaling', + 'viewport-transform', + 'terminal-theme', + 'fit-scale', + 'mouse-mode-decset-scan', + 'write-queue', + 'webgl-recovery', + 'terminal-init', + 'reflow', + 'host-notify', + 'host-message-router', + 'selection-state-and-eviction', + 'mode-mirroring', + 'keyboard-avoidance-metrics', + 'term-observers', + 'viewport-cell', + 'mouse-report-cell', + 'mouse-input-encoding', + 'normal-buffer-smooth-scroll', + 'cell-geometry', + 'path-tap', + 'url-tap', + 'osc-link-tap', + 'surface-tap', + 'selection-range', + 'selection-overlay', + 'tap-dispatch', + 'wheel-scroll', + 'mouse-click-drag', + 'selection-menu-buttons', + 'surface-touch-gestures', + 'message-bridge' +] diff --git a/mobile/src/terminal/document/fit-scale.test.ts b/mobile/src/terminal/document/fit-scale.test.ts index a9701972669..659a9374665 100644 --- a/mobile/src/terminal/document/fit-scale.test.ts +++ b/mobile/src/terminal/document/fit-scale.test.ts @@ -21,7 +21,8 @@ describe('the terminal fit-scale slice', () => { bracedBodies: 20, unboundCatches: 0, numberProperties: 9, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/host-message-router.test.ts b/mobile/src/terminal/document/host-message-router.test.ts index 3916290526d..d8f098ace42 100644 --- a/mobile/src/terminal/document/host-message-router.test.ts +++ b/mobile/src/terminal/document/host-message-router.test.ts @@ -23,7 +23,8 @@ describe('the host-message-router slice', () => { bracedBodies: 12, unboundCatches: 2, numberProperties: 0, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/keyboard-avoidance-metrics.test.ts b/mobile/src/terminal/document/keyboard-avoidance-metrics.test.ts index c624c5732d9..9b75a302e5a 100644 --- a/mobile/src/terminal/document/keyboard-avoidance-metrics.test.ts +++ b/mobile/src/terminal/document/keyboard-avoidance-metrics.test.ts @@ -23,7 +23,8 @@ describe('the keyboard-avoidance metrics module', () => { // The row scan and the alternate-screen probe. unboundCatches: 2, numberProperties: 0, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/message-bridge.test.ts b/mobile/src/terminal/document/message-bridge.test.ts index 9c2d662df66..c09223cf73c 100644 --- a/mobile/src/terminal/document/message-bridge.test.ts +++ b/mobile/src/terminal/document/message-bridge.test.ts @@ -20,7 +20,8 @@ describe('the message-bridge module', () => { // The parse guard. The second catch names its error and reports it, so it keeps its binding. unboundCatches: 1, numberProperties: 0, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/mouse-click-drag.test.ts b/mobile/src/terminal/document/mouse-click-drag.test.ts index 99861cd926a..195f4a3a0bc 100644 --- a/mobile/src/terminal/document/mouse-click-drag.test.ts +++ b/mobile/src/terminal/document/mouse-click-drag.test.ts @@ -22,7 +22,8 @@ describe('the mouse-click-drag module', () => { // The pointer-capture call, which throws when capture is unavailable. unboundCatches: 1, numberProperties: 0, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/mouse-mode-decset-scan.test.ts b/mobile/src/terminal/document/mouse-mode-decset-scan.test.ts index 07bd7a50cf9..e954a06c169 100644 --- a/mobile/src/terminal/document/mouse-mode-decset-scan.test.ts +++ b/mobile/src/terminal/document/mouse-mode-decset-scan.test.ts @@ -21,7 +21,8 @@ describe('the mouse-mode DECSET scan module', () => { bracedBodies: 9, unboundCatches: 0, numberProperties: 0, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/mouse-report-and-scroll-routing.test.ts b/mobile/src/terminal/document/mouse-report-and-scroll-routing.test.ts index bd31d3b0c24..fa9adc5adb6 100644 --- a/mobile/src/terminal/document/mouse-report-and-scroll-routing.test.ts +++ b/mobile/src/terminal/document/mouse-report-and-scroll-routing.test.ts @@ -30,7 +30,8 @@ describe('the mouse-report and scroll-routing slice', () => { // Three reads of xterm's mode state, each of which may not exist. unboundCatches: 3, numberProperties: 0, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/mouse-report-cell.test.ts b/mobile/src/terminal/document/mouse-report-cell.test.ts index 6270e92ae85..6e736d47fbb 100644 --- a/mobile/src/terminal/document/mouse-report-cell.test.ts +++ b/mobile/src/terminal/document/mouse-report-cell.test.ts @@ -29,7 +29,8 @@ describe('the mouse-report cell module', () => { bracedBodies: 13, unboundCatches: 0, numberProperties: 0, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } } ) diff --git a/mobile/src/terminal/document/path-tap.test.ts b/mobile/src/terminal/document/path-tap.test.ts index c8ee7b212b0..1c5d854919d 100644 --- a/mobile/src/terminal/document/path-tap.test.ts +++ b/mobile/src/terminal/document/path-tap.test.ts @@ -21,7 +21,8 @@ describe('the path-tap module', () => { unboundCatches: 0, // Both `parseInt` calls take a digit run a capture group already matched. numberProperties: 2, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/query-reply.test.ts b/mobile/src/terminal/document/query-reply.test.ts index cef85062b03..546dc75ecec 100644 --- a/mobile/src/terminal/document/query-reply.test.ts +++ b/mobile/src/terminal/document/query-reply.test.ts @@ -23,7 +23,8 @@ describe('the query-reply module', () => { // Both `catch (e) {}` clauses, whose binding was never read. unboundCatches: 2, numberProperties: 0, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/reflow.test.ts b/mobile/src/terminal/document/reflow.test.ts index c2ce02007fa..280b36196d2 100644 --- a/mobile/src/terminal/document/reflow.test.ts +++ b/mobile/src/terminal/document/reflow.test.ts @@ -22,7 +22,8 @@ describe('the reflow module', () => { bracedBodies: 2, unboundCatches: 0, numberProperties: 0, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/runtime-constants.test.ts b/mobile/src/terminal/document/runtime-constants.test.ts index 76b35e3e57e..7dc2e72eed8 100644 --- a/mobile/src/terminal/document/runtime-constants.test.ts +++ b/mobile/src/terminal/document/runtime-constants.test.ts @@ -21,7 +21,8 @@ describe('the runtime-constants module', () => { bracedBodies: 0, unboundCatches: 0, numberProperties: 0, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/runtime-state.test.ts b/mobile/src/terminal/document/runtime-state.test.ts index 186f94503a1..86d8a6f9c74 100644 --- a/mobile/src/terminal/document/runtime-state.test.ts +++ b/mobile/src/terminal/document/runtime-state.test.ts @@ -35,7 +35,8 @@ describe('the runtime-state and text-scaling slice', () => { bracedBodies: 0, unboundCatches: 0, numberProperties: 0, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) @@ -57,7 +58,8 @@ describe('the runtime-state and text-scaling slice', () => { bracedBodies: 13, unboundCatches: 1, numberProperties: 0, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/selection-overlay.test.ts b/mobile/src/terminal/document/selection-overlay.test.ts index 5c95d4d47a2..2cd7d853052 100644 --- a/mobile/src/terminal/document/selection-overlay.test.ts +++ b/mobile/src/terminal/document/selection-overlay.test.ts @@ -28,7 +28,8 @@ describe('the selection-overlay slice', () => { bracedBodies: 63, unboundCatches: 9, numberProperties: 6, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/selection-state-and-eviction.test.ts b/mobile/src/terminal/document/selection-state-and-eviction.test.ts index cb94391b3aa..e5e1c3c2981 100644 --- a/mobile/src/terminal/document/selection-state-and-eviction.test.ts +++ b/mobile/src/terminal/document/selection-state-and-eviction.test.ts @@ -23,7 +23,8 @@ describe('the selection-state-and-eviction module', () => { bracedBodies: 3, unboundCatches: 0, numberProperties: 0, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/smooth-scroll-and-cell-geometry.test.ts b/mobile/src/terminal/document/smooth-scroll-and-cell-geometry.test.ts index 288c6fab7b2..e430a0a5bda 100644 --- a/mobile/src/terminal/document/smooth-scroll-and-cell-geometry.test.ts +++ b/mobile/src/terminal/document/smooth-scroll-and-cell-geometry.test.ts @@ -28,7 +28,8 @@ describe('the smooth-scroll and cell-geometry slice', () => { bracedBodies: 16, unboundCatches: 0, numberProperties: 0, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/surface-swap.test.ts b/mobile/src/terminal/document/surface-swap.test.ts index 667afde5265..149a6c360a0 100644 --- a/mobile/src/terminal/document/surface-swap.test.ts +++ b/mobile/src/terminal/document/surface-swap.test.ts @@ -22,7 +22,8 @@ describe('the surface-swap module', () => { bracedBodies: 2, unboundCatches: 2, numberProperties: 0, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/surface-touch-gestures.test.ts b/mobile/src/terminal/document/surface-touch-gestures.test.ts index c8d65c4b2d3..9ba8aec5b66 100644 --- a/mobile/src/terminal/document/surface-touch-gestures.test.ts +++ b/mobile/src/terminal/document/surface-touch-gestures.test.ts @@ -27,7 +27,8 @@ describe('the surface-touch-gestures slice', () => { bracedBodies: 57, unboundCatches: 2, numberProperties: 2, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/tap-dispatch.test.ts b/mobile/src/terminal/document/tap-dispatch.test.ts index 95464bfb9fc..eedd56a9c1d 100644 --- a/mobile/src/terminal/document/tap-dispatch.test.ts +++ b/mobile/src/terminal/document/tap-dispatch.test.ts @@ -20,7 +20,8 @@ describe('the tap-dispatch module', () => { bracedBodies: 11, unboundCatches: 0, numberProperties: 0, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/term-observers.test.ts b/mobile/src/terminal/document/term-observers.test.ts index 5c616982554..f927b6a8a2c 100644 --- a/mobile/src/terminal/document/term-observers.test.ts +++ b/mobile/src/terminal/document/term-observers.test.ts @@ -31,7 +31,8 @@ describe('the term-observers slice', () => { numberProperties: 0, // The two SGR mode flags, written twice each: the document's shorthand cannot survive a // qualified value. - shorthandProperties: 4 + shorthandProperties: 4, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/terminal-document-equivalence.test-support.ts b/mobile/src/terminal/document/terminal-document-equivalence.test-support.ts index 05ad0311eae..eb74c223e35 100644 --- a/mobile/src/terminal/document/terminal-document-equivalence.test-support.ts +++ b/mobile/src/terminal/document/terminal-document-equivalence.test-support.ts @@ -50,6 +50,11 @@ export type TerminalDocumentNormalisations = { readonly numberProperties: number /** `{ name: name }` was shorthand; qualifying the value spells the property out again. */ readonly shorthandProperties: number + /** + * An inner binding that shadowed a document variable stopped being a shadow once that variable + * moved onto the scope, so the printer stopped renaming it. + */ + readonly unshadowedNames: number } /** @@ -60,6 +65,17 @@ export type TerminalDocumentNormalisations = { * behind a `typeof … === 'number'` check or is parsing a string, which is what the `Number` form * does with no coercion of its own. */ +/** + * Whether `printed` is the printer's disambiguated form of `original`: the same name with a decimal + * suffix it appends when two bindings of that name are visible at once. + */ +function isPrinterDisambiguation(printed: string, original: string): boolean { + if (!printed.startsWith(original) || printed.length === original.length) { + return false + } + return /^[2-9][0-9]*$/.test(printed.slice(original.length)) +} + const NUMBER_GLOBALS = new Set(['isFinite', 'isNaN', 'parseInt', 'parseFloat']) export type TerminalDocumentEquivalence = @@ -179,6 +195,7 @@ export function compareTerminalDocumentScripts( let unboundCatches = 0 let numberProperties = 0 let shorthandProperties = 0 + let unshadowedNames = 0 // Braces arrive in pairs around one statement, so a counter is enough: a close is only ever // absorbed while an inserted open is outstanding, which bounds how far this can mask a real one. let openInsertedBraces = 0 @@ -194,6 +211,19 @@ export function compareTerminalDocumentScripts( right += 1 continue } + // `term2` -> `term`: the printer disambiguated a shadowed binding on the baseline side, and + // qualifying the outer name removed the shadow, so the inner one keeps its own name. + if ( + expected.label === 'name' && + actual.label === 'name' && + isPrinterDisambiguation(expected.text, actual.text) + ) { + unshadowedNames += 1 + lastMatched = actual + left += 1 + right += 1 + continue + } // `{ name }` -> `{ name: .name }`: the printer writes the baseline's shorthand back // as one token, and qualifying the value makes the property name unavoidable again. if ( @@ -303,7 +333,8 @@ export function compareTerminalDocumentScripts( bracedBodies, unboundCatches, numberProperties, - shorthandProperties + shorthandProperties, + unshadowedNames } } } diff --git a/mobile/src/terminal/document/terminal-document-equivalence.test.ts b/mobile/src/terminal/document/terminal-document-equivalence.test.ts index 2a55359415f..538155343ec 100644 --- a/mobile/src/terminal/document/terminal-document-equivalence.test.ts +++ b/mobile/src/terminal/document/terminal-document-equivalence.test.ts @@ -24,7 +24,8 @@ const NONE: TerminalDocumentNormalisations = { bracedBodies: 0, unboundCatches: 0, numberProperties: 0, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } function normalisationsOf(before: string, after: string): TerminalDocumentNormalisations | string { @@ -92,7 +93,8 @@ describe('terminal document script equivalence', () => { ...NONE, unboundCatches: 1, numberProperties: 0, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 }) }) @@ -133,6 +135,15 @@ describe('terminal document script equivalence', () => { expect(normalisationsOf(script, script)).toEqual(NONE) }) + it('counts a name the printer no longer has to disambiguate', () => { + expect( + normalisationsOf( + 'var term = null; function f(term) { return term; }', + 'scope.term = null; function f(term) { return term; }' + ) + ).toEqual({ ...NONE, scopeFieldDeclarations: 1, unshadowedNames: 2 }) + }) + it('refuses a changed literal', () => { expect(normalisationsOf('var a = 1;', 'var a = 2')).toBe( 'token 3: expected num 1, generated num 2' diff --git a/mobile/src/terminal/document/terminal-document-flip.test.ts b/mobile/src/terminal/document/terminal-document-flip.test.ts new file mode 100644 index 00000000000..b4532254a1b --- /dev/null +++ b/mobile/src/terminal/document/terminal-document-flip.test.ts @@ -0,0 +1,51 @@ +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' +import { emitTerminalDocumentModule } from '../../../scripts/build-terminal-document-script.mjs' +import { TERMINAL_DOCUMENT_MODULE_ORDER } from '../../../scripts/terminal-document-module-order.mjs' +import { XTERM_ENGINE_JS } from '../terminal-webview-engine.generated' +import { XTERM_HTML } from '../terminal-webview-html' +import { + compareTerminalDocumentScripts, + readTerminalDocumentScript +} from './terminal-document-equivalence.test-support' + +/** + * The review of the move, as one number per difference class. + * + * Every line of the document's script is now a module, and this says the two are the same program + * modulo the qualifier and the repository's own rules rewriting an ES5 document the moment its + * source is a linted module. Anything outside those classes refuses with the token index and both + * sides, so a reordered statement, a changed literal or a renamed local cannot pass here. + */ +describe('the whole terminal document script', () => { + it('is what the modules emit, modulo the seven normalisations', async () => { + const emitted = await Promise.all( + TERMINAL_DOCUMENT_MODULE_ORDER.map((name) => + emitTerminalDocumentModule(fileURLToPath(new URL(`./${name}.ts`, import.meta.url))) + ) + ) + const candidate = `(function() {\n${emitted.join('\n')}\n})();` + const baseline = readTerminalDocumentScript(XTERM_HTML, XTERM_ENGINE_JS) + expect(compareTerminalDocumentScripts(baseline, candidate, 'scope')).toEqual({ + equivalent: true, + normalisations: { + // The qualifier, partitioned: 609 reads and writes of a name whose declaration stayed put, + // and 73 declarations that moved onto the scope object. 682 sites in all. + qualifiedReferences: 609, + scopeFieldDeclarations: 73, + // The document's 446 `var` declarators, less the 73 that became scope fields. + rebindings: 373, + // `curly`, measured over the whole script before any of this started. + bracedBodies: 279, + // Of the document's 38 catch clauses, two name their error and report it, so they keep it. + unboundCatches: 36, + // `unicorn/prefer-number-properties`, also measured up front. + numberProperties: 17, + // Two SGR mode flags written twice each: shorthand cannot survive a qualified value. + shorthandProperties: 4, + // Names the printer had to disambiguate while an outer binding of the same name existed. + unshadowedNames: 7 + } + }) + }) +}) diff --git a/mobile/src/terminal/document/terminal-init.test.ts b/mobile/src/terminal/document/terminal-init.test.ts index b7de5d42dbf..517e1c99872 100644 --- a/mobile/src/terminal/document/terminal-init.test.ts +++ b/mobile/src/terminal/document/terminal-init.test.ts @@ -23,7 +23,8 @@ describe('the terminal init-and-write slice', () => { bracedBodies: 18, unboundCatches: 7, numberProperties: 0, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/terminal-theme.test.ts b/mobile/src/terminal/document/terminal-theme.test.ts index a70aa6969b8..fe3d69bb8fc 100644 --- a/mobile/src/terminal/document/terminal-theme.test.ts +++ b/mobile/src/terminal/document/terminal-theme.test.ts @@ -22,7 +22,8 @@ describe('the terminal-theme module', () => { // Every `parseInt`, `parseFloat` and `isFinite` here is applied to a value already proved // numeric, or to a string the two forms agree on. numberProperties: 9, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/url-tap.test.ts b/mobile/src/terminal/document/url-tap.test.ts index 28090a3a531..8b7f69e7a47 100644 --- a/mobile/src/terminal/document/url-tap.test.ts +++ b/mobile/src/terminal/document/url-tap.test.ts @@ -29,7 +29,8 @@ describe('the url-tap group', () => { unboundCatches: 6, // All four take a digit run a capture group already matched. numberProperties: 4, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/webgl-recovery.test.ts b/mobile/src/terminal/document/webgl-recovery.test.ts index e93f4bc8619..9f5df9e488b 100644 --- a/mobile/src/terminal/document/webgl-recovery.test.ts +++ b/mobile/src/terminal/document/webgl-recovery.test.ts @@ -19,7 +19,8 @@ describe('the WebGL recovery module', () => { // Five of the six catch clauses; the attach failure reads its error and keeps its binding. unboundCatches: 5, numberProperties: 0, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/wheel-scroll.test.ts b/mobile/src/terminal/document/wheel-scroll.test.ts index dcc53e82bcc..b9488964cc2 100644 --- a/mobile/src/terminal/document/wheel-scroll.test.ts +++ b/mobile/src/terminal/document/wheel-scroll.test.ts @@ -23,7 +23,8 @@ describe('the wheel-scroll module', () => { // The one `isFinite`, behind a `typeof delta !== 'number'` check that makes the two forms // the same test. numberProperties: 1, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) }) diff --git a/mobile/src/terminal/document/write-queue.test.ts b/mobile/src/terminal/document/write-queue.test.ts index 1b88f0ca6d2..ffba8b3ea29 100644 --- a/mobile/src/terminal/document/write-queue.test.ts +++ b/mobile/src/terminal/document/write-queue.test.ts @@ -21,7 +21,8 @@ describe('the write-queue module', () => { // Disposing an observer that is already gone. unboundCatches: 1, numberProperties: 0, - shorthandProperties: 0 + shorthandProperties: 0, + unshadowedNames: 0 } }) })