From 6806e542f9c27084885010cc6e849df9aa3fc5de Mon Sep 17 00:00:00 2001 From: Jaap-Jan de Wit | DodoTech Date: Sun, 4 Oct 2026 13:55:20 +0200 Subject: [PATCH] Terminal: measure characters with Unicode 11 widths, so an emoji takes the two columns the remote counts --- docs/manual-checks.md | 24 +++++++++++ .../Terminal/AvaloniaTerminalAssetProvider.cs | 1 + .../WebAssets/terminal.html | 1 + .../WebAssets/terminal.js | 22 ++++++++++ .../WebAssets/vendor/README.md | 11 +++-- .../WebAssets/vendor/addon-unicode11.js | 2 + .../Renderer/renderer-harness.js | 30 ++++++++++++- .../RendererPage.cs | 8 ++++ .../RendererUnicodeTests.cs | 43 +++++++++++++++++++ 9 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 src/DodoSSH.Client.Shell/WebAssets/vendor/addon-unicode11.js create mode 100644 tests/DodoSSH.Client.Terminal.Tests/RendererUnicodeTests.cs diff --git a/docs/manual-checks.md b/docs/manual-checks.md index 4d096c5..97cd152 100644 --- a/docs/manual-checks.md +++ b/docs/manual-checks.md @@ -322,6 +322,30 @@ closing the only open window ends the process, so the real window is shown while Nothing appearing at all means composition is running in the same dispatcher turn that has to paint the panel, which is what posting it at `DispatcherPriority.Background` prevents. +### 1.13 An emoji takes two columns, and the line after it stays where the shell thinks it is + +Open a shell on a Linux host and run: + +``` +printf '😀|\n12|\n日|\n' +``` + +**Pass:** the three bars line up in one column. The emoji, the two digits and the CJK character each take two +columns, which is what the remote counted. + +Then type `echo 😀😀 hello` at the prompt without pressing Enter, press **←** five times, and type `X`. + +**Pass:** the `X` appears between `he` and `llo`, which is where bash put it, and the line does not redraw over +itself. + +**What the tests already own.** `RendererUnicodeTests` proves the page loads `@xterm/addon-unicode11` and +selects its table before the pane opens. What is left here is xterm itself drawing to that table. + +**Failure means:** the emoji's bar sits one column left of the others, or the `X` lands a column away from the +cursor. The pane is on xterm's default Unicode 6 widths, where an emoji is one column; check that +`terminal.html` still loads `vendor/addon-unicode11.js` and that `AvaloniaTerminalAssetProvider` serves it. +A script the provider does not list is a 404 the page never reports. + --- ## Phase 2 — Known Hosts as its own page diff --git a/src/DodoSSH.Client.Shell/Terminal/AvaloniaTerminalAssetProvider.cs b/src/DodoSSH.Client.Shell/Terminal/AvaloniaTerminalAssetProvider.cs index 185c911..29dd706 100644 --- a/src/DodoSSH.Client.Shell/Terminal/AvaloniaTerminalAssetProvider.cs +++ b/src/DodoSSH.Client.Shell/Terminal/AvaloniaTerminalAssetProvider.cs @@ -24,6 +24,7 @@ internal sealed class AvaloniaTerminalAssetProvider : ITerminalAssetProvider ("/vendor/xterm.css", "vendor/xterm.css", "text/css; charset=utf-8"), ("/vendor/addon-fit.js", "vendor/addon-fit.js", "text/javascript; charset=utf-8"), ("/vendor/addon-webgl.js", "vendor/addon-webgl.js", "text/javascript; charset=utf-8"), + ("/vendor/addon-unicode11.js", "vendor/addon-unicode11.js", "text/javascript; charset=utf-8"), ]; private readonly Dictionary assets = new(StringComparer.Ordinal); diff --git a/src/DodoSSH.Client.Shell/WebAssets/terminal.html b/src/DodoSSH.Client.Shell/WebAssets/terminal.html index ff0b9b0..36b3456 100644 --- a/src/DodoSSH.Client.Shell/WebAssets/terminal.html +++ b/src/DodoSSH.Client.Shell/WebAssets/terminal.html @@ -52,6 +52,7 @@ + diff --git a/src/DodoSSH.Client.Shell/WebAssets/terminal.js b/src/DodoSSH.Client.Shell/WebAssets/terminal.js index 68aaf73..00a93cf 100644 --- a/src/DodoSSH.Client.Shell/WebAssets/terminal.js +++ b/src/DodoSSH.Client.Shell/WebAssets/terminal.js @@ -387,6 +387,28 @@ function createSession(sessionId) { const fit = new FitAddon.FitAddon(); term.loadAddon(fit); + + /* + ── CHARACTER WIDTHS FROM UNICODE 11, NOT 6 ──────────────────────────────────────────────────────── + + How many columns a character takes is not something the terminal and the remote can each decide for + themselves: the shell's line editor counts columns to know where the cursor is, and the terminal + counts them to know where to draw. When the two disagree, every character after the one they + disagree about lands a column away from where the remote believes it is — a prompt that redraws over + itself, a cursor one cell off, a line that wraps early. + + xterm.js ships with Unicode 6's table, which predates emoji as a wide class. So 😀 was one column + here and two on any current remote (glibc, musl and every modern wcwidth follow Unicode 9 and later), + and a line with an emoji in it drifted from the first emoji on. Measured against xterm.js 6.0 + headless: 😀 took one cell as shipped and two with this addon, and CJK, combining marks and every + escape sequence behaved the same either way. + + Before open() so the first frame is measured with the right table. activeVersion is a proposed API, + which is what allowProposedApi above is already on for. + */ + term.loadAddon(new Unicode11Addon.Unicode11Addon()); + term.unicode.activeVersion = '11'; + term.open(pane); // WebGL where it is available. Falling back rather than failing matters because a software diff --git a/src/DodoSSH.Client.Shell/WebAssets/vendor/README.md b/src/DodoSSH.Client.Shell/WebAssets/vendor/README.md index 24fa42f..e97bacf 100644 --- a/src/DodoSSH.Client.Shell/WebAssets/vendor/README.md +++ b/src/DodoSSH.Client.Shell/WebAssets/vendor/README.md @@ -12,18 +12,21 @@ All of the following are MIT licensed, the same licence as this repository. | `xterm.css` | `@xterm/xterm` | 6.0.0 | `https://unpkg.com/@xterm/xterm@6.0.0/css/xterm.css` | | `addon-fit.js` | `@xterm/addon-fit` | 0.11.0 | `https://unpkg.com/@xterm/addon-fit@0.11.0/lib/addon-fit.js` | | `addon-webgl.js` | `@xterm/addon-webgl` | 0.19.0 | `https://unpkg.com/@xterm/addon-webgl@0.19.0/lib/addon-webgl.js` | +| `addon-unicode11.js` | `@xterm/addon-unicode11` | 0.9.0 | `https://unpkg.com/@xterm/addon-unicode11@0.9.0/lib/addon-unicode11.js` | Copyright (c) 2017 The xterm.js authors. See for the full licence text. -These are the UMD builds, so they attach to `globalThis`: `Terminal`, `FitAddon.FitAddon` and -`WebglAddon.WebglAddon`. `terminal.js` depends on those exact names, so check them after any upgrade +These are the UMD builds, so they attach to `globalThis`: `Terminal`, `FitAddon.FitAddon`, +`WebglAddon.WebglAddon` and `Unicode11Addon.Unicode11Addon`. `terminal.js` depends on those exact names, so check them after any upgrade — a bundle that switched to ES modules would load without error and leave `Terminal` undefined. ## Upgrading -1. Download the four files at the new versions and update the table above. -2. Confirm the UMD global names are unchanged. +1. Download the five files at the new versions and update the table above. +2. Confirm the UMD global names are unchanged, and that `term.unicode.activeVersion = '11'` still selects + the addon's table: a version string xterm no longer recognises throws, and an addon that registered + under another name would leave the page on Unicode 6 widths without a word. 3. Confirm the right-click still belongs to `terminal.js`. Two things about xterm are load-bearing for it and neither is a global: `rightClickSelectsWord` has to still be an option — the page sets it false, and an option xterm no longer knows is silently ignored — and xterm has to still register its own right-click diff --git a/src/DodoSSH.Client.Shell/WebAssets/vendor/addon-unicode11.js b/src/DodoSSH.Client.Shell/WebAssets/vendor/addon-unicode11.js new file mode 100644 index 0000000..941130a --- /dev/null +++ b/src/DodoSSH.Client.Shell/WebAssets/vendor/addon-unicode11.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Unicode11Addon=t():e.Unicode11Addon=t()}(globalThis,(()=>(()=>{"use strict";var e={384:(e,t,s)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.UnicodeV11=void 0;const r=s(765),n=[[768,879],[1155,1161],[1425,1469],[1471,1471],[1473,1474],[1476,1477],[1479,1479],[1536,1541],[1552,1562],[1564,1564],[1611,1631],[1648,1648],[1750,1757],[1759,1764],[1767,1768],[1770,1773],[1807,1807],[1809,1809],[1840,1866],[1958,1968],[2027,2035],[2045,2045],[2070,2073],[2075,2083],[2085,2087],[2089,2093],[2137,2139],[2259,2306],[2362,2362],[2364,2364],[2369,2376],[2381,2381],[2385,2391],[2402,2403],[2433,2433],[2492,2492],[2497,2500],[2509,2509],[2530,2531],[2558,2558],[2561,2562],[2620,2620],[2625,2626],[2631,2632],[2635,2637],[2641,2641],[2672,2673],[2677,2677],[2689,2690],[2748,2748],[2753,2757],[2759,2760],[2765,2765],[2786,2787],[2810,2815],[2817,2817],[2876,2876],[2879,2879],[2881,2884],[2893,2893],[2902,2902],[2914,2915],[2946,2946],[3008,3008],[3021,3021],[3072,3072],[3076,3076],[3134,3136],[3142,3144],[3146,3149],[3157,3158],[3170,3171],[3201,3201],[3260,3260],[3263,3263],[3270,3270],[3276,3277],[3298,3299],[3328,3329],[3387,3388],[3393,3396],[3405,3405],[3426,3427],[3530,3530],[3538,3540],[3542,3542],[3633,3633],[3636,3642],[3655,3662],[3761,3761],[3764,3772],[3784,3789],[3864,3865],[3893,3893],[3895,3895],[3897,3897],[3953,3966],[3968,3972],[3974,3975],[3981,3991],[3993,4028],[4038,4038],[4141,4144],[4146,4151],[4153,4154],[4157,4158],[4184,4185],[4190,4192],[4209,4212],[4226,4226],[4229,4230],[4237,4237],[4253,4253],[4448,4607],[4957,4959],[5906,5908],[5938,5940],[5970,5971],[6002,6003],[6068,6069],[6071,6077],[6086,6086],[6089,6099],[6109,6109],[6155,6158],[6277,6278],[6313,6313],[6432,6434],[6439,6440],[6450,6450],[6457,6459],[6679,6680],[6683,6683],[6742,6742],[6744,6750],[6752,6752],[6754,6754],[6757,6764],[6771,6780],[6783,6783],[6832,6846],[6912,6915],[6964,6964],[6966,6970],[6972,6972],[6978,6978],[7019,7027],[7040,7041],[7074,7077],[7080,7081],[7083,7085],[7142,7142],[7144,7145],[7149,7149],[7151,7153],[7212,7219],[7222,7223],[7376,7378],[7380,7392],[7394,7400],[7405,7405],[7412,7412],[7416,7417],[7616,7673],[7675,7679],[8203,8207],[8234,8238],[8288,8292],[8294,8303],[8400,8432],[11503,11505],[11647,11647],[11744,11775],[12330,12333],[12441,12442],[42607,42610],[42612,42621],[42654,42655],[42736,42737],[43010,43010],[43014,43014],[43019,43019],[43045,43046],[43204,43205],[43232,43249],[43263,43263],[43302,43309],[43335,43345],[43392,43394],[43443,43443],[43446,43449],[43452,43453],[43493,43493],[43561,43566],[43569,43570],[43573,43574],[43587,43587],[43596,43596],[43644,43644],[43696,43696],[43698,43700],[43703,43704],[43710,43711],[43713,43713],[43756,43757],[43766,43766],[44005,44005],[44008,44008],[44013,44013],[64286,64286],[65024,65039],[65056,65071],[65279,65279],[65529,65531]],i=[[66045,66045],[66272,66272],[66422,66426],[68097,68099],[68101,68102],[68108,68111],[68152,68154],[68159,68159],[68325,68326],[68900,68903],[69446,69456],[69633,69633],[69688,69702],[69759,69761],[69811,69814],[69817,69818],[69821,69821],[69837,69837],[69888,69890],[69927,69931],[69933,69940],[70003,70003],[70016,70017],[70070,70078],[70089,70092],[70191,70193],[70196,70196],[70198,70199],[70206,70206],[70367,70367],[70371,70378],[70400,70401],[70459,70460],[70464,70464],[70502,70508],[70512,70516],[70712,70719],[70722,70724],[70726,70726],[70750,70750],[70835,70840],[70842,70842],[70847,70848],[70850,70851],[71090,71093],[71100,71101],[71103,71104],[71132,71133],[71219,71226],[71229,71229],[71231,71232],[71339,71339],[71341,71341],[71344,71349],[71351,71351],[71453,71455],[71458,71461],[71463,71467],[71727,71735],[71737,71738],[72148,72151],[72154,72155],[72160,72160],[72193,72202],[72243,72248],[72251,72254],[72263,72263],[72273,72278],[72281,72283],[72330,72342],[72344,72345],[72752,72758],[72760,72765],[72767,72767],[72850,72871],[72874,72880],[72882,72883],[72885,72886],[73009,73014],[73018,73018],[73020,73021],[73023,73029],[73031,73031],[73104,73105],[73109,73109],[73111,73111],[73459,73460],[78896,78904],[92912,92916],[92976,92982],[94031,94031],[94095,94098],[113821,113822],[113824,113827],[119143,119145],[119155,119170],[119173,119179],[119210,119213],[119362,119364],[121344,121398],[121403,121452],[121461,121461],[121476,121476],[121499,121503],[121505,121519],[122880,122886],[122888,122904],[122907,122913],[122915,122916],[122918,122922],[123184,123190],[123628,123631],[125136,125142],[125252,125258],[917505,917505],[917536,917631],[917760,917999]],o=[[4352,4447],[8986,8987],[9001,9002],[9193,9196],[9200,9200],[9203,9203],[9725,9726],[9748,9749],[9800,9811],[9855,9855],[9875,9875],[9889,9889],[9898,9899],[9917,9918],[9924,9925],[9934,9934],[9940,9940],[9962,9962],[9970,9971],[9973,9973],[9978,9978],[9981,9981],[9989,9989],[9994,9995],[10024,10024],[10060,10060],[10062,10062],[10067,10069],[10071,10071],[10133,10135],[10160,10160],[10175,10175],[11035,11036],[11088,11088],[11093,11093],[11904,11929],[11931,12019],[12032,12245],[12272,12283],[12288,12329],[12334,12350],[12353,12438],[12443,12543],[12549,12591],[12593,12686],[12688,12730],[12736,12771],[12784,12830],[12832,12871],[12880,19903],[19968,42124],[42128,42182],[43360,43388],[44032,55203],[63744,64255],[65040,65049],[65072,65106],[65108,65126],[65128,65131],[65281,65376],[65504,65510]],a=[[94176,94179],[94208,100343],[100352,101106],[110592,110878],[110928,110930],[110948,110951],[110960,111355],[126980,126980],[127183,127183],[127374,127374],[127377,127386],[127488,127490],[127504,127547],[127552,127560],[127568,127569],[127584,127589],[127744,127776],[127789,127797],[127799,127868],[127870,127891],[127904,127946],[127951,127955],[127968,127984],[127988,127988],[127992,128062],[128064,128064],[128066,128252],[128255,128317],[128331,128334],[128336,128359],[128378,128378],[128405,128406],[128420,128420],[128507,128591],[128640,128709],[128716,128716],[128720,128722],[128725,128725],[128747,128748],[128756,128762],[128992,129003],[129293,129393],[129395,129398],[129402,129442],[129445,129450],[129454,129482],[129485,129535],[129648,129651],[129656,129658],[129664,129666],[129680,129685],[131072,196605],[196608,262141]];let l;function c(e,t){let s,r=0,n=t.length-1;if(et[n][1])return!1;for(;n>=r;)if(s=r+n>>1,e>t[s][1])r=s+1;else{if(!(es&&(s=e)}return r.UnicodeService.createPropertyValue(0,s,n)}}},546:(e,t,s)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.UnicodeV6=void 0;const r=s(765),n=[[768,879],[1155,1158],[1160,1161],[1425,1469],[1471,1471],[1473,1474],[1476,1477],[1479,1479],[1536,1539],[1552,1557],[1611,1630],[1648,1648],[1750,1764],[1767,1768],[1770,1773],[1807,1807],[1809,1809],[1840,1866],[1958,1968],[2027,2035],[2305,2306],[2364,2364],[2369,2376],[2381,2381],[2385,2388],[2402,2403],[2433,2433],[2492,2492],[2497,2500],[2509,2509],[2530,2531],[2561,2562],[2620,2620],[2625,2626],[2631,2632],[2635,2637],[2672,2673],[2689,2690],[2748,2748],[2753,2757],[2759,2760],[2765,2765],[2786,2787],[2817,2817],[2876,2876],[2879,2879],[2881,2883],[2893,2893],[2902,2902],[2946,2946],[3008,3008],[3021,3021],[3134,3136],[3142,3144],[3146,3149],[3157,3158],[3260,3260],[3263,3263],[3270,3270],[3276,3277],[3298,3299],[3393,3395],[3405,3405],[3530,3530],[3538,3540],[3542,3542],[3633,3633],[3636,3642],[3655,3662],[3761,3761],[3764,3769],[3771,3772],[3784,3789],[3864,3865],[3893,3893],[3895,3895],[3897,3897],[3953,3966],[3968,3972],[3974,3975],[3984,3991],[3993,4028],[4038,4038],[4141,4144],[4146,4146],[4150,4151],[4153,4153],[4184,4185],[4448,4607],[4959,4959],[5906,5908],[5938,5940],[5970,5971],[6002,6003],[6068,6069],[6071,6077],[6086,6086],[6089,6099],[6109,6109],[6155,6157],[6313,6313],[6432,6434],[6439,6440],[6450,6450],[6457,6459],[6679,6680],[6912,6915],[6964,6964],[6966,6970],[6972,6972],[6978,6978],[7019,7027],[7616,7626],[7678,7679],[8203,8207],[8234,8238],[8288,8291],[8298,8303],[8400,8431],[12330,12335],[12441,12442],[43014,43014],[43019,43019],[43045,43046],[64286,64286],[65024,65039],[65056,65059],[65279,65279],[65529,65531]],i=[[68097,68099],[68101,68102],[68108,68111],[68152,68154],[68159,68159],[119143,119145],[119155,119170],[119173,119179],[119210,119213],[119362,119364],[917505,917505],[917536,917631],[917760,917999]];let o;t.UnicodeV6=class{constructor(){if(this.version="6",!o){o=new Uint8Array(65536),o.fill(1),o[0]=0,o.fill(0,1,32),o.fill(0,127,160),o.fill(2,4352,4448),o[9001]=2,o[9002]=2,o.fill(2,11904,42192),o[12351]=1,o.fill(2,44032,55204),o.fill(2,63744,64256),o.fill(2,65040,65050),o.fill(2,65072,65136),o.fill(2,65280,65377),o.fill(2,65504,65511);for(let e=0;et[n][1])return!1;for(;n>=r;)if(s=r+n>>1,e>t[s][1])r=s+1;else{if(!(e=131072&&e<=196605||e>=196608&&e<=262141?2:1}charProperties(e,t){let s=this.wcwidth(e),n=0===s&&0!==t;if(n){const e=r.UnicodeService.extractWidth(t);0===e?n=!1:e>s&&(s=e)}return r.UnicodeService.createPropertyValue(0,s,n)}}},765:(e,t,s)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.UnicodeService=void 0;const r=s(546),n=s(276);class i{static extractShouldJoin(e){return!!(1&e)}static extractWidth(e){return e>>1&3}static extractCharKind(e){return e>>3}static createPropertyValue(e,t,s=!1){return(16777215&e)<<3|(3&t)<<1|(s?1:0)}constructor(){this._providers=Object.create(null),this._active="",this._onChange=new n.Emitter,this.onChange=this._onChange.event;const e=new r.UnicodeV6;this.register(e),this._active=e.version,this._activeProvider=e}dispose(){this._onChange.dispose()}get versions(){return Object.keys(this._providers)}get activeVersion(){return this._active}set activeVersion(e){if(!this._providers[e])throw new Error(`unknown Unicode version "${e}"`);this._active=e,this._activeProvider=this._providers[e],this._onChange.fire(e)}register(e){this._providers[e.version]=e}wcwidth(e){return this._activeProvider.wcwidth(e)}getStringCellWidth(e){let t=0,s=0;const r=e.length;for(let n=0;n=r)return t+this.wcwidth(o);const s=e.charCodeAt(n);56320<=s&&s<=57343?o=1024*(o-55296)+s-56320+65536:t+=this.wcwidth(s)}const a=this.charProperties(o,s);let l=i.extractWidth(a);i.extractShouldJoin(a)&&(l-=i.extractWidth(s)),t+=l,s=a}return t}charProperties(e,t){return this._activeProvider.charProperties(e,t)}}t.UnicodeService=i},732:(e,t,s)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.Permutation=t.CallbackIterable=t.ArrayQueue=t.booleanComparator=t.numberComparator=t.CompareResult=void 0,t.tail=function(e,t=0){return e[e.length-(1+t)]},t.tail2=function(e){if(0===e.length)throw new Error("Invalid tail call");return[e.slice(0,e.length-1),e[e.length-1]]},t.equals=function(e,t,s=(e,t)=>e===t){if(e===t)return!0;if(!e||!t)return!1;if(e.length!==t.length)return!1;for(let r=0,n=e.length;rs(e[r],t)))},t.binarySearch2=i,t.quickSelect=function e(t,s,r){if((t|=0)>=s.length)throw new TypeError("invalid index");const n=s[Math.floor(s.length*Math.random())],i=[],o=[],a=[];for(const e of s){const t=r(e,n);t<0?i.push(e):t>0?o.push(e):a.push(e)}return t{(async()=>{const o=e.length,l=e.slice(0,s).sort(t);for(let c=s,h=Math.min(s+n,o);cs&&await new Promise((e=>setTimeout(e))),i&&i.isCancellationRequested)throw new r.CancellationError;a(e,t,l,c,h)}return l})().then(o,l)}))},t.coalesce=function(e){return e.filter((e=>!!e))},t.coalesceInPlace=function(e){let t=0;for(let s=0;s0},t.distinct=function(e,t=e=>e){const s=new Set;return e.filter((e=>{const r=t(e);return!s.has(r)&&(s.add(r),!0)}))},t.uniqueFilter=function(e){const t=new Set;return s=>{const r=e(s);return!t.has(r)&&(t.add(r),!0)}},t.firstOrDefault=function(e,t){return e.length>0?e[0]:t},t.lastOrDefault=function(e,t){return e.length>0?e[e.length-1]:t},t.commonPrefixLength=function(e,t,s=(e,t)=>e===t){let r=0;for(let n=0,i=Math.min(e.length,t.length);nt;e--)r.push(e);return r},t.index=function(e,t,s){return e.reduce(((e,r)=>(e[t(r)]=s?s(r):r,e)),Object.create(null))},t.insert=function(e,t){return e.push(t),()=>l(e,t)},t.remove=l,t.arrayInsert=function(e,t,s){const r=e.slice(0,t),n=e.slice(t);return r.concat(s,n)},t.shuffle=function(e,t){let s;if("number"==typeof t){let e=t;s=()=>{const t=179426549*Math.sin(e++);return t-Math.floor(t)}}else s=Math.random;for(let t=e.length-1;t>0;t-=1){const r=Math.floor(s()*(t+1)),n=e[t];e[t]=e[r],e[r]=n}},t.pushToStart=function(e,t){const s=e.indexOf(t);s>-1&&(e.splice(s,1),e.unshift(t))},t.pushToEnd=function(e,t){const s=e.indexOf(t);s>-1&&(e.splice(s,1),e.push(t))},t.pushMany=function(e,t){for(const s of t)e.push(s)},t.mapArrayOrNot=function(e,t){return Array.isArray(e)?e.map(t):t(e)},t.asArray=function(e){return Array.isArray(e)?e:[e]},t.getRandomElement=function(e){return e[Math.floor(Math.random()*e.length)]},t.insertInto=c,t.splice=function(e,t,s,r){const n=h(e,t);let i=e.splice(n,s);return void 0===i&&(i=[]),c(e,n,r),i},t.compareBy=function(e,t){return(s,r)=>t(e(s),e(r))},t.tieBreakComparators=function(...e){return(t,s)=>{for(const r of e){const e=r(t,s);if(!u.isNeitherLessOrGreaterThan(e))return e}return u.neitherLessOrGreaterThan}},t.reverseOrder=function(e){return(t,s)=>-e(t,s)};const r=s(577),n=s(411);function i(e,t){let s=0,r=e-1;for(;s<=r;){const e=(s+r)/2|0,n=t(e);if(n<0)s=e+1;else{if(!(n>0))return e;r=e-1}}return-(s+1)}function o(e,t,s){const r=[];function n(e,t,s){if(0===t&&0===s.length)return;const n=r[r.length-1];n&&n.start+n.deleteCount===e?(n.deleteCount+=t,n.toInsert.push(...s)):r.push({start:e,deleteCount:t,toInsert:s})}let i=0,o=0;for(;;){if(i===e.length){n(i,0,t.slice(o));break}if(o===t.length){n(i,e.length-i,[]);break}const r=e[i],a=t[o],l=s(r,a);0===l?(i+=1,o+=1):l<0?(n(i,1,[]),i+=1):l>0&&(n(i,0,[a]),o+=1)}return r}function a(e,t,s,r,i){for(const o=s.length;rt(i,e)<0));s.splice(e,0,i)}}}function l(e,t){const s=e.indexOf(t);if(s>-1)return e.splice(s,1),t}function c(e,t,s){const r=h(e,t),n=e.length,i=s.length;e.length=n+i;for(let t=n-1;t>=r;t--)e[t+i]=e[t];for(let t=0;t0},e.isNeitherLessOrGreaterThan=function(e){return 0===e},e.greaterThan=1,e.lessThan=-1,e.neitherLessOrGreaterThan=0}(u||(t.CompareResult=u={})),t.numberComparator=(e,t)=>e-t,t.booleanComparator=(e,s)=>(0,t.numberComparator)(e?1:0,s?1:0),t.ArrayQueue=class{constructor(e){this.items=e,this.firstIdx=0,this.lastIdx=this.items.length-1}get length(){return this.lastIdx-this.firstIdx+1}takeWhile(e){let t=this.firstIdx;for(;t=0&&e(this.items[t]);)t--;const s=t===this.lastIdx?null:this.items.slice(t+1,this.lastIdx+1);return this.lastIdx=t,s}peek(){if(0!==this.length)return this.items[this.firstIdx]}peekLast(){if(0!==this.length)return this.items[this.lastIdx]}dequeue(){const e=this.items[this.firstIdx];return this.firstIdx++,e}removeLast(){const e=this.items[this.lastIdx];return this.lastIdx--,e}takeCount(e){const t=this.items.slice(this.firstIdx,this.firstIdx+e);return this.firstIdx+=e,t}};class d{static{this.empty=new d((e=>{}))}constructor(e){this.iterate=e}forEach(e){this.iterate((t=>(e(t),!0)))}toArray(){const e=[];return this.iterate((t=>(e.push(t),!0))),e}filter(e){return new d((t=>this.iterate((s=>!e(s)||t(s)))))}map(e){return new d((t=>this.iterate((s=>t(e(s))))))}some(e){let t=!1;return this.iterate((s=>(t=e(s),!t))),t}findFirst(e){let t;return this.iterate((s=>!e(s)||(t=s,!1))),t}findLast(e){let t;return this.iterate((s=>(e(s)&&(t=s),!0))),t}findLastMaxBy(e){let t,s=!0;return this.iterate((r=>((s||u.isGreaterThan(e(r,t)))&&(s=!1,t=r),!0))),t}}t.CallbackIterable=d;class f{constructor(e){this._indexMap=e}static createSortPermutation(e,t){const s=Array.from(e.keys()).sort(((s,r)=>t(e[s],e[r])));return new f(s)}apply(e){return e.map(((t,s)=>e[this._indexMap[s]]))}inverse(){const e=this._indexMap.slice();for(let t=0;t{function s(e,t,s=e.length-1){for(let r=s;r>=0;r--)if(t(e[r]))return r;return-1}function r(e,t,s=0,r=e.length){let n=s,i=r;for(;n=0&&(s=n)}return s},t.findFirstMin=function(e,t){return o(e,((e,s)=>-t(e,s)))},t.findMaxIdx=function(e,t){if(0===e.length)return-1;let s=0;for(let r=1;r0&&(s=r);return s},t.mapFindFirst=function(e,t){for(const s of e){const e=t(s);if(void 0!==e)return e}};class i{static{this.assertInvariants=!1}constructor(e){this._array=e,this._findLastMonotonousLastIdx=0}findLastMonotonous(e){if(i.assertInvariants){if(this._prevFindLastPredicate)for(const t of this._array)if(this._prevFindLastPredicate(t)&&!e(t))throw new Error("MonotonousArray: current predicate must be weaker than (or equal to) the previous predicate.");this._prevFindLastPredicate=e}const t=r(this._array,e,this._findLastMonotonousLastIdx);return this._findLastMonotonousLastIdx=t+1,-1===t?void 0:this._array[t]}}function o(e,t){if(0===e.length)return;let s=e[0];for(let r=1;r0&&(s=n)}return s}t.MonotonousArray=i},33:(e,t)=>{var s;Object.defineProperty(t,"__esModule",{value:!0}),t.SetWithKey=void 0,t.groupBy=function(e,t){const s=Object.create(null);for(const r of e){const e=t(r);let n=s[e];n||(n=s[e]=[]),n.push(r)}return s},t.diffSets=function(e,t){const s=[],r=[];for(const r of e)t.has(r)||s.push(r);for(const s of t)e.has(s)||r.push(s);return{removed:s,added:r}},t.diffMaps=function(e,t){const s=[],r=[];for(const[r,n]of e)t.has(r)||s.push(n);for(const[s,n]of t)e.has(s)||r.push(n);return{removed:s,added:r}},t.intersection=function(e,t){const s=new Set;for(const r of t)e.has(r)&&s.add(r);return s};class r{static{s=Symbol.toStringTag}constructor(e,t){this.toKey=t,this._map=new Map,this[s]="SetWithKey";for(const t of e)this.add(t)}get size(){return this._map.size}add(e){const t=this.toKey(e);return this._map.set(t,e),this}delete(e){return this._map.delete(this.toKey(e))}has(e){return this._map.has(this.toKey(e))}*entries(){for(const e of this._map.values())yield[e,e]}keys(){return this.values()}*values(){for(const e of this._map.values())yield e}clear(){this._map.clear()}forEach(e,t){this._map.forEach((s=>e.call(t,s,s,this)))}[Symbol.iterator](){return this.values()}}t.SetWithKey=r},577:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.BugIndicatingError=t.ErrorNoTelemetry=t.ExpectedError=t.NotSupportedError=t.NotImplementedError=t.ReadonlyError=t.CancellationError=t.errorHandler=t.ErrorHandler=void 0,t.setUnexpectedErrorHandler=function(e){t.errorHandler.setUnexpectedErrorHandler(e)},t.isSigPipeError=function(e){if(!e||"object"!=typeof e)return!1;const t=e;return"EPIPE"===t.code&&"WRITE"===t.syscall?.toUpperCase()},t.onUnexpectedError=function(e){n(e)||t.errorHandler.onUnexpectedError(e)},t.onUnexpectedExternalError=function(e){n(e)||t.errorHandler.onUnexpectedExternalError(e)},t.transformErrorForSerialization=function(e){if(e instanceof Error){const{name:t,message:s}=e;return{$isError:!0,name:t,message:s,stack:e.stacktrace||e.stack,noTelemetry:h.isErrorNoTelemetry(e)}}return e},t.transformErrorFromSerialization=function(e){let t;return e.noTelemetry?t=new h:(t=new Error,t.name=e.name),t.message=e.message,t.stack=e.stack,t},t.isCancellationError=n,t.canceled=function(){const e=new Error(r);return e.name=e.message,e},t.illegalArgument=function(e){return e?new Error(`Illegal argument: ${e}`):new Error("Illegal argument")},t.illegalState=function(e){return e?new Error(`Illegal state: ${e}`):new Error("Illegal state")},t.getErrorMessage=function(e){return e?e.message?e.message:e.stack?e.stack.split("\n")[0]:String(e):"Error"};class s{constructor(){this.listeners=[],this.unexpectedErrorHandler=function(e){setTimeout((()=>{if(e.stack){if(h.isErrorNoTelemetry(e))throw new h(e.message+"\n\n"+e.stack);throw new Error(e.message+"\n\n"+e.stack)}throw e}),0)}}addListener(e){return this.listeners.push(e),()=>{this._removeListener(e)}}emit(e){this.listeners.forEach((t=>{t(e)}))}_removeListener(e){this.listeners.splice(this.listeners.indexOf(e),1)}setUnexpectedErrorHandler(e){this.unexpectedErrorHandler=e}getUnexpectedErrorHandler(){return this.unexpectedErrorHandler}onUnexpectedError(e){this.unexpectedErrorHandler(e),this.emit(e)}onUnexpectedExternalError(e){this.unexpectedErrorHandler(e)}}t.ErrorHandler=s,t.errorHandler=new s;const r="Canceled";function n(e){return e instanceof i||e instanceof Error&&e.name===r&&e.message===r}class i extends Error{constructor(){super(r),this.name=this.message}}t.CancellationError=i;class o extends TypeError{constructor(e){super(e?`${e} is read-only and cannot be changed`:"Cannot change read-only property")}}t.ReadonlyError=o;class a extends Error{constructor(e){super("NotImplemented"),e&&(this.message=e)}}t.NotImplementedError=a;class l extends Error{constructor(e){super("NotSupported"),e&&(this.message=e)}}t.NotSupportedError=l;class c extends Error{constructor(){super(...arguments),this.isExpected=!0}}t.ExpectedError=c;class h extends Error{constructor(e){super(e),this.name="CodeExpectedError"}static fromError(e){if(e instanceof h)return e;const t=new h;return t.message=e.message,t.stack=e.stack,t}static isErrorNoTelemetry(e){return"CodeExpectedError"===e.name}}t.ErrorNoTelemetry=h;class u extends Error{constructor(e){super(e||"An unexpected bug occurred."),Object.setPrototypeOf(this,u.prototype)}}t.BugIndicatingError=u},276:(e,t,s)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.ValueWithChangeEvent=t.Relay=t.EventBufferer=t.DynamicListEventMultiplexer=t.EventMultiplexer=t.MicrotaskEmitter=t.DebounceEmitter=t.PauseableEmitter=t.AsyncEmitter=t.createEventDeliveryQueue=t.Emitter=t.ListenerRefusalError=t.ListenerLeakError=t.EventProfiling=t.Event=void 0,t.setGlobalLeakWarningThreshold=function(e){const t=h;return h=e,{dispose(){h=t}}};const r=s(577),n=s(355),i=s(540),o=s(711),a=s(79);var l;!function(e){function t(e){return(t,s=null,r)=>{let n,i=!1;return n=e((e=>{if(!i)return n?n.dispose():i=!0,t.call(s,e)}),null,r),i&&n.dispose(),n}}function s(e,t,s){return n(((s,r=null,n)=>e((e=>s.call(r,t(e))),null,n)),s)}function r(e,t,s){return n(((s,r=null,n)=>e((e=>t(e)&&s.call(r,e)),null,n)),s)}function n(e,t){let s;const r=new m({onWillAddFirstListener(){s=e(r.fire,r)},onDidRemoveLastListener(){s?.dispose()}});return t?.add(r),r.event}function o(e,t,s=100,r=!1,n=!1,i,o){let a,l,c,h,u=0;const d=new m({leakWarningThreshold:i,onWillAddFirstListener(){a=e((e=>{u++,l=t(l,e),r&&!c&&(d.fire(l),l=void 0),h=()=>{const e=l;l=void 0,c=void 0,(!r||u>1)&&d.fire(e),u=0},"number"==typeof s?(clearTimeout(c),c=setTimeout(h,s)):void 0===c&&(c=0,queueMicrotask(h))}))},onWillRemoveListener(){n&&u>0&&h?.()},onDidRemoveLastListener(){h=void 0,a.dispose()}});return o?.add(d),d.event}e.None=()=>i.Disposable.None,e.defer=function(e,t){return o(e,(()=>{}),0,void 0,!0,void 0,t)},e.once=t,e.map=s,e.forEach=function(e,t,s){return n(((s,r=null,n)=>e((e=>{t(e),s.call(r,e)}),null,n)),s)},e.filter=r,e.signal=function(e){return e},e.any=function(...e){return(t,s=null,r)=>{return n=(0,i.combinedDisposable)(...e.map((e=>e((e=>t.call(s,e)))))),(o=r)instanceof Array?o.push(n):o&&o.add(n),n;var n,o}},e.reduce=function(e,t,r,n){let i=r;return s(e,(e=>(i=t(i,e),i)),n)},e.debounce=o,e.accumulate=function(t,s=0,r){return e.debounce(t,((e,t)=>e?(e.push(t),e):[t]),s,void 0,!0,void 0,r)},e.latch=function(e,t=(e,t)=>e===t,s){let n,i=!0;return r(e,(e=>{const s=i||!t(e,n);return i=!1,n=e,s}),s)},e.split=function(t,s,r){return[e.filter(t,s,r),e.filter(t,(e=>!s(e)),r)]},e.buffer=function(e,t=!1,s=[],r){let n=s.slice(),i=e((e=>{n?n.push(e):a.fire(e)}));r&&r.add(i);const o=()=>{n?.forEach((e=>a.fire(e))),n=null},a=new m({onWillAddFirstListener(){i||(i=e((e=>a.fire(e))),r&&r.add(i))},onDidAddFirstListener(){n&&(t?setTimeout(o):o())},onDidRemoveLastListener(){i&&i.dispose(),i=null}});return r&&r.add(a),a.event},e.chain=function(e,t){return(s,r,n)=>{const i=t(new l);return e((function(e){const t=i.evaluate(e);t!==a&&s.call(r,t)}),void 0,n)}};const a=Symbol("HaltChainable");class l{constructor(){this.steps=[]}map(e){return this.steps.push(e),this}forEach(e){return this.steps.push((t=>(e(t),t))),this}filter(e){return this.steps.push((t=>e(t)?t:a)),this}reduce(e,t){let s=t;return this.steps.push((t=>(s=e(s,t),s))),this}latch(e=(e,t)=>e===t){let t,s=!0;return this.steps.push((r=>{const n=s||!e(r,t);return s=!1,t=r,n?r:a})),this}evaluate(e){for(const t of this.steps)if((e=t(e))===a)break;return e}}e.fromNodeEventEmitter=function(e,t,s=e=>e){const r=(...e)=>n.fire(s(...e)),n=new m({onWillAddFirstListener:()=>e.on(t,r),onDidRemoveLastListener:()=>e.removeListener(t,r)});return n.event},e.fromDOMEventEmitter=function(e,t,s=e=>e){const r=(...e)=>n.fire(s(...e)),n=new m({onWillAddFirstListener:()=>e.addEventListener(t,r),onDidRemoveLastListener:()=>e.removeEventListener(t,r)});return n.event},e.toPromise=function(e){return new Promise((s=>t(e)(s)))},e.fromPromise=function(e){const t=new m;return e.then((e=>{t.fire(e)}),(()=>{t.fire(void 0)})).finally((()=>{t.dispose()})),t.event},e.forward=function(e,t){return e((e=>t.fire(e)))},e.runAndSubscribe=function(e,t,s){return t(s),e((e=>t(e)))};class c{constructor(e,t){this._observable=e,this._counter=0,this._hasChanged=!1;const s={onWillAddFirstListener:()=>{e.addObserver(this)},onDidRemoveLastListener:()=>{e.removeObserver(this)}};this.emitter=new m(s),t&&t.add(this.emitter)}beginUpdate(e){this._counter++}handlePossibleChange(e){}handleChange(e,t){this._hasChanged=!0}endUpdate(e){this._counter--,0===this._counter&&(this._observable.reportChanges(),this._hasChanged&&(this._hasChanged=!1,this.emitter.fire(this._observable.get())))}}e.fromObservable=function(e,t){return new c(e,t).emitter.event},e.fromObservableLight=function(e){return(t,s,r)=>{let n=0,o=!1;const a={beginUpdate(){n++},endUpdate(){n--,0===n&&(e.reportChanges(),o&&(o=!1,t.call(s)))},handlePossibleChange(){},handleChange(){o=!0}};e.addObserver(a),e.reportChanges();const l={dispose(){e.removeObserver(a)}};return r instanceof i.DisposableStore?r.add(l):Array.isArray(r)&&r.push(l),l}}}(l||(t.Event=l={}));class c{static{this.all=new Set}static{this._idPool=0}constructor(e){this.listenerCount=0,this.invocationCount=0,this.elapsedOverall=0,this.durations=[],this.name=`${e}_${c._idPool++}`,c.all.add(this)}start(e){this._stopWatch=new a.StopWatch,this.listenerCount=e}stop(){if(this._stopWatch){const e=this._stopWatch.elapsed();this.durations.push(e),this.elapsedOverall+=e,this.invocationCount+=1,this._stopWatch=void 0}}}t.EventProfiling=c;let h=-1;class u{static{this._idPool=1}constructor(e,t,s=(u._idPool++).toString(16).padStart(3,"0")){this._errorHandler=e,this.threshold=t,this.name=s,this._warnCountdown=0}dispose(){this._stacks?.clear()}check(e,t){const s=this.threshold;if(s<=0||t{const t=this._stacks.get(e.value)||0;this._stacks.set(e.value,t-1)}}getMostFrequentStack(){if(!this._stacks)return;let e,t=0;for(const[s,r]of this._stacks)(!e||t0||this._options?.leakWarningThreshold?new u(e?.onListenerError??r.onUnexpectedError,this._options?.leakWarningThreshold??h):void 0,this._perfMon=this._options?._profName?new c(this._options._profName):void 0,this._deliveryQueue=this._options?.deliveryQueue}dispose(){this._disposed||(this._disposed=!0,this._deliveryQueue?.current===this&&this._deliveryQueue.reset(),this._listeners&&(this._listeners=void 0,this._size=0),this._options?.onDidRemoveLastListener?.(),this._leakageMon?.dispose())}get event(){return this._event??=(e,t,s)=>{if(this._leakageMon&&this._size>this._leakageMon.threshold**2){const e=`[${this._leakageMon.name}] REFUSES to accept new listeners because it exceeded its threshold by far (${this._size} vs ${this._leakageMon.threshold})`;console.warn(e);const t=this._leakageMon.getMostFrequentStack()??["UNKNOWN stack",-1],s=new p(`${e}. HINT: Stack shows most frequent listener (${t[1]}-times)`,t[0]);return(this._options?.onListenerError||r.onUnexpectedError)(s),i.Disposable.None}if(this._disposed)return i.Disposable.None;t&&(e=e.bind(t));const n=new v(e);let o;this._leakageMon&&this._size>=Math.ceil(.2*this._leakageMon.threshold)&&(n.stack=d.create(),o=this._leakageMon.check(n.stack,this._size+1)),this._listeners?this._listeners instanceof v?(this._deliveryQueue??=new g,this._listeners=[this._listeners,n]):this._listeners.push(n):(this._options?.onWillAddFirstListener?.(this),this._listeners=n,this._options?.onDidAddFirstListener?.(this)),this._size++;const a=(0,i.toDisposable)((()=>{o?.(),this._removeListener(n)}));return s instanceof i.DisposableStore?s.add(a):Array.isArray(s)&&s.push(a),a},this._event}_removeListener(e){if(this._options?.onWillRemoveListener?.(this),!this._listeners)return;if(1===this._size)return this._listeners=void 0,this._options?.onDidRemoveLastListener?.(this),void(this._size=0);const t=this._listeners,s=t.indexOf(e);if(-1===s)throw console.log("disposed?",this._disposed),console.log("size?",this._size),console.log("arr?",JSON.stringify(this._listeners)),new Error("Attempted to dispose unknown listener");this._size--,t[s]=void 0;const r=this._deliveryQueue.current===this;if(2*this._size<=t.length){let e=0;for(let s=0;s0}}t.Emitter=m,t.createEventDeliveryQueue=()=>new g;class g{constructor(){this.i=-1,this.end=0}enqueue(e,t,s){this.i=0,this.end=s,this.current=e,this.value=t}reset(){this.i=this.end,this.current=void 0,this.value=void 0}}t.AsyncEmitter=class extends m{async fireAsync(e,t,s){if(this._listeners)for(this._asyncDeliveryQueue||(this._asyncDeliveryQueue=new o.LinkedList),((e,t)=>{if(e instanceof v)t(e);else for(let s=0;sthis._asyncDeliveryQueue.push([t.value,e])));this._asyncDeliveryQueue.size>0&&!t.isCancellationRequested;){const[e,n]=this._asyncDeliveryQueue.shift(),i=[],o={...n,token:t,waitUntil:t=>{if(Object.isFrozen(i))throw new Error("waitUntil can NOT be called asynchronous");s&&(t=s(t,e)),i.push(t)}};try{e(o)}catch(e){(0,r.onUnexpectedError)(e);continue}Object.freeze(i),await Promise.allSettled(i).then((e=>{for(const t of e)"rejected"===t.status&&(0,r.onUnexpectedError)(t.reason)}))}}};class y extends m{get isPaused(){return 0!==this._isPaused}constructor(e){super(e),this._isPaused=0,this._eventQueue=new o.LinkedList,this._mergeFn=e?.merge}pause(){this._isPaused++}resume(){if(0!==this._isPaused&&0==--this._isPaused)if(this._mergeFn){if(this._eventQueue.size>0){const e=Array.from(this._eventQueue);this._eventQueue.clear(),super.fire(this._mergeFn(e))}}else for(;!this._isPaused&&0!==this._eventQueue.size;)super.fire(this._eventQueue.shift())}fire(e){this._size&&(0!==this._isPaused?this._eventQueue.push(e):super.fire(e))}}t.PauseableEmitter=y,t.DebounceEmitter=class extends y{constructor(e){super(e),this._delay=e.delay??100}fire(e){this._handle||(this.pause(),this._handle=setTimeout((()=>{this._handle=void 0,this.resume()}),this._delay)),super.fire(e)}},t.MicrotaskEmitter=class extends m{constructor(e){super(e),this._queuedEvents=[],this._mergeFn=e?.merge}fire(e){this.hasListeners()&&(this._queuedEvents.push(e),1===this._queuedEvents.length&&queueMicrotask((()=>{this._mergeFn?super.fire(this._mergeFn(this._queuedEvents)):this._queuedEvents.forEach((e=>super.fire(e))),this._queuedEvents=[]})))}};class b{constructor(){this.hasListeners=!1,this.events=[],this.emitter=new m({onWillAddFirstListener:()=>this.onFirstListenerAdd(),onDidRemoveLastListener:()=>this.onLastListenerRemove()})}get event(){return this.emitter.event}add(e){const t={event:e,listener:null};return this.events.push(t),this.hasListeners&&this.hook(t),(0,i.toDisposable)((0,n.createSingleCallFunction)((()=>{this.hasListeners&&this.unhook(t);const e=this.events.indexOf(t);this.events.splice(e,1)})))}onFirstListenerAdd(){this.hasListeners=!0,this.events.forEach((e=>this.hook(e)))}onLastListenerRemove(){this.hasListeners=!1,this.events.forEach((e=>this.unhook(e)))}hook(e){e.listener=e.event((e=>this.emitter.fire(e)))}unhook(e){e.listener?.dispose(),e.listener=null}dispose(){this.emitter.dispose();for(const e of this.events)e.listener?.dispose();this.events=[]}}t.EventMultiplexer=b,t.DynamicListEventMultiplexer=class{constructor(e,t,s,r){this._store=new i.DisposableStore;const n=this._store.add(new b),o=this._store.add(new i.DisposableMap);function a(e){o.set(e,n.add(r(e)))}for(const t of e)a(t);this._store.add(t((e=>{a(e)}))),this._store.add(s((e=>{o.deleteAndDispose(e)}))),this.event=n.event}dispose(){this._store.dispose()}},t.EventBufferer=class{constructor(){this.data=[]}wrapEvent(e,t,s){return(r,n,i)=>e((e=>{const i=this.data[this.data.length-1];if(!t)return void(i?i.buffers.push((()=>r.call(n,e))):r.call(n,e));const o=i;o?(o.items??=[],o.items.push(e),0===o.buffers.length&&i.buffers.push((()=>{o.reducedResult??=s?o.items.reduce(t,s):o.items.reduce(t),r.call(n,o.reducedResult)}))):r.call(n,t(s,e))}),void 0,i)}bufferEvents(e){const t={buffers:new Array};this.data.push(t);const s=e();return this.data.pop(),t.buffers.forEach((e=>e())),s}},t.Relay=class{constructor(){this.listening=!1,this.inputEvent=l.None,this.inputEventListener=i.Disposable.None,this.emitter=new m({onDidAddFirstListener:()=>{this.listening=!0,this.inputEventListener=this.inputEvent(this.emitter.fire,this.emitter)},onDidRemoveLastListener:()=>{this.listening=!1,this.inputEventListener.dispose()}}),this.event=this.emitter.event}set input(e){this.inputEvent=e,this.listening&&(this.inputEventListener.dispose(),this.inputEventListener=e(this.emitter.fire,this.emitter))}dispose(){this.inputEventListener.dispose(),this.emitter.dispose()}},t.ValueWithChangeEvent=class{static const(e){return new E(e)}constructor(e){this._value=e,this._onDidChange=new m,this.onDidChange=this._onDidChange.event}get value(){return this._value}set value(e){e!==this._value&&(this._value=e,this._onDidChange.fire(void 0))}};class E{constructor(e){this.value=e,this.onDidChange=l.None}}},355:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.createSingleCallFunction=function(e,t){const s=this;let r,n=!1;return function(){if(n)return r;if(n=!0,t)try{r=e.apply(s,arguments)}finally{t()}else r=e.apply(s,arguments);return r}}},956:(e,t)=>{var s;Object.defineProperty(t,"__esModule",{value:!0}),t.Iterable=void 0,function(e){function t(e){return e&&"object"==typeof e&&"function"==typeof e[Symbol.iterator]}e.is=t;const s=Object.freeze([]);function*r(e){yield e}e.empty=function(){return s},e.single=r,e.wrap=function(e){return t(e)?e:r(e)},e.from=function(e){return e||s},e.reverse=function*(e){for(let t=e.length-1;t>=0;t--)yield e[t]},e.isEmpty=function(e){return!e||!0===e[Symbol.iterator]().next().done},e.first=function(e){return e[Symbol.iterator]().next().value},e.some=function(e,t){let s=0;for(const r of e)if(t(r,s++))return!0;return!1},e.find=function(e,t){for(const s of e)if(t(s))return s},e.filter=function*(e,t){for(const s of e)t(s)&&(yield s)},e.map=function*(e,t){let s=0;for(const r of e)yield t(r,s++)},e.flatMap=function*(e,t){let s=0;for(const r of e)yield*t(r,s++)},e.concat=function*(...e){for(const t of e)yield*t},e.reduce=function(e,t,s){let r=s;for(const s of e)r=t(r,s);return r},e.slice=function*(e,t,s=e.length){for(t<0&&(t+=e.length),s<0?s+=e.length:s>e.length&&(s=e.length);tn}]},e.asyncToArray=async function(e){const t=[];for await(const s of e)t.push(s);return Promise.resolve(t)}}(s||(t.Iterable=s={}))},540:(e,t,s)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.DisposableMap=t.ImmortalReference=t.AsyncReferenceCollection=t.ReferenceCollection=t.SafeDisposable=t.RefCountedDisposable=t.MandatoryMutableDisposable=t.MutableDisposable=t.Disposable=t.DisposableStore=t.DisposableTracker=void 0,t.setDisposableTracker=function(e){l=e},t.trackDisposable=h,t.markAsDisposed=u,t.markAsSingleton=function(e){return l?.markAsSingleton(e),e},t.isDisposable=f,t.dispose=p,t.disposeIfDisposable=function(e){for(const t of e)f(t)&&t.dispose();return[]},t.combinedDisposable=function(...e){const t=_((()=>p(e)));return function(e,t){if(l)for(const s of e)l.setParent(s,t)}(e,t),t},t.toDisposable=_,t.disposeOnReturn=function(e){const t=new v;try{e(t)}finally{t.dispose()}};const r=s(732),n=s(33),i=s(714),o=s(355),a=s(956);let l=null;class c{constructor(){this.livingDisposables=new Map}static{this.idx=0}getDisposableData(e){let t=this.livingDisposables.get(e);return t||(t={parent:null,source:null,isSingleton:!1,value:e,idx:c.idx++},this.livingDisposables.set(e,t)),t}trackDisposable(e){const t=this.getDisposableData(e);t.source||(t.source=(new Error).stack)}setParent(e,t){this.getDisposableData(e).parent=t}markAsDisposed(e){this.livingDisposables.delete(e)}markAsSingleton(e){this.getDisposableData(e).isSingleton=!0}getRootParent(e,t){const s=t.get(e);if(s)return s;const r=e.parent?this.getRootParent(this.getDisposableData(e.parent),t):e;return t.set(e,r),r}getTrackedDisposables(){const e=new Map;return[...this.livingDisposables.entries()].filter((([,t])=>null!==t.source&&!this.getRootParent(t,e).isSingleton)).flatMap((([e])=>e))}computeLeakingDisposables(e=10,t){let s;if(t)s=t;else{const e=new Map,t=[...this.livingDisposables.values()].filter((t=>null!==t.source&&!this.getRootParent(t,e).isSingleton));if(0===t.length)return;const r=new Set(t.map((e=>e.value)));if(s=t.filter((e=>!(e.parent&&r.has(e.parent)))),0===s.length)throw new Error("There are cyclic diposable chains!")}if(!s)return;function o(e){const t=e.source.split("\n").map((e=>e.trim().replace("at ",""))).filter((e=>""!==e));return function(e,t){for(;e.length>0&&t.some((t=>"string"==typeof t?t===e[0]:e[0].match(t)));)e.shift()}(t,["Error",/^trackDisposable \(.*\)$/,/^DisposableTracker.trackDisposable \(.*\)$/]),t.reverse()}const a=new i.SetMap;for(const e of s){const t=o(e);for(let s=0;s<=t.length;s++)a.add(t.slice(0,s).join("\n"),e)}s.sort((0,r.compareBy)((e=>e.idx),r.numberComparator));let l="",c=0;for(const t of s.slice(0,e)){c++;const e=o(t),r=[];for(let t=0;to(e)[t])),(e=>e));delete c[e[t]];for(const[e,t]of Object.entries(c))r.unshift(` - stacktraces of ${t.length} other leaks continue with ${e}`);r.unshift(i)}l+=`\n\n\n==================== Leaking disposable ${c}/${s.length}: ${t.value.constructor.name} ====================\n${r.join("\n")}\n============================================================\n\n`}return s.length>e&&(l+=`\n\n\n... and ${s.length-e} more leaking disposables\n\n`),{leaks:s,details:l}}}function h(e){return l?.trackDisposable(e),e}function u(e){l?.markAsDisposed(e)}function d(e,t){l?.setParent(e,t)}function f(e){return"object"==typeof e&&null!==e&&"function"==typeof e.dispose&&0===e.dispose.length}function p(e){if(a.Iterable.is(e)){const t=[];for(const s of e)if(s)try{s.dispose()}catch(e){t.push(e)}if(1===t.length)throw t[0];if(t.length>1)throw new AggregateError(t,"Encountered errors while disposing of store");return Array.isArray(e)?[]:e}if(e)return e.dispose(),e}function _(e){const t=h({dispose:(0,o.createSingleCallFunction)((()=>{u(t),e()}))});return t}t.DisposableTracker=c;class v{static{this.DISABLE_DISPOSED_WARNING=!1}constructor(){this._toDispose=new Set,this._isDisposed=!1,h(this)}dispose(){this._isDisposed||(u(this),this._isDisposed=!0,this.clear())}get isDisposed(){return this._isDisposed}clear(){if(0!==this._toDispose.size)try{p(this._toDispose)}finally{this._toDispose.clear()}}add(e){if(!e)return e;if(e===this)throw new Error("Cannot register a disposable on itself!");return d(e,this),this._isDisposed?v.DISABLE_DISPOSED_WARNING||console.warn(new Error("Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!").stack):this._toDispose.add(e),e}delete(e){if(e){if(e===this)throw new Error("Cannot dispose a disposable on itself!");this._toDispose.delete(e),e.dispose()}}deleteAndLeak(e){e&&this._toDispose.has(e)&&(this._toDispose.delete(e),d(e,null))}}t.DisposableStore=v;class m{static{this.None=Object.freeze({dispose(){}})}constructor(){this._store=new v,h(this),d(this._store,this)}dispose(){u(this),this._store.dispose()}_register(e){if(e===this)throw new Error("Cannot register a disposable on itself!");return this._store.add(e)}}t.Disposable=m;class g{constructor(){this._isDisposed=!1,h(this)}get value(){return this._isDisposed?void 0:this._value}set value(e){this._isDisposed||e===this._value||(this._value?.dispose(),e&&d(e,this),this._value=e)}clear(){this.value=void 0}dispose(){this._isDisposed=!0,u(this),this._value?.dispose(),this._value=void 0}clearAndLeak(){const e=this._value;return this._value=void 0,e&&d(e,null),e}}t.MutableDisposable=g,t.MandatoryMutableDisposable=class{constructor(e){this._disposable=new g,this._isDisposed=!1,this._disposable.value=e}get value(){return this._disposable.value}set value(e){this._isDisposed||e===this._disposable.value||(this._disposable.value=e)}dispose(){this._isDisposed=!0,this._disposable.dispose()}},t.RefCountedDisposable=class{constructor(e){this._disposable=e,this._counter=1}acquire(){return this._counter++,this}release(){return 0==--this._counter&&this._disposable.dispose(),this}},t.SafeDisposable=class{constructor(){this.dispose=()=>{},this.unset=()=>{},this.isset=()=>!1,h(this)}set(e){let t=e;return this.unset=()=>t=void 0,this.isset=()=>void 0!==t,this.dispose=()=>{t&&(t(),t=void 0,u(this))},this}},t.ReferenceCollection=class{constructor(){this.references=new Map}acquire(e,...t){let s=this.references.get(e);s||(s={counter:0,object:this.createReferencedObject(e,...t)},this.references.set(e,s));const{object:r}=s,n=(0,o.createSingleCallFunction)((()=>{0==--s.counter&&(this.destroyReferencedObject(e,s.object),this.references.delete(e))}));return s.counter++,{object:r,dispose:n}}},t.AsyncReferenceCollection=class{constructor(e){this.referenceCollection=e}async acquire(e,...t){const s=this.referenceCollection.acquire(e,...t);try{return{object:await s.object,dispose:()=>s.dispose()}}catch(e){throw s.dispose(),e}}},t.ImmortalReference=class{constructor(e){this.object=e}dispose(){}};class y{constructor(){this._store=new Map,this._isDisposed=!1,h(this)}dispose(){u(this),this._isDisposed=!0,this.clearAndDisposeAll()}clearAndDisposeAll(){if(this._store.size)try{p(this._store.values())}finally{this._store.clear()}}has(e){return this._store.has(e)}get size(){return this._store.size}get(e){return this._store.get(e)}set(e,t,s=!1){this._isDisposed&&console.warn(new Error("Trying to add a disposable to a DisposableMap that has already been disposed of. The added object will be leaked!").stack),s||this._store.get(e)?.dispose(),this._store.set(e,t)}deleteAndDispose(e){this._store.get(e)?.dispose(),this._store.delete(e)}deleteAndLeak(e){const t=this._store.get(e);return this._store.delete(e),t}keys(){return this._store.keys()}values(){return this._store.values()}[Symbol.iterator](){return this._store[Symbol.iterator]()}}t.DisposableMap=y},711:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.LinkedList=void 0;class s{static{this.Undefined=new s(void 0)}constructor(e){this.element=e,this.next=s.Undefined,this.prev=s.Undefined}}class r{constructor(){this._first=s.Undefined,this._last=s.Undefined,this._size=0}get size(){return this._size}isEmpty(){return this._first===s.Undefined}clear(){let e=this._first;for(;e!==s.Undefined;){const t=e.next;e.prev=s.Undefined,e.next=s.Undefined,e=t}this._first=s.Undefined,this._last=s.Undefined,this._size=0}unshift(e){return this._insert(e,!1)}push(e){return this._insert(e,!0)}_insert(e,t){const r=new s(e);if(this._first===s.Undefined)this._first=r,this._last=r;else if(t){const e=this._last;this._last=r,r.prev=e,e.next=r}else{const e=this._first;this._first=r,r.next=e,e.prev=r}this._size+=1;let n=!1;return()=>{n||(n=!0,this._remove(r))}}shift(){if(this._first!==s.Undefined){const e=this._first.element;return this._remove(this._first),e}}pop(){if(this._last!==s.Undefined){const e=this._last.element;return this._remove(this._last),e}}_remove(e){if(e.prev!==s.Undefined&&e.next!==s.Undefined){const t=e.prev;t.next=e.next,e.next.prev=t}else e.prev===s.Undefined&&e.next===s.Undefined?(this._first=s.Undefined,this._last=s.Undefined):e.next===s.Undefined?(this._last=this._last.prev,this._last.next=s.Undefined):e.prev===s.Undefined&&(this._first=this._first.next,this._first.prev=s.Undefined);this._size-=1}*[Symbol.iterator](){let e=this._first;for(;e!==s.Undefined;)yield e.element,e=e.next}}t.LinkedList=r},714:(e,t)=>{var s;Object.defineProperty(t,"__esModule",{value:!0}),t.SetMap=t.BidirectionalMap=t.CounterSet=t.Touch=void 0,t.getOrSet=function(e,t,s){let r=e.get(t);return void 0===r&&(r=s,e.set(t,r)),r},t.mapToString=function(e){const t=[];return e.forEach(((e,s)=>{t.push(`${s} => ${e}`)})),`Map(${e.size}) {${t.join(", ")}}`},t.setToString=function(e){const t=[];return e.forEach((e=>{t.push(e)})),`Set(${e.size}) {${t.join(", ")}}`},t.mapsStrictEqualIgnoreOrder=function(e,t){if(e===t)return!0;if(e.size!==t.size)return!1;for(const[s,r]of e)if(!t.has(s)||t.get(s)!==r)return!1;for(const[s]of t)if(!e.has(s))return!1;return!0},function(e){e[e.None=0]="None",e[e.AsOld=1]="AsOld",e[e.AsNew=2]="AsNew"}(s||(t.Touch=s={})),t.CounterSet=class{constructor(){this.map=new Map}add(e){return this.map.set(e,(this.map.get(e)||0)+1),this}delete(e){let t=this.map.get(e)||0;return 0!==t&&(t--,0===t?this.map.delete(e):this.map.set(e,t),!0)}has(e){return this.map.has(e)}},t.BidirectionalMap=class{constructor(e){if(this._m1=new Map,this._m2=new Map,e)for(const[t,s]of e)this.set(t,s)}clear(){this._m1.clear(),this._m2.clear()}set(e,t){this._m1.set(e,t),this._m2.set(t,e)}get(e){return this._m1.get(e)}getKey(e){return this._m2.get(e)}delete(e){const t=this._m1.get(e);return void 0!==t&&(this._m1.delete(e),this._m2.delete(t),!0)}forEach(e,t){this._m1.forEach(((s,r)=>{e.call(t,s,r,this)}))}keys(){return this._m1.keys()}values(){return this._m1.values()}},t.SetMap=class{constructor(){this.map=new Map}add(e,t){let s=this.map.get(e);s||(s=new Set,this.map.set(e,s)),s.add(t)}delete(e,t){const s=this.map.get(e);s&&(s.delete(t),0===s.size&&this.map.delete(e))}forEach(e,t){const s=this.map.get(e);s&&s.forEach(t)}get(e){return this.map.get(e)||new Set}}},79:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.StopWatch=void 0;const s=globalThis.performance&&"function"==typeof globalThis.performance.now;class r{static create(e){return new r(e)}constructor(e){this._now=s&&!1===e?Date.now:globalThis.performance.now.bind(globalThis.performance),this._startTime=this._now(),this._stopTime=-1}stop(){this._stopTime=this._now()}reset(){this._startTime=this._now(),this._stopTime=-1}elapsed(){return-1!==this._stopTime?this._stopTime-this._startTime:this._now()-this._startTime}}t.StopWatch=r}},t={};function s(r){var n=t[r];if(void 0!==n)return n.exports;var i=t[r]={exports:{}};return e[r](i,i.exports,s),i.exports}var r={};return(()=>{var e=r;Object.defineProperty(e,"__esModule",{value:!0}),e.Unicode11Addon=void 0;const t=s(384);e.Unicode11Addon=class{activate(e){e.unicode.register(new t.UnicodeV11)}dispose(){}}})(),r})())); +//# sourceMappingURL=addon-unicode11.js.map \ No newline at end of file diff --git a/tests/DodoSSH.Client.Terminal.Tests/Renderer/renderer-harness.js b/tests/DodoSSH.Client.Terminal.Tests/Renderer/renderer-harness.js index 10a21bf..e9b0ae3 100644 --- a/tests/DodoSSH.Client.Terminal.Tests/Renderer/renderer-harness.js +++ b/tests/DodoSSH.Client.Terminal.Tests/Renderer/renderer-harness.js @@ -203,12 +203,22 @@ function FakeTerminal(options) { this.focuses = 0; this.disposed = false; this.pane = null; + this.addons = []; + + // The proposed unicode API, at xterm's own default, so a test can see which table the page picked. + this.unicode = { activeVersion: '6' }; terminals.push(this); } -FakeTerminal.prototype.loadAddon = function () {}; -FakeTerminal.prototype.open = function (pane) { this.pane = pane; }; +FakeTerminal.prototype.loadAddon = function (addon) { this.addons.push(addon); }; +FakeTerminal.prototype.open = function (pane) { + this.pane = pane; + + // Taken at open() because that is when the first frame is laid out: a table chosen afterwards would leave + // whatever was drawn first measured with the old one. + this.unicodeAtOpen = this.unicode.activeVersion; +}; FakeTerminal.prototype.attachCustomKeyEventHandler = function (handler) { this.keys = handler; }; FakeTerminal.prototype.onData = function (handler) { this.data = handler; }; FakeTerminal.prototype.onResize = function (handler) { this.resized = handler; }; @@ -242,6 +252,12 @@ var FitAddon = { }, }; +var Unicode11Addon = { + Unicode11Addon: function () { + this.kind = 'unicode11'; + }, +}; + var WebglAddon = { WebglAddon: function () { this.onContextLoss = function () {}; @@ -489,6 +505,16 @@ function typed(sessionId) { return terminalFor(sessionId).inputs.join('|'); } +/** Which character-width table the pane was opened with. */ +function unicodeAtOpen(sessionId) { + return terminalFor(sessionId).unicodeAtOpen; +} + +/** Whether that pane loaded the Unicode 11 addon. */ +function hasUnicode11(sessionId) { + return terminalFor(sessionId).addons.some(function (addon) { return addon.kind === 'unicode11'; }); +} + /** How many times that pane has been asked for the caret. */ function focuses(sessionId) { return terminalFor(sessionId).focuses; diff --git a/tests/DodoSSH.Client.Terminal.Tests/RendererPage.cs b/tests/DodoSSH.Client.Terminal.Tests/RendererPage.cs index 52b4a09..7964290 100644 --- a/tests/DodoSSH.Client.Terminal.Tests/RendererPage.cs +++ b/tests/DodoSSH.Client.Terminal.Tests/RendererPage.cs @@ -94,6 +94,14 @@ internal sealed class RendererPage public string Typed(uint sessionId) => engine.Evaluate(string.Create(CultureInfo.InvariantCulture, $"typed({sessionId})")).AsString(); + /// Which character-width table that pane was opened with. + public string UnicodeAtOpen(uint sessionId) => + engine.Evaluate(string.Create(CultureInfo.InvariantCulture, $"unicodeAtOpen({sessionId})")).AsString(); + + /// Whether that pane loaded the Unicode 11 addon. + public bool HasUnicode11(uint sessionId) => + engine.Evaluate(string.Create(CultureInfo.InvariantCulture, $"hasUnicode11({sessionId})")).AsBoolean(); + /// How many times that session's pane has been asked for the caret. public int Focuses(uint sessionId) => (int)engine.Evaluate(string.Create(CultureInfo.InvariantCulture, $"focuses({sessionId})")).AsNumber(); diff --git a/tests/DodoSSH.Client.Terminal.Tests/RendererUnicodeTests.cs b/tests/DodoSSH.Client.Terminal.Tests/RendererUnicodeTests.cs new file mode 100644 index 0000000..b313d07 --- /dev/null +++ b/tests/DodoSSH.Client.Terminal.Tests/RendererUnicodeTests.cs @@ -0,0 +1,43 @@ +namespace DodoSSH.Client.Terminal.Tests; + +/// +/// The renderer page measures characters with Unicode 11's widths, so an emoji takes the two columns the +/// remote counts for it. +/// +/// +/// What is asserted is that the page asks for the table, and asks before the first frame. Whether xterm then +/// really draws 😀 across two cells is xterm's business, and the fake terminal in the harness is a stub with +/// a memory, not an emulator; docs/manual-checks.md 1.13 is where a person looks at the real one. +/// +public sealed class RendererUnicodeTests +{ + [Fact] + public void APane_LoadsTheUnicode11Addon() + { + var page = Opened(); + + page.HasUnicode11(7).ShouldBeTrue(); + } + + /// + /// Before open() and not after it: a table chosen once the pane is open leaves whatever it drew first + /// measured with Unicode 6, which is the drift this exists to stop. + /// + [Fact] + public void APane_IsOpenedWithUnicode11WidthsAlreadyActive() + { + var page = Opened(); + + page.UnicodeAtOpen(7).ShouldBe("11"); + } + + private static RendererPage Opened() + { + var page = RendererPage.Load(); + + page.Do("accept(0)"); + page.Do("openSession(7)"); + + return page; + } +}