// @ts-nocheck /** Software-side bidirectional reordering for clustered terminal cells. */ import bidiFactory from 'bidi-js'; type ClusteredChar = { value: string; width: number; styleId: number; hyperlink: string | undefined; }; let bidiInstance: ReturnType | undefined; let cachedNeedsBidi: boolean | undefined; // Windows lacks terminal-side bidi entirely. Windows Terminal hosting WSL // still routes output through conhost, so WSL processes inherit the same // gap — we detect that via WT_SESSION rather than process.platform. VS Code's // xterm.js doesn't implement bidi either and is reachable on any OS. function needsSoftwareBidi(): boolean { if (cachedNeedsBidi === undefined) { cachedNeedsBidi = process.platform === 'win32' || typeof process.env['WT_SESSION'] === 'string' || process.env['TERM_PROGRAM'] === 'vscode'; } return cachedNeedsBidi; } function getBidi() { if (!bidiInstance) { bidiInstance = bidiFactory(); } return bidiInstance; } export function reorderBidi(characters: ClusteredChar[]): ClusteredChar[] { if (!needsSoftwareBidi() || characters.length === 0) { return characters; } const plainText = characters.map(c => c.value).join(''); // Probing for RTL code points first lets us skip the bidi-js call entirely // on the dominant LTR-only path — paragraph-level scan with one regex test. if (!hasRtlCharacters(plainText)) { return characters; } const bidi = getBidi(); const { levels } = bidi.getEmbeddingLevels(plainText, 'auto'); // bidi-js returns one level per UTF-16 code unit. Walk the cluster array // in parallel, taking the level of the first code unit of each cluster as // the cluster's level — within a cluster all units share a level. const charLevels: number[] = []; let offset = 0; for (let i = 0; i < characters.length; i++) { charLevels.push(levels[offset]!); offset += characters[i]!.value.length; } // Classic UBA reorder: from the deepest embedding level down to 1, reverse // each maximal run of clusters whose level is >= the current level. The // result is identical to bidi-js's reorderSegments output but expressed // over cluster indices so we don't have to map back from string offsets. const reordered = [...characters]; const maxLevel = Math.max(...charLevels); for (let level = maxLevel; level >= 1; level--) { let i = 0; while (i < reordered.length) { if (charLevels[i]! >= level) { let j = i + 1; while (j < reordered.length && charLevels[j]! >= level) { j++; } reverseRange(reordered, i, j - 1); reverseRange(charLevels, i, j - 1); i = j; } else { i++; } } } return reordered; } function reverseRange(arr: T[], start: number, end: number): void { while (start < end) { const tmp = arr[start]!; arr[start] = arr[end]!; arr[end] = tmp; start++; end--; } } // Conservative pre-check for any code point that could carry an RTL bidi // class. We err on the side of false positives (running the full algorithm // on a string that turns out to be neutral is harmless) and avoid false // negatives that would leave RTL text visibly reversed. // // Covers: Hebrew (incl. presentation forms), Arabic (incl. supplement, // extended-A, presentation forms A/B), Thaana, and Syriac. function hasRtlCharacters(text: string): boolean { return /[\u0590-\u05FF\uFB1D-\uFB4F\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF\u0780-\u07BF\u0700-\u074F]/u.test( text, ); }