Compare commits

..
175 Commits
Author SHA1 Message Date
Daniel Imms 24f431b290 Merge pull request #3333 from Tyriar/v4_12
v4.12.0
2021-05-11 07:11:42 -07:00
Daniel Imms 0c47cd4d4d v4.12.0 2021-05-11 07:09:41 -07:00
Daniel Imms 33f0105d29 Merge pull request #3332 from Tyriar/scroll_refresh
Make sure all rows are refreshed on input
2021-05-11 07:02:41 -07:00
Daniel Imms bcbcc71bfc Trigger scroll when ignoring event
This is a less aggressive fix that doesn't regress tests
2021-05-11 06:52:04 -07:00
Daniel Imms da39129bd2 Revert "Make sure all rows are refreshed on input"
This reverts commit 973eb7b350.
2021-05-11 06:48:17 -07:00
Daniel Imms 973eb7b350 Make sure all rows are refreshed on input
Fixes #3323
2021-05-11 05:10:04 -07:00
Daniel Imms 8f9f2550ef Merge pull request #3327 from Tyriar/tasks
Don't start npm start until watch is done
2021-05-10 05:36:54 -07:00
Daniel Imms 673695068e Don't start npm start until watch is done 2021-05-10 05:35:06 -07:00
Daniel Imms 07c123309f Merge pull request #3322 from vladdoster/patch-1
(docs) update README.md
2021-05-07 04:59:19 -07:00
vlad doster 6a63be9107 (docs) update README.md
- correct spelling
- correct grammar
- reduce verbiage
- correct punctuation
2021-05-07 04:38:12 -05:00
Daniel Imms 978b2a9886 Merge pull request #3321 from xtermjs/dependabot/npm_and_yarn/addons/xterm-addon-ligatures/lodash-4.17.21
Bump lodash from 4.17.19 to 4.17.21 in /addons/xterm-addon-ligatures
2021-05-06 14:18:43 -07:00
dependabot[bot] d1ce8a0586 Bump lodash from 4.17.19 to 4.17.21 in /addons/xterm-addon-ligatures
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.19 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.19...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-06 21:08:45 +00:00
Daniel Imms dc61950311 Merge pull request #3320 from xtermjs/dependabot/npm_and_yarn/lodash-4.17.21
[Security] Bump lodash from 4.17.20 to 4.17.21
2021-05-06 14:07:58 -07:00
dependabot-preview[bot] 0ba0845563 [Security] Bump lodash from 4.17.20 to 4.17.21
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21. **This update includes a security fix.**
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-05-06 20:39:34 +00:00
Daniel Imms d70ba1dc23 Merge pull request #3318 from meganrogge/bufferRange
Fix selection range
2021-05-06 05:02:25 -07:00
meganrogge 9403478049 tweak test 2021-05-05 19:49:33 -07:00
meganrogge 20f320327c Tweak range 2021-05-05 19:45:36 -07:00
meganrogge 9a5f85cc24 remove .only 2021-05-05 12:29:55 -07:00
meganrogge be32590ef0 fix #https://github.com/microsoft/vscode/issues/123000 2021-05-05 08:21:26 -07:00
Daniel Imms e5c9b0e27f Merge pull request #3314 from jeanp413/fix-undefined-soundService
Fixes undefined soundService
2021-05-04 10:39:25 -07:00
jeanp413 58527bb2ff Fixes undefined soundService 2021-05-04 11:23:31 -05:00
Daniel Imms 4a7bbe6738 Merge pull request #3313 from Tyriar/scroll_suppress
Ensure all viewport scroll events are handled by buffer
2021-04-28 13:00:38 -07:00
Daniel Imms d616aaa4f8 Ensure all viewport scroll events are handled by buffer
Some scroll events were never making it to the buffer service when using
a trackpad.

See microsoft/vscode#121017
Fixes #3311
2021-04-28 12:51:40 -07:00
Daniel Imms 376b29673b Merge pull request #3296 from Tyriar/asserttimeout
Don't assert on leave events
2021-04-06 14:05:39 -07:00
Daniel Imms 636e59b949 Merge branch 'master' into asserttimeout 2021-04-06 13:15:01 -07:00
Daniel Imms e3615f63e0 Merge pull request #3299 from bmf-ribeiro/feat/enable-windows-integration-tests
Feat/enable windows integration tests
2021-04-06 13:07:01 -07:00
Daniel Imms 0b02a3c774 Merge branch 'master' into asserttimeout 2021-04-06 13:00:42 -07:00
Daniel Imms 87ef131945 Merge branch 'master' into feat/enable-windows-integration-tests 2021-04-06 12:59:16 -07:00
Daniel Imms 6370a936b8 Merge pull request #3298 from bmf-ribeiro/feat/2714-start-server-on-integration-tests
Fix #2714 - Start demo server before running integration tests
2021-04-06 12:58:31 -07:00
Bruno Ribeiro e686527f87 Try to enable windows integration tests 2021-04-06 17:47:12 +01:00
Bruno Ribeiro c312317af0 Merge branch 'master' into feat/2714-start-server-on-integration-tests 2021-04-06 17:34:01 +01:00
Bruno Ribeiro b8af0ecf05 Await for server to be fully started 2021-04-06 17:21:39 +01:00
Bruno Ribeiro ab659fda5f Remove Start Test Server from pipelines as it now starts in the tests 2021-04-06 16:51:17 +01:00
Bruno Ribeiro 27c87319c1 Fix #2714 - Start demo server before running integration tests
- Use port 3001 instead of  300 to not conflict with the demo
2021-04-06 15:48:48 +01:00
Daniel Imms 5d4cea2119 Merge branch 'master' into asserttimeout 2021-04-06 07:07:58 -07:00
Daniel Imms 26cda84eab Merge pull request #3295 from xtermjs/Tyriar-patch-1
Fix whitespace
2021-04-06 06:50:22 -07:00
Daniel Imms c4fc90e3f8 Don't assert on leave events 2021-04-06 06:50:07 -07:00
Daniel Imms 985040e1cc Merge branch 'master' into Tyriar-patch-1 2021-04-06 06:34:52 -07:00
Daniel Imms 95fcf2f354 Merge pull request #3294 from Tyriar/pollfor
Give actionable error when pollFor times out
2021-04-06 06:34:43 -07:00
Daniel Imms 1422d935d0 Fix whitespace 2021-04-06 06:34:20 -07:00
Daniel Imms 5973e8f0dd Remove unused function 2021-04-06 06:28:49 -07:00
Daniel Imms 934467d484 Give actionable error when pollFor times out 2021-04-06 06:27:39 -07:00
Daniel Imms eedc314ca2 Merge pull request #3290 from nikonso/feat/3014-add-onBell-event-listener
Fix #3014  - Add onBell event listener to allow embeders to hook into it
2021-04-06 05:56:43 -07:00
Daniel Imms 2556137b51 Remove only 2021-04-06 05:44:36 -07:00
Daniel Imms 826493161a Api polish 2021-04-06 05:40:56 -07:00
Daniel Imms 19716957e4 Fix api test 2021-04-06 05:40:29 -07:00
Daniel Imms 3d29357d1d Fix test/lint 2021-04-06 05:34:20 -07:00
Daniel Imms 9143cf4904 Merge branch 'master' into feat/3014-add-onBell-event-listener 2021-04-06 05:28:29 -07:00
Daniel Imms c7978f78a2 Merge pull request #3291 from whattheserver/patch-1
Update README.md
2021-04-06 05:25:57 -07:00
WhatTheServer af26e27e23 Update README.md 2021-04-04 00:09:10 -04:00
Bruno Ribeiro 7b7b7a36db Fix #3014
- Add onBell event listener to allow embeders to hook into it
2021-04-03 16:34:00 +01:00
Daniel Imms 8a1d2cf0f4 Whitespace change to force ligatures publish 2021-04-02 14:22:49 -07:00
Daniel Imms 98f2576497 Merge pull request #3286 from Tyriar/webgl_liga
Webgl ligature support
2021-04-02 11:39:45 -07:00
Daniel Imms 00fa7bec42 Null out secondary ligature chars in cache 2021-04-02 11:32:02 -07:00
Daniel Imms b03a25f4c7 Webgl ligature support
Fixes #3094
2021-04-02 11:15:57 -07:00
Daniel Imms cf8a3514bc Merge pull request #3285 from Tyriar/dom_ligatures
Support for ligatures in DOM renderer
2021-04-02 10:45:41 -07:00
Daniel Imms b7320794bb Ligature support for dom renderer
Fixes #3283
2021-04-02 10:34:14 -07:00
Daniel Imms d8113aa533 Default to Fira Code in demo 2021-04-02 10:21:20 -07:00
Daniel Imms 16a70bdeb7 Merge pull request #3264 from LabhanshAgrawal/local_fonts
Enable using ligatures addon outside electron
2021-04-02 09:43:34 -07:00
Daniel Imms 0564ad4653 Add comments to webpack configs 2021-04-02 09:42:10 -07:00
Daniel Imms a57910e2fa Check navigator safely in node 2021-04-02 08:33:00 -07:00
Daniel Imms 4fa58c978b Fix lint 2021-04-02 07:58:59 -07:00
Daniel Imms 54cdbb3035 Merge branch 'master' into local_fonts 2021-04-02 07:57:51 -07:00
Daniel Imms 367eb45586 Merge pull request #3284 from Tyriar/tyriar/charJoinerService
Convert CharacterJoinerRegistry to a service
2021-04-02 07:57:38 -07:00
Daniel Imms 1124f8b513 Improve error when adding char joiner before open 2021-04-02 07:57:24 -07:00
Daniel Imms 233b78d258 Merge pull request #3282 from Tyriar/spacing_eslint
Require spacing of objects outside of test files
2021-04-02 07:52:37 -07:00
Daniel Imms 5f96f33cf0 Fix dangling comma 2021-04-02 07:50:22 -07:00
Daniel Imms b511f1b52a Fix other case of no spacing 2021-04-02 07:46:30 -07:00
Daniel Imms 6b3fd2f31a Merge branch 'master' into spacing_eslint 2021-04-02 07:45:31 -07:00
Daniel Imms 6501ab044c Merge branch 'master' into tyriar/charJoinerService 2021-04-02 07:44:28 -07:00
Daniel Imms 999116c536 Convert CharacterJoinerRegistry to a service
Part of #3094, #3283
2021-04-02 07:43:39 -07:00
Daniel Imms f19d7f0b70 Require spacing of objects outside of test files 2021-04-02 07:08:06 -07:00
Daniel Imms 1e499c7867 Disable ligatures addon by default
Since it needs a user action to trigger the permissions prompt
2021-04-02 06:57:11 -07:00
Daniel Imms b9d3deae56 Merge branch 'master' into local_fonts 2021-04-02 06:42:48 -07:00
Daniel Imms 0421f6ebcf Merge pull request #3279 from Tyriar/tyriar/powerline
Don't pad powerline glyphs and use ideographic text baseline
2021-04-01 17:45:44 -07:00
Daniel Imms a510ffb3c0 Use ideographic baseline for canvas 2021-04-01 17:10:14 -07:00
Daniel Imms 62f5b8291d Use ideographic over middle
This seems to correctly center the glyphs within the cell, making powerline
fonts appear perfectly aligned.

Fixes #3281
2021-04-01 17:05:29 -07:00
Megan Rogge f9945b3439 Merge pull request #3276 from meganrogge/webgl
add onContextLoss to webgl addon
2021-04-01 17:02:57 -07:00
Daniel Imms 62ca4b7e13 Correct restricted glyph padding 2021-04-01 17:00:02 -07:00
Megan Rogge 6f59b56958 Merge branch 'master' into webgl 2021-04-01 16:57:21 -07:00
meganrogge 113086f97a tweak readme, expose API 2021-04-01 16:57:19 -07:00
Daniel Imms b6d3120086 Restrict all sides of powerline glyphs 2021-04-01 16:55:27 -07:00
Daniel Imms 117f990055 Don't pad powerline glyphs
Fixes #3278
2021-04-01 16:39:59 -07:00
Daniel Imms 337c11d580 Merge pull request #3277 from meganrogge/bufferService
Move scroll to BufferService
2021-04-01 16:01:30 -07:00
Daniel Imms ecb485d2f5 Remove _cachedBlankLine 2021-04-01 15:54:47 -07:00
Daniel Imms c5339116b5 Add note to readme on handling context loss 2021-04-01 15:45:10 -07:00
meganrogge b1877b4dc7 onRecoverContext -> onContextLoss 2021-04-01 13:32:35 -07:00
meganrogge 1c087f6bcd cherry picked commits and scrollSource 2021-04-01 13:24:02 -07:00
meganrogge e40f340036 use bufferService methods 2021-04-01 13:18:20 -07:00
meganrogge c096633f91 get tests to pass
Co-authored-by: Daniel Imms <daimms@microsoft.com>
2021-04-01 13:17:53 -07:00
meganrogge b074c2552a use bufferService in terminal to reduce code a bunch 2021-04-01 13:17:48 -07:00
meganrogge e2885f3a38 move stuff to inputHandler and move scroll to bufferService 2021-04-01 13:16:39 -07:00
Megan Rogge 47ac4dd9fe Merge branch 'master' into webgl 2021-04-01 12:18:21 -07:00
meganrogge fa7889cdd0 add onRecovercontext event 2021-04-01 12:17:34 -07:00
Daniel Imms 759379dfb1 Merge pull request #3205 from schrej/fix/scroll-events
emit onScroll events when user is scrolling
2021-04-01 12:11:47 -07:00
Daniel Imms 87ed4f07cd Tidy up syncTextArea 2021-04-01 12:05:24 -07:00
Daniel Imms 07499380d1 Remove RenderService check, it must be passed to ctor 2021-04-01 12:05:21 -07:00
Daniel Imms 3ae61fc1dd Use buffer y for relative cursor pos 2021-04-01 12:05:17 -07:00
Daniel Imms 085a2b545f Remove now unneeded CharSizeService 2021-04-01 12:05:12 -07:00
kena0ki 49cb6f74bc Remove non-null assertion 2021-04-01 12:05:05 -07:00
kena0ki d4a93fe7e4 Use RenderService.dimensions instead of CharSizeService for textarea position 2021-04-01 12:05:00 -07:00
Daniel Imms f4861aa74a Move onScroll next to _onScroll 2021-04-01 08:54:29 -07:00
Daniel Imms 1925f1f442 Improve whitespace 2021-04-01 08:41:29 -07:00
Daniel Imms 6355852d09 Merge remote-tracking branch 'ups/master' into pr/schrej/3205 2021-04-01 08:36:14 -07:00
Daniel Imms e1641c57fc Merge pull request #3169 from kena0ki/issue#3024
Use RenderService.dimensions instead of CharSizeService for textarea position
2021-04-01 08:34:39 -07:00
Daniel Imms 7b920adbe9 Tidy up syncTextArea 2021-04-01 08:26:35 -07:00
Daniel Imms 05516750b5 Remove RenderService check, it must be passed to ctor 2021-04-01 08:23:35 -07:00
Daniel Imms b62a64519f Use buffer y for relative cursor pos 2021-04-01 08:23:18 -07:00
Daniel Imms e9e716964e Remove now unneeded CharSizeService 2021-04-01 08:19:12 -07:00
kena0ki a15f61bb85 Remove non-null assertion 2021-04-01 00:11:30 +09:00
kena0ki de334ea116 Use RenderService.dimensions instead of CharSizeService for textarea position 2021-03-31 23:32:53 +09:00
Daniel Imms dd952ac166 Merge pull request #3230 from marvinthepa/682_url_selector
Use linkifier2 to double click select links
2021-03-30 06:25:26 -07:00
Daniel Imms d2471b4301 Update src/browser/Types.d.ts 2021-03-30 06:19:39 -07:00
Daniel Imms 04092c80e8 Update src/browser/services/SelectionService.ts 2021-03-30 06:19:34 -07:00
Daniel Imms b7e0489a53 Remove offset from link
The link range is absolute, not relative to viewport
2021-03-30 06:18:17 -07:00
Daniel Imms daf1128df7 Merge branch 'master' into 682_url_selector 2021-03-30 05:34:38 -07:00
Daniel Imms 36c436d4e0 Merge pull request #3273 from Tyriar/webgl_stable
Remove experimental note from webgl renderer
2021-03-30 05:33:59 -07:00
Daniel Imms edf783f155 Merge pull request #3270 from xtermjs/dependabot/npm_and_yarn/y18n-4.0.1
[Security] Bump y18n from 4.0.0 to 4.0.1
2021-03-30 05:16:20 -07:00
Daniel Imms dafe7ea3da Remove experimental note from webgl renderer
Fixes #2033
2021-03-30 04:45:41 -07:00
dependabot-preview[bot] cbe056c27e [Security] Bump y18n from 4.0.0 to 4.0.1
Bumps [y18n](https://github.com/yargs/y18n) from 4.0.0 to 4.0.1. **This update includes a security fix.**
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-03-29 18:46:53 +00:00
Daniel Imms e78561b7b6 Merge pull request #3269 from coderaiser/lint-using-putout
Lint using putout part 3
2021-03-29 02:07:47 -07:00
coderaiser de791cfb35 chore: lint using putout 2021-03-28 15:27:03 +03:00
Daniel Imms 6d1a2f3b57 Merge pull request #3267 from jerch/writeSync_fix
writeSync callstack fix
2021-03-26 05:10:35 -07:00
Jörg Breitbart e106cd02cf fix callstack overflow of writeSync 2021-03-26 12:46:39 +01:00
Thomas Zilz 787ef30279 Revert "Noch mehr änderungen"
This reverts commit b666e6a924.
2021-03-25 14:09:47 +01:00
Thomas Zilz 61519cc10c Revert "Test test"
This reverts commit 61213159ee.
2021-03-25 14:09:44 +01:00
Thomas Zilz b666e6a924 Noch mehr änderungen 2021-03-25 13:59:20 +01:00
Thomas Zilz 61213159ee Test test 2021-03-25 13:55:29 +01:00
Labhansh Agrawal 67934ff35f Enable using ligatures addon outside electron 2021-03-24 20:44:14 +05:30
Daniel Imms 8c1833f117 Merge pull request #3258 from dstein64/master
Add gifcast to "Real-world uses" in README.md
2021-03-19 18:10:37 -07:00
Daniel Steinberg c2f4c4dd32 Add gifcast to "Real-world uses" in README.md 2021-03-19 18:40:36 -04:00
Daniel Imms c4ed61e831 Merge pull request #3257 from xtermjs/Tyriar-patch-1
Change warning to one per session not terminal
2021-03-19 06:32:45 -07:00
Daniel Imms 5c5ca4e213 Change warning to one per session not terminal 2021-03-19 06:24:42 -07:00
Daniel Imms 6edfa513f3 Merge pull request #3256 from jerch/silence_writeSync_warning
silence writeSync warning
2021-03-19 06:02:40 -07:00
Jörg Breitbart 5170a12cb1 silence writeSync warning 2021-03-19 01:07:43 +01:00
Daniel Imms 9a90f1f4ab Merge pull request #3251 from Python-37/Fix-bug-of-some-IMEs-cannot-input-in-terminal
Fix bug of some IMEs cannot input in terminal
2021-03-18 08:49:56 -07:00
Daniel Imms 886f285ab7 Remove old setting dimensions 2021-03-18 08:31:13 -07:00
Daniel Imms 00bdd2805a Ensure in js that the textarea is always > 1x1px 2021-03-18 08:14:27 -07:00
Daniel Imms d392bdb5ea Merge branch 'master' into Fix-bug-of-some-IMEs-cannot-input-in-terminal 2021-03-18 07:00:43 -07:00
jerch 145b4874cb Merge pull request #3222 from jerch/async_handlers
async parser handler support
2021-03-17 22:50:14 +01:00
Jörg Breitbart 869c11f1d8 interface for subparser stack saves 2021-03-17 22:39:09 +01:00
Jörg Breitbart 8e7cdd2587 interface for inputhandler parse stack, proper typing of LogService.logLevel 2021-03-17 22:29:01 +01:00
Jörg Breitbart 76fdf9d378 Merge branch 'async_handlers' of github.com:jerch/xterm.js into async_handlers 2021-03-17 22:08:36 +01:00
Jörg Breitbart a7488456d8 Merge branch 'master' into async_handlers 2021-03-17 22:08:19 +01:00
Martin Sander 5185e5f894 Use linkifier2 to double click select links
Also works with right click select.
Fixes #682.
2021-03-16 16:42:38 +01:00
jerch c19cc9b2d6 Merge branch 'master' into async_handlers 2021-03-15 21:53:59 +01:00
Python-37 7968b7ce7e Fix bug of some IMEs cannot input in terminal
Fix bug of QQ pinyin and Rime IME cannot input characters in terminal.
2021-03-10 17:27:58 +08:00
Jörg Breitbart cdd238d75e do not timeout on faulty async handlers, continue with false to give default handlers chance to run 2021-02-28 13:53:06 +01:00
Jörg Breitbart 48a93c379f log a warning if an async handler takes too long 2021-02-27 22:24:55 +01:00
Jörg Breitbart cc005a274d multiple changes:
- reorder exception throwing to be sync to band position
- document WriteBuffer._innerWrite
- remove any declarations
- remove conditional assigments
- use faster return value branching in all parsers
- remove dead code in benchmark
2021-02-27 20:18:41 +01:00
Jörg Breitbart 26bf1ab840 Merge branch 'master' into async_handlers 2021-02-25 19:07:28 +01:00
Jörg Breitbart b4635eddf3 make reset async aware 2021-02-25 19:04:37 +01:00
Jörg Breitbart 6b57921567 async handler API tests 2021-02-25 17:41:16 +01:00
Jörg Breitbart d500b0f204 inputhandler tests 2021-02-25 17:06:32 +01:00
Jörg Breitbart 9985ae2c38 fix linter warning 2021-02-25 15:44:39 +01:00
Jörg Breitbart bce3269e44 async OSC/DCS registering/dispose tests on parser instance 2021-02-25 15:41:10 +01:00
Jörg Breitbart 26502114d4 minor tweaks, API docs update 2021-02-15 16:24:29 +01:00
Jörg Breitbart 5db479c25d update public API 2021-02-08 03:35:42 +01:00
Jörg Breitbart d0b555687f update parser hooks api and tests 2021-02-08 03:28:49 +01:00
Jörg Breitbart 041e259c11 OSC impl, OscParser tests 2021-02-08 03:03:28 +01:00
Jörg Breitbart d59f4d2f6f async impl for DCS, DcsParser tests 2021-02-08 01:53:59 +01:00
Jörg Breitbart 898a18db21 Merge branch 'master' into async_handlers 2021-02-07 23:14:30 +01:00
Jörg Breitbart 68b7f88523 async parser tests for CSI and ESC 2021-01-24 17:09:16 +01:00
Jörg Breitbart a9eb1de4b4 Merge branch 'master' into async_handlers 2021-01-24 09:11:51 +01:00
Jörg Breitbart 4ba7aef441 Merge branch 'master' into async_handlers 2021-01-23 13:06:50 +01:00
Jörg Breitbart 2f07ed9976 simplify stack save in parser 2021-01-23 13:05:57 +01:00
Jörg Breitbart b07ca8bb24 fix timeout issue on macos 2021-01-22 21:34:12 +01:00
Jörg Breitbart 22d777c9f9 make linter happy 2021-01-22 21:24:54 +01:00
Jörg Breitbart adfeffec76 deprecate writeSync 2021-01-22 21:17:34 +01:00
Jörg Breitbart 4a1bdad759 fix tests for async handlers in inputhandler 2021-01-22 19:48:50 +01:00
Jörg Breitbart 0ad9f70645 Merge branch 'master' into async_handlers 2021-01-22 17:49:31 +01:00
Jörg Breitbart bc1048062d early version of async support for CSI and ESC handlers 2021-01-20 20:05:14 +01:00
Jakob b0605e4cd1 Merge branch 'master' into fix/scroll-events 2021-01-13 11:12:14 +01:00
Jakob Schrettenbrunner 82a9ee62d7 emit onScroll event when user is scrolling 2021-01-12 22:35:47 +00:00
Jakob Schrettenbrunner 8b2b0f6abc add richer ScrollEvent that includes the source 2021-01-12 22:32:20 +00:00
91 changed files with 4086 additions and 1882 deletions
+13 -1
View File
@@ -151,6 +151,10 @@
"warn",
"never"
],
"object-curly-spacing": [
"warn",
"always"
],
"prefer-const": "warn",
"spaced-comment": [
"warn",
@@ -160,5 +164,13 @@
"exceptions": ["-"]
}
]
}
},
"overrides": [
{
"files": ["**/*.test.ts"],
"rules": {
"object-curly-spacing": "off"
}
}
]
}
+68 -49
View File
@@ -1,51 +1,70 @@
{
"version": "2.0.0",
"presentation": {
"echo": false,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": true
},
"tasks": [
{
"type": "npm",
"script": "test",
"group": "test",
"problemMatcher": []
},
{
"type": "npm",
"script": "watch",
"group": "build",
"isBackground": true,
"problemMatcher": [],
"presentation": {
"group": "vscode"
}
},
{
"type": "npm",
"script": "start",
"group": "build",
"isBackground": true,
"problemMatcher": [],
"presentation": {
"group": "vscode"
}
},
{
"label": "Start demo",
"dependsOn": ["npm: watch", "npm: start"],
"group": {
"kind": "build",
"isDefault": true
},
"isBackground": true,
"problemMatcher": [],
"presentation": {
"group": "vscode"
}
}
]
"version": "2.0.0",
"presentation": {
"echo": false,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": true
},
"tasks": [
{
"type": "npm",
"script": "test",
"group": "test",
"problemMatcher": []
},
{
"type": "npm",
"script": "watch",
"group": "build",
"isBackground": true,
"problemMatcher": "$tsc-watch",
"presentation": {
"group": "vscode"
}
},
{
"type": "npm",
"script": "start",
"dependsOn": "npm: watch",
"group": "build",
"isBackground": true,
"problemMatcher": [],
"presentation": {
"group": "vscode"
}
},
{
"label": "Start demo",
"dependsOn": "npm: start",
"group": {
"kind": "build",
"isDefault": true
},
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"fileLocation": [
"relative",
"${workspaceFolder}"
],
"pattern": [
{
"regexp": "^([^\\\\s].*)\\\\((\\\\d+,\\\\d+)\\\\):\\\\s*(.*)$",
"file": 1,
"location": 2,
"message": 3
}
],
"background": {
"beginsPattern": "assets by",
"endsPattern": "webpack \\d+\\.\\d+\\.\\d+ compiled successfully"
}
},
"presentation": {
"group": "vscode"
}
}
]
}
+25 -22
View File
@@ -6,11 +6,11 @@ Xterm.js is a front-end component written in TypeScript that lets applications b
## Features
- **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim` and `tmux`, this includes support for curses-based apps and mouse event support.
- **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim`, and `tmux`, including support for curses-based apps and mouse events.
- **Performant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer.
- **Rich unicode support**: Supports CJK, emojis and IMEs.
- **Rich Unicode support**: Supports CJK, emojis, and IMEs.
- **Self-contained**: Requires zero dependencies to work.
- **Accessible**: Screen reader and minimum contrast ratio support can be turned on
- **Accessible**: Screen reader and minimum contrast ratio support can be turned on.
- **And much more**: Links, theming, addons, well documented API, etc.
## What xterm.js is not
@@ -20,13 +20,13 @@ Xterm.js is a front-end component written in TypeScript that lets applications b
## Getting Started
First you need to install the module, we ship exclusively through [npm](https://www.npmjs.com/) so you need that installed and then add xterm.js as a dependency by running:
First, you need to install the module, we ship exclusively through [npm](https://www.npmjs.com/), so you need that installed and then add xterm.js as a dependency by running:
```
```bash
npm install xterm
```
To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your html page. Then create a `<div id="terminal"></div>` onto which xterm can attach itself. Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your HTML page. Then create a `<div id="terminal"></div>` onto which xterm can attach itself. Finally, instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
```html
<!doctype html>
@@ -58,7 +58,7 @@ import { Terminal } from 'xterm';
⚠️ *This section describes the new addon format introduced in v3.14.0, see [here](https://github.com/xtermjs/xterm.js/blob/3.14.2/README.md#addons) for the instructions on the old format*
Addons are separate modules that extend the `Terminal` by building on the [xterm.js API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts). To use an addon you first need to install it in your project:
Addons are separate modules that extend the `Terminal` by building on the [xterm.js API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts). To use an addon, you first need to install it in your project:
```bash
npm i -S xterm-addon-web-links
@@ -76,7 +76,7 @@ const terminal = new Terminal();
terminal.loadAddon(new WebLinksAddon());
```
The xterm.js team maintains the following addons but they can be built by anyone:
The xterm.js team maintains the following addons, but anyone can build them:
- [`xterm-addon-attach`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-attach): Attaches to a server running a process via a websocket
- [`xterm-addon-fit`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-fit): Fits the terminal to the containing element
@@ -85,23 +85,23 @@ The xterm.js team maintains the following addons but they can be built by anyone
## Browser Support
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Specifically the latest versions of *Chrome*, *Edge*, *Firefox* and *Safari*.
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Specifically the latest versions of *Chrome*, *Edge*, *Firefox*, and *Safari*.
We also partially support *Internet Explorer 11*, meaning xterm.js should work for the most part, but we reserve the right to not provide workarounds specifically for it unless it's absolutely necessary to get the basic input/output flow working.
Xterm.js works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers, these are the versions we strive to keep working.
Xterm.js works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers. These are the versions we strive to keep working.
## API
The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API.
Note that some APIs are marked *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs.
Note that some APIs are marked *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions, so be sure to read release notes if you plan on using experimental APIs.
## Real-world uses
Xterm.js is used in several world-class applications to provide great terminal experiences.
- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js.
- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js.
- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile, and powerful open source code editor that provides an integrated terminal based on xterm.js.
- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js.
- [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies.
- [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE.
@@ -113,10 +113,10 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**Next Tech**](https://next.tech "Next Tech"): Online platform for interactive coding and web development courses. Live container-backed terminal uses xterm.js.
- [**RStudio**](https://www.rstudio.com/products/RStudio "RStudio"): RStudio is an integrated development environment (IDE) for R.
- [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor.
- [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy and run in the cloud.
- [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy, and run in the cloud.
- [**Gravitational Teleport**](https://github.com/gravitational/teleport): Gravitational Teleport is a modern SSH server for remotely accessing clusters of Linux servers via SSH or HTTPS.
- [**Hexlet**](https://en.hexlet.io): Practical programming courses (JavaScript, PHP, Unix, databases, functional programming). A steady path from the first line of code to the first job.
- [**Selenoid UI**](https://github.com/aerokube/selenoid-ui): Simple UI for the scallable golang implementation of Selenium Hub named Selenoid. We use XTerm for streaming logs over websockets from docker containers.
- [**Selenoid UI**](https://github.com/aerokube/selenoid-ui): Simple UI for the scalable golang implementation of Selenium Hub named Selenoid. We use XTerm for streaming logs over websockets from docker containers.
- [**Portainer**](https://portainer.io): Simple management UI for Docker.
- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising xterm.js, SJCL & websockets.
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages.
@@ -167,14 +167,17 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**Laravel Ssh Web Client**](https://github.com/roke22/Laravel-ssh-client): Laravel server inventory with ssh web client to connect at server using xterm.js
- [**Repl.it**](https://repl.it): Collaborative browser based IDE with support for 50+ different languages.
- [**TeleType**](https://github.com/akshaykmr/TeleType): cli tool that allows you to share your terminal online conveniently. Show off mad cli-fu, help a colleague, teach, or troubleshoot.
- [**Intervue**](https://www.intervue.io): Pair programming for interviews. Multiple programming languages supported, with results displayed by xterm.js.
- [**TRASA**](https://trasa.io): Zero trust access to Web, SSH, RDP and Database services.
- [**Intervue**](https://www.intervue.io): Pair programming for interviews. Multiple programming languages are supported, with results displayed by xterm.js.
- [**TRASA**](https://trasa.io): Zero trust access to Web, SSH, RDP, and Database services.
- [**Commas**](https://github.com/CyanSalt/commas): Commas is a hackable terminal and command runner.
- [**Devtron**](https://github.com/devtron-labs/devtron): Software Delivery Workflow For Kubernetes.
- [**NxShell**](https://github.com/nxshell/nxshell): An easy to use new terminal for SSH.
- [**gifcast**](https://dstein64.github.io/gifcast/): Converts an asciinema cast to an animated GIF.
- [**WizardWebssh**](https://gitlab.com/mikeramsey/wizardwebssh): A terminal with Pyqt5 Widget for embedding, which can be used as an ssh client to connect to your ssh servers. It is written in Python, based on tornado, paramiko, and xterm.js.
- [**Wizard Assistant**](https://wizardassistant.com/): Wizard Assistant comes with advanced automation tools, preloaded common and special time-saving commands, and a built-in SSH terminal. Now you can remotely administer, troubleshoot, and analyze any system with ease.
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list. Note: Please add any new contributions to the end of the list only.
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Note: Please add any new contributions to the end of the list only.
## Releases
@@ -184,21 +187,21 @@ All current and past releases are available on this repo's [Releases page](https
### Beta builds
Our CI releases beta builds to npm for every change that goes into master, install the latest beta build with:
Our CI releases beta builds to npm for every change that goes into master. Install the latest beta build with:
```
```bash
npm install -S xterm@beta
```
These should generally be stable but some bugs may slip in, we recommend using the beta build primarily to test out new features and for verifying bug fixes.
These should generally be stable, but some bugs may slip in. We recommend using the beta build primarily to test out new features and to verify bug fixes.
## Contributing
You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and setup xterm.js for development.
You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and set up xterm.js for development.
## License Agreement
If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work.
If you contribute code to this project, you implicitly allow your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work.
Copyright (c) 2017-2019, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)<br>
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)<br>
+3 -1
View File
@@ -41,7 +41,9 @@ export class AttachAddon implements ITerminalAddon {
}
public dispose(): void {
this._disposables.forEach(d => d.dispose());
for (const d of this._disposables) {
d.dispose();
}
}
private _sendData(data: string): void {
@@ -7,7 +7,7 @@ import WebSocket = require('ws');
import { openTerminal, pollFor, getBrowserType } from '../../../out-test/api/TestUtils';
import { Browser, Page } from 'playwright';
const APP = 'http://127.0.0.1:3000/test';
const APP = 'http://127.0.0.1:3001/test';
let browser: Browser;
let page: Page;
@@ -18,7 +18,7 @@ describe('AttachAddon', () => {
before(async function(): Promise<any> {
const browserType = getBrowserType();
browser = await browserType.launch({
headless: process.argv.indexOf('--headless') !== -1
headless: process.argv.includes('--headless')
});
page = await (await browser.newContext()).newPage();
await page.setViewportSize({ width, height });
+4 -4
View File
@@ -7,7 +7,7 @@ import { assert } from 'chai';
import { openTerminal, getBrowserType } from '../../../out-test/api/TestUtils';
import { Browser, Page } from 'playwright';
const APP = 'http://127.0.0.1:3000/test';
const APP = 'http://127.0.0.1:3001/test';
let browser: Browser;
let page: Page;
@@ -18,7 +18,7 @@ describe('FitAddon', () => {
before(async function(): Promise<any> {
const browserType = getBrowserType();
browser = await browserType.launch({
headless: process.argv.indexOf('--headless') !== -1
headless: process.argv.includes('--headless')
});
page = await (await browser.newContext()).newPage();
await page.setViewportSize({ width, height });
@@ -45,7 +45,7 @@ describe('FitAddon', () => {
describe('proposeDimensions', () => {
afterEach(async () => {
return unloadFit();
return await unloadFit();
});
it('default', async function(): Promise<any> {
@@ -84,7 +84,7 @@ describe('FitAddon', () => {
describe('fit', () => {
afterEach(async () => {
return unloadFit();
return await unloadFit();
});
it('default', async function(): Promise<any> {
+1
View File
@@ -51,3 +51,4 @@ This package makes use of the following fonts for testing:
[Fira Code License]: https://github.com/tonsky/FiraCode/blob/master/LICENSE
[Iosevka]: https://github.com/be5invis/Iosevka
[Iosevka License]: https://github.com/be5invis/Iosevka/blob/master/LICENSE.md
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-ligatures",
"version": "0.4.0",
"version": "0.5.0",
"description": "Add support for programming ligatures to xterm.js",
"author": {
"name": "The xterm.js authors",
@@ -32,7 +32,7 @@
"license": "MIT",
"dependencies": {
"font-finder": "^1.1.0",
"font-ligatures": "^1.3.3"
"font-ligatures": "^1.4.0"
},
"devDependencies": {
"@types/sinon": "^5.0.1",
+58 -6
View File
@@ -3,12 +3,19 @@
* @license MIT
*/
import * as fontFinder from 'font-finder';
import * as fontLigatures from 'font-ligatures';
import { FontList } from 'font-finder';
import { Font, loadBuffer, loadFile } from 'font-ligatures';
import parse from './parse';
let fontsPromise: Promise<fontFinder.FontList> | undefined = undefined;
interface IFontMetadata {
family: string;
fullName: string;
postscriptName: string;
blob: () => Promise<Blob>;
}
let fontsPromise: Promise<FontList | Record<string, IFontMetadata[]>> | undefined = undefined;
/**
* Loads the font ligature wrapper for the specified font family if it could be
@@ -16,9 +23,50 @@ let fontsPromise: Promise<fontFinder.FontList> | undefined = undefined;
* @param fontFamily The CSS font family definition to resolve
* @param cacheSize The size of the ligature cache to maintain if the font is resolved
*/
export default async function load(fontFamily: string, cacheSize: number): Promise<fontLigatures.Font | undefined> {
export default async function load(fontFamily: string, cacheSize: number): Promise<Font | undefined> {
if (!fontsPromise) {
fontsPromise = fontFinder.list();
// Web environment that supports font access API
if (typeof navigator !== 'undefined' && 'fonts' in navigator) {
try {
const status = await (navigator as any).permissions.request?.({
name: 'local-fonts'
});
if (status && status.state !== 'granted') {
throw new Error('Permission to access local fonts not granted.');
}
} catch (err) {
// A `TypeError` indicates the 'local-fonts'
// permission is not yet implemented, so
// only `throw` if this is _not_ the problem.
if (err.name !== 'TypeError') {
throw err;
}
}
const fonts: Record<string, IFontMetadata[]> = {};
try {
const fontsIterator: AsyncIterableIterator<IFontMetadata> = (navigator as any).fonts.query();
for await (const metadata of fontsIterator) {
if (!fonts.hasOwnProperty(metadata.family)) {
fonts[metadata.family] = [];
}
fonts[metadata.family].push(metadata);
}
fontsPromise = Promise.resolve(fonts);
} catch (err) {
console.error(err.name, err.message);
}
}
// Node environment or no font access API
else {
try {
fontsPromise = (await import('font-finder')).list();
} catch (err) {
// No-op
}
}
if (!fontsPromise) {
fontsPromise = Promise.resolve({});
}
}
const fonts = await fontsPromise;
@@ -31,7 +79,11 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
}
if (fonts.hasOwnProperty(family) && fonts[family].length > 0) {
return await fontLigatures.loadFile(fonts[family][0].path, { cacheSize });
const font = fonts[family][0];
if ('blob' in font) {
return loadBuffer(await (await font.blob()).arrayBuffer(), { cacheSize });
}
return await loadFile(font.path, { cacheSize });
}
}
+3 -3
View File
@@ -68,7 +68,7 @@ function parseString(context: IParseContext, quoteChar: '\'' | '"'): string {
while (context.offset < context.input.length) {
const char = context.input[context.offset++];
if (escaped) {
if (/[0-9a-fA-F]/.test(char)) {
if (/[\dA-Fa-f]/.test(char)) {
// Unicode escape
context.offset--;
str += parseUnicode(context);
@@ -107,7 +107,7 @@ function parseIdentifier(context: IParseContext): string {
while (context.offset < context.input.length) {
const char = context.input[context.offset++];
if (escaped) {
if (/[0-9a-fA-F]/.test(char)) {
if (/[\dA-Fa-f]/.test(char)) {
// Unicode escape
context.offset--;
str += parseUnicode(context);
@@ -156,7 +156,7 @@ function parseUnicode(context: IParseContext): string {
// of the escape and is swallowed.
return unicodeToString(str);
}
if (str.length >= 6 || !/[0-9a-fA-F]/.test(char)) {
if (str.length >= 6 || !/[\dA-Fa-f]/.test(char)) {
// If the next character is not a valid hex digit or we have reached the
// maximum of 6 digits in the escape, terminate the escape.
context.offset--;
+13 -2
View File
@@ -30,7 +30,18 @@ module.exports = {
},
mode: 'production',
externals: {
'font-finder':'font-finder',
'font-ligatures':'font-ligatures'
'font-finder': 'font-finder',
'stream': 'stream',
'os': 'os',
'util': 'util'
},
resolve: {
// The ligature modules contains fallbacks for node environments, we never want to browserify them
fallback: {
stream: false,
util: false,
os: false,
path: false
}
}
};
+18 -21
View File
@@ -87,19 +87,19 @@ font-finder@^1.0.3:
font-finder@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/font-finder/-/font-finder-1.1.0.tgz#2bff2b2762acba720239c8bec898a96daae90858"
resolved "https://registry.npmjs.org/font-finder/-/font-finder-1.1.0.tgz#2bff2b2762acba720239c8bec898a96daae90858"
integrity sha512-wpCL2uIbi6GurJbU7ZlQ3nGd61Ho+dSU6U83/xJT5UPFfN35EeCW/rOtS+5k+IuEZu2SYmHzDIPL9eA5tSYRAw==
dependencies:
get-system-fonts "^2.0.0"
promise-stream-reader "^1.0.1"
font-ligatures@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/font-ligatures/-/font-ligatures-1.3.3.tgz#63fff18dc8adb3a11fe5eec1f4e8d7edfa8075b9"
integrity sha512-NSGpHgVNX81M7AWS1XylK1UZbN3QllfUIDAAuPv6TUcl5O2b781JcKS5L2RopAU0AqlTyX3ZuX/04eaMpbVrHA==
font-ligatures@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/font-ligatures/-/font-ligatures-1.4.0.tgz#6a7b370d96be1358dddfad67830e82fbfd59e6dc"
integrity sha512-n7DFnnEpJ0NrVoLqZIL4tMGVs+CnFwQc92m80LWyrbgAFO4x234+t2/H9o4eOYA1eh6ta9dZAEEsJAwsBdNezA==
dependencies:
font-finder "^1.0.3"
lru-cache "^4.1.3"
lru-cache "^6.0.0"
opentype.js "^0.8.0"
get-system-fonts@^2.0.0:
@@ -128,9 +128,9 @@ lodash.get@^4.4.2:
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
lodash@^4.17.15:
version "4.17.19"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
lolex@^2.7.5:
version "2.7.5"
@@ -144,12 +144,12 @@ lolex@^5.0.1:
dependencies:
"@sinonjs/commons" "^1.7.0"
lru-cache@^4.1.3:
version "4.1.3"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c"
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
dependencies:
pseudomap "^1.0.2"
yallist "^2.1.2"
yallist "^4.0.0"
minimist@^1.2.5:
version "1.2.5"
@@ -198,10 +198,6 @@ promise-stream-reader@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/promise-stream-reader/-/promise-stream-reader-1.0.1.tgz#4e793a79c9d49a73ccd947c6da9c127f12923649"
pseudomap@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
sinon@6.3.5:
version "6.3.5"
resolved "https://registry.yarnpkg.com/sinon/-/sinon-6.3.5.tgz#0f6d6a5b4ebaad1f6e8e019395542d1d02c144a0"
@@ -232,9 +228,10 @@ type-detect@4.0.8, type-detect@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
yallist@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yauzl@^2.10.0:
version "2.10.0"
+3 -3
View File
@@ -240,8 +240,8 @@ export class SearchAddon implements ITerminalAddon {
* @param term the substring that starts at searchIndex
*/
private _isWholeWord(searchIndex: number, line: string, term: string): boolean {
return (((searchIndex === 0) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex - 1]) !== -1)) &&
(((searchIndex + term.length) === line.length) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex + term.length]) !== -1)));
return ((searchIndex === 0) || (NON_WORD_CHARACTERS.includes(line[searchIndex - 1]))) &&
(((searchIndex + term.length) === line.length) || (NON_WORD_CHARACTERS.includes(line[searchIndex + term.length])));
}
/**
@@ -262,7 +262,7 @@ export class SearchAddon implements ITerminalAddon {
// Ignore wrapped lines, only consider on unwrapped line (first row of command string).
const firstLine = terminal.buffer.active.getLine(row);
if (firstLine && firstLine.isWrapped) {
if (firstLine?.isWrapped) {
if (isReverseSearch) {
searchPosition.startCol += terminal.cols;
return;
@@ -9,7 +9,7 @@ import { resolve } from 'path';
import { openTerminal, writeSync, getBrowserType } from '../../../out-test/api/TestUtils';
import { Browser, Page } from 'playwright';
const APP = 'http://127.0.0.1:3000/test';
const APP = 'http://127.0.0.1:3001/test';
let browser: Browser;
let page: Page;
@@ -20,7 +20,7 @@ describe('Search Tests', function(): void {
before(async function(): Promise<any> {
const browserType = getBrowserType();
browser = await browserType.launch({
headless: process.argv.indexOf('--headless') !== -1
headless: process.argv.includes('--headless')
});
page = await (await browser.newContext()).newPage();
await page.setViewportSize({ width, height });
@@ -7,7 +7,7 @@ import { assert } from 'chai';
import { openTerminal, writeSync, getBrowserType } from '../../../out-test/api/TestUtils';
import { Browser, Page } from 'playwright';
const APP = 'http://127.0.0.1:3000/test';
const APP = 'http://127.0.0.1:3001/test';
let browser: Browser;
let page: Page;
@@ -34,7 +34,7 @@ describe('SerializeAddon', () => {
before(async function(): Promise<any> {
const browserType = getBrowserType();
browser = await browserType.launch({
headless: process.argv.indexOf('--headless') !== -1
headless: process.argv.includes('--headless')
});
page = await (await browser.newContext()).newPage();
await page.setViewportSize({ width, height });
@@ -7,7 +7,7 @@ import { assert } from 'chai';
import { openTerminal, getBrowserType } from '../../../out-test/api/TestUtils';
import { Browser, Page } from 'playwright';
const APP = 'http://127.0.0.1:3000/test';
const APP = 'http://127.0.0.1:3001/test';
let browser: Browser;
let page: Page;
@@ -18,7 +18,7 @@ describe('Unicode11Addon', () => {
before(async function(): Promise<any> {
const browserType = getBrowserType();
browser = await browserType.launch({
headless: process.argv.indexOf('--headless') !== -1
headless: process.argv.includes('--headless')
});
page = await (await browser.newContext()).newPage();
await page.setViewportSize({ width, height });
@@ -41,7 +41,7 @@ export class WebLinkProvider implements ILinkProvider {
}
export class LinkComputer {
public static computeLink(y: number, regex: RegExp, terminal: Terminal, handler: (event: MouseEvent, uri: string) => void): ILink[] {
public static computeLink(y: number, regex: RegExp, terminal: Terminal, activate: (event: MouseEvent, uri: string) => void): ILink[] {
const rex = new RegExp(regex.source, (regex.flags || '') + 'g');
const [line, startLineIndex] = LinkComputer._translateBufferLineToStringWithWrap(y - 1, false, terminal);
@@ -89,7 +89,7 @@ export class LinkComputer {
}
};
result.push({ range, text, activate: handler });
result.push({ range, text, activate });
}
return result;
@@ -7,7 +7,7 @@ import { assert } from 'chai';
import { openTerminal, pollFor, writeSync, getBrowserType } from '../../../out-test/api/TestUtils';
import { Browser, Page } from 'playwright';
const APP = 'http://127.0.0.1:3000/test';
const APP = 'http://127.0.0.1:3001/test';
let browser: Browser;
let page: Page;
@@ -18,7 +18,7 @@ describe('WebLinksAddon', () => {
before(async function(): Promise<any> {
const browserType = getBrowserType();
browser = await browserType.launch({
headless: process.argv.indexOf('--headless') !== -1
headless: process.argv.includes('--headless')
});
page = await (await browser.newContext()).newPage();
await page.setViewportSize({ width, height });
+16 -3
View File
@@ -1,8 +1,6 @@
## xterm-addon-webgl
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables a WebGL-based renderer. This addon requires xterm.js v4+.
⚠️ This is an experimental addon that is [missing some features and may be unstable](https://github.com/xtermjs/xterm.js/issues?q=is%3Aopen+is%3Aissue+label%3Aarea%2Faddon%2Fwebgl) ⚠️
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables a WebGL2-based renderer. This addon requires xterm.js v4+.
### Install
@@ -21,3 +19,18 @@ terminal.loadAddon(new WebglAddon());
```
See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts) for more advanced usage.
### Handling Context Loss
The browser may drop WebGL contexts for various reasons like OOM or after the system has been suspended. There is an API exposed that fires the `webglcontextlost` event fired on the canvas so embedders can handle it however they wish. An easy, but suboptimal way, to handle this is by disposing of WebglAddon when the event fires:
```ts
const terminal = new Terminal();
const addon = new WebglAddon();
addon.onContextLoss(e => {
addon.dispose();
});
terminal.loadAddon(addon);
```
Read more about handling WebGL context losses on the [Khronos wiki](https://www.khronos.org/webgl/wiki/HandlingContextLost).
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-webgl",
"version": "0.10.0",
"version": "0.11.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
+8 -3
View File
@@ -3,14 +3,17 @@
* @license MIT
*/
import { Terminal, ITerminalAddon } from 'xterm';
import { Terminal, ITerminalAddon, IEvent } from 'xterm';
import { WebglRenderer } from './WebglRenderer';
import { IRenderService } from 'browser/services/Services';
import { ICharacterJoinerService, IRenderService } from 'browser/services/Services';
import { IColorSet } from 'browser/Types';
import { EventEmitter } from 'common/EventEmitter';
export class WebglAddon implements ITerminalAddon {
private _terminal?: Terminal;
private _renderer?: WebglRenderer;
private _onContextLoss = new EventEmitter<void>();
public get onContextLoss(): IEvent<void> { return this._onContextLoss.event; }
constructor(
private _preserveDrawingBuffer?: boolean
@@ -22,8 +25,10 @@ export class WebglAddon implements ITerminalAddon {
}
this._terminal = terminal;
const renderService: IRenderService = (<any>terminal)._core._renderService;
const characterJoinerService: ICharacterJoinerService = (<any>terminal)._core._characterJoinerService;
const colors: IColorSet = (<any>terminal)._core._colorManager.colors;
this._renderer = new WebglRenderer(terminal, colors, this._preserveDrawingBuffer);
this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, this._preserveDrawingBuffer);
this._renderer.onContextLoss(() => this._onContextLoss.fire());
renderService.setRenderer(this._renderer);
}
+134 -20
View File
@@ -12,13 +12,17 @@ import { RectangleRenderer } from './RectangleRenderer';
import { IWebGL2RenderingContext } from './Types';
import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
import { Disposable } from 'common/Lifecycle';
import { NULL_CELL_CODE } from 'common/buffer/Constants';
import { Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
import { Terminal, IEvent } from 'xterm';
import { IRenderLayer } from './renderLayer/Types';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
import { ITerminal, IColorSet } from 'browser/Types';
import { EventEmitter } from 'common/EventEmitter';
import { CellData } from 'common/buffer/CellData';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { ICharacterJoinerService } from 'browser/services/Services';
import { CharData, ICellData } from 'common/Types';
import { AttributeData } from 'common/buffer/AttributeData';
export class WebglRenderer extends Disposable implements IRenderer {
private _renderLayers: IRenderLayer[];
@@ -41,9 +45,13 @@ export class WebglRenderer extends Disposable implements IRenderer {
private _onRequestRedraw = new EventEmitter<IRequestRedrawEvent>();
public get onRequestRedraw(): IEvent<IRequestRedrawEvent> { return this._onRequestRedraw.event; }
private _onContextLoss = new EventEmitter<void>();
public get onContextLoss(): IEvent<void> { return this._onContextLoss.event; }
constructor(
private _terminal: Terminal,
private _colors: IColorSet,
private readonly _characterJoinerService: ICharacterJoinerService,
preserveDrawingBuffer?: boolean
) {
super();
@@ -82,6 +90,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
if (!this._gl) {
throw new Error('WebGL2 not supported ' + this._gl);
}
this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => { this._onContextLoss.fire(e); }));
this._core.screenElement!.appendChild(this._canvas);
this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions);
@@ -94,7 +105,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
public dispose(): void {
this._renderLayers.forEach(l => l.dispose());
for (const l of this._renderLayers) {
l.dispose();
}
this._core.screenElement!.removeChild(this._canvas);
super.dispose();
}
@@ -106,10 +119,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
public setColors(colors: IColorSet): void {
this._colors = colors;
// Clear layers and force a full render
this._renderLayers.forEach(l => {
for (const l of this._renderLayers) {
l.setColors(this._terminal, this._colors);
l.reset(this._terminal);
});
}
this._rectangleRenderer.setColors();
this._glyphRenderer.setColors();
@@ -136,7 +149,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._model.resize(this._terminal.cols, this._terminal.rows);
// Resize all render layers
this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions));
for (const l of this._renderLayers) {
l.resize(this._terminal, this.dimensions);
}
// Resize the canvas
this._canvas.width = this.dimensions.scaledCanvasWidth;
@@ -168,15 +183,21 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
public onBlur(): void {
this._renderLayers.forEach(l => l.onBlur(this._terminal));
for (const l of this._renderLayers) {
l.onBlur(this._terminal);
}
}
public onFocus(): void {
this._renderLayers.forEach(l => l.onFocus(this._terminal));
for (const l of this._renderLayers) {
l.onFocus(this._terminal);
}
}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
this._renderLayers.forEach(l => l.onSelectionChanged(this._terminal, start, end, columnSelectMode));
for (const l of this._renderLayers) {
l.onSelectionChanged(this._terminal, start, end, columnSelectMode);
}
this._updateSelectionModel(start, end, columnSelectMode);
@@ -184,11 +205,15 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
public onCursorMove(): void {
this._renderLayers.forEach(l => l.onCursorMove(this._terminal));
for (const l of this._renderLayers) {
l.onCursorMove(this._terminal);
}
}
public onOptionsChanged(): void {
this._renderLayers.forEach(l => l.onOptionsChanged(this._terminal));
for (const l of this._renderLayers) {
l.onOptionsChanged(this._terminal);
}
this._updateDimensions();
this._refreshCharAtlas();
}
@@ -222,7 +247,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
public clear(): void {
this._renderLayers.forEach(l => l.reset(this._terminal));
for (const l of this._renderLayers) {
l.reset(this._terminal);
}
}
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
@@ -245,7 +272,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
// Update render layers
this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end));
for (const l of this._renderLayers) {
l.onGridChanged(this._terminal, start, end);
}
// Tell renderer the frame is beginning
if (this._glyphRenderer.beginFrame()) {
@@ -263,16 +292,41 @@ export class WebglRenderer extends Disposable implements IRenderer {
private _updateModel(start: number, end: number): void {
const terminal = this._core;
let cell: ICellData = this._workCell;
for (let y = start; y <= end; y++) {
const row = y + terminal.buffer.ydisp;
const line = terminal.buffer.lines.get(row)!;
this._model.lineLengths[y] = 0;
const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
for (let x = 0; x < terminal.cols; x++) {
line.loadCell(x, this._workCell);
line.loadCell(x, cell);
const chars = this._workCell.getChars();
let code = this._workCell.getCode();
// If true, indicates that the current character(s) to draw were joined.
let isJoined = false;
let lastCharX = x;
// Process any joined character ranges as needed. Because of how the
// ranges are produced, we know that they are valid for the characters
// and attributes of our input.
if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
isJoined = true;
const range = joinedRanges.shift()!;
// We already know the exact start and end column of the joined range,
// so we get the string and width representing it directly
cell = new JoinedCellData(
cell,
line!.translateToString(true, range[0], range[1]),
range[1] - range[0]
);
// Skip over the cells occupied by this range in the loop
lastCharX = range[1] - 1;
}
const chars = cell.getChars();
let code = cell.getCode();
const i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
if (code !== NULL_CELL_CODE) {
@@ -281,8 +335,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Nothing has changed, no updates needed
if (this._model.cells[i] === code &&
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workCell.bg &&
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workCell.fg) {
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === cell.bg &&
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === cell.fg) {
continue;
}
@@ -293,10 +347,24 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Cache the results in the model
this._model.cells[i] = code;
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workCell.bg;
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workCell.fg;
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = cell.bg;
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = cell.fg;
this._glyphRenderer.updateCell(x, y, code, this._workCell.bg, this._workCell.fg, chars);
this._glyphRenderer.updateCell(x, y, code, cell.bg, cell.fg, chars);
if (isJoined) {
// Restore work cell
cell = this._workCell;
// Null out non-first cells
for (x++; x < lastCharX; x++) {
const j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
this._glyphRenderer.updateCell(x, y, NULL_CELL_CODE, 0, 0, NULL_CELL_CHAR);
this._model.cells[j] = NULL_CELL_CODE;
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workCell.bg;
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workCell.fg;
}
}
}
}
this._rectangleRenderer.updateBackgrounds(this._model);
@@ -413,3 +481,49 @@ export class WebglRenderer extends Disposable implements IRenderer {
this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio;
}
}
// TODO: Share impl with core
export class JoinedCellData extends AttributeData implements ICellData {
private _width: number;
// .content carries no meaning for joined CellData, simply nullify it
// thus we have to overload all other .content accessors
public content: number = 0;
public fg: number;
public bg: number;
public combinedData: string = '';
constructor(firstCell: ICellData, chars: string, width: number) {
super();
this.fg = firstCell.fg;
this.bg = firstCell.bg;
this.combinedData = chars;
this._width = width;
}
public isCombined(): number {
// always mark joined cell data as combined
return Content.IS_COMBINED_MASK;
}
public getWidth(): number {
return this._width;
}
public getChars(): string {
return this.combinedData;
}
public getCode(): number {
// code always gets the highest possible fake codepoint (read as -1)
// this is needed as code is used by caches as identifier
return 0x1FFFFF;
}
public setFromCharData(value: CharData): void {
throw new Error('not implemented');
}
public getAsCharData(): CharData {
return [this.fg, this.getChars(), this.getWidth(), this.getCode()];
}
}
@@ -80,12 +80,12 @@ export class WebglCharAtlas implements IDisposable {
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
// It might also contain some characters with transparent backgrounds if allowTransparency is
// set.
this._cacheCtx = throwIfFalsy(this.cacheCanvas.getContext('2d', {alpha: true}));
this._cacheCtx = throwIfFalsy(this.cacheCanvas.getContext('2d', { alpha: true }));
this._tmpCanvas = document.createElement('canvas');
this._tmpCanvas.width = this._config.scaledCharWidth * 2 + TMP_CANVAS_GLYPH_PADDING * 2;
this._tmpCanvas.width = this._config.scaledCharWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2;
this._tmpCanvas.height = this._config.scaledCharHeight + TMP_CANVAS_GLYPH_PADDING * 2;
this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', {alpha: this._config.allowTransparency}));
this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency }));
}
public dispose(): void {
@@ -317,6 +317,13 @@ export class WebglCharAtlas implements IDisposable {
this.hasCanvasChanged = true;
// Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used
// to draw the glyph to the canvas as well as to restrict the bounding box search to ensure
// giant ligatures (eg. =====>) don't impact overall performance.
const allowedWidth = this._config.scaledCharWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2;
if (this._tmpCanvas.width < allowedWidth) {
this._tmpCanvas.width = allowedWidth;
}
this._tmpCtx.save();
this._workAttributeData.fg = fg;
@@ -358,7 +365,7 @@ export class WebglCharAtlas implements IDisposable {
const fontStyle = italic ? 'italic' : '';
this._tmpCtx.font =
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
this._tmpCtx.textBaseline = 'middle';
this._tmpCtx.textBaseline = 'ideographic';
this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold);
@@ -367,8 +374,22 @@ export class WebglCharAtlas implements IDisposable {
this._tmpCtx.globalAlpha = DIM_OPACITY;
}
// Check if the char is a powerline glyph, these will be restricted to a single cell glyph, no
// padding on either side that are allowed for other glyphs since they are designed to be pixel
// perfect but may render with "bad" anti-aliasing
let isPowerlineGlyph = false;
if (chars.length === 1) {
const code = chars.charCodeAt(0);
if (code >= 0xE0A0 && code <= 0xE0D6) {
isPowerlineGlyph = true;
}
}
// For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129)
const padding = isPowerlineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING;
// Draw the character
this._tmpCtx.fillText(chars, TMP_CANVAS_GLYPH_PADDING, TMP_CANVAS_GLYPH_PADDING + this._config.scaledCharHeight / 2);
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight);
this._tmpCtx.restore();
// clear the background from the character to avoid issues with drawing over the previous
@@ -391,7 +412,7 @@ export class WebglCharAtlas implements IDisposable {
return NULL_RASTERIZED_GLYPH;
}
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox);
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, isPowerlineGlyph);
const clippedImageData = this._clipImageData(imageData, this._workBoundingBox);
// Check if there is enough room in the current row and go to next if needed
@@ -424,11 +445,13 @@ export class WebglCharAtlas implements IDisposable {
* @param imageData The image data to read.
* @param boundingBox An IBoundingBox to put the clipped bounding box values.
*/
private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox): IRasterizedGlyph {
private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, allowedWidth: number, restrictedGlyph: boolean): IRasterizedGlyph {
boundingBox.top = 0;
const height = restrictedGlyph ? this._config.scaledCharHeight : this._tmpCanvas.height;
const width = restrictedGlyph ? this._config.scaledCharWidth : allowedWidth;
let found = false;
for (let y = 0; y < this._tmpCanvas.height; y++) {
for (let x = 0; x < this._tmpCanvas.width; x++) {
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
if (imageData.data[alphaOffset] !== 0) {
boundingBox.top = y;
@@ -442,8 +465,8 @@ export class WebglCharAtlas implements IDisposable {
}
boundingBox.left = 0;
found = false;
for (let x = 0; x < this._tmpCanvas.width; x++) {
for (let y = 0; y < this._tmpCanvas.height; y++) {
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
if (imageData.data[alphaOffset] !== 0) {
boundingBox.left = x;
@@ -455,10 +478,10 @@ export class WebglCharAtlas implements IDisposable {
break;
}
}
boundingBox.right = this._tmpCanvas.width;
boundingBox.right = width;
found = false;
for (let x = this._tmpCanvas.width - 1; x >= 0; x--) {
for (let y = 0; y < this._tmpCanvas.height; y++) {
for (let x = width - 1; x >= 0; x--) {
for (let y = 0; y < height; y++) {
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
if (imageData.data[alphaOffset] !== 0) {
boundingBox.right = x;
@@ -470,10 +493,10 @@ export class WebglCharAtlas implements IDisposable {
break;
}
}
boundingBox.bottom = this._tmpCanvas.height;
boundingBox.bottom = height;
found = false;
for (let y = this._tmpCanvas.height - 1; y >= 0; y--) {
for (let x = 0; x < this._tmpCanvas.width; x++) {
for (let y = height - 1; y >= 0; y--) {
for (let x = 0; x < width; x++) {
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
if (imageData.data[alphaOffset] !== 0) {
boundingBox.bottom = y;
@@ -497,8 +520,8 @@ export class WebglCharAtlas implements IDisposable {
y: (boundingBox.bottom - boundingBox.top + 1) / TEXTURE_HEIGHT
},
offset: {
x: -boundingBox.left + TMP_CANVAS_GLYPH_PADDING,
y: -boundingBox.top + TMP_CANVAS_GLYPH_PADDING
x: -boundingBox.left + (restrictedGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING),
y: -boundingBox.top + (restrictedGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING)
}
};
}
@@ -46,7 +46,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
}
private _initCanvas(): void {
this._ctx = throwIfFalsy(this._canvas.getContext('2d', {alpha: this._alpha}));
this._ctx = throwIfFalsy(this._canvas.getContext('2d', { alpha: this._alpha }));
// Draw the background if this is an opaque layer
if (!this._alpha) {
this._clearAll();
@@ -224,12 +224,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
*/
protected _fillCharTrueColor(terminal: Terminal, cell: CellData, x: number, y: number): void {
this._ctx.font = this._getFont(terminal, false, false);
this._ctx.textBaseline = 'middle';
this._ctx.textBaseline = 'ideographic';
this._clipRow(terminal, y);
this._ctx.fillText(
cell.getChars(),
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
}
/**
@@ -9,7 +9,7 @@ import { assert } from 'chai';
import { openTerminal, pollFor, writeSync, getBrowserType } from '../../../out-test/api/TestUtils';
import { Browser, Page } from 'playwright';
const APP = 'http://127.0.0.1:3000/test';
const APP = 'http://127.0.0.1:3001/test';
let browser: Browser;
let page: Page;
@@ -18,7 +18,7 @@ const height = 600;
describe('WebGL Renderer Integration Tests', async () => {
const browserType = getBrowserType();
const isHeadless = process.argv.indexOf('--headless') !== -1;
const isHeadless = process.argv.includes('--headless');
// Firefox works only in non-headless mode https://github.com/microsoft/playwright/issues/1032
const areTestsEnabled = browserType.name() === 'chromium' || (browserType.name() === 'firefox' && !isHeadless);
const itWebgl = areTestsEnabled ? it : it.skip;
@@ -893,7 +893,7 @@ async function getCellColor(col: number, row: number): Promise<number[]> {
async function setupBrowser(options: ITerminalOptions = { rendererType: 'dom' }): Promise<void> {
const browserType = getBrowserType();
browser = await browserType.launch({
headless: process.argv.indexOf('--headless') !== -1
headless: process.argv.includes('--headless')
});
page = await (await browser.newContext()).newPage();
await page.setViewportSize({ width, height });
@@ -3,6 +3,7 @@
* @license MIT
*/
import { IEvent } from 'node-pty';
import { Terminal, ITerminalAddon } from 'xterm';
declare module 'xterm-addon-webgl' {
@@ -29,5 +30,10 @@ declare module 'xterm-addon-webgl' {
* Clears the terminal's texture atlas and triggers a redraw.
*/
public clearTextureAtlas(): void;
/**
* Fired when the WebglRenderer loses context
*/
public get onContextLoss(): IEvent<void>;
}
}
+16 -8
View File
@@ -100,10 +100,6 @@ jobs:
displayName: 'Install Yarn'
- script: yarn --frozen-lockfile
displayName: 'Install dependencies and build'
- script: |
yarn start &
sleep 10
displayName: 'Start test server'
- script: yarn test-api-chromium --headless --forbid-only
displayName: 'Integration tests (Chromium)'
- script: xvfb-run --auto-servernum -- bash -c "yarn test-api-firefox --headless --forbid-only"
@@ -119,10 +115,6 @@ jobs:
displayName: 'Install Node.js'
- script: yarn --frozen-lockfile
displayName: 'Install dependencies and build'
- script: |
yarn start &
sleep 10
displayName: 'Start test server'
- script: yarn test-api-chromium --headless --forbid-only
displayName: 'Integration tests (Chromium)'
- script: yarn test-api-firefox --headless --forbid-only
@@ -130,6 +122,21 @@ jobs:
- script: yarn test-api-webkit --headless --forbid-only
displayName: 'Integration tests (Webkit)'
- job: Windows_IntegrationTests
pool:
vmImage: 'vs2017-win2016'
steps:
- task: NodeTool@0
inputs:
versionSpec: '10.x'
displayName: 'Install Node.js'
- script: yarn --frozen-lockfile
displayName: 'Install dependencies and build'
- script: yarn test-api-chromium --headless --forbid-only
displayName: 'Integration tests (Chromium)'
- script: yarn test-api-firefox --headless --forbid-only
displayName: 'Integration tests (Firefox)'
- job: Release
dependsOn:
- Linux
@@ -137,6 +144,7 @@ jobs:
- Windows
- Linux_IntegrationTests
- macOS_IntegrationTests
- Windows_IntegrationTests
condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['FORCE_RELEASE'], 'true')))
pool:
vmImage: 'ubuntu-16.04'
+26 -14
View File
@@ -34,19 +34,31 @@ if (process.argv.length > 2) {
env.DEBUG = flagArgs.indexOf('--debug') >= 0 ? 'debug' : '';
env.PORT = 3001;
const run = cp.spawnSync(
npmBinScript('mocha'),
[...testFiles, ...flagArgs],
{
cwd: path.resolve(__dirname, '..'),
env,
stdio: 'inherit'
const server = cp.spawn('node', ['demo/start'], {
cwd: path.resolve(__dirname, '..'),
env,
stdio: 'pipe'
})
server.stdout.on('data', (data) => {
// await for the server to fully start
if (data.indexOf("successfully") !== -1) {
const run = cp.spawnSync(
npmBinScript('mocha'),
[...testFiles, ...flagArgs], {
cwd: path.resolve(__dirname, '..'),
env,
stdio: 'inherit'
}
);
function npmBinScript(script) {
return path.resolve(__dirname, `../node_modules/.bin/` + (process.platform === 'win32' ?
`${script}.cmd` : script));
}
process.exit(run.status);
}
);
function npmBinScript(script) {
return path.resolve(__dirname, `../node_modules/.bin/` + (process.platform === 'win32' ? `${script}.cmd` : script));
}
process.exit(run.status);
});
-1
View File
@@ -36,7 +36,6 @@
*/
.xterm {
font-feature-settings: "liga" 0;
position: relative;
user-select: none;
-ms-user-select: none;
+13 -4
View File
@@ -16,6 +16,7 @@ import { SerializeAddon } from '../addons/xterm-addon-serialize/out/SerializeAdd
import { WebLinksAddon } from '../addons/xterm-addon-web-links/out/WebLinksAddon';
import { WebglAddon } from '../addons/xterm-addon-webgl/out/WebglAddon';
import { Unicode11Addon } from '../addons/xterm-addon-unicode11/out/Unicode11Addon';
import { LigaturesAddon } from '../addons/xterm-addon-ligatures/out/LigaturesAddon';
// Use webpacked version (yarn package)
// import { Terminal } from '../lib/xterm';
@@ -26,6 +27,7 @@ import { Unicode11Addon } from '../addons/xterm-addon-unicode11/out/Unicode11Add
// import { WebLinksAddon } from 'xterm-addon-web-links';
// import { WebglAddon } from 'xterm-addon-webgl';
// import { Unicode11Addon } from 'xterm-addon-unicode11';
// import { LigaturesAddon } from 'xterm-addon-ligatures';
// Pulling in the module's types relies on the <reference> above, it's looks a
// little weird here as we're importing "this" module
@@ -41,6 +43,7 @@ export interface IWindowWithTerminal extends Window {
WebLinksAddon?: typeof WebLinksAddon;
WebglAddon?: typeof WebglAddon;
Unicode11Addon?: typeof Unicode11Addon;
LigaturesAddon?: typeof LigaturesAddon;
}
declare let window: IWindowWithTerminal;
@@ -50,7 +53,7 @@ let socketURL;
let socket;
let pid;
type AddonType = 'attach' | 'fit' | 'search' | 'serialize' | 'unicode11' | 'web-links' | 'webgl';
type AddonType = 'attach' | 'fit' | 'search' | 'serialize' | 'unicode11' | 'web-links' | 'webgl' | 'ligatures';
interface IDemoAddon<T extends AddonType> {
name: T;
@@ -62,8 +65,9 @@ interface IDemoAddon<T extends AddonType> {
T extends 'serialize' ? typeof SerializeAddon :
T extends 'web-links' ? typeof WebLinksAddon :
T extends 'unicode11' ? typeof Unicode11Addon :
T extends 'ligatures' ? typeof LigaturesAddon :
typeof WebglAddon;
instance?:
instance?:
T extends 'attach' ? AttachAddon :
T extends 'fit' ? FitAddon :
T extends 'search' ? SearchAddon :
@@ -71,6 +75,7 @@ interface IDemoAddon<T extends AddonType> {
T extends 'web-links' ? WebLinksAddon :
T extends 'webgl' ? WebglAddon :
T extends 'unicode11' ? typeof Unicode11Addon :
T extends 'ligatures' ? typeof LigaturesAddon :
never;
}
@@ -81,7 +86,8 @@ const addons: { [T in AddonType]: IDemoAddon<T>} = {
serialize: { name: 'serialize', ctor: SerializeAddon, canChange: true },
'web-links': { name: 'web-links', ctor: WebLinksAddon, canChange: true },
webgl: { name: 'webgl', ctor: WebglAddon, canChange: true },
unicode11: { name: 'unicode11', ctor: Unicode11Addon, canChange: true }
unicode11: { name: 'unicode11', ctor: Unicode11Addon, canChange: true },
ligatures: { name: 'ligatures', ctor: LigaturesAddon, canChange: true }
};
const terminalContainer = document.getElementById('terminal-container');
@@ -117,6 +123,7 @@ const disposeRecreateButtonHandler = () => {
addons.search.instance = undefined;
addons.serialize.instance = undefined;
addons.unicode11.instance = undefined;
addons.ligatures.instance = undefined;
addons['web-links'].instance = undefined;
addons.webgl.instance = undefined;
document.getElementById('dispose').innerHTML = 'Recreate Terminal';
@@ -133,6 +140,7 @@ if (document.location.pathname === '/test') {
window.SearchAddon = SearchAddon;
window.SerializeAddon = SerializeAddon;
window.Unicode11Addon = Unicode11Addon;
window.LigaturesAddon = LigaturesAddon;
window.WebLinksAddon = WebLinksAddon;
window.WebglAddon = WebglAddon;
} else {
@@ -149,7 +157,8 @@ function createTerminal(): void {
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
term = new Terminal({
windowsMode: isWindows
windowsMode: isWindows,
fontFamily: 'Fira Code, courier-new, courier, monospace'
} as ITerminalOptions);
// Load addons
+8
View File
@@ -49,6 +49,14 @@ const clientConfig = {
alias: {
common: path.resolve('./out/common'),
browser: path.resolve('./out/browser')
},
fallback: {
// The ligature modules contains fallbacks for node environments, we never want to browserify them
stream: false,
util: false,
os: false,
path: false,
fs: false
}
},
output: {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
"version": "4.11.0",
"version": "4.12.0",
"main": "lib/xterm.js",
"style": "css/xterm.css",
"types": "typings/xterm.d.ts",
+2 -11
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent, ILinkDecorations } from 'browser/Types';
import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent, ILinkDecorations, ILinkWithState } from 'browser/Types';
import { IDisposable } from 'common/Types';
import { IMouseService, IRenderService } from './services/Services';
import { IBufferService } from 'common/services/Services';
@@ -11,21 +11,12 @@ import { EventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable, getDisposeArrayDisposable, disposeArray } from 'common/Lifecycle';
import { addDisposableDomListener } from 'browser/Lifecycle';
interface ILinkState {
decorations: ILinkDecorations;
isHovered: boolean;
}
interface ILinkWithState {
link: ILink;
state?: ILinkState;
}
export class Linkifier2 extends Disposable implements ILinkifier2 {
private _element: HTMLElement | undefined;
private _mouseService: IMouseService | undefined;
private _renderService: IRenderService | undefined;
private _linkProviders: ILinkProvider[] = [];
public get currentLink(): ILinkWithState | undefined { return this._currentLink; }
protected _currentLink: ILinkWithState | undefined;
private _lastMouseEvent: MouseEvent | undefined;
private _linkCacheDisposables: IDisposable[] = [];
+1 -3
View File
@@ -156,9 +156,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
private _onTooltip(e: MouseEvent): void {
this._tooltipTimeout = undefined;
const zone = this._findZoneEventAt(e);
if (zone && zone.tooltipCallback) {
zone.tooltipCallback(e);
}
zone?.tooltipCallback(e);
}
private _onMouseDown(e: MouseEvent): void {
+219 -314
View File
@@ -55,22 +55,30 @@ describe('Terminal', () => {
// term.onData(() => done());
// term.handler('fake');
// });
it('should fire the onCursorMove event', (done) => {
term.onCursorMove(() => done());
term.writeSync('foo');
it('should fire the onCursorMove event', () => {
return new Promise<void>(async r => {
term.onCursorMove(() => r());
await term.writeP('foo');
});
});
it('should fire the onLineFeed event', (done) => {
term.onLineFeed(() => done());
term.writeSync('\n');
it('should fire the onLineFeed event', () => {
return new Promise<void>(async r => {
term.onLineFeed(() => r());
await term.writeP('\n');
});
});
it('should fire a scroll event when scrollback is created', (done) => {
term.onScroll(() => done());
term.writeSync('\n'.repeat(INIT_ROWS));
it('should fire a scroll event when scrollback is created', () => {
return new Promise<void>(async r => {
term.onScroll(() => r());
await term.writeP('\n'.repeat(INIT_ROWS));
});
});
it('should fire a scroll event when scrollback is cleared', (done) => {
term.writeSync('\n'.repeat(INIT_ROWS));
term.onScroll(() => done());
term.clear();
it('should fire a scroll event when scrollback is cleared', () => {
return new Promise<void>(async r => {
await term.writeP('\n'.repeat(INIT_ROWS));
term.onScroll(() => r());
term.clear();
});
});
it('should fire a key event after a keypress DOM event', (done) => {
term.onKey(e => {
@@ -123,6 +131,12 @@ describe('Terminal', () => {
});
term.write('\x1b]2;title\x07');
});
it('should fire the onBell event', (done) => {
term.onBell(e => {
done();
});
term.write('\x07');
});
});
describe('attachCustomKeyEventHandler', () => {
@@ -177,10 +191,10 @@ describe('Terminal', () => {
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA));
}
});
it('should clear a buffer larger than rows', () => {
it('should clear a buffer larger than rows', async () => {
// Fill the buffer with dummy rows
for (let i = 0; i < term.rows * 2; i++) {
term.writeSync('test\n');
await term.writeP('test\n');
}
const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
@@ -224,22 +238,24 @@ describe('Terminal', () => {
});
term.paste('\r\nfoo\nbar\r');
});
it('should respect bracketed paste mode', done => {
term.onData(e => {
assert.equal(e, '\x1b[200~foo\x1b[201~');
done();
it('should respect bracketed paste mode', () => {
return new Promise<void>(async r => {
term.onData(e => {
assert.equal(e, '\x1b[200~foo\x1b[201~');
r();
});
await term.writeP('\x1b[?2004h');
term.paste('foo');
});
term.writeSync('\x1b[?2004h');
term.paste('foo');
});
});
describe('scroll', () => {
describe('scrollLines', () => {
let startYDisp: number;
beforeEach(() => {
beforeEach(async () => {
for (let i = 0; i < INIT_ROWS * 2; i++) {
term.writeSync('test\r\n');
await term.writeP('test\r\n');
}
startYDisp = INIT_ROWS + 1;
});
@@ -272,9 +288,9 @@ describe('Terminal', () => {
describe('scrollPages', () => {
let startYDisp: number;
beforeEach(() => {
beforeEach(async () => {
for (let i = 0; i < term.rows * 3; i++) {
term.writeSync('test\r\n');
await term.writeP('test\r\n');
}
startYDisp = (term.rows * 2) + 1;
});
@@ -295,9 +311,9 @@ describe('Terminal', () => {
});
describe('scrollToTop', () => {
beforeEach(() => {
beforeEach(async () => {
for (let i = 0; i < term.rows * 3; i++) {
term.writeSync('test\r\n');
await term.writeP('test\r\n');
}
});
it('should scroll to the top', () => {
@@ -309,9 +325,9 @@ describe('Terminal', () => {
describe('scrollToBottom', () => {
let startYDisp: number;
beforeEach(() => {
beforeEach(async () => {
for (let i = 0; i < term.rows * 3; i++) {
term.writeSync('test\r\n');
await term.writeP('test\r\n');
}
startYDisp = (term.rows * 2) + 1;
});
@@ -330,9 +346,9 @@ describe('Terminal', () => {
describe('scrollToLine', () => {
let startYDisp: number;
beforeEach(() => {
beforeEach(async () => {
for (let i = 0; i < term.rows * 3; i++) {
term.writeSync('test\r\n');
await term.writeP('test\r\n');
}
startYDisp = (term.rows * 2) + 1;
});
@@ -374,10 +390,10 @@ describe('Terminal', () => {
assert.equal(term.buffer.ydisp, term.buffer.ybase);
});
it('should not scroll down, when a custom keydown handler prevents the event', () => {
it('should not scroll down, when a custom keydown handler prevents the event', async () => {
// Add some output to the terminal
for (let i = 0; i < term.rows * 3; i++) {
term.writeSync('test\r\n');
await term.writeP('test\r\n');
}
const startYDisp = (term.rows * 2) + 1;
term.attachCustomKeyEventHandler(() => {
@@ -716,12 +732,12 @@ describe('Terminal', () => {
});
describe('unicode - surrogates', () => {
it('2 characters per cell', function (): void {
it('2 characters per cell', async function (): Promise<void> {
this.timeout(10000); // This is needed because istanbul patches code and slows it down
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.writeSync(high + String.fromCharCode(i));
await term.writeP(high + String.fromCharCode(i));
const tchar = term.buffer.lines.get(0)!.loadCell(0, cell);
assert.equal(tchar.getChars(), high + String.fromCharCode(i));
assert.equal(tchar.getChars().length, 2);
@@ -730,25 +746,26 @@ describe('Terminal', () => {
term.reset();
}
});
it('2 characters at last cell', () => {
it('2 characters at last cell', async () => {
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1;
term.writeSync(high + String.fromCharCode(i));
await term.writeP(high + String.fromCharCode(i));
assert.equal(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars(), high + String.fromCharCode(i));
assert.equal(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars().length, 2);
assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars(), '');
term.reset();
}
});
it('2 characters per cell over line end with autowrap', () => {
it('2 characters per cell over line end with autowrap', async function (): Promise<void> {
this.timeout(10000);
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1;
term.writeSync('a' + high + String.fromCharCode(i));
await term.writeP('a' + high + String.fromCharCode(i));
assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars(), 'a');
assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars(), high + String.fromCharCode(i));
assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars().length, 2);
@@ -756,17 +773,18 @@ describe('Terminal', () => {
term.reset();
}
});
it('2 characters per cell over line end without autowrap', () => {
it('2 characters per cell over line end without autowrap', async function (): Promise<void> {
this.timeout(10000);
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1;
term.writeSync('\x1b[?7l'); // Disable wraparound mode
await term.writeP('\x1b[?7l'); // Disable wraparound mode
const width = wcwidth((0xD800 - 0xD800) * 0x400 + i - 0xDC00 + 0x10000);
if (width !== 1) {
continue;
}
term.writeSync('a' + high + String.fromCharCode(i));
await term.writeP('a' + high + String.fromCharCode(i));
// auto wraparound mode should cut off the rest of the line
assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars(), high + String.fromCharCode(i));
assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars().length, 2);
@@ -774,12 +792,12 @@ describe('Terminal', () => {
term.reset();
}
});
it('splitted surrogates', () => {
it('splitted surrogates', async function (): Promise<void> {
this.timeout(10000);
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.writeSync(high);
term.writeSync(String.fromCharCode(i));
await term.writeP(high + String.fromCharCode(i));
const tchar = term.buffer.lines.get(0)!.loadCell(0, cell);
assert.equal(tchar.getChars(), high + String.fromCharCode(i));
assert.equal(tchar.getChars().length, 2);
@@ -792,16 +810,16 @@ describe('Terminal', () => {
describe('unicode - combining characters', () => {
const cell = new CellData();
it('café', () => {
term.writeSync('cafe\u0301');
it('café', async () => {
await term.writeP('cafe\u0301');
term.buffer.lines.get(0)!.loadCell(3, cell);
assert.equal(cell.getChars(), 'e\u0301');
assert.equal(cell.getChars().length, 2);
assert.equal(cell.getWidth(), 1);
});
it('café - end of line', () => {
it('café - end of line', async () => {
term.buffer.x = term.cols - 1 - 3;
term.writeSync('cafe\u0301');
await term.writeP('cafe\u0301');
term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell);
assert.equal(cell.getChars(), 'e\u0301');
assert.equal(cell.getChars().length, 2);
@@ -811,8 +829,8 @@ describe('Terminal', () => {
assert.equal(cell.getChars().length, 0);
assert.equal(cell.getWidth(), 1);
});
it('multiple combined é', () => {
term.writeSync(Array(100).join('e\u0301'));
it('multiple combined é', async () => {
await term.writeP(Array(100).join('e\u0301'));
for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0)!.loadCell(i, cell);
assert.equal(cell.getChars(), 'e\u0301');
@@ -824,8 +842,8 @@ describe('Terminal', () => {
assert.equal(cell.getChars().length, 2);
assert.equal(cell.getWidth(), 1);
});
it('multiple surrogate with combined', () => {
term.writeSync(Array(100).join('\uD800\uDC00\u0301'));
it('multiple surrogate with combined', async () => {
await term.writeP(Array(100).join('\uD800\uDC00\u0301'));
for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0)!.loadCell(i, cell);
assert.equal(cell.getChars(), '\uD800\uDC00\u0301');
@@ -841,19 +859,19 @@ describe('Terminal', () => {
describe('unicode - fullwidth characters', () => {
const cell = new CellData();
it('cursor movement even', () => {
it('cursor movement even', async () => {
assert.equal(term.buffer.x, 0);
term.writeSync('¥');
await term.writeP('¥');
assert.equal(term.buffer.x, 2);
});
it('cursor movement odd', () => {
it('cursor movement odd', async () => {
term.buffer.x = 1;
assert.equal(term.buffer.x, 1);
term.writeSync('¥');
await term.writeP('¥');
assert.equal(term.buffer.x, 3);
});
it('line of ¥ even', () => {
term.writeSync(Array(50).join('¥'));
it('line of ¥ even', async () => {
await term.writeP(Array(50).join('¥'));
for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0)!.loadCell(i, cell);
if (i % 2) {
@@ -871,9 +889,9 @@ describe('Terminal', () => {
assert.equal(cell.getChars().length, 1);
assert.equal(cell.getWidth(), 2);
});
it('line of ¥ odd', () => {
it('line of ¥ odd', async () => {
term.buffer.x = 1;
term.writeSync(Array(50).join('¥'));
await term.writeP(Array(50).join('¥'));
for (let i = 1; i < term.cols - 1; ++i) {
term.buffer.lines.get(0)!.loadCell(i, cell);
if (!(i % 2)) {
@@ -895,9 +913,9 @@ describe('Terminal', () => {
assert.equal(cell.getChars().length, 1);
assert.equal(cell.getWidth(), 2);
});
it('line of ¥ with combining odd', () => {
it('line of ¥ with combining odd', async () => {
term.buffer.x = 1;
term.writeSync(Array(50).join('¥\u0301'));
await term.writeP(Array(50).join('¥\u0301'));
for (let i = 1; i < term.cols - 1; ++i) {
term.buffer.lines.get(0)!.loadCell(i, cell);
if (!(i % 2)) {
@@ -919,8 +937,8 @@ describe('Terminal', () => {
assert.equal(cell.getChars().length, 2);
assert.equal(cell.getWidth(), 2);
});
it('line of ¥ with combining even', () => {
term.writeSync(Array(50).join('¥\u0301'));
it('line of ¥ with combining even', async () => {
await term.writeP(Array(50).join('¥\u0301'));
for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0)!.loadCell(i, cell);
if (i % 2) {
@@ -938,9 +956,9 @@ describe('Terminal', () => {
assert.equal(cell.getChars().length, 2);
assert.equal(cell.getWidth(), 2);
});
it('line of surrogate fullwidth with combining odd', () => {
it('line of surrogate fullwidth with combining odd', async () => {
term.buffer.x = 1;
term.writeSync(Array(50).join('\ud843\ude6d\u0301'));
await term.writeP(Array(50).join('\ud843\ude6d\u0301'));
for (let i = 1; i < term.cols - 1; ++i) {
term.buffer.lines.get(0)!.loadCell(i, cell);
if (!(i % 2)) {
@@ -962,8 +980,8 @@ describe('Terminal', () => {
assert.equal(cell.getChars().length, 3);
assert.equal(cell.getWidth(), 2);
});
it('line of surrogate fullwidth with combining even', () => {
term.writeSync(Array(50).join('\ud843\ude6d\u0301'));
it('line of surrogate fullwidth with combining even', async () => {
await term.writeP(Array(50).join('\ud843\ude6d\u0301'));
for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0)!.loadCell(i, cell);
if (i % 2) {
@@ -985,24 +1003,24 @@ describe('Terminal', () => {
describe('insert mode', () => {
const cell = new CellData();
it('halfwidth - all', () => {
term.writeSync(Array(9).join('0123456789').slice(-80));
it('halfwidth - all', async () => {
await term.writeP(Array(9).join('0123456789').slice(-80));
term.buffer.x = 10;
term.buffer.y = 0;
term.write('\x1b[4h');
term.writeSync('abcde');
await term.writeP('abcde');
assert.equal(term.buffer.lines.get(0)!.length, term.cols);
assert.equal(term.buffer.lines.get(0)!.loadCell(10, cell).getChars(), 'a');
assert.equal(term.buffer.lines.get(0)!.loadCell(14, cell).getChars(), 'e');
assert.equal(term.buffer.lines.get(0)!.loadCell(15, cell).getChars(), '0');
assert.equal(term.buffer.lines.get(0)!.loadCell(79, cell).getChars(), '4');
});
it('fullwidth - insert', () => {
term.writeSync(Array(9).join('0123456789').slice(-80));
it('fullwidth - insert', async () => {
await term.writeP(Array(9).join('0123456789').slice(-80));
term.buffer.x = 10;
term.buffer.y = 0;
term.write('\x1b[4h');
term.writeSync('¥¥¥');
await term.writeP('¥¥¥');
assert.equal(term.buffer.lines.get(0)!.length, term.cols);
assert.equal(term.buffer.lines.get(0)!.loadCell(10, cell).getChars(), '¥');
assert.equal(term.buffer.lines.get(0)!.loadCell(11, cell).getChars(), '');
@@ -1010,17 +1028,17 @@ describe('Terminal', () => {
assert.equal(term.buffer.lines.get(0)!.loadCell(15, cell).getChars(), '');
assert.equal(term.buffer.lines.get(0)!.loadCell(79, cell).getChars(), '3');
});
it('fullwidth - right border', () => {
term.writeSync(Array(41).join('¥'));
it('fullwidth - right border', async () => {
await term.writeP(Array(41).join('¥'));
term.buffer.x = 10;
term.buffer.y = 0;
term.write('\x1b[4h');
term.writeSync('a');
await term.writeP('a');
assert.equal(term.buffer.lines.get(0)!.length, term.cols);
assert.equal(term.buffer.lines.get(0)!.loadCell(10, cell).getChars(), 'a');
assert.equal(term.buffer.lines.get(0)!.loadCell(11, cell).getChars(), '¥');
assert.equal(term.buffer.lines.get(0)!.loadCell(79, cell).getChars(), ''); // fullwidth char got replaced
term.writeSync('b');
await term.writeP('b');
assert.equal(term.buffer.lines.get(0)!.length, term.cols);
assert.equal(term.buffer.lines.get(0)!.loadCell(11, cell).getChars(), 'b');
assert.equal(term.buffer.lines.get(0)!.loadCell(12, cell).getChars(), '¥');
@@ -1042,85 +1060,87 @@ describe('Terminal', () => {
linkifier.attachToDom({} as any, mouseZoneManager);
});
function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[], done: Mocha.Done): void {
terminal.writeSync(rowText);
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
linkifier.linkifyRows();
// Allow linkify to happen
setTimeout(() => {
assert.equal(mouseZoneManager.zones.length, links.length);
links.forEach((l, i) => {
assert.equal(mouseZoneManager.zones[i].x1, l.x1 + 1);
assert.equal(mouseZoneManager.zones[i].x2, l.x2 + 1);
assert.equal(mouseZoneManager.zones[i].y1, l.y1 + 1);
assert.equal(mouseZoneManager.zones[i].y2, l.y2 + 1);
});
done();
}, 0);
function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: { x1: number, y1: number, x2: number, y2: number }[]): Promise<void> {
return new Promise(async r => {
await terminal.writeP(rowText);
linkifier.registerLinkMatcher(linkMatcherRegex, () => { });
linkifier.linkifyRows();
// Allow linkify to happen
setTimeout(() => {
assert.equal(mouseZoneManager.zones.length, links.length);
links.forEach((l, i) => {
assert.equal(mouseZoneManager.zones[i].x1, l.x1 + 1);
assert.equal(mouseZoneManager.zones[i].x2, l.x2 + 1);
assert.equal(mouseZoneManager.zones[i].y1, l.y1 + 1);
assert.equal(mouseZoneManager.zones[i].y2, l.y2 + 1);
});
r();
}, 0);
});
}
describe('unicode before the match', () => {
it('combining - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
it('combining - match within one line', () => {
return assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{ x1: 4, x2: 7, y1: 0, y2: 0 }]);
});
it('combining - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
it('combining - match over two lines', () => {
return assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]);
});
it('surrogate - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
it('surrogate - match within one line', () => {
return assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{ x1: 4, x2: 7, y1: 0, y2: 0 }]);
});
it('surrogate - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
it('surrogate - match over two lines', () => {
return assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]);
});
it('combining surrogate - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
it('combining surrogate - match within one line', () => {
return assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{ x1: 4, x2: 7, y1: 0, y2: 0 }]);
});
it('combining surrogate - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
it('combining surrogate - match over two lines', () => {
return assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]);
});
it('fullwidth - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
it('fullwidth - match within one line', () => {
return assertLinkifiesInTerminal('12 foo', /foo/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]);
});
it('fullwidth - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
it('fullwidth - match over two lines', () => {
return assertLinkifiesInTerminal('12 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]);
});
it('combining fullwidth - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
it('combining fullwidth - match within one line', () => {
return assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]);
});
it('combining fullwidth - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
it('combining fullwidth - match over two lines', () => {
return assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]);
});
});
describe('unicode within the match', () => {
it('combining - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
it('combining - match within one line', () => {
return assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{ x1: 5, x2: 9, y1: 0, y2: 0 }]);
});
it('combining - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('testtest cafe\u0301', /cafe\u0301/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
it('combining - match over two lines', () => {
return assertLinkifiesInTerminal('testtest cafe\u0301', /cafe\u0301/, [{ x1: 9, x2: 3, y1: 0, y2: 1 }]);
});
it('surrogate - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('test a𝄞b', /a𝄞b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
it('surrogate - match within one line', () => {
return assertLinkifiesInTerminal('test a𝄞b', /a𝄞b/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]);
});
it('surrogate - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('testtest a𝄞b', /a𝄞b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done);
it('surrogate - match over two lines', () => {
return assertLinkifiesInTerminal('testtest a𝄞b', /a𝄞b/, [{ x1: 9, x2: 2, y1: 0, y2: 1 }]);
});
it('combining surrogate - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('test a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
it('combining surrogate - match within one line', () => {
return assertLinkifiesInTerminal('test a𓂀\u0301b', /a𓂀\u0301b/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]);
});
it('combining surrogate - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('testtest a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done);
it('combining surrogate - match over two lines', () => {
return assertLinkifiesInTerminal('testtest a𓂀\u0301b', /a𓂀\u0301b/, [{ x1: 9, x2: 2, y1: 0, y2: 1 }]);
});
it('fullwidth - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('test a1b', /a1b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
it('fullwidth - match within one line', () => {
return assertLinkifiesInTerminal('test a1b', /a1b/, [{ x1: 5, x2: 9, y1: 0, y2: 0 }]);
});
it('fullwidth - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('testtest a1b', /a1b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
it('fullwidth - match over two lines', () => {
return assertLinkifiesInTerminal('testtest a1b', /a1b/, [{ x1: 9, x2: 3, y1: 0, y2: 1 }]);
});
it('combining fullwidth - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('test a¥\u0301b', /a¥\u0301b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
it('combining fullwidth - match within one line', () => {
return assertLinkifiesInTerminal('test a¥\u0301b', /a¥\u0301b/, [{ x1: 5, x2: 9, y1: 0, y2: 0 }]);
});
it('combining fullwidth - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
it('combining fullwidth - match over two lines', () => {
return assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{ x1: 9, x2: 3, y1: 0, y2: 1 }]);
});
});
});
@@ -1129,12 +1149,12 @@ describe('Terminal', () => {
let terminal: TestTerminal;
beforeEach(() => {
terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5});
terminal = new TestTerminal({ rows: 5, cols: 10, scrollback: 5 });
});
it('multiline ascii', () => {
it('multiline ascii', async () => {
const input = 'This is ASCII text spanning multiple lines.';
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < input.length; ++i) {
@@ -1143,9 +1163,9 @@ describe('Terminal', () => {
}
});
it('combining e\u0301 in a sentence', () => {
it('combining e\u0301 in a sentence', async () => {
const input = 'Sitting in the cafe\u0301 drinking coffee.';
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < 19; ++i) {
@@ -1163,9 +1183,9 @@ describe('Terminal', () => {
}
});
it('multiline combining e\u0301', () => {
it('multiline combining e\u0301', async () => {
const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301';
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
// every buffer cell index contains 2 string indices
@@ -1175,9 +1195,9 @@ describe('Terminal', () => {
}
});
it('surrogate char in a sentence', () => {
it('surrogate char in a sentence', async () => {
const input = 'The 𝄞 is a clef widely used in modern notation.';
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < 5; ++i) {
@@ -1195,9 +1215,9 @@ describe('Terminal', () => {
}
});
it('multiline surrogate char', () => {
it('multiline surrogate char', async () => {
const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞';
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
// every buffer cell index contains 2 string indices
@@ -1207,10 +1227,10 @@ describe('Terminal', () => {
}
});
it('surrogate char with combining', () => {
it('surrogate char with combining', async () => {
// eye of Ra with acute accent - string length of 3
const input = '𓂀\u0301 - the eye hiroglyph with an acute accent.';
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
// index 0..2 should map to 0
@@ -1222,9 +1242,9 @@ describe('Terminal', () => {
}
});
it('multiline surrogate with combining', () => {
it('multiline surrogate with combining', async () => {
const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301';
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
// every buffer cell index contains 3 string indices
@@ -1234,9 +1254,9 @@ describe('Terminal', () => {
}
});
it('fullwidth chars', () => {
it('fullwidth chars', async () => {
const input = 'These 123 are some fat numbers.';
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < 6; ++i) {
@@ -1253,9 +1273,9 @@ describe('Terminal', () => {
}
});
it('multiline fullwidth chars', () => {
it('multiline fullwidth chars', async () => {
const input = '12345678901234567890';
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 9; i < input.length; ++i) {
@@ -1264,9 +1284,9 @@ describe('Terminal', () => {
}
});
it('fullwidth combining with emoji - match emoji cell', () => {
it('fullwidth combining with emoji - match emoji cell', async () => {
const input = 'Lots of ¥\u0301 make me 😃.';
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
const stringIndex = s.match(/😃/)!.index!;
@@ -1274,14 +1294,14 @@ describe('Terminal', () => {
assert(terminal.buffer.lines.get(bufferIndex[0])!.loadCell(bufferIndex[1], new CellData()).getChars(), '😃');
});
it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => {
it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', async () => {
const input = 'a12345678901234567890';
// the 'a' at the beginning moves all fullwidth chars one to the right
// now the end of the line contains a dangling empty cell since
// the next fullwidth char has to wrap early
// the dangling last cell is wrongly added in the string
// --> fixable after resolving #1685
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 10; i < input.length; ++i) {
@@ -1291,9 +1311,9 @@ describe('Terminal', () => {
}
});
it('test fully wrapped buffer up to last char', () => {
it('test fully wrapped buffer up to last char', async () => {
const input = Array(6).join('1234567890');
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < input.length; ++i) {
@@ -1302,10 +1322,10 @@ describe('Terminal', () => {
}
});
it('test fully wrapped buffer up to last char with full width odd', () => {
it('test fully wrapped buffer up to last char with full width odd', async () => {
const input = 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301'
+ 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301';
terminal.writeSync(input);
+ 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301';
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < input.length; ++i) {
@@ -1320,17 +1340,17 @@ describe('Terminal', () => {
}
});
it('should handle \t in lines correctly', () => {
it('should handle \t in lines correctly', async () => {
const input = '\thttps://google.de';
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(s, Array(terminal.optionsService.options.tabStopWidth + 1).join(' ') + 'https://google.de');
});
});
describe('BufferStringIterator', function(): void {
it('iterator does not overflow buffer limits', function(): void {
const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5});
describe('BufferStringIterator', function (): void {
it('iterator does not overflow buffer limits', async () => {
const terminal = new TestTerminal({ rows: 5, cols: 10, scrollback: 5 });
const data = [
'aaaaaaaaaa',
'aaaaaaaaa\n',
@@ -1343,7 +1363,7 @@ describe('Terminal', () => {
'aaaaaaaaaa',
'aaaaaaaaaa'
];
terminal.writeSync(data.join(''));
await terminal.writeP(data.join(''));
// brute force test with insane values
assert.doesNotThrow(() => {
for (let overscan = 0; overscan < 20; ++overscan) {
@@ -1361,178 +1381,63 @@ describe('Terminal', () => {
});
describe('Windows Mode', () => {
it('should mark lines as wrapped when the line ends in a non-null character after a LF', () => {
it('should mark lines as wrapped when the line ends in a non-null character after a LF', async () => {
const data = [
'aaaaaaaaaa\n\r', // cannot wrap as it's the first
'aaaaaaaaa\n\r', // wrapped (windows mode only)
'aaaaaaaaa' // not wrapped
];
const normalTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: false});
normalTerminal.writeSync(data.join(''));
const normalTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: false });
await normalTerminal.writeP(data.join(''));
assert.equal(normalTerminal.buffer.lines.get(0)!.isWrapped, false);
assert.equal(normalTerminal.buffer.lines.get(1)!.isWrapped, false);
assert.equal(normalTerminal.buffer.lines.get(2)!.isWrapped, false);
const windowsModeTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: true});
windowsModeTerminal.writeSync(data.join(''));
const windowsModeTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: true });
await windowsModeTerminal.writeP(data.join(''));
assert.equal(windowsModeTerminal.buffer.lines.get(0)!.isWrapped, false);
assert.equal(windowsModeTerminal.buffer.lines.get(1)!.isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character');
assert.equal(windowsModeTerminal.buffer.lines.get(2)!.isWrapped, false);
});
it('should mark lines as wrapped when the line ends in a non-null character after a CUP', () => {
it('should mark lines as wrapped when the line ends in a non-null character after a CUP', async () => {
const data = [
'aaaaaaaaaa\x1b[2;1H', // cannot wrap as it's the first
'aaaaaaaaa\x1b[3;1H', // wrapped (windows mode only)
'aaaaaaaaa' // not wrapped
];
const normalTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: false});
normalTerminal.writeSync(data.join(''));
const normalTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: false });
await normalTerminal.writeP(data.join(''));
assert.equal(normalTerminal.buffer.lines.get(0)!.isWrapped, false);
assert.equal(normalTerminal.buffer.lines.get(1)!.isWrapped, false);
assert.equal(normalTerminal.buffer.lines.get(2)!.isWrapped, false);
const windowsModeTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: true});
windowsModeTerminal.writeSync(data.join(''));
const windowsModeTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: true });
await windowsModeTerminal.writeP(data.join(''));
assert.equal(windowsModeTerminal.buffer.lines.get(0)!.isWrapped, false);
assert.equal(windowsModeTerminal.buffer.lines.get(1)!.isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character');
assert.equal(windowsModeTerminal.buffer.lines.get(2)!.isWrapped, false);
});
});
it('convertEol setting', function(): void {
it('convertEol setting', async () => {
// not converting
const termNotConverting = new TestTerminal({cols: 15, rows: 10});
termNotConverting.writeSync('Hello\nWorld');
const termNotConverting = new TestTerminal({ cols: 15, rows: 10 });
await termNotConverting.writeP('Hello\nWorld');
assert.equal(termNotConverting.buffer.lines.get(0)!.translateToString(false), 'Hello ');
assert.equal(termNotConverting.buffer.lines.get(1)!.translateToString(false), ' World ');
assert.equal(termNotConverting.buffer.lines.get(0)!.translateToString(true), 'Hello');
assert.equal(termNotConverting.buffer.lines.get(1)!.translateToString(true), ' World');
// converting
const termConverting = new TestTerminal({cols: 15, rows: 10, convertEol: true});
termConverting.writeSync('Hello\nWorld');
const termConverting = new TestTerminal({ cols: 15, rows: 10, convertEol: true });
await termConverting.writeP('Hello\nWorld');
assert.equal(termConverting.buffer.lines.get(0)!.translateToString(false), 'Hello ');
assert.equal(termConverting.buffer.lines.get(1)!.translateToString(false), 'World ');
assert.equal(termConverting.buffer.lines.get(0)!.translateToString(true), 'Hello');
assert.equal(termConverting.buffer.lines.get(1)!.translateToString(true), 'World');
});
describe('Terminal InputHandler integration', () => {
function getLines(term: TestTerminal, limit: number = term.rows): string[] {
const res: string[] = [];
for (let i = 0; i < limit; ++i) {
res.push(term.buffer.lines.get(i)!.translateToString(true));
}
return res;
}
// This suite cannot live in InputHandler unless Terminal.scroll moved into IBufferService
describe('SL/SR/DECIC/DECDC', () => {
let term: TestTerminal;
beforeEach(() => {
term = new TestTerminal({cols: 5, rows: 5, scrollback: 1});
});
it('SL (scrollLeft)', () => {
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[ @');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '2345', '2345', '2345', '2345', '2345']);
term.writeSync('\x1b[0 @');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '345', '345', '345', '345', '345']);
term.writeSync('\x1b[2 @');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '5', '5', '5', '5', '5']);
});
it('SR (scrollRight)', () => {
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[ A');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 1234', ' 1234', ' 1234', ' 1234', ' 1234']);
term.writeSync('\x1b[0 A');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']);
term.writeSync('\x1b[2 A');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']);
});
it('insertColumns (DECIC)', () => {
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[3;3H');
term.writeSync('\x1b[\'}');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']);
term.reset();
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[3;3H');
term.writeSync('\x1b[1\'}');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']);
term.reset();
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[3;3H');
term.writeSync('\x1b[2\'}');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 3', '12 3', '12 3', '12 3', '12 3']);
});
it('deleteColumns (DECDC)', () => {
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[3;3H');
term.writeSync('\x1b[\'~');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '1245', '1245', '1245', '1245', '1245']);
term.reset();
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[3;3H');
term.writeSync('\x1b[1\'~');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '1245', '1245', '1245', '1245', '1245']);
term.reset();
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[3;3H');
term.writeSync('\x1b[2\'~');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '125', '125', '125', '125', '125']);
});
});
describe('BS with reverseWraparound set/unset', () => {
const ttyBS = '\x08 \x08'; // tty ICANON sends <BS SP BS> on pressing BS
beforeEach(() => {
term = new TestTerminal({cols: 5, rows: 5, scrollback: 1});
});
describe('reverseWraparound set', () => {
it('should not reverse outside of scroll margins', () => {
// prepare buffer content
term.writeSync('#####abcdefghijklmnopqrstuvwxy');
assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', 'uvwxy']);
assert.equal(term.buffer.ydisp, 1);
assert.equal(term.buffer.x, 5);
assert.equal(term.buffer.y, 4);
term.writeSync(ttyBS.repeat(100));
assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' y']);
term.writeSync('\x1b[?45h');
term.writeSync('uvwxy');
// set top/bottom to 1/3 (0-based)
term.writeSync('\x1b[2;4r');
// place cursor below scroll bottom
term.buffer.x = 5;
term.buffer.y = 4;
term.writeSync(ttyBS.repeat(100));
assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' ']);
term.writeSync('uvwxy');
// place cursor within scroll margins
term.buffer.x = 5;
term.buffer.y = 3;
term.writeSync(ttyBS.repeat(100));
assert.deepEqual(getLines(term, 6), ['#####', 'abcde', ' ', ' ', ' ', 'uvwxy']);
assert.equal(term.buffer.x, 0);
assert.equal(term.buffer.y, term.buffer.scrollTop); // stops at 0, scrollTop
term.writeSync('fghijklmnopqrst');
// place cursor above scroll top
term.buffer.x = 5;
term.buffer.y = 0;
term.writeSync(ttyBS.repeat(100));
assert.deepEqual(getLines(term, 6), ['#####', ' ', 'fghij', 'klmno', 'pqrst', 'uvwxy']);
});
});
});
});
// FIXME: move to common/CoreTerminal.test once the trimming is moved over
describe('marker lifecycle', () => {
@@ -1541,22 +1446,22 @@ describe('Terminal', () => {
let markers: IMarker[];
let disposeStack: IMarker[];
let term: TestTerminal;
beforeEach(() => {
beforeEach(async () => {
term = new TestTerminal({});
markers = [];
disposeStack = [];
term.optionsService.setOption('scrollback', 1);
term.resize(10, 5);
markers.push(term.buffers.active.addMarker(term.buffers.active.y));
term.writeSync('\x1b[r0\r\n');
await term.writeP('\x1b[r0\r\n');
markers.push(term.buffers.active.addMarker(term.buffers.active.y));
term.writeSync('1\r\n');
await term.writeP('1\r\n');
markers.push(term.buffers.active.addMarker(term.buffers.active.y));
term.writeSync('2\r\n');
await term.writeP('2\r\n');
markers.push(term.buffers.active.addMarker(term.buffers.active.y));
term.writeSync('3\r\n');
await term.writeP('3\r\n');
markers.push(term.buffers.active.addMarker(term.buffers.active.y));
term.writeSync('4');
await term.writeP('4');
for (let i = 0; i < markers.length; ++i) {
const marker = markers[i];
marker.onDispose(() => disposeStack.push(marker));
@@ -1565,15 +1470,15 @@ describe('Terminal', () => {
it('initial', () => {
assert.deepEqual(markers.map(m => m.line), [0, 1, 2, 3, 4]);
});
it('should dispose on normal trim off the top', () => {
it('should dispose on normal trim off the top', async () => {
// moves top line into scrollback
term.writeSync('\n');
await term.writeP('\n');
assert.deepEqual(disposeStack, []);
// trims first marker
term.writeSync('\n');
await term.writeP('\n');
assert.deepEqual(disposeStack, [markers[0]]);
// trims second marker
term.writeSync('\n');
await term.writeP('\n');
assert.deepEqual(disposeStack, [markers[0], markers[1]]);
// trimmed marker objs should be disposed
assert.deepEqual(disposeStack.map(el => el.isDisposed), [true, true]);
@@ -1581,14 +1486,14 @@ describe('Terminal', () => {
// trimmed markers should contain line -1
assert.deepEqual(disposeStack.map(el => el.line), [-1, -1]);
});
it('should dispose on DL', () => {
term.writeSync('\x1b[3;1H'); // move cursor to 0, 2
term.writeSync('\x1b[2M'); // delete 2 lines
it('should dispose on DL', async () => {
await term.writeP('\x1b[3;1H'); // move cursor to 0, 2
await term.writeP('\x1b[2M'); // delete 2 lines
assert.deepEqual(disposeStack, [markers[2], markers[3]]);
});
it('should dispose on IL', () => {
term.writeSync('\x1b[3;1H'); // move cursor to 0, 2
term.writeSync('\x1b[2L'); // insert 2 lines
it('should dispose on IL', async () => {
await term.writeP('\x1b[3;1H'); // move cursor to 0, 2
await term.writeP('\x1b[2L'); // insert 2 lines
assert.deepEqual(disposeStack, [markers[4], markers[3]]);
assert.deepEqual(markers.map(el => el.line), [0, 1, 4, -1, -1]);
});
+58 -35
View File
@@ -21,8 +21,8 @@
* http://linux.die.net/man/7/urxvt
*/
import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport, ILinkifier2 } from 'browser/Types';
import { IRenderer, CharacterJoinerHandler } from 'browser/renderer/Types';
import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport, ILinkifier2, CharacterJoinerHandler } from 'browser/Types';
import { IRenderer } from 'browser/renderer/Types';
import { CompositionHelper } from 'browser/input/CompositionHelper';
import { Viewport } from 'browser/Viewport';
import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard';
@@ -39,13 +39,13 @@ import { MouseZoneManager } from 'browser/MouseZoneManager';
import { AccessibilityManager } from './AccessibilityManager';
import { ITheme, IMarker, IDisposable, ISelectionPosition, ILinkProvider } from 'xterm';
import { DomRenderer } from 'browser/renderer/dom/DomRenderer';
import { IKeyboardEvent, KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions, IAnsiColorChangeEvent } from 'common/Types';
import { IKeyboardEvent, KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions, ScrollSource, IAnsiColorChangeEvent } from 'common/Types';
import { evaluateKeyboardEvent } from 'common/input/Keyboard';
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { ColorManager } from 'browser/ColorManager';
import { RenderService } from 'browser/services/RenderService';
import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService } from 'browser/services/Services';
import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService, ICharacterJoinerService } from 'browser/services/Services';
import { CharSizeService } from 'browser/services/CharSizeService';
import { IBuffer } from 'common/buffer/Types';
import { MouseService } from 'browser/services/MouseService';
@@ -54,6 +54,7 @@ import { CoreBrowserService } from 'browser/services/CoreBrowserService';
import { CoreTerminal } from 'common/CoreTerminal';
import { ITerminalOptions as IInitializedTerminalOptions } from 'common/services/Services';
import { rgba } from 'browser/Color';
import { CharacterJoinerService } from 'browser/services/CharacterJoinerService';
// Let it work inside Node.js for automated testing purposes.
const document: Document = (typeof window !== 'undefined') ? window.document : null as any;
@@ -82,6 +83,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
private _charSizeService: ICharSizeService | undefined;
private _mouseService: IMouseService | undefined;
private _renderService: IRenderService | undefined;
private _characterJoinerService: ICharacterJoinerService | undefined;
private _selectionService: ISelectionService | undefined;
private _soundService: ISoundService | undefined;
@@ -111,6 +113,8 @@ export class Terminal extends CoreTerminal implements ITerminal {
public get onSelectionChange(): IEvent<void> { return this._onSelectionChange.event; }
private _onTitleChange = new EventEmitter<string>();
public get onTitleChange(): IEvent<string> { return this._onTitleChange.event; }
private _onBell = new EventEmitter<void>();
public get onBell (): IEvent<void> { return this._onBell.event; }
private _onFocus = new EventEmitter<void>();
public get onFocus(): IEvent<void> { return this._onFocus.event; }
@@ -147,7 +151,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.register(this._inputHandler.onRequestBell(() => this.bell()));
this.register(this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end)));
this.register(this._inputHandler.onRequestReset(() => this.reset()));
this.register(this._inputHandler.onRequestScroll((eraseAttr, isWrapped) => this.scroll(eraseAttr, isWrapped || undefined)));
this.register(this._inputHandler.onRequestWindowsOptionsReport(type => this._reportWindowsOptions(type)));
this.register(this._inputHandler.onAnsiColorChange((event) => this._changeAnsiColor(event)));
this.register(forwardEvent(this._inputHandler.onCursorMove, this._onCursorMove));
@@ -162,11 +165,11 @@ export class Terminal extends CoreTerminal implements ITerminal {
private _changeAnsiColor(event: IAnsiColorChangeEvent): void {
if (!this._colorManager) { return; }
event.colors.forEach(ansiColor => {
for (const ansiColor of event.colors) {
const color = rgba.toColor(ansiColor.red, ansiColor.green, ansiColor.blue);
this._colorManager!.colors.ansi[ansiColor.colorIndex] = color;
});
}
this._renderService?.setColors(this._colorManager!.colors);
this.viewport?.onThemeChange(this._colorManager!.colors);
@@ -297,19 +300,26 @@ export class Terminal extends CoreTerminal implements ITerminal {
}
private _syncTextArea(): void {
if (!this.textarea || !this.buffer.isCursorInViewport || this._compositionHelper!.isComposing) {
if (!this.textarea || !this.buffer.isCursorInViewport || this._compositionHelper!.isComposing || !this._renderService) {
return;
}
const cellHeight = Math.ceil(this._charSizeService!.height * this.optionsService.options.lineHeight);
const cursorTop = this._bufferService.buffer.y * cellHeight;
const cursorLeft = this._bufferService.buffer.x * this._charSizeService!.width;
const cursorY = this.buffer.ybase + this.buffer.y;
const bufferLine = this.buffer.lines.get(cursorY);
if (!bufferLine) {
return;
}
const cursorX = Math.min(this.buffer.x, this.cols - 1);
const cellHeight = this._renderService.dimensions.actualCellHeight;
const width = bufferLine.getWidth(cursorX);
const cellWidth = this._renderService.dimensions.actualCellWidth * width;
const cursorTop = this.buffer.y * this._renderService.dimensions.actualCellHeight;
const cursorLeft = cursorX * this._renderService.dimensions.actualCellWidth;
// Sync the textarea to the exact position of the composition view so the IME knows where the
// text is.
this.textarea.style.left = cursorLeft + 'px';
this.textarea.style.top = cursorTop + 'px';
this.textarea.style.width = this._charSizeService!.width + 'px';
this.textarea.style.width = cellWidth + 'px';
this.textarea.style.height = cellHeight + 'px';
this.textarea.style.lineHeight = cellHeight + 'px';
this.textarea.style.zIndex = '-5';
@@ -438,6 +448,20 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer);
this._instantiationService.setService(ICharSizeService, this._charSizeService);
this._theme = this.options.theme || this._theme;
this._colorManager = new ColorManager(document, this.options.allowTransparency);
this.register(this.optionsService.onOptionChange(e => this._colorManager!.onOptionsChange(e)));
this._colorManager.setTheme(this._theme);
this._characterJoinerService = this._instantiationService.createInstance(CharacterJoinerService);
this._instantiationService.setService(ICharacterJoinerService, this._characterJoinerService);
const renderer = this._createRenderer();
this._renderService = this.register(this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement));
this._instantiationService.setService(IRenderService, this._renderService);
this.register(this._renderService.onRenderedBufferChange(e => this._onRender.fire(e)));
this.onResize(e => this._renderService!.resize(e.cols, e.rows));
this._compositionView = document.createElement('div');
this._compositionView.classList.add('composition-view');
this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView);
@@ -446,24 +470,13 @@ export class Terminal extends CoreTerminal implements ITerminal {
// Performance: Add viewport and helper elements from the fragment
this.element.appendChild(fragment);
this._theme = this.options.theme || this._theme;
this._colorManager = new ColorManager(document, this.options.allowTransparency);
this.register(this.optionsService.onOptionChange(e => this._colorManager!.onOptionsChange(e)));
this._colorManager.setTheme(this._theme);
const renderer = this._createRenderer();
this._renderService = this.register(this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement));
this._instantiationService.setService(IRenderService, this._renderService);
this.register(this._renderService.onRenderedBufferChange(e => this._onRender.fire(e)));
this.onResize(e => this._renderService!.resize(e.cols, e.rows));
this._soundService = this._instantiationService.createInstance(SoundService);
this._instantiationService.setService(ISoundService, this._soundService);
this._mouseService = this._instantiationService.createInstance(MouseService);
this._instantiationService.setService(IMouseService, this._mouseService);
this.viewport = this._instantiationService.createInstance(Viewport,
(amount: number, suppressEvent: boolean) => this.scrollLines(amount, suppressEvent),
(amount: number) => this.scrollLines(amount, true, ScrollSource.VIEWPORT),
this._viewportElement,
this._viewportScrollArea
);
@@ -482,7 +495,9 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._selectionService = this.register(this._instantiationService.createInstance(SelectionService,
this.element,
this.screenElement));
this.screenElement,
this.linkifier2
));
this._instantiationService.setService(ISelectionService, this._selectionService);
this.register(this._selectionService.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent)));
this.register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire()));
@@ -495,7 +510,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.textarea!.focus();
this.textarea!.select();
}));
this.register(this.onScroll(() => {
this.register(this._onScroll.event(ev => {
this.viewport!.syncScrollArea();
this._selectionService!.refresh();
}));
@@ -834,7 +849,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
* Change the cursor style for different selection modes
*/
public updateCursorStyle(ev: KeyboardEvent): void {
if (this._selectionService && this._selectionService.shouldColumnSelect(ev)) {
if (this._selectionService?.shouldColumnSelect(ev)) {
this.element!.classList.add('column-select');
} else {
this.element!.classList.remove('column-select');
@@ -851,8 +866,8 @@ export class Terminal extends CoreTerminal implements ITerminal {
}
}
public scrollLines(disp: number, suppressScrollEvent?: boolean): void {
super.scrollLines(disp, suppressScrollEvent);
public scrollLines(disp: number, suppressScrollEvent?: boolean, source = ScrollSource.TERMINAL): void {
super.scrollLines(disp, suppressScrollEvent, source);
this.refresh(0, this.rows - 1);
}
@@ -904,13 +919,19 @@ export class Terminal extends CoreTerminal implements ITerminal {
}
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
const joinerId = this._renderService!.registerCharacterJoiner(handler);
if (!this._characterJoinerService) {
throw new Error('Terminal must be opened first');
}
const joinerId = this._characterJoinerService.register(handler);
this.refresh(0, this.rows - 1);
return joinerId;
}
public deregisterCharacterJoiner(joinerId: number): void {
if (this._renderService!.deregisterCharacterJoiner(joinerId)) {
if (!this._characterJoinerService) {
throw new Error('Terminal must be opened first');
}
if (this._characterJoinerService.deregister(joinerId)) {
this.refresh(0, this.rows - 1);
}
}
@@ -999,7 +1020,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
if (!this._compositionHelper!.keydown(event)) {
if (this.buffer.ybase !== this.buffer.ydisp) {
this.scrollToBottom();
this._bufferService.scrollToBottom();
}
return false;
}
@@ -1128,9 +1149,11 @@ export class Terminal extends CoreTerminal implements ITerminal {
*/
public bell(): void {
if (this._soundBell()) {
this._soundService!.playBellSound();
this._soundService?.playBellSound();
}
this._onBell.fire();
// if (this._visualBell()) {
// this.element.classList.add('visual-bell-active');
// clearTimeout(this._visualBellTimer);
@@ -1183,7 +1206,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR_DATA));
}
this.refresh(0, this.rows - 1);
this._onScroll.fire(this.buffer.ydisp);
this._onScroll.fire({ position: this.buffer.ydisp, source: ScrollSource.TERMINAL });
}
/**
+1 -1
View File
@@ -74,7 +74,7 @@ describe('Escape Sequence Files', function(): void {
let content = '';
const OSC_CODE = 12345;
await new Promise(resolve => {
customHandler = term.addOscHandler(OSC_CODE, () => {
customHandler = term.registerOscHandler(OSC_CODE, () => {
// grab terminal viewport content
content = terminalToString(term);
resolve();
+24 -15
View File
@@ -5,9 +5,9 @@
import { IDisposable, IMarker, ISelectionPosition, ILinkProvider } from 'xterm';
import { IEvent, EventEmitter } from 'common/EventEmitter';
import { ICharSizeService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
import { IRenderDimensions, IRenderer, CharacterJoinerHandler, IRequestRedrawEvent } from 'browser/renderer/Types';
import { IColorSet, ILinkMatcherOptions, ITerminal, ILinkifier, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper } from 'browser/Types';
import { ICharacterJoinerService, ICharSizeService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
import { IColorSet, ILinkMatcherOptions, ITerminal, ILinkifier, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler } from 'browser/Types';
import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types';
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types';
import { Buffer } from 'common/buffer/Buffer';
@@ -21,6 +21,9 @@ export class TestTerminal extends Terminal {
public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; }
public keyDown(ev: any): boolean | undefined { return this._keyDown(ev); }
public keyPress(ev: any): boolean { return this._keyPress(ev); }
public writeP(data: string | Uint8Array): Promise<void> {
return new Promise(r => this.write(data, r));
}
}
export class MockTerminal implements ITerminal {
@@ -34,6 +37,7 @@ export class MockTerminal implements ITerminal {
public onData!: IEvent<string>;
public onBinary!: IEvent<string>;
public onTitleChange!: IEvent<string>;
public onBell!: IEvent<void>;
public onScroll!: IEvent<number>;
public onKey!: IEvent<{ key: string, domEvent: KeyboardEvent }>;
public onRender!: IEvent<{ start: number, end: number }>;
@@ -75,16 +79,16 @@ export class MockTerminal implements ITerminal {
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
throw new Error('Method not implemented.');
}
public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable {
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable {
throw new Error('Method not implemented.');
}
public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable {
public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable {
throw new Error('Method not implemented.');
}
public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise<boolean>): IDisposable {
throw new Error('Method not implemented.');
}
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
throw new Error('Method not implemented.');
}
public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number {
@@ -281,8 +285,6 @@ export class MockRenderer implements IRenderer {
public onDevicePixelRatioChange(): void { }
public clear(): void { }
public renderRows(start: number, end: number): void { }
public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; }
public deregisterCharacterJoiner(): boolean { return true; }
}
export class MockViewport implements IViewport {
@@ -406,13 +408,20 @@ export class MockRenderService implements IRenderService {
public clear(): void {
throw new Error('Method not implemented.');
}
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
throw new Error('Method not implemented.');
}
public deregisterCharacterJoiner(joinerId: number): boolean {
throw new Error('Method not implemented.');
}
public dispose(): void {
throw new Error('Method not implemented.');
}
}
export class MockCharacterJoinerService implements ICharacterJoinerService {
public serviceBrand: undefined;
public register(handler: (text: string) => [number, number][]): number {
return 0;
}
public deregister(joinerId: number): boolean {
return true;
}
public getJoinedCharacters(row: number): [number, number][] {
return [];
}
}
+22 -4
View File
@@ -47,15 +47,16 @@ export interface IPublicTerminal extends IDisposable {
onRender: IEvent<{ start: number, end: number }>;
onResize: IEvent<{ cols: number, rows: number }>;
onTitleChange: IEvent<string>;
onBell: IEvent<void>;
blur(): void;
focus(): void;
resize(columns: number, rows: number): void;
open(parent: HTMLElement): void;
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable;
addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable;
addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable;
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable;
registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable;
registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable;
registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number;
deregisterLinkMatcher(matcherId: number): void;
registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
@@ -205,9 +206,19 @@ export interface ILinkifier {
deregisterLinkMatcher(matcherId: number): boolean;
}
interface ILinkState {
decorations: ILinkDecorations;
isHovered: boolean;
}
export interface ILinkWithState {
link: ILink;
state?: ILinkState;
}
export interface ILinkifier2 {
onShowLinkUnderline: IEvent<ILinkifierEvent>;
onHideLinkUnderline: IEvent<ILinkifierEvent>;
readonly currentLink: ILinkWithState | undefined;
attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void;
registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
@@ -292,3 +303,10 @@ interface IBufferCellPosition {
x: number;
y: number;
}
export type CharacterJoinerHandler = (text: string) => [number, number][];
export interface ICharacterJoiner {
id: number;
handler: CharacterJoinerHandler;
}
+4 -2
View File
@@ -33,7 +33,7 @@ export class Viewport extends Disposable implements IViewport {
private _ignoreNextScrollEvent: boolean = false;
constructor(
private readonly _scrollLines: (amount: number, suppressEvent: boolean) => void,
private readonly _scrollLines: (amount: number) => void,
private readonly _viewportElement: HTMLElement,
private readonly _scrollArea: HTMLElement,
@IBufferService private readonly _bufferService: IBufferService,
@@ -151,12 +151,14 @@ export class Viewport extends Disposable implements IViewport {
// Ignore the event if it was flagged to ignore (when the source of the event is from Viewport)
if (this._ignoreNextScrollEvent) {
this._ignoreNextScrollEvent = false;
// Still trigger the scroll so lines get refreshed
this._scrollLines(0);
return;
}
const newRow = Math.round(this._lastScrollTop / this._currentRowHeight);
const diff = newRow - this._bufferService.buffer.ydisp;
this._scrollLines(diff, true);
this._scrollLines(diff);
}
/**
+2 -2
View File
@@ -5,7 +5,7 @@
import { assert } from 'chai';
import { CompositionHelper } from 'browser/input/CompositionHelper';
import { MockCharSizeService } from 'browser/TestUtils.test';
import { MockRenderService } from 'browser/TestUtils.test';
import { MockCoreService, MockBufferService, MockOptionsService } from 'common/TestUtils.test';
describe('CompositionHelper', () => {
@@ -42,7 +42,7 @@ describe('CompositionHelper', () => {
};
handledText = '';
const bufferService = new MockBufferService(10, 5);
compositionHelper = new CompositionHelper(textarea, compositionView, bufferService, new MockOptionsService(), new MockCharSizeService(10, 10), coreService);
compositionHelper = new CompositionHelper(textarea, compositionView, bufferService, new MockOptionsService(), coreService, new MockRenderService());
});
describe('Input', () => {
+11 -8
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ICharSizeService } from 'browser/services/Services';
import { IRenderService } from 'browser/services/Services';
import { IBufferService, ICoreService, IOptionsService } from 'common/services/Services';
interface IPosition {
@@ -45,8 +45,8 @@ export class CompositionHelper {
private readonly _compositionView: HTMLElement,
@IBufferService private readonly _bufferService: IBufferService,
@IOptionsService private readonly _optionsService: IOptionsService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@ICoreService private readonly _coreService: ICoreService
@ICoreService private readonly _coreService: ICoreService,
@IRenderService private readonly _renderService: IRenderService
) {
this._isComposing = false;
this._isSendingComposition = false;
@@ -207,9 +207,11 @@ export class CompositionHelper {
}
if (this._bufferService.buffer.isCursorInViewport) {
const cellHeight = Math.ceil(this._charSizeService.height * this._optionsService.options.lineHeight);
const cursorTop = this._bufferService.buffer.y * cellHeight;
const cursorLeft = this._bufferService.buffer.x * this._charSizeService.width;
const cursorX = Math.min(this._bufferService.buffer.x, this._bufferService.cols - 1);
const cellHeight = this._renderService.dimensions.actualCellHeight;
const cursorTop = this._bufferService.buffer.y * this._renderService.dimensions.actualCellHeight;
const cursorLeft = cursorX * this._renderService.dimensions.actualCellWidth;
this._compositionView.style.left = cursorLeft + 'px';
this._compositionView.style.top = cursorTop + 'px';
@@ -222,8 +224,9 @@ export class CompositionHelper {
const compositionViewBounds = this._compositionView.getBoundingClientRect();
this._textarea.style.left = cursorLeft + 'px';
this._textarea.style.top = cursorTop + 'px';
this._textarea.style.width = compositionViewBounds.width + 'px';
this._textarea.style.height = compositionViewBounds.height + 'px';
// Ensure the text area is at least 1x1, otherwise certain IMEs may break
this._textarea.style.width = Math.max(compositionViewBounds.width, 1) + 'px';
this._textarea.style.height = Math.max(compositionViewBounds.height, 1) + 'px';
this._textarea.style.lineHeight = compositionViewBounds.height + 'px';
}
+13 -12
View File
@@ -38,6 +38,7 @@ export class Terminal implements ITerminalApi {
public get onData(): IEvent<string> { return this._core.onData; }
public get onBinary(): IEvent<string> { return this._core.onBinary; }
public get onTitleChange(): IEvent<string> { return this._core.onTitleChange; }
public get onBell(): IEvent<void> { return this._core.onBell; }
public get onScroll(): IEvent<number> { return this._core.onScroll; }
public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; }
public get onRender(): IEvent<{ start: number, end: number }> { return this._core.onRender; }
@@ -292,28 +293,28 @@ class BufferLineApiView implements IBufferLineApi {
class ParserApi implements IParser {
constructor(private _core: ITerminal) { }
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable {
return this._core.addCsiHandler(id, (params: IParams) => callback(params.toArray()));
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise<boolean>): IDisposable {
return this._core.registerCsiHandler(id, (params: IParams) => callback(params.toArray()));
}
public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable {
public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise<boolean>): IDisposable {
return this.registerCsiHandler(id, callback);
}
public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable {
return this._core.addDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray()));
public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise<boolean>): IDisposable {
return this._core.registerDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray()));
}
public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable {
public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise<boolean>): IDisposable {
return this.registerDcsHandler(id, callback);
}
public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
return this._core.addEscHandler(id, handler);
public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise<boolean>): IDisposable {
return this._core.registerEscHandler(id, handler);
}
public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
public addEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise<boolean>): IDisposable {
return this.registerEscHandler(id, handler);
}
public registerOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
return this._core.addOscHandler(ident, callback);
public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
return this._core.registerOscHandler(ident, callback);
}
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
public addOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
return this.registerOscHandler(ident, callback);
}
}
+5 -5
View File
@@ -66,7 +66,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
}
private _initCanvas(): void {
this._ctx = throwIfFalsy(this._canvas.getContext('2d', {alpha: this._alpha}));
this._ctx = throwIfFalsy(this._canvas.getContext('2d', { alpha: this._alpha }));
// Draw the background if this is an opaque layer
if (!this._alpha) {
this._clearAll();
@@ -242,12 +242,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
*/
protected _fillCharTrueColor(cell: CellData, x: number, y: number): void {
this._ctx.font = this._getFont(false, false);
this._ctx.textBaseline = 'middle';
this._ctx.textBaseline = 'ideographic';
this._clipRow(y);
this._ctx.fillText(
cell.getChars(),
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
}
/**
@@ -320,7 +320,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
private _drawUncachedChars(cell: ICellData, x: number, y: number, fgOverride?: IColor): void {
this._ctx.save();
this._ctx.font = this._getFont(!!cell.isBold(), !!cell.isItalic());
this._ctx.textBaseline = 'middle';
this._ctx.textBaseline = 'ideographic';
if (cell.isInverse()) {
if (fgOverride) {
@@ -362,7 +362,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._ctx.fillText(
cell.getChars(),
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
this._ctx.restore();
}
+4 -4
View File
@@ -37,10 +37,10 @@ export class CursorRenderLayer extends BaseRenderLayer {
colors: IColorSet,
rendererId: number,
private _onRequestRedraw: IEventEmitter<IRequestRedrawEvent>,
bufferService: IBufferService,
optionsService: IOptionsService,
private readonly _coreService: ICoreService,
private readonly _coreBrowserService: ICoreBrowserService
@IBufferService bufferService: IBufferService,
@IOptionsService optionsService: IOptionsService,
@ICoreService private readonly _coreService: ICoreService,
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
) {
super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService);
this._state = {
+2 -2
View File
@@ -20,8 +20,8 @@ export class LinkRenderLayer extends BaseRenderLayer {
rendererId: number,
linkifier: ILinkifier,
linkifier2: ILinkifier2,
bufferService: IBufferService,
optionsService: IOptionsService
@IBufferService bufferService: IBufferService,
@IOptionsService optionsService: IOptionsService
) {
super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService);
linkifier.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
+8 -20
View File
@@ -6,13 +6,12 @@
import { TextRenderLayer } from 'browser/renderer/TextRenderLayer';
import { SelectionRenderLayer } from 'browser/renderer/SelectionRenderLayer';
import { CursorRenderLayer } from 'browser/renderer/CursorRenderLayer';
import { IRenderLayer, IRenderer, IRenderDimensions, CharacterJoinerHandler, ICharacterJoinerRegistry, IRequestRedrawEvent } from 'browser/renderer/Types';
import { IRenderLayer, IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
import { LinkRenderLayer } from 'browser/renderer/LinkRenderLayer';
import { CharacterJoinerRegistry } from 'browser/renderer/CharacterJoinerRegistry';
import { Disposable } from 'common/Lifecycle';
import { IColorSet, ILinkifier, ILinkifier2 } from 'browser/Types';
import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services';
import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services';
import { IBufferService, IOptionsService, ICoreService, IInstantiationService } from 'common/services/Services';
import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache';
import { EventEmitter, IEvent } from 'common/EventEmitter';
@@ -23,7 +22,6 @@ export class Renderer extends Disposable implements IRenderer {
private _renderLayers: IRenderLayer[];
private _devicePixelRatio: number;
private _characterJoinerRegistry: ICharacterJoinerRegistry;
public dimensions: IRenderDimensions;
@@ -35,20 +33,18 @@ export class Renderer extends Disposable implements IRenderer {
private readonly _screenElement: HTMLElement,
linkifier: ILinkifier,
linkifier2: ILinkifier2,
@IInstantiationService instantiationService: IInstantiationService,
@IBufferService private readonly _bufferService: IBufferService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@IOptionsService private readonly _optionsService: IOptionsService,
@ICoreService coreService: ICoreService,
@ICoreBrowserService coreBrowserService: ICoreBrowserService
@IOptionsService private readonly _optionsService: IOptionsService
) {
super();
const allowTransparency = this._optionsService.options.allowTransparency;
this._characterJoinerRegistry = new CharacterJoinerRegistry(this._bufferService);
this._renderLayers = [
new TextRenderLayer(this._screenElement, 0, this._colors, this._characterJoinerRegistry, allowTransparency, this._id, this._bufferService, _optionsService),
new SelectionRenderLayer(this._screenElement, 1, this._colors, this._id, this._bufferService, _optionsService),
new LinkRenderLayer(this._screenElement, 2, this._colors, this._id, linkifier, linkifier2, this._bufferService, _optionsService),
new CursorRenderLayer(this._screenElement, 3, this._colors, this._id, this._onRequestRedraw, this._bufferService, _optionsService, coreService, coreBrowserService)
instantiationService.createInstance(TextRenderLayer, this._screenElement, 0, this._colors, allowTransparency, this._id),
instantiationService.createInstance(SelectionRenderLayer, this._screenElement, 1, this._colors, this._id),
instantiationService.createInstance(LinkRenderLayer, this._screenElement, 2, this._colors, this._id, linkifier, linkifier2),
instantiationService.createInstance(CursorRenderLayer, this._screenElement, 3, this._colors, this._id, this._onRequestRedraw)
];
this.dimensions = {
scaledCharWidth: 0,
@@ -210,12 +206,4 @@ export class Renderer extends Disposable implements IRenderer {
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows;
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols;
}
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
return this._characterJoinerRegistry.registerCharacterJoiner(handler);
}
public deregisterCharacterJoiner(joinerId: number): boolean {
return this._characterJoinerRegistry.deregisterCharacterJoiner(joinerId);
}
}
+2 -2
View File
@@ -23,8 +23,8 @@ export class SelectionRenderLayer extends BaseRenderLayer {
zIndex: number,
colors: IColorSet,
rendererId: number,
bufferService: IBufferService,
optionsService: IOptionsService
@IBufferService bufferService: IBufferService,
@IOptionsService optionsService: IOptionsService
) {
super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService);
this._clearState();
+9 -12
View File
@@ -3,16 +3,17 @@
* @license MIT
*/
import { ICharacterJoinerRegistry, IRenderDimensions } from 'browser/renderer/Types';
import { IRenderDimensions } from 'browser/renderer/Types';
import { CharData, ICellData } from 'common/Types';
import { GridCache } from 'browser/renderer/GridCache';
import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
import { AttributeData } from 'common/buffer/AttributeData';
import { NULL_CELL_CODE, Content } from 'common/buffer/Constants';
import { JoinedCellData } from 'browser/renderer/CharacterJoinerRegistry';
import { IColorSet } from 'browser/Types';
import { CellData } from 'common/buffer/CellData';
import { IOptionsService, IBufferService } from 'common/services/Services';
import { ICharacterJoinerService } from 'browser/services/Services';
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
/**
* This CharData looks like a null character, which will forc a clear and render
@@ -26,22 +27,20 @@ export class TextRenderLayer extends BaseRenderLayer {
private _characterWidth: number = 0;
private _characterFont: string = '';
private _characterOverlapCache: { [key: string]: boolean } = {};
private _characterJoinerRegistry: ICharacterJoinerRegistry;
private _workCell = new CellData();
constructor(
container: HTMLElement,
zIndex: number,
colors: IColorSet,
characterJoinerRegistry: ICharacterJoinerRegistry,
alpha: boolean,
rendererId: number,
bufferService: IBufferService,
optionsService: IOptionsService
@IBufferService bufferService: IBufferService,
@IOptionsService optionsService: IOptionsService,
@ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService
) {
super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService);
this._state = new GridCache<CharData>();
this._characterJoinerRegistry = characterJoinerRegistry;
}
public resize(dim: IRenderDimensions): void {
@@ -67,7 +66,6 @@ export class TextRenderLayer extends BaseRenderLayer {
private _forEachCell(
firstRow: number,
lastRow: number,
joinerRegistry: ICharacterJoinerRegistry | null,
callback: (
cell: ICellData,
x: number,
@@ -77,7 +75,7 @@ export class TextRenderLayer extends BaseRenderLayer {
for (let y = firstRow; y <= lastRow; y++) {
const row = y + this._bufferService.buffer.ydisp;
const line = this._bufferService.buffer.lines.get(row);
const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : [];
const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
for (let x = 0; x < this._bufferService.cols; x++) {
line!.loadCell(x, this._workCell);
let cell = this._workCell;
@@ -101,7 +99,6 @@ export class TextRenderLayer extends BaseRenderLayer {
// We already know the exact start and end column of the joined range,
// so we get the string and width representing it directly
cell = new JoinedCellData(
this._workCell,
line!.translateToString(true, range[0], range[1]),
@@ -160,7 +157,7 @@ export class TextRenderLayer extends BaseRenderLayer {
ctx.save();
this._forEachCell(firstRow, lastRow, null, (cell, x, y) => {
this._forEachCell(firstRow, lastRow, (cell, x, y) => {
// libvte and xterm both draw the background (but not foreground) of invisible characters,
// so we should too.
let nextFillStyle = null; // null represents default background color
@@ -213,7 +210,7 @@ export class TextRenderLayer extends BaseRenderLayer {
}
private _drawForeground(firstRow: number, lastRow: number): void {
this._forEachCell(firstRow, lastRow, this._characterJoinerRegistry, (cell, x, y) => {
this._forEachCell(firstRow, lastRow, (cell, x, y) => {
if (cell.isInvisible()) {
return;
}
-25
View File
@@ -7,8 +7,6 @@ import { IDisposable } from 'common/Types';
import { IColorSet } from 'browser/Types';
import { IEvent } from 'common/EventEmitter';
export type CharacterJoinerHandler = (text: string) => [number, number][];
export interface IRenderDimensions {
scaledCharWidth: number;
scaledCharHeight: number;
@@ -54,19 +52,6 @@ export interface IRenderer extends IDisposable {
onOptionsChanged(): void;
clear(): void;
renderRows(start: number, end: number): void;
registerCharacterJoiner(handler: CharacterJoinerHandler): number;
deregisterCharacterJoiner(joinerId: number): boolean;
}
export interface ICharacterJoiner {
id: number;
handler: CharacterJoinerHandler;
}
export interface ICharacterJoinerRegistry {
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
deregisterCharacterJoiner(joinerId: number): boolean;
getJoinedCharacters(row: number): [number, number][];
}
export interface IRenderLayer extends IDisposable {
@@ -106,16 +91,6 @@ export interface IRenderLayer extends IDisposable {
*/
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
/**
* Registers a handler to join characters to render as a group
*/
registerCharacterJoiner?(joiner: ICharacterJoiner): void;
/**
* Deregisters the specified character joiner handler
*/
deregisterCharacterJoiner?(joinerId: number): void;
/**
* Resize the render layer.
*/
@@ -91,12 +91,12 @@ export class DynamicCharAtlas extends BaseCharAtlas {
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
// It might also contain some characters with transparent backgrounds if allowTransparency is
// set.
this._cacheCtx = throwIfFalsy(this._cacheCanvas.getContext('2d', {alpha: true}));
this._cacheCtx = throwIfFalsy(this._cacheCanvas.getContext('2d', { alpha: true }));
const tmpCanvas = document.createElement('canvas');
tmpCanvas.width = this._config.scaledCharWidth;
tmpCanvas.height = this._config.scaledCharHeight;
this._tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d', {alpha: this._config.allowTransparency}));
this._tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency }));
this._width = Math.floor(TEXTURE_WIDTH / this._config.scaledCharWidth);
this._height = Math.floor(TEXTURE_HEIGHT / this._config.scaledCharHeight);
@@ -256,7 +256,7 @@ export class DynamicCharAtlas extends BaseCharAtlas {
const fontStyle = glyph.italic ? 'italic' : '';
this._tmpCtx.font =
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
this._tmpCtx.textBaseline = 'middle';
this._tmpCtx.textBaseline = 'ideographic';
this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css;
@@ -265,7 +265,7 @@ export class DynamicCharAtlas extends BaseCharAtlas {
this._tmpCtx.globalAlpha = DIM_OPACITY;
}
// Draw the character
this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight / 2);
this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight);
this._tmpCtx.restore();
// clear the background from the character to avoid issues with drawing over the previous
+5 -7
View File
@@ -3,13 +3,13 @@
* @license MIT
*/
import { IRenderer, IRenderDimensions, CharacterJoinerHandler, IRequestRedrawEvent } from 'browser/renderer/Types';
import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { Disposable } from 'common/Lifecycle';
import { IColorSet, ILinkifierEvent, ILinkifier, ILinkifier2 } from 'browser/Types';
import { ICharSizeService } from 'browser/services/Services';
import { IOptionsService, IBufferService } from 'common/services/Services';
import { IOptionsService, IBufferService, IInstantiationService } from 'common/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { color } from 'browser/Color';
import { removeElementFromParent } from 'browser/Dom';
@@ -49,6 +49,7 @@ export class DomRenderer extends Disposable implements IRenderer {
private readonly _viewportElement: HTMLElement,
private readonly _linkifier: ILinkifier,
private readonly _linkifier2: ILinkifier2,
@IInstantiationService instantiationService: IInstantiationService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@IOptionsService private readonly _optionsService: IOptionsService,
@IBufferService private readonly _bufferService: IBufferService
@@ -80,7 +81,7 @@ export class DomRenderer extends Disposable implements IRenderer {
this._updateDimensions();
this._injectCss();
this._rowFactory = new DomRendererRowFactory(document, this._optionsService, this._colors);
this._rowFactory = instantiationService.createInstance(DomRendererRowFactory, document, this._colors);
this._element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass);
this._screenElement.appendChild(this._rowContainer);
@@ -364,7 +365,7 @@ export class DomRenderer extends Disposable implements IRenderer {
const row = y + this._bufferService.buffer.ydisp;
const lineData = this._bufferService.buffer.lines.get(row);
const cursorStyle = this._optionsService.options.cursorStyle;
rowElement.appendChild(this._rowFactory.createRow(lineData!, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, this._bufferService.cols));
rowElement.appendChild(this._rowFactory.createRow(lineData!, row, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, this._bufferService.cols));
}
}
@@ -372,9 +373,6 @@ export class DomRenderer extends Disposable implements IRenderer {
return `.${TERMINAL_CLASS_PREFIX}${this._terminalClass}`;
}
public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return -1; }
public deregisterCharacterJoiner(joinerId: number): boolean { return false; }
private _onLinkHover(e: ILinkifierEvent): void {
this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, true);
}
@@ -12,6 +12,7 @@ import { IBufferLine } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { MockOptionsService } from 'common/TestUtils.test';
import { css } from 'browser/Color';
import { MockCharacterJoinerService } from 'browser/TestUtils.test';
describe('DomRendererRowFactory', () => {
let dom: jsdom.JSDOM;
@@ -20,7 +21,7 @@ describe('DomRendererRowFactory', () => {
beforeEach(() => {
dom = new jsdom.JSDOM('');
rowFactory = new DomRendererRowFactory(dom.window.document, new MockOptionsService({ drawBoldTextInBrightColors: true }), {
rowFactory = new DomRendererRowFactory(dom.window.document, {
background: css.toColor('#010101'),
foreground: css.toColor('#020202'),
ansi: [
@@ -43,13 +44,13 @@ describe('DomRendererRowFactory', () => {
css.toColor('#34e2e2'),
css.toColor('#eeeeec')
]
} as any);
} as any, new MockCharacterJoinerService(), new MockOptionsService({ drawBoldTextInBrightColors: true }));
lineData = createEmptyLineData(2);
});
describe('createRow', () => {
it('should not create anything for an empty row', () => {
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
''
);
@@ -59,7 +60,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)]));
// There should be no element for the following "empty" cell
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '', 0, 0]));
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span style="width: 10px;">語</span>'
);
@@ -67,7 +68,7 @@ describe('DomRendererRowFactory', () => {
it('should add class for cursor and cursor style', () => {
for (const style of ['block', 'bar', 'underline']) {
const fragment = rowFactory.createRow(lineData, true, style, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, true, style, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-cursor xterm-cursor-${style}"> </span>`
);
@@ -75,7 +76,7 @@ describe('DomRendererRowFactory', () => {
});
it('should add class for cursor blink', () => {
const fragment = rowFactory.createRow(lineData, true, 'block', 0, true, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, true, 'block', 0, true, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-cursor xterm-cursor-blink xterm-cursor-block"> </span>`
);
@@ -84,7 +85,7 @@ describe('DomRendererRowFactory', () => {
it('should not render cells that go beyond the terminal\'s columns', () => {
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 1);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 1);
assert.equal(getFragmentHtml(fragment),
'<span>a</span>'
);
@@ -95,7 +96,7 @@ describe('DomRendererRowFactory', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.BOLD;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-bold">a</span>'
);
@@ -105,7 +106,7 @@ describe('DomRendererRowFactory', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.ITALIC;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-italic">a</span>'
);
@@ -115,7 +116,7 @@ describe('DomRendererRowFactory', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.DIM;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-dim">a</span>'
);
@@ -125,7 +126,7 @@ describe('DomRendererRowFactory', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-underline">a</span>'
);
@@ -138,7 +139,7 @@ describe('DomRendererRowFactory', () => {
cell.fg &= ~Attributes.PCOLOR_MASK;
cell.fg |= i;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-fg-${i}">a</span>`
);
@@ -152,7 +153,7 @@ describe('DomRendererRowFactory', () => {
cell.bg &= ~Attributes.PCOLOR_MASK;
cell.bg |= i;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-bg-${i}">a</span>`
);
@@ -164,7 +165,7 @@ describe('DomRendererRowFactory', () => {
cell.fg |= Attributes.CM_P16 | 2 | FgFlags.INVERSE;
cell.bg |= Attributes.CM_P16 | 1;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-1 xterm-bg-2">a</span>'
);
@@ -175,7 +176,7 @@ describe('DomRendererRowFactory', () => {
cell.fg |= FgFlags.INVERSE;
cell.bg |= Attributes.CM_P16 | 1;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-1 xterm-bg-257">a</span>'
);
@@ -185,7 +186,7 @@ describe('DomRendererRowFactory', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.fg |= Attributes.CM_P16 | 1 | FgFlags.INVERSE;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-257 xterm-bg-1">a</span>'
);
@@ -198,7 +199,7 @@ describe('DomRendererRowFactory', () => {
cell.fg &= ~Attributes.PCOLOR_MASK;
cell.fg |= i;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-bold xterm-fg-${i + 8}">a</span>`
);
@@ -210,7 +211,7 @@ describe('DomRendererRowFactory', () => {
cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3;
cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span style="color:#010203;background-color:#040506;">a</span>'
);
@@ -221,7 +222,7 @@ describe('DomRendererRowFactory', () => {
cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3 | FgFlags.INVERSE;
cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span style="color:#040506;background-color:#010203;">a</span>'
);
@@ -10,6 +10,8 @@ import { CellData } from 'common/buffer/CellData';
import { IOptionsService } from 'common/services/Services';
import { color, rgba } from 'browser/Color';
import { IColorSet, IColor } from 'browser/Types';
import { ICharacterJoinerService } from 'browser/services/Services';
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
export const BOLD_CLASS = 'xterm-bold';
export const DIM_CLASS = 'xterm-dim';
@@ -26,8 +28,9 @@ export class DomRendererRowFactory {
constructor(
private readonly _document: Document,
private readonly _optionsService: IOptionsService,
private _colors: IColorSet
private _colors: IColorSet,
@ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService,
@IOptionsService private readonly _optionsService: IOptionsService
) {
}
@@ -35,9 +38,10 @@ export class DomRendererRowFactory {
this._colors = colors;
}
public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment {
public createRow(lineData: IBufferLine, row: number, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment {
const fragment = this._document.createDocumentFragment();
const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
// Find the line length first, this prevents the need to output a bunch of
// empty cells at the end. This cannot easily be integrated into the main
// loop below because of the colCount feature (which can be removed after we
@@ -53,18 +57,58 @@ export class DomRendererRowFactory {
for (let x = 0; x < lineLength; x++) {
lineData.loadCell(x, this._workCell);
const width = this._workCell.getWidth();
let width = this._workCell.getWidth();
// The character to the left is a wide character, drawing is owned by the char at x-1
if (width === 0) {
continue;
}
// If true, indicates that the current character(s) to draw were joined.
let isJoined = false;
let lastCharX = x;
// Process any joined character ranges as needed. Because of how the
// ranges are produced, we know that they are valid for the characters
// and attributes of our input.
let cell = this._workCell;
if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
isJoined = true;
const range = joinedRanges.shift()!;
// We already know the exact start and end column of the joined range,
// so we get the string and width representing it directly
cell = new JoinedCellData(
this._workCell,
lineData.translateToString(true, range[0], range[1]),
range[1] - range[0]
);
// Skip over the cells occupied by this range in the loop
lastCharX = range[1] - 1;
// Recalculate width
width = cell.getWidth();
}
const charElement = this._document.createElement('span');
if (width > 1) {
charElement.style.width = `${cellWidth * width}px`;
}
if (isJoined) {
// Ligatures in the DOM renderer must use display inline, as they may not show with
// inline-block if they are outside the bounds of the element
charElement.style.display = 'inline';
// The DOM renderer colors the background of the cursor but for ligatures all cells are
// joined. The workaround here is to show a cursor around the whole ligature so it shows up,
// the cursor looks the same when on any character of the ligature though
if (cursorX >= x && cursorX <= lastCharX) {
cursorX = x;
}
}
if (isCursorRow && x === cursorX) {
charElement.classList.add(CURSOR_CLASS);
@@ -85,33 +129,33 @@ export class DomRendererRowFactory {
}
}
if (this._workCell.isBold()) {
if (cell.isBold()) {
charElement.classList.add(BOLD_CLASS);
}
if (this._workCell.isItalic()) {
if (cell.isItalic()) {
charElement.classList.add(ITALIC_CLASS);
}
if (this._workCell.isDim()) {
if (cell.isDim()) {
charElement.classList.add(DIM_CLASS);
}
if (this._workCell.isUnderline()) {
if (cell.isUnderline()) {
charElement.classList.add(UNDERLINE_CLASS);
}
if (this._workCell.isInvisible()) {
if (cell.isInvisible()) {
charElement.textContent = WHITESPACE_CELL_CHAR;
} else {
charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR;
charElement.textContent = cell.getChars() || WHITESPACE_CELL_CHAR;
}
let fg = this._workCell.getFgColor();
let fgColorMode = this._workCell.getFgColorMode();
let bg = this._workCell.getBgColor();
let bgColorMode = this._workCell.getBgColorMode();
const isInverse = !!this._workCell.isInverse();
let fg = cell.getFgColor();
let fgColorMode = cell.getFgColorMode();
let bg = cell.getBgColor();
let bgColorMode = cell.getBgColorMode();
const isInverse = !!cell.isInverse();
if (isInverse) {
const temp = fg;
fg = bg;
@@ -125,7 +169,7 @@ export class DomRendererRowFactory {
switch (fgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
if (this._workCell.isBold() && fg < 8 && this._optionsService.options.drawBoldTextInBrightColors) {
if (cell.isBold() && fg < 8 && this._optionsService.options.drawBoldTextInBrightColors) {
fg += 8;
}
if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.ansi[fg])) {
@@ -168,6 +212,8 @@ export class DomRendererRowFactory {
}
fragment.appendChild(charElement);
x = lastCharX;
}
return fragment;
}
@@ -4,15 +4,15 @@
*/
import { assert } from 'chai';
import { ICharacterJoinerRegistry } from 'browser/renderer/Types';
import { CharacterJoinerRegistry } from 'browser/renderer/CharacterJoinerRegistry';
import { ICharacterJoinerService } from 'browser/services/Services';
import { CharacterJoinerService } from 'browser/services/CharacterJoinerService';
import { BufferLine } from 'common/buffer/BufferLine';
import { IBufferLine } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { MockBufferService } from 'common/TestUtils.test';
describe('CharacterJoinerRegistry', () => {
let registry: ICharacterJoinerRegistry;
describe('CharacterJoinerService', () => {
let service: ICharacterJoinerService;
beforeEach(() => {
const bufferService = new MockBufferService(16, 10);
@@ -39,225 +39,225 @@ describe('CharacterJoinerRegistry', () => {
for (let i = 0; i < sub.length; ++i) line6.setCell(i + oldSize, sub.loadCell(i, new CellData()));
lines.set(6, line6);
registry = new CharacterJoinerRegistry(bufferService);
service = new CharacterJoinerService(bufferService);
});
it('has no joiners upon creation', () => {
assert.deepEqual(registry.getJoinedCharacters(0), []);
assert.deepEqual(service.getJoinedCharacters(0), []);
});
it('returns ranges matched by the registered joiners', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
service.register(substringJoiner('->'));
assert.deepEqual(
registry.getJoinedCharacters(0),
service.getJoinedCharacters(0),
[[2, 4], [7, 9], [12, 14]]
);
});
it('processes the input using all provided joiners', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
service.register(substringJoiner('->'));
assert.deepEqual(
registry.getJoinedCharacters(1),
service.getJoinedCharacters(1),
[[2, 4], [12, 14]]
);
registry.registerCharacterJoiner(substringJoiner('=>'));
service.register(substringJoiner('=>'));
assert.deepEqual(
registry.getJoinedCharacters(1),
service.getJoinedCharacters(1),
[[2, 4], [7, 9], [12, 14]]
);
});
it('removes deregistered joiners from future calls', () => {
const joiner1 = registry.registerCharacterJoiner(substringJoiner('->'));
const joiner2 = registry.registerCharacterJoiner(substringJoiner('=>'));
const joiner1 = service.register(substringJoiner('->'));
const joiner2 = service.register(substringJoiner('=>'));
assert.deepEqual(
registry.getJoinedCharacters(1),
service.getJoinedCharacters(1),
[[2, 4], [7, 9], [12, 14]]
);
registry.deregisterCharacterJoiner(joiner1);
service.deregister(joiner1);
assert.deepEqual(
registry.getJoinedCharacters(1),
service.getJoinedCharacters(1),
[[7, 9]]
);
registry.deregisterCharacterJoiner(joiner2);
service.deregister(joiner2);
assert.deepEqual(
registry.getJoinedCharacters(1),
service.getJoinedCharacters(1),
[]
);
});
it('doesn\'t process joins on differently-styled characters', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
service.register(substringJoiner('->'));
assert.deepEqual(
registry.getJoinedCharacters(2),
service.getJoinedCharacters(2),
[[2, 4], [12, 14]]
);
});
it('returns an empty list of ranges if there is nothing to be joined', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
service.register(substringJoiner('->'));
assert.deepEqual(
registry.getJoinedCharacters(3),
service.getJoinedCharacters(3),
[]
);
});
it('returns an empty list of ranges if the line is empty', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
service.register(substringJoiner('->'));
assert.deepEqual(
registry.getJoinedCharacters(4),
service.getJoinedCharacters(4),
[]
);
});
it('returns false when trying to deregister a joiner that does not exist', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
assert.deepEqual(registry.deregisterCharacterJoiner(123), false);
service.register(substringJoiner('->'));
assert.deepEqual(service.deregister(123), false);
assert.deepEqual(
registry.getJoinedCharacters(0),
service.getJoinedCharacters(0),
[[2, 4], [7, 9], [12, 14]]
);
});
it('doesn\'t process same-styled ranges that only have one character', () => {
registry.registerCharacterJoiner(substringJoiner('a'));
registry.registerCharacterJoiner(substringJoiner('b'));
registry.registerCharacterJoiner(substringJoiner('d'));
service.register(substringJoiner('a'));
service.register(substringJoiner('b'));
service.register(substringJoiner('d'));
assert.deepEqual(
registry.getJoinedCharacters(5),
service.getJoinedCharacters(5),
[[5, 6]]
);
});
it('handles ranges that extend all the way to the end of the line', () => {
registry.registerCharacterJoiner(substringJoiner('-> d'));
service.register(substringJoiner('-> d'));
assert.deepEqual(
registry.getJoinedCharacters(2),
service.getJoinedCharacters(2),
[[12, 16]]
);
});
it('handles adjacent ranges', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
registry.registerCharacterJoiner(substringJoiner('> c '));
service.register(substringJoiner('->'));
service.register(substringJoiner('> c '));
assert.deepEqual(
registry.getJoinedCharacters(2),
service.getJoinedCharacters(2),
[[2, 4], [8, 12], [12, 14]]
);
});
it('handles fullwidth characters in the middle of ranges', () => {
registry.registerCharacterJoiner(substringJoiner('wi¥de'));
service.register(substringJoiner('wi¥de'));
assert.deepEqual(
registry.getJoinedCharacters(6),
service.getJoinedCharacters(6),
[[0, 6]]
);
});
it('handles fullwidth characters at the end of ranges', () => {
registry.registerCharacterJoiner(substringJoiner('wi¥'));
service.register(substringJoiner('wi¥'));
assert.deepEqual(
registry.getJoinedCharacters(6),
service.getJoinedCharacters(6),
[[0, 4]]
);
});
it('handles emojis in the middle of ranges', () => {
registry.registerCharacterJoiner(substringJoiner('emo\xf0\x9f\x98\x81 ji'));
service.register(substringJoiner('emo\xf0\x9f\x98\x81 ji'));
assert.deepEqual(
registry.getJoinedCharacters(6),
service.getJoinedCharacters(6),
[[6, 13]]
);
});
it('handles emojis at the end of ranges', () => {
registry.registerCharacterJoiner(substringJoiner('emo\xf0\x9f\x98\x81 '));
service.register(substringJoiner('emo\xf0\x9f\x98\x81 '));
assert.deepEqual(
registry.getJoinedCharacters(6),
service.getJoinedCharacters(6),
[[6, 11]]
);
});
it('handles ranges after wide and emoji characters', () => {
registry.registerCharacterJoiner(substringJoiner('abc'));
service.register(substringJoiner('abc'));
assert.deepEqual(
registry.getJoinedCharacters(6),
service.getJoinedCharacters(6),
[[13, 16]]
);
});
describe('range merging', () => {
it('inserts a new range before the existing ones', () => {
registry.registerCharacterJoiner(() => [[1, 2], [2, 3]]);
registry.registerCharacterJoiner(() => [[0, 1]]);
service.register(() => [[1, 2], [2, 3]]);
service.register(() => [[0, 1]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
service.getJoinedCharacters(0),
[[0, 1], [1, 2], [2, 3]]
);
});
it('inserts in between two ranges', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[2, 4]]);
service.register(() => [[0, 2], [4, 6]]);
service.register(() => [[2, 4]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
service.getJoinedCharacters(0),
[[0, 2], [2, 4], [4, 6]]
);
});
it('inserts after the last range', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[6, 8]]);
service.register(() => [[0, 2], [4, 6]]);
service.register(() => [[6, 8]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
service.getJoinedCharacters(0),
[[0, 2], [4, 6], [6, 8]]
);
});
it('extends the beginning of a range', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[3, 5]]);
service.register(() => [[0, 2], [4, 6]]);
service.register(() => [[3, 5]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
service.getJoinedCharacters(0),
[[0, 2], [3, 6]]
);
});
it('extends the end of a range', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[1, 4]]);
service.register(() => [[0, 2], [4, 6]]);
service.register(() => [[1, 4]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
service.getJoinedCharacters(0),
[[0, 4], [4, 6]]
);
});
it('extends the last range', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[5, 7]]);
service.register(() => [[0, 2], [4, 6]]);
service.register(() => [[5, 7]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
service.getJoinedCharacters(0),
[[0, 2], [4, 7]]
);
});
it('connects two ranges', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[1, 5]]);
service.register(() => [[0, 2], [4, 6]]);
service.register(() => [[1, 5]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
service.getJoinedCharacters(0),
[[0, 6]]
);
});
it('connects more than two ranges', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6], [8, 10], [12, 14]]);
registry.registerCharacterJoiner(() => [[1, 10]]);
service.register(() => [[0, 2], [4, 6], [8, 10], [12, 14]]);
service.register(() => [[1, 10]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
service.getJoinedCharacters(0),
[[0, 10], [12, 14]]
);
});
@@ -4,11 +4,12 @@
*/
import { IBufferLine, ICellData, CharData } from 'common/Types';
import { ICharacterJoinerRegistry, ICharacterJoiner } from 'browser/renderer/Types';
import { ICharacterJoiner } from 'browser/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { WHITESPACE_CELL_CHAR, Content } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { IBufferService } from 'common/services/Services';
import { ICharacterJoinerService } from 'browser/services/Services';
export class JoinedCellData extends AttributeData implements ICellData {
private _width: number;
@@ -55,15 +56,18 @@ export class JoinedCellData extends AttributeData implements ICellData {
}
}
export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
export class CharacterJoinerService implements ICharacterJoinerService {
public serviceBrand: undefined;
private _characterJoiners: ICharacterJoiner[] = [];
private _nextCharacterJoinerId: number = 0;
private _workCell: CellData = new CellData();
constructor(private _bufferService: IBufferService) { }
constructor(
@IBufferService private _bufferService: IBufferService
) { }
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
public register(handler: (text: string) => [number, number][]): number {
const joiner: ICharacterJoiner = {
id: this._nextCharacterJoinerId++,
handler
@@ -73,7 +77,7 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
return joiner.id;
}
public deregisterCharacterJoiner(joinerId: number): boolean {
public deregister(joinerId: number): boolean {
for (let i = 0; i < this._characterJoiners.length; i++) {
if (this._characterJoiners[i].id === joinerId) {
this._characterJoiners.splice(i, 1);
@@ -177,7 +181,7 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
// We merge any overlapping ranges across the different joiners
const joinerRanges = this._characterJoiners[i].handler(text);
for (let j = 0; j < joinerRanges.length; j++) {
CharacterJoinerRegistry._mergeRanges(joinedRanges, joinerRanges[j]);
CharacterJoinerService._mergeRanges(joinedRanges, joinerRanges[j]);
}
}
this._stringRangesToCellRanges(joinedRanges, lineData, startCol);
+1 -9
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types';
import { IRenderer, IRenderDimensions } from 'browser/renderer/Types';
import { RenderDebouncer } from 'browser/RenderDebouncer';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
@@ -214,12 +214,4 @@ export class RenderService extends Disposable implements IRenderService {
public clear(): void {
this._renderer.clear();
}
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
return this._renderer.registerCharacterJoiner(handler);
}
public deregisterCharacterJoiner(joinerId: number): boolean {
return this._renderer.deregisterCharacterJoiner(joinerId);
}
}
@@ -21,7 +21,7 @@ class TestSelectionService extends SelectionService {
optionsService: IOptionsService,
renderService: IRenderService
) {
super(null!, null!, bufferService, new MockCoreService(), new MockMouseService(), optionsService, renderService);
super(null!, null!, null!, bufferService, new MockCoreService(), new MockMouseService(), optionsService, renderService);
}
public get model(): SelectionModel { return this._model; }
+22 -9
View File
@@ -10,11 +10,13 @@ import * as Browser from 'common/Platform';
import { SelectionModel } from 'browser/selection/SelectionModel';
import { CellData } from 'common/buffer/CellData';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { ICharSizeService, IMouseService, ISelectionService, IRenderService } from 'browser/services/Services';
import { IMouseService, ISelectionService, IRenderService } from 'browser/services/Services';
import { ILinkifier2 } from 'browser/Types';
import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services';
import { getCoordsRelativeToElement } from 'browser/input/Mouse';
import { moveToCellSequence } from 'browser/input/MoveToCell';
import { Disposable } from 'common/Lifecycle';
import { getRangeLength } from 'common/buffer/BufferRange';
/**
* The number of pixels the mouse needs to be above or below the viewport in
@@ -121,6 +123,7 @@ export class SelectionService extends Disposable implements ISelectionService {
constructor(
private readonly _element: HTMLElement,
private readonly _screenElement: HTMLElement,
private readonly _linkifier: ILinkifier2,
@IBufferService private readonly _bufferService: IBufferService,
@ICoreService private readonly _coreService: ICoreService,
@IMouseService private readonly _mouseService: IMouseService,
@@ -316,13 +319,23 @@ export class SelectionService extends Disposable implements ISelectionService {
* Selects word at the current mouse event coordinates.
* @param event The mouse event.
*/
private _selectWordAtCursor(event: MouseEvent): void {
private _selectWordAtCursor(event: MouseEvent, allowWhitespaceOnlySelection: boolean): boolean {
// Check if there is a link under the cursor first and select that if so
const range = this._linkifier.currentLink?.link?.range;
if (range) {
this._model.selectionStart = [range.start.x - 1, range.start.y - 1];
this._model.selectionStartLength = getRangeLength(range, this._bufferService.cols);
this._model.selectionEnd = undefined;
return true;
}
const coords = this._getMouseBufferCoords(event);
if (coords) {
this._selectWordAt(coords, false);
this._selectWordAt(coords, allowWhitespaceOnlySelection);
this._model.selectionEnd = undefined;
this.refresh(true);
return true;
}
return false;
}
/**
@@ -527,14 +540,12 @@ export class SelectionService extends Disposable implements ISelectionService {
}
/**
* Performs a double click, selecting the current work.
* Performs a double click, selecting the current word.
* @param event The mouse event.
*/
private _onDoubleClick(event: MouseEvent): void {
const coords = this._getMouseBufferCoords(event);
if (coords) {
if (this._selectWordAtCursor(event, true)) {
this._activeSelectionMode = SelectionMode.WORD;
this._selectWordAt(coords, true);
}
}
@@ -764,7 +775,9 @@ export class SelectionService extends Disposable implements ISelectionService {
public rightClickSelect(ev: MouseEvent): void {
if (!this._isClickInSelection(ev)) {
this._selectWordAtCursor(ev);
if (this._selectWordAtCursor(ev, false)) {
this.refresh(true);
}
this._fireEventIfSelectionChanged();
}
}
+11 -3
View File
@@ -4,7 +4,7 @@
*/
import { IEvent } from 'common/EventEmitter';
import { IRenderDimensions, IRenderer, CharacterJoinerHandler } from 'browser/renderer/Types';
import { IRenderDimensions, IRenderer } from 'browser/renderer/Types';
import { IColorSet } from 'browser/Types';
import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
import { createDecorator } from 'common/services/ServiceRegistry';
@@ -66,8 +66,6 @@ export interface IRenderService extends IDisposable {
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
onCursorMove(): void;
clear(): void;
registerCharacterJoiner(handler: CharacterJoinerHandler): number;
deregisterCharacterJoiner(joinerId: number): boolean;
}
export const ISelectionService = createDecorator<ISelectionService>('SelectionService');
@@ -104,3 +102,13 @@ export interface ISoundService {
playBellSound(): void;
}
export const ICharacterJoinerService = createDecorator<ICharacterJoinerService>('CharacterJoinerService');
export interface ICharacterJoinerService {
serviceBrand: undefined;
register(handler: (text: string) => [number, number][]): number;
deregister(joinerId: number): boolean;
getJoinedCharacters(row: number): [number, number][];
}
+2 -2
View File
@@ -158,7 +158,7 @@ export class CircularList<T> implements ICircularList<T> {
this._array[this._getCyclicIndex(i)] = this._array[this._getCyclicIndex(i + deleteCount)];
}
this._length -= deleteCount;
this.onDeleteEmitter.fire({index: start, amount: deleteCount});
this.onDeleteEmitter.fire({ index: start, amount: deleteCount });
}
// Add items
@@ -169,7 +169,7 @@ export class CircularList<T> implements ICircularList<T> {
this._array[this._getCyclicIndex(start + i)] = items[i];
}
if (items.length) {
this.onInsertEmitter.fire({index: start, amount: items.length});
this.onInsertEmitter.fire({ index: start, amount: items.length });
}
// Adjust length as needed
+56 -107
View File
@@ -22,12 +22,12 @@
*/
import { Disposable } from 'common/Lifecycle';
import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService } from 'common/services/Services';
import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService, LogLevelEnum } from 'common/services/Services';
import { InstantiationService } from 'common/services/InstantiationService';
import { LogService } from 'common/services/LogService';
import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService';
import { OptionsService } from 'common/services/OptionsService';
import { ITerminalOptions, IDisposable, IBufferLine, IAttributeData, ICoreTerminal } from 'common/Types';
import { ITerminalOptions, IDisposable, IBufferLine, IAttributeData, ICoreTerminal, IKeyboardEvent, IScrollEvent, ScrollSource } from 'common/Types';
import { CoreService } from 'common/services/CoreService';
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
import { CoreMouseService } from 'common/services/CoreMouseService';
@@ -40,6 +40,9 @@ import { IBufferSet } from 'common/buffer/Types';
import { InputHandler } from 'common/InputHandler';
import { WriteBuffer } from 'common/input/WriteBuffer';
// Only trigger this warning a single time per session
let hasWriteSyncWarnHappened = false;
export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
protected readonly _instantiationService: IInstantiationService;
protected readonly _bufferService: IBufferService;
@@ -55,8 +58,6 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
protected _inputHandler: InputHandler;
private _writeBuffer: WriteBuffer;
private _windowsMode: IDisposable | undefined;
/** An IBufferline to clone/copy from for new blank lines */
private _cachedBlankLine: IBufferLine | undefined;
private _onBinary = new EventEmitter<string>();
public get onBinary(): IEvent<string> { return this._onBinary.event; }
@@ -66,8 +67,21 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
public get onLineFeed(): IEvent<void> { return this._onLineFeed.event; }
private _onResize = new EventEmitter<{ cols: number, rows: number }>();
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; }
protected _onScroll = new EventEmitter<number>();
public get onScroll(): IEvent<number> { return this._onScroll.event; }
protected _onScroll = new EventEmitter<IScrollEvent, void>();
/**
* Internally we track the source of the scroll but this is meaningless outside the library so
* it's filtered out.
*/
protected _onScrollApi?: EventEmitter<number, void>;
public get onScroll(): IEvent<number, void> {
if (!this._onScrollApi) {
this._onScrollApi = new EventEmitter<number, void>();
this.register(this._onScroll.event(ev => {
this._onScrollApi?.fire(ev.position);
}));
}
return this._onScrollApi.event;
}
public get cols(): number { return this._bufferService.cols; }
public get rows(): number { return this._bufferService.rows; }
@@ -107,9 +121,13 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this.register(forwardEvent(this._coreService.onData, this._onData));
this.register(forwardEvent(this._coreService.onBinary, this._onBinary));
this.register(this.optionsService.onOptionChange(key => this._updateOptions(key)));
this.register(this._bufferService.onScroll(event => {
this._onScroll.fire({ position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL });
this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
}));
// Setup WriteBuffer
this._writeBuffer = new WriteBuffer(data => this._inputHandler.parse(data));
this._writeBuffer = new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult));
}
public dispose(): void {
@@ -125,8 +143,21 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this._writeBuffer.write(data, callback);
}
public writeSync(data: string | Uint8Array): void {
this._writeBuffer.writeSync(data);
/**
* Write data to terminal synchonously.
*
* This method is unreliable with async parser handlers, thus should not
* be used anymore. If you need blocking semantics on data input consider
* `write` with a callback instead.
*
* @deprecated Unreliable, will be removed soon.
*/
public writeSync(data: string | Uint8Array, maxSubsequentCalls?: number): void {
if (this._logService.logLevel <= LogLevelEnum.WARN && !hasWriteSyncWarnHappened) {
this._logService.warn('writeSync is unreliable and will be removed soon.');
hasWriteSyncWarnHappened = true;
}
this._writeBuffer.writeSync(data, maxSubsequentCalls);
}
public resize(x: number, y: number): void {
@@ -145,66 +176,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
* @param isWrapped Whether the new line is wrapped from the previous line.
*/
public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void {
const buffer = this._bufferService.buffer;
let newLine: IBufferLine | undefined;
newLine = this._cachedBlankLine;
if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) {
newLine = buffer.getBlankLine(eraseAttr, isWrapped);
this._cachedBlankLine = newLine;
}
newLine.isWrapped = isWrapped;
const topRow = buffer.ybase + buffer.scrollTop;
const bottomRow = buffer.ybase + buffer.scrollBottom;
if (buffer.scrollTop === 0) {
// Determine whether the buffer is going to be trimmed after insertion.
const willBufferBeTrimmed = buffer.lines.isFull;
// Insert the line using the fastest method
if (bottomRow === buffer.lines.length - 1) {
if (willBufferBeTrimmed) {
buffer.lines.recycle().copyFrom(newLine);
} else {
buffer.lines.push(newLine.clone());
}
} else {
buffer.lines.splice(bottomRow + 1, 0, newLine.clone());
}
// Only adjust ybase and ydisp when the buffer is not trimmed
if (!willBufferBeTrimmed) {
buffer.ybase++;
// Only scroll the ydisp with ybase if the user has not scrolled up
if (!this._bufferService.isUserScrolling) {
buffer.ydisp++;
}
} else {
// When the buffer is full and the user has scrolled up, keep the text
// stable unless ydisp is right at the top
if (this._bufferService.isUserScrolling) {
buffer.ydisp = Math.max(buffer.ydisp - 1, 0);
}
}
} else {
// scrollTop is non-zero which means no line will be going to the
// scrollback, instead we can just shift them in-place.
const scrollRegionHeight = bottomRow - topRow + 1 /* as it's zero-based */;
buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1);
buffer.lines.set(bottomRow, newLine.clone());
}
// Move the viewport to the bottom of the buffer unless the user is
// scrolling.
if (!this._bufferService.isUserScrolling) {
buffer.ydisp = buffer.ybase;
}
// Flag rows that need updating
this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom);
this._onScroll.fire(buffer.ydisp);
this._bufferService.scroll(eraseAttr, isWrapped);
}
/**
@@ -214,28 +186,8 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
* to avoid unwanted events being handled by the viewport when the event was triggered from the
* viewport originally.
*/
public scrollLines(disp: number, suppressScrollEvent?: boolean): void {
const buffer = this._bufferService.buffer;
if (disp < 0) {
if (buffer.ydisp === 0) {
return;
}
this._bufferService.isUserScrolling = true;
} else if (disp + buffer.ydisp >= buffer.ybase) {
this._bufferService.isUserScrolling = false;
}
const oldYdisp = buffer.ydisp;
buffer.ydisp = Math.max(Math.min(buffer.ydisp + disp, buffer.ybase), 0);
// No change occurred, don't trigger scroll/refresh
if (oldYdisp === buffer.ydisp) {
return;
}
if (!suppressScrollEvent) {
this._onScroll.fire(buffer.ydisp);
}
public scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void {
this._bufferService.scrollLines(disp, suppressScrollEvent, source);
}
/**
@@ -243,48 +195,45 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
* @param pageCount The number of pages to scroll (negative scrolls up).
*/
public scrollPages(pageCount: number): void {
this.scrollLines(pageCount * (this.rows - 1));
this._bufferService.scrollPages(pageCount);
}
/**
* Scrolls the display of the terminal to the top.
*/
public scrollToTop(): void {
this.scrollLines(-this._bufferService.buffer.ydisp);
this._bufferService.scrollToTop();
}
/**
* Scrolls the display of the terminal to the bottom.
*/
public scrollToBottom(): void {
this.scrollLines(this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp);
this._bufferService.scrollToBottom();
}
public scrollToLine(line: number): void {
const scrollAmount = line - this._bufferService.buffer.ydisp;
if (scrollAmount !== 0) {
this.scrollLines(scrollAmount);
}
this._bufferService.scrollToLine(line);
}
/** Add handler for ESC escape sequence. See xterm.d.ts for details. */
public addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable {
return this._inputHandler.addEscHandler(id, callback);
public registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable {
return this._inputHandler.registerEscHandler(id, callback);
}
/** Add handler for DCS escape sequence. See xterm.d.ts for details. */
public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable {
return this._inputHandler.addDcsHandler(id, callback);
public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable {
return this._inputHandler.registerDcsHandler(id, callback);
}
/** Add handler for CSI escape sequence. See xterm.d.ts for details. */
public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable {
return this._inputHandler.addCsiHandler(id, callback);
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable {
return this._inputHandler.registerCsiHandler(id, callback);
}
/** Add handler for OSC escape sequence. See xterm.d.ts for details. */
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
return this._inputHandler.addOscHandler(ident, callback);
public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
return this._inputHandler.registerOscHandler(ident, callback);
}
protected _setup(): void {
@@ -322,7 +271,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
if (!this._windowsMode) {
const disposables: IDisposable[] = [];
disposables.push(this.onLineFeed(updateWindowsModeWrappedState.bind(null, this._bufferService)));
disposables.push(this.addCsiHandler({ final: 'H' }, () => {
disposables.push(this.registerCsiHandler({ final: 'H' }, () => {
updateWindowsModeWrappedState(this._bufferService);
return false;
}));
File diff suppressed because it is too large Load Diff
+176 -100
View File
@@ -4,7 +4,7 @@
* @license MIT
*/
import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IAnsiColorChangeEvent } from 'common/Types';
import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IAnsiColorChangeEvent, IParseStack } from 'common/Types';
import { C0, C1 } from 'common/data/EscapeSequences';
import { CHARSETS, DEFAULT_CHARSET } from 'common/data/Charsets';
import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser';
@@ -17,14 +17,14 @@ import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IFunctionId
import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { AttributeData } from 'common/buffer/AttributeData';
import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService } from 'common/services/Services';
import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum } from 'common/services/Services';
import { OscHandler } from 'common/parser/OscParser';
import { DcsHandler } from 'common/parser/DcsParser';
/**
* Map collect to glevel. Used in `selectCharset`.
*/
const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2};
const GLEVEL: {[key: string]: number} = { '(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2 };
/**
* VT commands done by the parser - FIXME: move this to the parser?
@@ -97,6 +97,9 @@ export enum WindowsOptionsReportType {
GET_CELL_SIZE_PIXELS = 1
}
// create a warning log if an async handler takes longer than the limit (in ms)
const SLOW_ASYNC_LIMIT = 5000;
/**
* DCS subparser implementations
*/
@@ -171,7 +174,7 @@ class DECRQSS implements IDcsHandler {
this._coreService.triggerDataEvent(`${C0.ESC}P1$r0m${C0.ESC}\\`);
break;
case ' q': // DECSCUSR
const STYLES: {[key: string]: number} = {'block': 2, 'underline': 4, 'bar': 6};
const STYLES: {[key: string]: number} = { 'block': 2, 'underline': 4, 'bar': 6 };
let style = STYLES[this._optionsService.options.cursorStyle];
style -= this._optionsService.options.cursorBlink ? 1 : 0;
this._coreService.triggerDataEvent(`${C0.ESC}P1$r${style} q${C0.ESC}\\`);
@@ -237,8 +240,6 @@ export class InputHandler extends Disposable implements IInputHandler {
public get onRequestRefreshRows(): IEvent<number, number> { return this._onRequestRefreshRows.event; }
private _onRequestReset = new EventEmitter<void>();
public get onRequestReset(): IEvent<void> { return this._onRequestReset.event; }
private _onRequestScroll = new EventEmitter<IAttributeData, boolean | void>();
public get onRequestScroll(): IEvent<IAttributeData, boolean | void> { return this._onRequestScroll.event; }
private _onRequestSyncScrollBar = new EventEmitter<void>();
public get onRequestSyncScrollBar(): IEvent<void> { return this._onRequestSyncScrollBar.event; }
private _onRequestWindowsOptionsReport = new EventEmitter<WindowsOptionsReportType>();
@@ -259,6 +260,14 @@ export class InputHandler extends Disposable implements IInputHandler {
private _onAnsiColorChange = new EventEmitter<IAnsiColorChangeEvent>();
public get onAnsiColorChange(): IEvent<IAnsiColorChangeEvent> { return this._onAnsiColorChange.event; }
private _parseStack: IParseStack = {
paused: false,
cursorStartX: 0,
cursorStartY: 0,
decodedLength: 0,
position: 0
};
constructor(
private readonly _bufferService: IBufferService,
private readonly _charsetService: ICharsetService,
@@ -303,53 +312,53 @@ export class InputHandler extends Disposable implements IInputHandler {
/**
* CSI handler
*/
this._parser.registerCsiHandler({final: '@'}, params => this.insertChars(params));
this._parser.registerCsiHandler({intermediates: ' ', final: '@'}, params => this.scrollLeft(params));
this._parser.registerCsiHandler({final: 'A'}, params => this.cursorUp(params));
this._parser.registerCsiHandler({intermediates: ' ', final: 'A'}, params => this.scrollRight(params));
this._parser.registerCsiHandler({final: 'B'}, params => this.cursorDown(params));
this._parser.registerCsiHandler({final: 'C'}, params => this.cursorForward(params));
this._parser.registerCsiHandler({final: 'D'}, params => this.cursorBackward(params));
this._parser.registerCsiHandler({final: 'E'}, params => this.cursorNextLine(params));
this._parser.registerCsiHandler({final: 'F'}, params => this.cursorPrecedingLine(params));
this._parser.registerCsiHandler({final: 'G'}, params => this.cursorCharAbsolute(params));
this._parser.registerCsiHandler({final: 'H'}, params => this.cursorPosition(params));
this._parser.registerCsiHandler({final: 'I'}, params => this.cursorForwardTab(params));
this._parser.registerCsiHandler({final: 'J'}, params => this.eraseInDisplay(params));
this._parser.registerCsiHandler({prefix: '?', final: 'J'}, params => this.eraseInDisplay(params));
this._parser.registerCsiHandler({final: 'K'}, params => this.eraseInLine(params));
this._parser.registerCsiHandler({prefix: '?', final: 'K'}, params => this.eraseInLine(params));
this._parser.registerCsiHandler({final: 'L'}, params => this.insertLines(params));
this._parser.registerCsiHandler({final: 'M'}, params => this.deleteLines(params));
this._parser.registerCsiHandler({final: 'P'}, params => this.deleteChars(params));
this._parser.registerCsiHandler({final: 'S'}, params => this.scrollUp(params));
this._parser.registerCsiHandler({final: 'T'}, params => this.scrollDown(params));
this._parser.registerCsiHandler({final: 'X'}, params => this.eraseChars(params));
this._parser.registerCsiHandler({final: 'Z'}, params => this.cursorBackwardTab(params));
this._parser.registerCsiHandler({final: '`'}, params => this.charPosAbsolute(params));
this._parser.registerCsiHandler({final: 'a'}, params => this.hPositionRelative(params));
this._parser.registerCsiHandler({final: 'b'}, params => this.repeatPrecedingCharacter(params));
this._parser.registerCsiHandler({final: 'c'}, params => this.sendDeviceAttributesPrimary(params));
this._parser.registerCsiHandler({prefix: '>', final: 'c'}, params => this.sendDeviceAttributesSecondary(params));
this._parser.registerCsiHandler({final: 'd'}, params => this.linePosAbsolute(params));
this._parser.registerCsiHandler({final: 'e'}, params => this.vPositionRelative(params));
this._parser.registerCsiHandler({final: 'f'}, params => this.hVPosition(params));
this._parser.registerCsiHandler({final: 'g'}, params => this.tabClear(params));
this._parser.registerCsiHandler({final: 'h'}, params => this.setMode(params));
this._parser.registerCsiHandler({prefix: '?', final: 'h'}, params => this.setModePrivate(params));
this._parser.registerCsiHandler({final: 'l'}, params => this.resetMode(params));
this._parser.registerCsiHandler({prefix: '?', final: 'l'}, params => this.resetModePrivate(params));
this._parser.registerCsiHandler({final: 'm'}, params => this.charAttributes(params));
this._parser.registerCsiHandler({final: 'n'}, params => this.deviceStatus(params));
this._parser.registerCsiHandler({prefix: '?', final: 'n'}, params => this.deviceStatusPrivate(params));
this._parser.registerCsiHandler({intermediates: '!', final: 'p'}, params => this.softReset(params));
this._parser.registerCsiHandler({intermediates: ' ', final: 'q'}, params => this.setCursorStyle(params));
this._parser.registerCsiHandler({final: 'r'}, params => this.setScrollRegion(params));
this._parser.registerCsiHandler({final: 's'}, params => this.saveCursor(params));
this._parser.registerCsiHandler({final: 't'}, params => this.windowOptions(params));
this._parser.registerCsiHandler({final: 'u'}, params => this.restoreCursor(params));
this._parser.registerCsiHandler({intermediates: '\'', final: '}'}, params => this.insertColumns(params));
this._parser.registerCsiHandler({intermediates: '\'', final: '~'}, params => this.deleteColumns(params));
this._parser.registerCsiHandler({ final: '@' }, params => this.insertChars(params));
this._parser.registerCsiHandler({ intermediates: ' ', final: '@' }, params => this.scrollLeft(params));
this._parser.registerCsiHandler({ final: 'A' }, params => this.cursorUp(params));
this._parser.registerCsiHandler({ intermediates: ' ', final: 'A' }, params => this.scrollRight(params));
this._parser.registerCsiHandler({ final: 'B' }, params => this.cursorDown(params));
this._parser.registerCsiHandler({ final: 'C' }, params => this.cursorForward(params));
this._parser.registerCsiHandler({ final: 'D' }, params => this.cursorBackward(params));
this._parser.registerCsiHandler({ final: 'E' }, params => this.cursorNextLine(params));
this._parser.registerCsiHandler({ final: 'F' }, params => this.cursorPrecedingLine(params));
this._parser.registerCsiHandler({ final: 'G' }, params => this.cursorCharAbsolute(params));
this._parser.registerCsiHandler({ final: 'H' }, params => this.cursorPosition(params));
this._parser.registerCsiHandler({ final: 'I' }, params => this.cursorForwardTab(params));
this._parser.registerCsiHandler({ final: 'J' }, params => this.eraseInDisplay(params));
this._parser.registerCsiHandler({ prefix: '?', final: 'J' }, params => this.eraseInDisplay(params));
this._parser.registerCsiHandler({ final: 'K' }, params => this.eraseInLine(params));
this._parser.registerCsiHandler({ prefix: '?', final: 'K' }, params => this.eraseInLine(params));
this._parser.registerCsiHandler({ final: 'L' }, params => this.insertLines(params));
this._parser.registerCsiHandler({ final: 'M' }, params => this.deleteLines(params));
this._parser.registerCsiHandler({ final: 'P' }, params => this.deleteChars(params));
this._parser.registerCsiHandler({ final: 'S' }, params => this.scrollUp(params));
this._parser.registerCsiHandler({ final: 'T' }, params => this.scrollDown(params));
this._parser.registerCsiHandler({ final: 'X' }, params => this.eraseChars(params));
this._parser.registerCsiHandler({ final: 'Z' }, params => this.cursorBackwardTab(params));
this._parser.registerCsiHandler({ final: '`' }, params => this.charPosAbsolute(params));
this._parser.registerCsiHandler({ final: 'a' }, params => this.hPositionRelative(params));
this._parser.registerCsiHandler({ final: 'b' }, params => this.repeatPrecedingCharacter(params));
this._parser.registerCsiHandler({ final: 'c' }, params => this.sendDeviceAttributesPrimary(params));
this._parser.registerCsiHandler({ prefix: '>', final: 'c' }, params => this.sendDeviceAttributesSecondary(params));
this._parser.registerCsiHandler({ final: 'd' }, params => this.linePosAbsolute(params));
this._parser.registerCsiHandler({ final: 'e' }, params => this.vPositionRelative(params));
this._parser.registerCsiHandler({ final: 'f' }, params => this.hVPosition(params));
this._parser.registerCsiHandler({ final: 'g' }, params => this.tabClear(params));
this._parser.registerCsiHandler({ final: 'h' }, params => this.setMode(params));
this._parser.registerCsiHandler({ prefix: '?', final: 'h' }, params => this.setModePrivate(params));
this._parser.registerCsiHandler({ final: 'l' }, params => this.resetMode(params));
this._parser.registerCsiHandler({ prefix: '?', final: 'l' }, params => this.resetModePrivate(params));
this._parser.registerCsiHandler({ final: 'm' }, params => this.charAttributes(params));
this._parser.registerCsiHandler({ final: 'n' }, params => this.deviceStatus(params));
this._parser.registerCsiHandler({ prefix: '?', final: 'n' }, params => this.deviceStatusPrivate(params));
this._parser.registerCsiHandler({ intermediates: '!', final: 'p' }, params => this.softReset(params));
this._parser.registerCsiHandler({ intermediates: ' ', final: 'q' }, params => this.setCursorStyle(params));
this._parser.registerCsiHandler({ final: 'r' }, params => this.setScrollRegion(params));
this._parser.registerCsiHandler({ final: 's' }, params => this.saveCursor(params));
this._parser.registerCsiHandler({ final: 't' }, params => this.windowOptions(params));
this._parser.registerCsiHandler({ final: 'u' }, params => this.restoreCursor(params));
this._parser.registerCsiHandler({ intermediates: '\'', final: '}' }, params => this.insertColumns(params));
this._parser.registerCsiHandler({ intermediates: '\'', final: '~' }, params => this.deleteColumns(params));
/**
* execute handler
@@ -415,32 +424,32 @@ export class InputHandler extends Disposable implements IInputHandler {
/**
* ESC handlers
*/
this._parser.registerEscHandler({final: '7'}, () => this.saveCursor());
this._parser.registerEscHandler({final: '8'}, () => this.restoreCursor());
this._parser.registerEscHandler({final: 'D'}, () => this.index());
this._parser.registerEscHandler({final: 'E'}, () => this.nextLine());
this._parser.registerEscHandler({final: 'H'}, () => this.tabSet());
this._parser.registerEscHandler({final: 'M'}, () => this.reverseIndex());
this._parser.registerEscHandler({final: '='}, () => this.keypadApplicationMode());
this._parser.registerEscHandler({final: '>'}, () => this.keypadNumericMode());
this._parser.registerEscHandler({final: 'c'}, () => this.fullReset());
this._parser.registerEscHandler({final: 'n'}, () => this.setgLevel(2));
this._parser.registerEscHandler({final: 'o'}, () => this.setgLevel(3));
this._parser.registerEscHandler({final: '|'}, () => this.setgLevel(3));
this._parser.registerEscHandler({final: '}'}, () => this.setgLevel(2));
this._parser.registerEscHandler({final: '~'}, () => this.setgLevel(1));
this._parser.registerEscHandler({intermediates: '%', final: '@'}, () => this.selectDefaultCharset());
this._parser.registerEscHandler({intermediates: '%', final: 'G'}, () => this.selectDefaultCharset());
this._parser.registerEscHandler({ final: '7' }, () => this.saveCursor());
this._parser.registerEscHandler({ final: '8' }, () => this.restoreCursor());
this._parser.registerEscHandler({ final: 'D' }, () => this.index());
this._parser.registerEscHandler({ final: 'E' }, () => this.nextLine());
this._parser.registerEscHandler({ final: 'H' }, () => this.tabSet());
this._parser.registerEscHandler({ final: 'M' }, () => this.reverseIndex());
this._parser.registerEscHandler({ final: '=' }, () => this.keypadApplicationMode());
this._parser.registerEscHandler({ final: '>' }, () => this.keypadNumericMode());
this._parser.registerEscHandler({ final: 'c' }, () => this.fullReset());
this._parser.registerEscHandler({ final: 'n' }, () => this.setgLevel(2));
this._parser.registerEscHandler({ final: 'o' }, () => this.setgLevel(3));
this._parser.registerEscHandler({ final: '|' }, () => this.setgLevel(3));
this._parser.registerEscHandler({ final: '}' }, () => this.setgLevel(2));
this._parser.registerEscHandler({ final: '~' }, () => this.setgLevel(1));
this._parser.registerEscHandler({ intermediates: '%', final: '@' }, () => this.selectDefaultCharset());
this._parser.registerEscHandler({ intermediates: '%', final: 'G' }, () => this.selectDefaultCharset());
for (const flag in CHARSETS) {
this._parser.registerEscHandler({intermediates: '(', final: flag}, () => this.selectCharset('(' + flag));
this._parser.registerEscHandler({intermediates: ')', final: flag}, () => this.selectCharset(')' + flag));
this._parser.registerEscHandler({intermediates: '*', final: flag}, () => this.selectCharset('*' + flag));
this._parser.registerEscHandler({intermediates: '+', final: flag}, () => this.selectCharset('+' + flag));
this._parser.registerEscHandler({intermediates: '-', final: flag}, () => this.selectCharset('-' + flag));
this._parser.registerEscHandler({intermediates: '.', final: flag}, () => this.selectCharset('.' + flag));
this._parser.registerEscHandler({intermediates: '/', final: flag}, () => this.selectCharset('/' + flag)); // TODO: supported?
this._parser.registerEscHandler({ intermediates: '(', final: flag }, () => this.selectCharset('(' + flag));
this._parser.registerEscHandler({ intermediates: ')', final: flag }, () => this.selectCharset(')' + flag));
this._parser.registerEscHandler({ intermediates: '*', final: flag }, () => this.selectCharset('*' + flag));
this._parser.registerEscHandler({ intermediates: '+', final: flag }, () => this.selectCharset('+' + flag));
this._parser.registerEscHandler({ intermediates: '-', final: flag }, () => this.selectCharset('-' + flag));
this._parser.registerEscHandler({ intermediates: '.', final: flag }, () => this.selectCharset('.' + flag));
this._parser.registerEscHandler({ intermediates: '/', final: flag }, () => this.selectCharset('/' + flag)); // TODO: supported?
}
this._parser.registerEscHandler({intermediates: '#', final: '8'}, () => this.screenAlignmentPattern());
this._parser.registerEscHandler({ intermediates: '#', final: '8' }, () => this.screenAlignmentPattern());
/**
* error handler
@@ -453,17 +462,71 @@ export class InputHandler extends Disposable implements IInputHandler {
/**
* DCS handler
*/
this._parser.registerDcsHandler({intermediates: '$', final: 'q'}, new DECRQSS(this._bufferService, this._coreService, this._logService, this._optionsService));
this._parser.registerDcsHandler({ intermediates: '$', final: 'q' }, new DECRQSS(this._bufferService, this._coreService, this._logService, this._optionsService));
}
public dispose(): void {
super.dispose();
}
public parse(data: string | Uint8Array): void {
/**
* Async parse support.
*/
private _preserveStack(cursorStartX: number, cursorStartY: number, decodedLength: number, position: number): void {
this._parseStack.paused = true;
this._parseStack.cursorStartX = cursorStartX;
this._parseStack.cursorStartY = cursorStartY;
this._parseStack.decodedLength = decodedLength;
this._parseStack.position = position;
}
private _logSlowResolvingAsync(p: Promise<boolean>): void {
// log a limited warning about an async handler taking too long
if (this._logService.logLevel <= LogLevelEnum.WARN) {
Promise.race([p, new Promise((res, rej) => setTimeout(() => rej('#SLOW_TIMEOUT'), SLOW_ASYNC_LIMIT))])
.catch(err => {
if (err !== '#SLOW_TIMEOUT') {
throw err;
}
console.warn(`async parser handler taking longer than ${SLOW_ASYNC_LIMIT} ms`);
});
}
}
/**
* Parse call with async handler support.
*
* Whether the stack state got preserved for the next call, is indicated by the return value:
* - undefined (void):
* all handlers were sync, no stack save, continue normally with next chunk
* - Promise\<boolean\>:
* execution stopped at async handler, stack saved, continue with
* same chunk and the promise resolve value as `promiseResult` until the method returns `undefined`
*
* Note: This method should only be called by `Terminal.write` to ensure correct execution order and
* proper continuation of async parser handlers.
*/
public parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise<boolean> {
let result: void | Promise<boolean>;
let buffer = this._bufferService.buffer;
const cursorStartX = buffer.x;
const cursorStartY = buffer.y;
let cursorStartX = buffer.x;
let cursorStartY = buffer.y;
let start = 0;
const wasPaused = this._parseStack.paused;
if (wasPaused) {
// assumption: _parseBuffer never mutates between async calls
if (result = this._parser.parse(this._parseBuffer, this._parseStack.decodedLength, promiseResult)) {
this._logSlowResolvingAsync(result);
return result;
}
cursorStartX = this._parseStack.cursorStartX;
cursorStartY = this._parseStack.cursorStartY;
this._parseStack.paused = false;
if (data.length > MAX_PARSEBUFFER_LENGTH) {
start = this._parseStack.position + MAX_PARSEBUFFER_LENGTH;
}
}
this._logService.debug('parsing data', data);
@@ -475,22 +538,35 @@ export class InputHandler extends Disposable implements IInputHandler {
}
// Clear the dirty row service so we know which lines changed as a result of parsing
this._dirtyRowService.clearRange();
// Important: do not clear between async calls, otherwise we lost pending update information.
if (!wasPaused) {
this._dirtyRowService.clearRange();
}
// process big data in smaller chunks
if (data.length > MAX_PARSEBUFFER_LENGTH) {
for (let i = 0; i < data.length; i += MAX_PARSEBUFFER_LENGTH) {
for (let i = start; i < data.length; i += MAX_PARSEBUFFER_LENGTH) {
const end = i + MAX_PARSEBUFFER_LENGTH < data.length ? i + MAX_PARSEBUFFER_LENGTH : data.length;
const len = (typeof data === 'string')
? this._stringDecoder.decode(data.substring(i, end), this._parseBuffer)
: this._utf8Decoder.decode(data.subarray(i, end), this._parseBuffer);
this._parser.parse(this._parseBuffer, len);
if (result = this._parser.parse(this._parseBuffer, len)) {
this._preserveStack(cursorStartX, cursorStartY, len, i);
this._logSlowResolvingAsync(result);
return result;
}
}
} else {
const len = (typeof data === 'string')
? this._stringDecoder.decode(data, this._parseBuffer)
: this._utf8Decoder.decode(data, this._parseBuffer);
this._parser.parse(this._parseBuffer, len);
if (!wasPaused) {
const len = (typeof data === 'string')
? this._stringDecoder.decode(data, this._parseBuffer)
: this._utf8Decoder.decode(data, this._parseBuffer);
if (result = this._parser.parse(this._parseBuffer, len)) {
this._preserveStack(cursorStartX, cursorStartY, len, 0);
this._logSlowResolvingAsync(result);
return result;
}
}
}
buffer = this._bufferService.buffer;
@@ -573,7 +649,7 @@ export class InputHandler extends Disposable implements IInputHandler {
buffer.y++;
if (buffer.y === buffer.scrollBottom + 1) {
buffer.y--;
this._onRequestScroll.fire(this._eraseAttrData(), true);
this._bufferService.scroll(this._eraseAttrData(), true);
} else {
if (buffer.y >= this._bufferService.rows) {
buffer.y = this._bufferService.rows - 1;
@@ -643,9 +719,9 @@ export class InputHandler extends Disposable implements IInputHandler {
}
/**
* Forward addCsiHandler from parser.
* Forward registerCsiHandler from parser.
*/
public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable {
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable {
if (id.final === 't' && !id.prefix && !id.intermediates) {
// security: always check whether window option is allowed
return this._parser.registerCsiHandler(id, params => {
@@ -659,23 +735,23 @@ export class InputHandler extends Disposable implements IInputHandler {
}
/**
* Forward addDcsHandler from parser.
* Forward registerDcsHandler from parser.
*/
public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable {
public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable {
return this._parser.registerDcsHandler(id, new DcsHandler(callback));
}
/**
* Forward addEscHandler from parser.
* Forward registerEscHandler from parser.
*/
public addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable {
public registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable {
return this._parser.registerEscHandler(id, callback);
}
/**
* Forward addOscHandler from parser.
* Forward registerOscHandler from parser.
*/
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
return this._parser.registerOscHandler(ident, new OscHandler(callback));
}
@@ -713,7 +789,7 @@ export class InputHandler extends Disposable implements IInputHandler {
buffer.y++;
if (buffer.y === buffer.scrollBottom + 1) {
buffer.y--;
this._onRequestScroll.fire(this._eraseAttrData());
this._bufferService.scroll(this._eraseAttrData());
} else if (buffer.y >= this._bufferService.rows) {
buffer.y = this._bufferService.rows - 1;
}
@@ -2778,7 +2854,7 @@ export class InputHandler extends Disposable implements IInputHandler {
protected _parseAnsiColorChange(data: string): IAnsiColorChangeEvent | null {
const result: IAnsiColorChangeEvent = { colors: [] };
// example data: 5;rgb:aa/bb/cc
const regex = /(\d+);rgb:([0-9a-f]{2})\/([0-9a-f]{2})\/([0-9a-f]{2})/gi;
const regex = /(\d+);rgb:([\da-f]{2})\/([\da-f]{2})\/([\da-f]{2})/gi;
let match;
while ((match = regex.exec(data)) !== null) {
@@ -2909,7 +2985,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._bufferService.buffer.y++;
if (buffer.y === buffer.scrollBottom + 1) {
buffer.y--;
this._onRequestScroll.fire(this._eraseAttrData());
this._bufferService.scroll(this._eraseAttrData());
} else if (buffer.y >= this._bufferService.rows) {
buffer.y = this._bufferService.rows - 1;
}
+41 -21
View File
@@ -3,13 +3,13 @@
* @license MIT
*/
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider } from 'common/services/Services';
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum } from 'common/services/Services';
import { IEvent, EventEmitter } from 'common/EventEmitter';
import { clone } from 'common/Clone';
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
import { IBufferSet, IBuffer } from 'common/buffer/Types';
import { BufferSet } from 'common/buffer/BufferSet';
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes } from 'common/Types';
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes, IAttributeData } from 'common/Types';
import { UnicodeV6 } from 'common/input/UnicodeV6';
export class MockBufferService implements IBufferService {
@@ -17,6 +17,7 @@ export class MockBufferService implements IBufferService {
public get buffer(): IBuffer { return this.buffers.active; }
public buffers: IBufferSet = {} as any;
public onResize: IEvent<{ cols: number, rows: number }> = new EventEmitter<{ cols: number, rows: number }>().event;
public onScroll: IEvent<number> = new EventEmitter<number>().event;
public isUserScrolling: boolean = false;
constructor(
public cols: number,
@@ -25,23 +26,41 @@ export class MockBufferService implements IBufferService {
) {
this.buffers = new BufferSet(optionsService, this);
}
public scrollPages(pageCount: number): void {
throw new Error('Method not implemented.');
}
public scrollToTop(): void {
throw new Error('Method not implemented.');
}
public scrollToLine(line: number): void {
throw new Error('Method not implemented.');
}
public scroll(eraseAttr: IAttributeData, isWrapped: boolean): void {
throw new Error('Method not implemented.');
}
public scrollToBottom(): void {
throw new Error('Method not implemented.');
}
public scrollLines(disp: number, suppressScrollEvent?: boolean): void {
throw new Error('Method not implemented.');
}
public resize(cols: number, rows: number): void {
this.cols = cols;
this.rows = rows;
}
public reset(): void {}
public reset(): void { }
}
export class MockCoreMouseService implements ICoreMouseService {
public areMouseEventsActive: boolean = false;
public activeEncoding: string = '';
public activeProtocol: string = '';
public addEncoding(name: string): void {}
public addProtocol(name: string): void {}
public reset(): void {}
public addEncoding(name: string): void { }
public addProtocol(name: string): void { }
public reset(): void { }
public triggerMouseEvent(event: ICoreMouseEvent): boolean { return false; }
public onProtocolChange: IEvent<CoreMouseEventType> = new EventEmitter<CoreMouseEventType>().event;
public explainEvents(events: CoreMouseEventType): {[event: string]: boolean} {
public explainEvents(events: CoreMouseEventType): { [event: string]: boolean } {
throw new Error('Method not implemented.');
}
}
@@ -50,9 +69,9 @@ export class MockCharsetService implements ICharsetService {
public serviceBrand: any;
public charset: ICharset | undefined;
public glevel: number = 0;
public reset(): void {}
public setgLevel(g: number): void {}
public setgCharset(g: number, charset: ICharset): void {}
public reset(): void { }
public setgLevel(g: number): void { }
public setgCharset(g: number, charset: ICharset): void { }
}
export class MockCoreService implements ICoreService {
@@ -75,27 +94,28 @@ export class MockCoreService implements ICoreService {
public onData: IEvent<string> = new EventEmitter<string>().event;
public onUserInput: IEvent<void> = new EventEmitter<void>().event;
public onBinary: IEvent<string> = new EventEmitter<string>().event;
public reset(): void {}
public triggerDataEvent(data: string, wasUserInput?: boolean): void {}
public triggerBinaryEvent(data: string): void {}
public reset(): void { }
public triggerDataEvent(data: string, wasUserInput?: boolean): void { }
public triggerBinaryEvent(data: string): void { }
}
export class MockDirtyRowService implements IDirtyRowService {
public serviceBrand: any;
public start: number = 0;
public end: number = 0;
public clearRange(): void {}
public markDirty(y: number): void {}
public markRangeDirty(y1: number, y2: number): void {}
public markAllDirty(): void {}
public clearRange(): void { }
public markDirty(y: number): void { }
public markRangeDirty(y1: number, y2: number): void { }
public markAllDirty(): void { }
}
export class MockLogService implements ILogService {
public serviceBrand: any;
public debug(message: any, ...optionalParams: any[]): void {}
public info(message: any, ...optionalParams: any[]): void {}
public warn(message: any, ...optionalParams: any[]): void {}
public error(message: any, ...optionalParams: any[]): void {}
public logLevel = LogLevelEnum.DEBUG;
public debug(message: any, ...optionalParams: any[]): void { }
public info(message: any, ...optionalParams: any[]): void { }
public warn(message: any, ...optionalParams: any[]): void { }
public error(message: any, ...optionalParams: any[]): void { }
}
export class MockOptionsService implements IOptionsService {
+24 -3
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ITerminalOptions as IPublicTerminalOptions } from 'xterm';
import { IFunctionIdentifier, ITerminalOptions as IPublicTerminalOptions } from 'xterm';
import { IEvent, IEventEmitter } from 'common/EventEmitter';
import { IDeleteEvent, IInsertEvent } from 'common/CircularList';
import { IParams } from 'common/parser/Types';
@@ -42,6 +42,16 @@ export interface IKeyboardEvent {
type: string;
}
export interface IScrollEvent {
position: number;
source: ScrollSource;
}
export const enum ScrollSource {
TERMINAL,
VIEWPORT,
}
export interface ICircularList<T> {
length: number;
maxLength: number;
@@ -347,10 +357,13 @@ export interface IAnsiColorChangeEvent {
*/
export interface IInputHandler {
onTitleChange: IEvent<string>;
onRequestScroll: IEvent<IAttributeData, boolean | void>;
parse(data: string | Uint8Array): void;
parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise<boolean>;
print(data: Uint32Array, start: number, end: number): void;
registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable;
registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable;
registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable;
registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
/** C0 BEL */ bell(): void;
/** C0 LF */ lineFeed(): void;
@@ -427,3 +440,11 @@ export interface IInputHandler {
ESC ~ */ setgLevel(level: number): void;
/** ESC # 8 */ screenAlignmentPattern(): void;
}
interface IParseStack {
paused: boolean;
cursorStartX: number;
cursorStartY: number;
decodedLength: number;
position: number;
}
+1 -1
View File
@@ -671,6 +671,6 @@ export class BufferStringIterator implements IBufferStringIterator {
content += this._buffer.translateBufferLineToString(i, this._trimRight);
}
this._current = range.last + 1;
return {range, content};
return { range, content };
}
}
+32
View File
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2021 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { getRangeLength } from 'common/buffer/BufferRange';
import { IBufferRange } from 'xterm';
describe('BufferRange', () => {
describe('getRangeLength', () => {
it('should get range for single line', () => {
assert.equal(getRangeLength(createRange(1, 1, 4, 1), 0), 4);
});
it('should throw for invalid range', () => {
assert.throws(() => getRangeLength(createRange(1, 3, 1, 1), 0));
});
it('should get range multiple lines', () => {
assert.equal(getRangeLength(createRange(1, 1, 4, 5), 5), 23);
});
it('should get range for end line right after start line', () => {
assert.equal(getRangeLength(createRange(1, 1, 7, 2), 5), 11);
});
});
});
function createRange(x1: number, y1: number, x2: number, y2: number): IBufferRange {
return {
start: { x: x1, y: y1 },
end: { x: x2, y: y2 }
};
}
+16
View File
@@ -0,0 +1,16 @@
/**
* Copyright (c) 2021 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IBufferRange } from 'xterm';
export function getRangeLength(range: IBufferRange, cols: number): number {
if (range.start.y === range.end.y) {
return range.end.x - range.start.x + 1;
}
if (range.start.y > range.end.y) {
throw new Error(`Buffer range end (${range.end.x}, ${range.end.y}) cannot be before start (${range.start.x}, ${range.start.y})`);
}
return cols * (range.end.y - range.start.y - 1) + cols - range.start.x + range.end.x;
}
+21
View File
@@ -85,5 +85,26 @@ describe('WriteBuffer', () => {
done();
});
});
it('writeSync called from action does not overflow callstack - issue #3265', () => {
wb = new WriteBuffer(data => {
const num = parseInt(data as string);
if (num < 1000000) {
wb.writeSync('' + (num + 1));
}
});
wb.writeSync('1');
});
it('writeSync maxSubsequentCalls argument', () => {
let last: string = '';
wb = new WriteBuffer(data => {
last = data as string;
const num = parseInt(data as string);
if (num < 1000000) {
wb.writeSync('' + (num + 1), 10);
}
});
wb.writeSync('1', 10);
assert.equal(last, '11'); // 1 + 10 sub calls = 11
});
});
});
+140 -26
View File
@@ -31,33 +31,66 @@ const WRITE_TIMEOUT_MS = 12;
*/
const WRITE_BUFFER_LENGTH_THRESHOLD = 50;
// queueMicrotask polyfill for nodejs < v11
const qmt: (cb: () => void) => void = (typeof queueMicrotask === 'undefined')
? (cb: () => void) => { Promise.resolve().then(cb); }
: queueMicrotask;
export class WriteBuffer {
private _writeBuffer: (string | Uint8Array)[] = [];
private _callbacks: ((() => void) | undefined)[] = [];
private _pendingData = 0;
private _bufferOffset = 0;
private _isSyncWriting = false;
private _syncCalls = 0;
constructor(private _action: (data: string | Uint8Array) => void) { }
constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise<boolean>) { }
/**
* @deprecated Unreliable, to be removed soon.
*/
public writeSync(data: string | Uint8Array, maxSubsequentCalls?: number): void {
// stop writeSync recursions with maxSubsequentCalls argument
// This is dangerous to use as it will lose the current data chunk
// and return immediately.
if (maxSubsequentCalls !== undefined && this._syncCalls > maxSubsequentCalls) {
// comment next line if a whole loop block should only contain x `writeSync` calls
// (total flat vs. deep nested limit)
this._syncCalls = 0;
return;
}
// append chunk to buffer
this._pendingData += data.length;
this._writeBuffer.push(data);
this._callbacks.push(undefined);
// increase recursion counter
this._syncCalls++;
// exit early if another writeSync loop is active
if (this._isSyncWriting) {
return;
}
this._isSyncWriting = true;
public writeSync(data: string | Uint8Array): void {
// force sync processing on pending data chunks to avoid in-band data scrambling
// does the same as innerWrite but without event loop
if (this._writeBuffer.length) {
for (let i = this._bufferOffset; i < this._writeBuffer.length; ++i) {
const data = this._writeBuffer[i];
const cb = this._callbacks[i];
this._action(data);
if (cb) cb();
}
// reset all to avoid reprocessing of chunks with scheduled innerWrite call
this._writeBuffer = [];
this._callbacks = [];
this._pendingData = 0;
// stop scheduled innerWrite by offset > length condition
this._bufferOffset = 0x7FFFFFFF;
// we have to do it here as single loop steps to not corrupt loop subject
// by another writeSync call triggered from _action
let chunk: string | Uint8Array | undefined;
while (chunk = this._writeBuffer.shift()) {
this._action(chunk);
const cb = this._callbacks.shift();
if (cb) cb();
}
// handle current data chunk
this._action(data);
// reset to avoid reprocessing of chunks with scheduled innerWrite call
// stopping scheduled innerWrite by offset > length condition
this._pendingData = 0;
this._bufferOffset = 0x7FFFFFFF;
// allow another writeSync to loop
this._isSyncWriting = false;
this._syncCalls = 0;
}
public write(data: string | Uint8Array, callback?: () => void): void {
@@ -76,16 +109,97 @@ export class WriteBuffer {
this._callbacks.push(callback);
}
protected _innerWrite(): void {
const startTime = Date.now();
/**
* Inner write call, that enters the sliced chunk processing by timing.
*
* `lastTime` indicates, when the last _innerWrite call had started.
* It is used to aggregate async handler execution under a timeout constraint
* effectively lowering the redrawing needs, schematically:
*
* macroTask _innerWrite:
* if (Date.now() - (lastTime | 0) < WRITE_TIMEOUT_MS):
* schedule microTask _innerWrite(lastTime)
* else:
* schedule macroTask _innerWrite(0)
*
* overall execution order on task queues:
*
* macrotasks: [...] --> _innerWrite(0) --> [...] --> screenUpdate --> [...]
* m t: |
* i a: [...]
* c s: |
* r k: while < timeout:
* o s: _innerWrite(timeout)
*
* `promiseResult` depicts the promise resolve value of an async handler.
* This value gets carried forward through all saved stack states of the
* paused parser for proper continuation.
*
* Note, for pure sync code `lastTime` and `promiseResult` have no meaning.
*/
protected _innerWrite(lastTime: number = 0, promiseResult: boolean = true): void {
const startTime = lastTime || Date.now();
while (this._writeBuffer.length > this._bufferOffset) {
const data = this._writeBuffer[this._bufferOffset];
const cb = this._callbacks[this._bufferOffset];
this._bufferOffset++;
const result = this._action(data, promiseResult);
if (result) {
/**
* If we get a promise as return value, we re-schedule the continuation
* as thenable on the promise and exit right away.
*
* The exit here means, that we block input processing at the current active chunk,
* the exact execution position within the chunk is preserved by the saved
* stack content in InputHandler and EscapeSequenceParser.
*
* Resuming happens automatically from that saved stack state.
* Also the resolved promise value is passed along the callstack to
* `EscapeSequenceParser.parse` to correctly resume the stopped handler loop.
*
* Exceptions on async handlers will be logged to console async, but do not interrupt
* the input processing (continues with next handler at the current input position).
*/
this._action(data);
this._pendingData -= data.length;
/**
* If a promise takes long to resolve, we should schedule continuation behind setTimeout.
* This might already be too late, if our .then enters really late (executor + prev thens took very long).
* This cannot be solved here for the handler itself (it is the handlers responsibility to slice hard work),
* but we can at least schedule a screen update as we gain control.
*/
const continuation: (r: boolean) => void = (r: boolean) => Date.now() - startTime >= WRITE_TIMEOUT_MS
? setTimeout(() => this._innerWrite(0, r))
: this._innerWrite(startTime, r);
/**
* Optimization considerations:
* The continuation above favors FPS over throughput by eval'ing `startTime` on resolve.
* This might schedule too many screen updates with bad throughput drops (in case a slow
* resolving handler sliced its work properly behind setTimeout calls). We cannot spot
* this condition here, also the renderer has no way to spot nonsense updates either.
* FIXME: A proper fix for this would track the FPS at the renderer entry level separately.
*
* If favoring of FPS shows bad throughtput impact, use the following instead. It favors
* throughput by eval'ing `startTime` upfront pulling at least one more chunk into the
* current microtask queue (executed before setTimeout).
*/
// const continuation: (r: boolean) => void = Date.now() - startTime >= WRITE_TIMEOUT_MS
// ? r => setTimeout(() => this._innerWrite(0, r))
// : r => this._innerWrite(startTime, r);
// Handle exceptions synchronously to current band position, idea:
// 1. spawn a single microtask which we allow to throw hard
// 2. spawn a promise immediately resolving to `true`
// (executed on the same queue, thus properly aligned before continuation happens)
result.catch(err => {
qmt(() => {throw err;});
return Promise.resolve(false);
}).then(continuation);
return;
}
const cb = this._callbacks[this._bufferOffset];
if (cb) cb();
this._bufferOffset++;
this._pendingData -= data.length;
if (Date.now() - startTime >= WRITE_TIMEOUT_MS) {
break;
@@ -99,10 +213,10 @@ export class WriteBuffer {
this._callbacks = this._callbacks.slice(this._bufferOffset);
this._bufferOffset = 0;
}
setTimeout(() => this._innerWrite(), 0);
setTimeout(() => this._innerWrite());
} else {
this._writeBuffer = [];
this._callbacks = [];
this._writeBuffer.length = 0;
this._callbacks.length = 0;
this._pendingData = 0;
this._bufferOffset = 0;
}
+205
View File
@@ -252,3 +252,208 @@ describe('DcsParser', () => {
});
});
});
class TestHandlerAsync implements IDcsHandler {
constructor(public output: any[], public msg: string, public returnFalse: boolean = false) {}
public hook(params: IParams): void {
this.output.push([this.msg, 'HOOK', params.toArray()]);
}
public put(data: Uint32Array, start: number, end: number): void {
this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]);
}
public async unhook(success: boolean): Promise<boolean> {
// simple sleep to check in tests whether ordering gets messed up
await new Promise(res => setTimeout(res, 20));
this.output.push([this.msg, 'UNHOOK', success]);
if (this.returnFalse) {
return false;
}
return true;
}
}
async function unhookP(parser: DcsParser, success: boolean): Promise<void> {
let result: void | Promise<boolean>;
let prev: boolean | undefined;
while (result = parser.unhook(success, prev)) {
prev = await result;
}
}
describe('DcsParser - async tests', () => {
let parser: DcsParser;
let reports: any[] = [];
beforeEach(() => {
reports = [];
parser = new DcsParser();
parser.setHandlerFallback((id, action, data) => {
if (action === 'HOOK') {
data = data.toArray();
}
reports.push([id, action, data]);
});
});
describe('sync and async mixed', () => {
describe('sync | async | sync', () => {
it('first should run, cleanup action for others', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', false));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', false));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's2', false));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['s2', 'HOOK', [1, 2, 3]],
['a1', 'HOOK', [1, 2, 3]],
['s1', 'HOOK', [1, 2, 3]],
['s2', 'PUT', 'Here comes'],
['a1', 'PUT', 'Here comes'],
['s1', 'PUT', 'Here comes'],
['s2', 'PUT', 'the mouse!'],
['a1', 'PUT', 'the mouse!'],
['s1', 'PUT', 'the mouse!'],
['s2', 'UNHOOK', true],
['a1', 'UNHOOK', false], // important: a1 before s1
['s1', 'UNHOOK', false]
]);
});
it('all should run', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', true));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', true));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's2', true));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['s2', 'HOOK', [1, 2, 3]],
['a1', 'HOOK', [1, 2, 3]],
['s1', 'HOOK', [1, 2, 3]],
['s2', 'PUT', 'Here comes'],
['a1', 'PUT', 'Here comes'],
['s1', 'PUT', 'Here comes'],
['s2', 'PUT', 'the mouse!'],
['a1', 'PUT', 'the mouse!'],
['s1', 'PUT', 'the mouse!'],
['s2', 'UNHOOK', true],
['a1', 'UNHOOK', true], // important: a1 before s1
['s1', 'UNHOOK', true]
]);
});
});
describe('async | sync | async', () => {
it('first should run, cleanup action for others', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', false));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', false));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a2', false));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['a2', 'HOOK', [1, 2, 3]],
['s1', 'HOOK', [1, 2, 3]],
['a1', 'HOOK', [1, 2, 3]],
['a2', 'PUT', 'Here comes'],
['s1', 'PUT', 'Here comes'],
['a1', 'PUT', 'Here comes'],
['a2', 'PUT', 'the mouse!'],
['s1', 'PUT', 'the mouse!'],
['a1', 'PUT', 'the mouse!'],
['a2', 'UNHOOK', true],
['s1', 'UNHOOK', false], // important: s1 between a2 .. a1
['a1', 'UNHOOK', false]
]);
});
it('all should run', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', true));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', true));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a2', true));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['a2', 'HOOK', [1, 2, 3]],
['s1', 'HOOK', [1, 2, 3]],
['a1', 'HOOK', [1, 2, 3]],
['a2', 'PUT', 'Here comes'],
['s1', 'PUT', 'Here comes'],
['a1', 'PUT', 'Here comes'],
['a2', 'PUT', 'the mouse!'],
['s1', 'PUT', 'the mouse!'],
['a1', 'PUT', 'the mouse!'],
['a2', 'UNHOOK', true],
['s1', 'UNHOOK', true], // important: s1 between a2 .. a1
['a1', 'UNHOOK', true]
]);
});
});
describe('DcsHandlerFactory', () => {
it('should be called once on end(true)', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push([params.toArray(), data]); return true; }));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]);
});
it('should not be called on end(false)', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push([params.toArray(), data]); return true; }));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, false);
assert.deepEqual(reports, []);
});
it('should be disposable', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['one', params.toArray(), data]); return true; }));
const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['two', params.toArray(), data]); return true; }));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!']]);
dispo.dispose();
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
data = toUtf32('some other');
parser.put(data, 0, data.length);
data = toUtf32(' data');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]);
});
it('should respect return false', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['one', params.toArray(), data]); return true; }));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['two', params.toArray(), data]); return false; }));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'Here comes the mouse!']]);
});
});
});
});
+56 -15
View File
@@ -4,7 +4,7 @@
*/
import { IDisposable } from 'common/Types';
import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandlerType } from 'common/parser/Types';
import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandlerType, ISubParserStackState } from 'common/parser/Types';
import { utf32ToString } from 'common/input/TextDecoder';
import { Params } from 'common/parser/Params';
import { PAYLOAD_LIMIT } from 'common/parser/Constants';
@@ -15,11 +15,16 @@ export class DcsParser implements IDcsParser {
private _handlers: IHandlerCollection<IDcsHandler> = Object.create(null);
private _active: IDcsHandler[] = EMPTY_HANDLERS;
private _ident: number = 0;
private _handlerFb: DcsFallbackHandlerType = () => {};
private _handlerFb: DcsFallbackHandlerType = () => { };
private _stack: ISubParserStackState = {
paused: false,
loopPosition: 0,
fallThrough: false
};
public dispose(): void {
this._handlers = Object.create(null);
this._handlerFb = () => {};
this._handlerFb = () => { };
this._active = EMPTY_HANDLERS;
}
@@ -48,9 +53,13 @@ export class DcsParser implements IDcsParser {
}
public reset(): void {
// force cleanup leftover handlers
if (this._active.length) {
this.unhook(false);
for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {
this._active[j].unhook(false);
}
}
this._stack.paused = false;
this._active = EMPTY_HANDLERS;
this._ident = 0;
}
@@ -79,20 +88,42 @@ export class DcsParser implements IDcsParser {
}
}
public unhook(success: boolean): void {
public unhook(success: boolean, promiseResult: boolean = true): void | Promise<boolean> {
if (!this._active.length) {
this._handlerFb(this._ident, 'UNHOOK', success);
} else {
let handlerResult: boolean | Promise<boolean> = false;
let j = this._active.length - 1;
for (; j >= 0; j--) {
if (this._active[j].unhook(success)) {
break;
}
let fallThrough = false;
if (this._stack.paused) {
j = this._stack.loopPosition - 1;
handlerResult = promiseResult;
fallThrough = this._stack.fallThrough;
this._stack.paused = false;
}
j--;
// cleanup left over handlers
if (!fallThrough && handlerResult === false) {
for (; j >= 0; j--) {
handlerResult = this._active[j].unhook(success);
if (handlerResult === true) {
break;
} else if (handlerResult instanceof Promise) {
this._stack.paused = true;
this._stack.loopPosition = j;
this._stack.fallThrough = false;
return handlerResult;
}
}
j--;
}
// cleanup left over handlers (fallThrough for async)
for (; j >= 0; j--) {
this._active[j].unhook(false);
handlerResult = this._active[j].unhook(false);
if (handlerResult instanceof Promise) {
this._stack.paused = true;
this._stack.loopPosition = j;
this._stack.fallThrough = true;
return handlerResult;
}
}
}
this._active = EMPTY_HANDLERS;
@@ -113,7 +144,7 @@ export class DcsHandler implements IDcsHandler {
private _params: IParams = EMPTY_PARAMS;
private _hitLimit: boolean = false;
constructor(private _handler: (data: string, params: IParams) => boolean) {}
constructor(private _handler: (data: string, params: IParams) => boolean | Promise<boolean>) { }
public hook(params: IParams): void {
// since we need to preserve params until `unhook`, we have to clone it
@@ -136,12 +167,22 @@ export class DcsHandler implements IDcsHandler {
}
}
public unhook(success: boolean): boolean {
let ret = false;
public unhook(success: boolean): boolean | Promise<boolean> {
let ret: boolean | Promise<boolean> = false;
if (this._hitLimit) {
ret = false;
} else if (success) {
ret = this._handler(this._data, this._params);
if (ret instanceof Promise) {
// need to hold data and params until `ret` got resolved
// dont care for errors, data will be freed anyway on next start
return ret.then(res => {
this._params = EMPTY_PARAMS;
this._data = '';
this._hitLimit = false;
return res;
});
}
}
this._params = EMPTY_PARAMS;
this._data = '';
+639 -99
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandlerType, IFunctionIdentifier } from 'common/parser/Types';
import { IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandlerType, IFunctionIdentifier, IParserStackState, ParserStackType, ResumableHandlersType } from 'common/parser/Types';
import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from 'common/parser/EscapeSequenceParser';
import { assert } from 'chai';
import { StringToUtf32, stringFromCodePoint, utf32ToString } from 'common/input/TextDecoder';
@@ -24,7 +24,7 @@ function r(a: number, b: number): string[] {
}
class MockOscPutParser implements IOscParser {
private _fallback: OscFallbackHandlerType = () => {};
private _fallback: OscFallbackHandlerType = () => { };
public data = '';
public reset(): void {
this.data = '';
@@ -92,6 +92,21 @@ class TestEscapeSequenceParser extends EscapeSequenceParser {
public identifier(id: IFunctionIdentifier): number {
return this._identifier(id);
}
public get parseStack(): IParserStackState {
return this._parseStack;
}
private _trackStack = false;
public trackStackSavesOnPause(): void {
this._trackStack = true;
}
public trackedStack: IParserStackState[] = [];
public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise<boolean> {
const result = super.parse(data, length, promiseResult);
if (result instanceof Promise && this._trackStack) {
this.trackedStack.push({ ...this.parseStack });
}
return result;
}
}
// test object to collect parser actions and compare them with expected values
@@ -1206,71 +1221,71 @@ describe('EscapeSequenceParser', () => {
assert.equal(print, '');
});
it('ESC handler', () => {
parser2.registerEscHandler({intermediates: '%', final: 'G'}, function (): boolean {
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, function (): boolean {
esc.push('%G');
return true;
});
parser2.registerEscHandler({final: 'E'}, function (): boolean {
parser2.registerEscHandler({ final: 'E' }, function (): boolean {
esc.push('E');
return true;
});
parse(parser2, INPUT);
assert.deepEqual(esc, ['%G', 'E']);
parser2.clearEscHandler({intermediates: '%', final: 'G'});
parser2.clearEscHandler({intermediates: '%', final: 'G'}); // should not throw
parser2.clearEscHandler({ intermediates: '%', final: 'G' });
parser2.clearEscHandler({ intermediates: '%', final: 'G' }); // should not throw
clearAccu();
parse(parser2, INPUT);
assert.deepEqual(esc, ['E']);
parser2.clearEscHandler({final: 'E'});
parser2.clearEscHandler({ final: 'E' });
clearAccu();
parse(parser2, INPUT);
assert.deepEqual(esc, []);
});
describe('ESC custom handlers', () => {
it('prevent fallback', () => {
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; });
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; });
parse(parser2, INPUT);
assert.deepEqual(esc, ['custom - %G']);
});
it('allow fallback', () => {
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; });
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return false; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return false; });
parse(parser2, INPUT);
assert.deepEqual(esc, ['custom - %G', 'default - %G']);
});
it('Multiple custom handlers fallback once', () => {
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; });
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom2 - %G'); return false; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom2 - %G'); return false; });
parse(parser2, INPUT);
assert.deepEqual(esc, ['custom2 - %G', 'custom - %G']);
});
it('Multiple custom handlers no fallback', () => {
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; });
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom2 - %G'); return true; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom2 - %G'); return true; });
parse(parser2, INPUT);
assert.deepEqual(esc, ['custom2 - %G']);
});
it('Execution order should go from latest handler down to the original', () => {
const order: number[] = [];
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { order.push(1); return true; });
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { order.push(2); return false; });
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { order.push(3); return false; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { order.push(1); return true; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { order.push(2); return false; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { order.push(3); return false; });
parse(parser2, '\x1b%G');
assert.deepEqual(order, [3, 2, 1]);
});
it('Dispose should work', () => {
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; });
const dispo = parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; });
const dispo = parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; });
dispo.dispose();
parse(parser2, INPUT);
assert.deepEqual(esc, ['default - %G']);
});
it('Should not corrupt the parser when dispose is called twice', () => {
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; });
const dispo = parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; });
const dispo = parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; });
dispo.dispose();
dispo.dispose();
parse(parser2, INPUT);
@@ -1278,14 +1293,14 @@ describe('EscapeSequenceParser', () => {
});
});
it('CSI handler', () => {
parser2.registerCsiHandler({final: 'm'}, function (params: IParams): boolean {
parser2.registerCsiHandler({ final: 'm' }, function (params: IParams): boolean {
csi.push(['m', params.toArray(), '']);
return true;
});
parse(parser2, INPUT);
assert.deepEqual(csi, [['m', [1, 31], ''], ['m', [0], '']]);
parser2.clearCsiHandler({final: 'm'});
parser2.clearCsiHandler({final: 'm'}); // should not throw
parser2.clearCsiHandler({ final: 'm' });
parser2.clearCsiHandler({ final: 'm' }); // should not throw
clearAccu();
parse(parser2, INPUT);
assert.deepEqual(csi, []);
@@ -1293,16 +1308,16 @@ describe('EscapeSequenceParser', () => {
describe('CSI custom handlers', () => {
it('Prevent fallback', () => {
const csiCustom: [string, ParamsArray, string][] = [];
parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
parse(parser2, INPUT);
assert.deepEqual(csi, [], 'Should not fallback to original handler');
assert.deepEqual(csiCustom, [['m', [1, 31], ''], ['m', [0], '']]);
});
it('Allow fallback', () => {
const csiCustom: [string, ParamsArray, string][] = [];
parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return false; });
parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return false; });
parse(parser2, INPUT);
assert.deepEqual(csi, [['m', [1, 31], ''], ['m', [0], '']], 'Should fallback to original handler');
assert.deepEqual(csiCustom, [['m', [1, 31], ''], ['m', [0], '']]);
@@ -1310,9 +1325,9 @@ describe('EscapeSequenceParser', () => {
it('Multiple custom handlers fallback once', () => {
const csiCustom: [string, ParamsArray, string][] = [];
const csiCustom2: [string, ParamsArray, string][] = [];
parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return false; });
parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom2.push(['m', params.toArray(), '']); return false; });
parse(parser2, INPUT);
assert.deepEqual(csi, [], 'Should not fallback to original handler');
assert.deepEqual(csiCustom, [['m', [1, 31], ''], ['m', [0], '']]);
@@ -1321,9 +1336,9 @@ describe('EscapeSequenceParser', () => {
it('Multiple custom handlers no fallback', () => {
const csiCustom: [string, ParamsArray, string][] = [];
const csiCustom2: [string, ParamsArray, string][] = [];
parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom2.push(['m', params.toArray(), '']); return true; });
parse(parser2, INPUT);
assert.deepEqual(csi, [], 'Should not fallback to original handler');
assert.deepEqual(csiCustom, [], 'Should not fallback once');
@@ -1331,16 +1346,16 @@ describe('EscapeSequenceParser', () => {
});
it('Execution order should go from latest handler down to the original', () => {
const order: number[] = [];
parser2.registerCsiHandler({final: 'm'}, () => { order.push(1); return true; });
parser2.registerCsiHandler({final: 'm'}, () => { order.push(2); return false; });
parser2.registerCsiHandler({final: 'm'}, () => { order.push(3); return false; });
parser2.registerCsiHandler({ final: 'm' }, () => { order.push(1); return true; });
parser2.registerCsiHandler({ final: 'm' }, () => { order.push(2); return false; });
parser2.registerCsiHandler({ final: 'm' }, () => { order.push(3); return false; });
parse(parser2, '\x1b[0m');
assert.deepEqual(order, [3, 2, 1]);
});
it('Dispose should work', () => {
const csiCustom: [string, ParamsArray, string][] = [];
parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; });
const customHandler = parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; });
const customHandler = parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
customHandler.dispose();
parse(parser2, INPUT);
assert.deepEqual(csi, [['m', [1, 31], ''], ['m', [0], '']]);
@@ -1348,8 +1363,8 @@ describe('EscapeSequenceParser', () => {
});
it('Should not corrupt the parser when dispose is called twice', () => {
const csiCustom: [string, ParamsArray, string][] = [];
parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; });
const customHandler = parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; });
const customHandler = parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
customHandler.dispose();
customHandler.dispose();
parse(parser2, INPUT);
@@ -1455,7 +1470,7 @@ describe('EscapeSequenceParser', () => {
});
});
it('DCS handler', () => {
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, {
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, {
hook: function (params: IParams): void {
dcs.push(['hook', '', params.toArray(), 0]);
},
@@ -1478,8 +1493,8 @@ describe('EscapeSequenceParser', () => {
['put', 'abc'], ['put', ';de'],
['unhook']
]);
parser2.clearDcsHandler({intermediates: '+', final: 'p'});
parser2.clearDcsHandler({intermediates: '+', final: 'p'}); // should not throw
parser2.clearDcsHandler({ intermediates: '+', final: 'p' });
parser2.clearDcsHandler({ intermediates: '+', final: 'p' }); // should not throw
clearAccu();
parse(parser2, '\x1bP1;2;3+pabc');
parse(parser2, ';de\x9c');
@@ -1489,54 +1504,54 @@ describe('EscapeSequenceParser', () => {
const DCS_INPUT = '\x1bP1;2;3+pabc\x1b\\';
it('Prevent fallback', () => {
const dcsCustom: [string, (number | number[])[], string][] = [];
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
parse(parser2, DCS_INPUT);
assert.deepEqual(dcsCustom, [['B', [1, 2, 3], 'abc']]);
});
it('Allow fallback', () => {
const dcsCustom: [string, (number | number[])[], string][] = [];
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return false; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return false; }));
parse(parser2, DCS_INPUT);
assert.deepEqual(dcsCustom, [['B', [1, 2, 3], 'abc'], ['A', [1, 2, 3], 'abc']]);
});
it('Multiple custom handlers fallback once', () => {
const dcsCustom: [string, (number | number[])[], string][] = [];
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return false; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return false; }));
parse(parser2, DCS_INPUT);
assert.deepEqual(dcsCustom, [['C', [1, 2, 3], 'abc'], ['B', [1, 2, 3], 'abc']]);
});
it('Multiple custom handlers no fallback', () => {
const dcsCustom: [string, (number | number[])[], string][] = [];
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return true; }));
parse(parser2, DCS_INPUT);
assert.deepEqual(dcsCustom, [['C', [1, 2, 3], 'abc']]);
});
it('Execution order should go from latest handler down to the original', () => {
const order: number[] = [];
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => { order.push(1); return true; }));
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => { order.push(2); return false; }));
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => { order.push(3); return false; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler(() => { order.push(1); return true; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler(() => { order.push(2); return false; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler(() => { order.push(3); return false; }));
parse(parser2, DCS_INPUT);
assert.deepEqual(order, [3, 2, 1]);
});
it('Dispose should work', () => {
const dcsCustom: [string, (number | number[])[], string][] = [];
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
const dispo = parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
const dispo = parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
dispo.dispose();
parse(parser2, DCS_INPUT);
assert.deepEqual(dcsCustom, [['A', [1, 2, 3], 'abc']]);
});
it('Should not corrupt the parser when dispose is called twice', () => {
const dcsCustom: [string, (number | number[])[], string][] = [];
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
const dispo = parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
const dispo = parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
dispo.dispose();
dispo.dispose();
parse(parser2, DCS_INPUT);
@@ -1570,54 +1585,54 @@ describe('EscapeSequenceParser', () => {
it('prefix range 0x3c .. 0x3f, one byte', () => {
for (let i = 0x3c; i <= 0x3f; ++i) {
const c = String.fromCharCode(i);
assert.equal(parser.identToString(parser.identifier({prefix: c, final: 'z'})), c + 'z');
assert.equal(parser.identToString(parser.identifier({ prefix: c, final: 'z' })), c + 'z');
}
assert.throws(() => { parser.identifier({prefix: '\x3b', final: 'z'}); }, 'prefix must be in range 0x3c .. 0x3f');
assert.throws(() => { parser.identifier({prefix: '\x40', final: 'z'}); }, 'prefix must be in range 0x3c .. 0x3f');
assert.throws(() => { parser.identifier({prefix: '??', final: 'z'}); }, 'only one byte as prefix supported');
assert.throws(() => { parser.identifier({ prefix: '\x3b', final: 'z' }); }, 'prefix must be in range 0x3c .. 0x3f');
assert.throws(() => { parser.identifier({ prefix: '\x40', final: 'z' }); }, 'prefix must be in range 0x3c .. 0x3f');
assert.throws(() => { parser.identifier({ prefix: '??', final: 'z' }); }, 'only one byte as prefix supported');
});
it('intermediates range 0x20 .. 0x2f, up to two bytes', () => {
for (let i = 0x20; i <= 0x2f; ++i) {
const c = String.fromCharCode(i);
assert.equal(parser.identToString(parser.identifier({intermediates: c + c, final: 'z'})), c + c + 'z');
assert.equal(parser.identToString(parser.identifier({ intermediates: c + c, final: 'z' })), c + c + 'z');
}
assert.throws(() => { parser.identifier({intermediates: '\x1f', final: 'z'}); }, 'intermediate must be in range 0x20 .. 0x2f');
assert.throws(() => { parser.identifier({intermediates: '\x30', final: 'z'}); }, 'intermediate must be in range 0x20 .. 0x2f');
assert.throws(() => { parser.identifier({intermediates: '!!!', final: 'z'}); }, 'only two bytes as intermediates are supported');
assert.throws(() => { parser.identifier({ intermediates: '\x1f', final: 'z' }); }, 'intermediate must be in range 0x20 .. 0x2f');
assert.throws(() => { parser.identifier({ intermediates: '\x30', final: 'z' }); }, 'intermediate must be in range 0x20 .. 0x2f');
assert.throws(() => { parser.identifier({ intermediates: '!!!', final: 'z' }); }, 'only two bytes as intermediates are supported');
});
it('final CSI/DCS range 0x40 .. 0x7e (default), one byte', () => {
for (let i = 0x40; i <= 0x7e; ++i) {
const c = String.fromCharCode(i);
assert.equal(parser.identToString(parser.identifier({final: c})), c);
assert.equal(parser.identToString(parser.identifier({ final: c })), c);
}
assert.throws(() => { parser.identifier({final: '\x3f'}); }, 'final must be in range 64 .. 126');
assert.throws(() => { parser.identifier({final: '\x7f'}); }, 'final must be in range 64 .. 126');
assert.throws(() => { parser.identifier({final: 'zz'}); }, 'final must be a single byte');
assert.throws(() => { parser.identifier({ final: '\x3f' }); }, 'final must be in range 64 .. 126');
assert.throws(() => { parser.identifier({ final: '\x7f' }); }, 'final must be in range 64 .. 126');
assert.throws(() => { parser.identifier({ final: 'zz' }); }, 'final must be a single byte');
});
it('final ESC range 0x30 .. 0x7e, one byte', () => {
for (let i = 0x30; i <= 0x7e; ++i) {
const final = String.fromCharCode(i);
let handler: IDisposable | undefined;
assert.doesNotThrow(() => { handler = parser.registerEscHandler({final}, () => true); }, 'final must be in range 48 .. 126');
assert.doesNotThrow(() => { handler = parser.registerEscHandler({ final }, () => true); }, 'final must be in range 48 .. 126');
if (handler) handler.dispose();
}
assert.throws(() => { parser.registerEscHandler({final: '\x2f'}, () => true); }, 'final must be in range 48 .. 126');
assert.throws(() => { parser.registerEscHandler({final: '\x7f'}, () => true); }, 'final must be in range 48 .. 126');
assert.throws(() => { parser.registerEscHandler({ final: '\x2f' }, () => true); }, 'final must be in range 48 .. 126');
assert.throws(() => { parser.registerEscHandler({ final: '\x7f' }, () => true); }, 'final must be in range 48 .. 126');
});
it('id calculation - should stacking prefix -> intermediate -> final', () => {
assert.equal(parser.identToString(parser.identifier({final: 'z'})), 'z');
assert.equal(parser.identToString(parser.identifier({prefix: '?', final: 'z'})), '?z');
assert.equal(parser.identToString(parser.identifier({intermediates: '!', final: 'z'})), '!z');
assert.equal(parser.identToString(parser.identifier({prefix: '?', intermediates: '!', final: 'z'})), '?!z');
assert.equal(parser.identToString(parser.identifier({prefix: '?', intermediates: '!!', final: 'z'})), '?!!z');
assert.equal(parser.identToString(parser.identifier({ final: 'z' })), 'z');
assert.equal(parser.identToString(parser.identifier({ prefix: '?', final: 'z' })), '?z');
assert.equal(parser.identToString(parser.identifier({ intermediates: '!', final: 'z' })), '!z');
assert.equal(parser.identToString(parser.identifier({ prefix: '?', intermediates: '!', final: 'z' })), '?!z');
assert.equal(parser.identToString(parser.identifier({ prefix: '?', intermediates: '!!', final: 'z' })), '?!!z');
});
});
describe('identifier invocation', () => {
it('ESC', () => {
const callstack: string[] = [];
const h1 = parser.registerEscHandler({final: 'z'}, () => { callstack.push('z'); return true; });
const h2 = parser.registerEscHandler({intermediates: '!', final: 'z'}, () => { callstack.push('!z'); return true; });
const h3 = parser.registerEscHandler({intermediates: '!!', final: 'z'}, () => { callstack.push('!!z'); return true; });
const h1 = parser.registerEscHandler({ final: 'z' }, () => { callstack.push('z'); return true; });
const h2 = parser.registerEscHandler({ intermediates: '!', final: 'z' }, () => { callstack.push('!z'); return true; });
const h3 = parser.registerEscHandler({ intermediates: '!!', final: 'z' }, () => { callstack.push('!!z'); return true; });
parse(parser, '\x1bz\x1b!z\x1b!!z');
h1.dispose();
h2.dispose();
@@ -1627,12 +1642,12 @@ describe('EscapeSequenceParser', () => {
});
it('CSI', () => {
const callstack: any[] = [];
const h1 = parser.registerCsiHandler({final: 'z'}, params => { callstack.push(['z', params.toArray()]); return true; });
const h2 = parser.registerCsiHandler({intermediates: '!', final: 'z'}, params => { callstack.push(['!z', params.toArray()]); return true; });
const h3 = parser.registerCsiHandler({intermediates: '!!', final: 'z'}, params => { callstack.push(['!!z', params.toArray()]); return true; });
const h4 = parser.registerCsiHandler({prefix: '?', final: 'z'}, params => { callstack.push(['?z', params.toArray()]); return true; });
const h5 = parser.registerCsiHandler({prefix: '?', intermediates: '!', final: 'z'}, params => { callstack.push(['?!z', params.toArray()]); return true; });
const h6 = parser.registerCsiHandler({prefix: '?', intermediates: '!!', final: 'z'}, params => { callstack.push(['?!!z', params.toArray()]); return true; });
const h1 = parser.registerCsiHandler({ final: 'z' }, params => { callstack.push(['z', params.toArray()]); return true; });
const h2 = parser.registerCsiHandler({ intermediates: '!', final: 'z' }, params => { callstack.push(['!z', params.toArray()]); return true; });
const h3 = parser.registerCsiHandler({ intermediates: '!!', final: 'z' }, params => { callstack.push(['!!z', params.toArray()]); return true; });
const h4 = parser.registerCsiHandler({ prefix: '?', final: 'z' }, params => { callstack.push(['?z', params.toArray()]); return true; });
const h5 = parser.registerCsiHandler({ prefix: '?', intermediates: '!', final: 'z' }, params => { callstack.push(['?!z', params.toArray()]); return true; });
const h6 = parser.registerCsiHandler({ prefix: '?', intermediates: '!!', final: 'z' }, params => { callstack.push(['?!!z', params.toArray()]); return true; });
parse(parser, '\x1b[1;z\x1b[1;!z\x1b[1;!!z\x1b[?1;z\x1b[?1;!z\x1b[?1;!!z');
h1.dispose();
h2.dispose();
@@ -1648,12 +1663,12 @@ describe('EscapeSequenceParser', () => {
});
it('DCS', () => {
const callstack: any[] = [];
const h1 = parser.registerDcsHandler({final: 'z'}, new DcsHandler((data, params) => { callstack.push(['z', params.toArray(), data]); return true; }));
const h2 = parser.registerDcsHandler({intermediates: '!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['!z', params.toArray(), data]); return true; }));
const h3 = parser.registerDcsHandler({intermediates: '!!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['!!z', params.toArray(), data]); return true; }));
const h4 = parser.registerDcsHandler({prefix: '?', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?z', params.toArray(), data]); return true; }));
const h5 = parser.registerDcsHandler({prefix: '?', intermediates: '!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?!z', params.toArray(), data]); return true; }));
const h6 = parser.registerDcsHandler({prefix: '?', intermediates: '!!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?!!z', params.toArray(), data]); return true; }));
const h1 = parser.registerDcsHandler({ final: 'z' }, new DcsHandler((data, params) => { callstack.push(['z', params.toArray(), data]); return true; }));
const h2 = parser.registerDcsHandler({ intermediates: '!', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['!z', params.toArray(), data]); return true; }));
const h3 = parser.registerDcsHandler({ intermediates: '!!', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['!!z', params.toArray(), data]); return true; }));
const h4 = parser.registerDcsHandler({ prefix: '?', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['?z', params.toArray(), data]); return true; }));
const h5 = parser.registerDcsHandler({ prefix: '?', intermediates: '!', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['?!z', params.toArray(), data]); return true; }));
const h6 = parser.registerDcsHandler({ prefix: '?', intermediates: '!!', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['?!!z', params.toArray(), data]); return true; }));
parse(parser, '\x1bP1;zAB\x1b\\\x1bP1;!zAB\x1b\\\x1bP1;!!zAB\x1b\\\x1bP?1;zAB\x1b\\\x1bP?1;!zAB\x1b\\\x1bP?1;!!zAB\x1b\\');
h1.dispose();
h2.dispose();
@@ -1678,3 +1693,528 @@ describe('EscapeSequenceParser', () => {
});
// TODO: error conditions and error recovery (not implemented yet in parser)
});
/**
* async handler tests.
*/
function parseSync(parser: TestEscapeSequenceParser, data: string): void | Promise<boolean> {
const container = new Uint32Array(data.length);
const decoder = new StringToUtf32();
return parser.parse(container, decoder.decode(data, container));
}
async function parseP(parser: TestEscapeSequenceParser, data: string): Promise<void> {
const container = new Uint32Array(data.length);
const decoder = new StringToUtf32();
const len = decoder.decode(data, container);
let result: void | Promise<boolean>;
let prev: boolean | undefined;
while (result = parser.parse(container, len, prev)) {
prev = await result;
}
}
function evalStackSaves(stackSaves: IParserStackState[], data: [number, ParserStackType, number][]): void {
assert.equal(stackSaves.length, data.length);
for (let i = 0; i < data.length; ++i) {
assert.equal(stackSaves[i].chunkPos, data[i][0]);
assert.equal(stackSaves[i].state, data[i][1]);
assert.equal(stackSaves[i].handlerPos, data[i][2]);
}
}
// helper similiar to assert.throws for async functions
async function throwsAsync(fn: () => Promise<any>, message?: string | undefined): Promise<void> {
let msg: string | undefined;
try {
await fn();
} catch (e) {
if (e instanceof Error) {
msg = e.message;
} else if (typeof e === 'string') {
msg = e;
}
if (typeof message === 'string') {
assert.equal(msg, message);
}
return;
}
assert.throws(fn, message);
}
describe('EscapeSequenceParser - async', () => {
// sequences: SGR 1;31 | hello SP | ESC %G | wor | ESC E | ld! | SGR 0 | EXE \r\n | $> | DCS 1;2 a [xyz] ST | OSC 1;foo=bar ST | FIN
// needed handlers: CSI m, PRINT, ESC %G, ESC E, EXE \r, EXE \n, OSC 1
const INPUT = '\x1b[1;31mhello \x1b%Gwor\x1bEld!\x1b[0m\r\n$>\x1bP1;2axyz\x1b\\\x1b]1;foo=bar\x1b\\FIN';
let RESULT: any[];
let parser: TestEscapeSequenceParser;
const callstack: any[] = [];
function clearAccu(): void {
callstack.length = 0;
parser.trackedStack.length = 0;
}
beforeEach(() => {
RESULT = [
['SGR', [1, 31]],
['PRINT', 'hello '],
['ESC %G'],
['PRINT', 'wor'],
['ESC E'],
['PRINT', 'ld!'],
['SGR', [0]],
['EXE \r'],
['EXE \n'],
['PRINT', '$>'],
['DCS a', ['xyz', [1, 2]]],
['OSC 1', 'foo=bar'],
['PRINT', 'FIN']
];
parser = new TestEscapeSequenceParser();
parser.reset();
parser.trackStackSavesOnPause();
clearAccu();
});
describe('sync handlers should behave as before', () => {
beforeEach(() => {
parser.setPrintHandler((data, start, end) => {
let result = '';
for (let i = start; i < end; ++i) {
result += stringFromCodePoint(data[i]);
}
callstack.push(['PRINT', result]);
});
parser.registerCsiHandler({ final: 'm' }, params => { callstack.push(['SGR', params.toArray()]); return true; });
parser.registerEscHandler({ intermediates: '%', final: 'G' }, () => { callstack.push(['ESC %G']); return true; });
parser.registerEscHandler({ final: 'E' }, () => { callstack.push(['ESC E']); return true; });
parser.setExecuteHandler('\r', () => { callstack.push(['EXE \r']); return true; });
parser.setExecuteHandler('\n', () => { callstack.push(['EXE \n']); return true; });
parser.registerOscHandler(1, new OscHandler(data => { callstack.push(['OSC 1', data]); return true; }));
parser.registerDcsHandler({final: 'a'}, new DcsHandler((data, params) => { callstack.push(['DCS a', [data, params.toArray()]]); return true;}));
});
it('sync handlers keep being parsed in sync mode', () => {
// note: if we have only sync handlers, a parse call should never return anything
assert.equal(!parseSync(parser, INPUT), true);
assert.equal(parser.parseStack.state, ParserStackType.NONE); // not paused
assert.equal(parser.trackedStack.length, 0); // never got paused
});
it('correct result on sync parse call', () => {
parseSync(parser, INPUT);
assert.deepEqual(callstack, RESULT);
assert.equal(parser.trackedStack.length, 0);
});
it('correct result on async parse call', async () => {
await parseP(parser, INPUT);
assert.deepEqual(callstack, RESULT);
assert.equal(parser.trackedStack.length, 0);
});
});
describe('async handlers', () => {
beforeEach(() => {
parser.setPrintHandler((data, start, end) => {
let result = '';
for (let i = start; i < end; ++i) {
result += stringFromCodePoint(data[i]);
}
callstack.push(['PRINT', result]);
});
parser.registerCsiHandler({ final: 'm' }, async params => { callstack.push(['SGR', params.toArray()]); return true; });
parser.registerEscHandler({ intermediates: '%', final: 'G' }, async () => { callstack.push(['ESC %G']); return true; });
parser.registerEscHandler({ final: 'E' }, async () => { callstack.push(['ESC E']); return true; });
parser.setExecuteHandler('\r', () => { callstack.push(['EXE \r']); return true; });
parser.setExecuteHandler('\n', () => { callstack.push(['EXE \n']); return true; });
parser.registerOscHandler(1, new OscHandler(async data => { callstack.push(['OSC 1', data]); return true; }));
parser.registerDcsHandler({final: 'a'}, new DcsHandler(async (data, params) => { callstack.push(['DCS a', [data, params.toArray()]]); return true;}));
});
it('sync parse call does not work anymore', () => {
assert.notEqual(!parseSync(parser, INPUT), true);
assert.notDeepEqual(callstack, RESULT);
// due to sync calling we should save exactly one saved stack
// proper continuation is not possible anymore, as we lost the promise resolve value
assert.equal(parser.trackedStack.length, 1);
});
it('improper continuation should throw', async () => {
/**
* Explanation:
* The first sync call will stop at the first promise returned,
* but does not await its resolve value.
* The second sync call to parse will fail due to missing `promiseResult`,
* which is needed for correct continuation.
*/
assert.notEqual(!parseSync(parser, INPUT), true);
assert.notDeepEqual(callstack, RESULT);
assert.throws(() => parseSync(parser, INPUT), 'improper continuation due to previous async handler, giving up parsing');
// keeps being broken for further parse calls (sync and async)
assert.throws(() => parseSync(parser, 'random'), 'improper continuation due to previous async handler, giving up parsing');
await throwsAsync(() => parseP(parser, 'foobar'), 'improper continuation due to previous async handler, giving up parsing');
// reset should lift the error condition
parser.reset();
await parseP(parser, INPUT); // does not throw anymore
});
it('correct result on awaited parse call', async () => {
await parseP(parser, INPUT);
assert.deepEqual(callstack, RESULT);
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
});
it('correct result on chunked awaited parse calls', async () => {
RESULT = [
['SGR', [1, 31]],
['PRINT', 'h'], // due to single char input PRINT is split
['PRINT', 'e'],
['PRINT', 'l'],
['PRINT', 'l'],
['PRINT', 'o'],
['PRINT', ' '],
['ESC %G'],
['PRINT', 'w'],
['PRINT', 'o'],
['PRINT', 'r'],
['ESC E'],
['PRINT', 'l'],
['PRINT', 'd'],
['PRINT', '!'],
['SGR', [0]],
['EXE \r'],
['EXE \n'],
['PRINT', '$'],
['PRINT', '>'],
['DCS a', ['xyz', [1, 2]]],
['OSC 1', 'foo=bar'],
['PRINT', 'F'],
['PRINT', 'I'],
['PRINT', 'N']
];
// split to single char input
for (let i = 0; i < INPUT.length; ++i) {
// Note: a single fully awaited parse call always ends in sync mode,
// which re-enables faster sync processing in the higher up callstack
await parseP(parser, INPUT[i]);
}
assert.deepEqual(callstack, RESULT);
evalStackSaves(parser.trackedStack, [
[0, ParserStackType.CSI, 0],
[0, ParserStackType.ESC, 0],
[0, ParserStackType.ESC, 0],
[0, ParserStackType.CSI, 0],
[0, ParserStackType.DCS, 0],
[0, ParserStackType.OSC, 0]
]);
});
it('multiple async SGR handlers', async () => {
// register with fallback
const SGR2 = parser.registerCsiHandler({ final: 'm' }, async params => { callstack.push(['2# SGR', params.toArray()]); return false; });
await parseP(parser, INPUT);
// should contain [2# SGR, SGR] call pairs
for (let i = 0; i < callstack.length; ++i) {
const entry = callstack[i];
if (entry[0] === '2# SGR') assert.equal(callstack[i + 1][0], 'SGR', 'Should fallback to original handler');
}
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 1],
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 1],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// after dispose we should be back to RESULT
SGR2.dispose();
await parseP(parser, INPUT);
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// register without fallback
const SGR22 = parser.registerCsiHandler({ final: 'm' }, async params => { callstack.push(['2# SGR', params.toArray()]); return true; });
await parseP(parser, INPUT);
// should only contain 2# SGR
for (let i = 0; i < callstack.length; ++i) {
const entry = callstack[i];
if (entry[0] === '2# SGR') assert.notEqual(callstack[i + 1][0], 'SGR', 'Should not fallback to original handler');
}
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 1],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 1],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// after dispose we should be back to RESULT
SGR22.dispose();
await parseP(parser, INPUT);
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
});
it('multiple async ESC handlers', async () => {
// register with fallback
const ESC2 = parser.registerEscHandler({ final: 'E' }, async () => { callstack.push(['2# ESC E']); return false; });
await parseP(parser, INPUT);
for (let i = 0; i < callstack.length; ++i) {
const entry = callstack[i];
if (entry[0] === '2# ESC E') assert.equal(callstack[i + 1][0], 'ESC E', 'Should fallback to original handler');
}
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 1],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// after dispose we should be back to RESULT
ESC2.dispose();
await parseP(parser, INPUT);
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// register without fallback
const ESC22 = parser.registerEscHandler({ final: 'E' }, async () => { callstack.push(['2# ESC E']); return true; });
await parseP(parser, INPUT);
for (let i = 0; i < callstack.length; ++i) {
const entry = callstack[i];
if (entry[0] === '2# ESC E') assert.notEqual(callstack[i + 1][0], 'ESC E', 'Should not fallback to original handler');
}
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 1],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// after dispose we should be back to RESULT
ESC22.dispose();
await parseP(parser, INPUT);
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
});
it('sync/async SGR mixed', async () => {
// sync with fallback
const SGR2 = parser.registerCsiHandler({ final: 'm' }, params => { callstack.push(['2# SGR', params.toArray()]); return false; });
// async with fallback
const SGR3 = parser.registerCsiHandler({ final: 'm' }, async params => { callstack.push(['3# SGR', params.toArray()]); return false; });
await parseP(parser, INPUT);
// should contain [3# SGR, 2# SGR, SGR] call triples
for (let i = 0; i < callstack.length; ++i) {
const entry = callstack[i];
if (entry[0] === '3# SGR') {
assert.equal(callstack[i + 1][0], '2# SGR', 'Should fallback to next handler');
assert.equal(callstack[i + 2][0], 'SGR', 'Should fallback to original handler');
}
}
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 2],
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 2],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// dispose SGR2 (sync one)
SGR2.dispose();
await parseP(parser, INPUT);
// should contain [3# SGR, SGR] call pairs
for (let i = 0; i < callstack.length; ++i) {
const entry = callstack[i];
if (entry[0] === '3# SGR') {
assert.equal(callstack[i + 1][0], 'SGR', 'Should fallback to original handler');
}
}
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 1],
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 1],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// dispose SGR3 (async one)
SGR3.dispose();
await parseP(parser, INPUT);
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
});
it('multiple async OSC handlers', async () => {
// register with fallback
const OSC2 = parser.registerOscHandler(1, new OscHandler(async data => { callstack.push(['2# OSC 1', data]); return false; }));
await parseP(parser, INPUT);
for (let i = 0; i < callstack.length; ++i) {
const entry = callstack[i];
if (entry[0] === '2# OSC 1') assert.equal(callstack[i + 1][0], 'OSC 1', 'Should fallback to original handler');
}
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// after dispose we should be back to RESULT
OSC2.dispose();
await parseP(parser, INPUT);
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// register without fallback
const OSC22 = parser.registerOscHandler(1, new OscHandler(async data => { callstack.push(['2# OSC 1', data]); return true; }));
await parseP(parser, INPUT);
for (let i = 0; i < callstack.length; ++i) {
const entry = callstack[i];
if (entry[0] === '2# OSC 1') assert.notEqual(callstack[i + 1][0], 'OSC 1', 'Should fallback to original handler');
}
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// after dispose we should be back to RESULT
OSC22.dispose();
await parseP(parser, INPUT);
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
});
it('multiple async DCS handlers', async () => {
// register with fallback
const DCS2 = parser.registerDcsHandler({final: 'a'}, new DcsHandler(async (data, params) => { callstack.push(['#2 DCS a', [data, params.toArray()]]); return false;}));
await parseP(parser, INPUT);
for (let i = 0; i < callstack.length; ++i) {
const entry = callstack[i];
if (entry[0] === '2# DCS a') assert.equal(callstack[i + 1][0], 'DCS a', 'Should fallback to original handler');
}
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// after dispose we should be back to RESULT
DCS2.dispose();
await parseP(parser, INPUT);
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// register without fallback
const DCS22 = parser.registerDcsHandler({final: 'a'}, new DcsHandler(async (data, params) => { callstack.push(['#2 DCS a', [data, params.toArray()]]); return true;}));
await parseP(parser, INPUT);
for (let i = 0; i < callstack.length; ++i) {
const entry = callstack[i];
if (entry[0] === '2# DCS a') assert.notEqual(callstack[i + 1][0], 'DCS a', 'Should fallback to original handler');
}
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// after dispose we should be back to RESULT
DCS22.dispose();
await parseP(parser, INPUT);
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
});
});
});
+219 -49
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType } from 'common/parser/Types';
import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType, IParserStackState, ParserStackType, ResumableHandlersType } from 'common/parser/Types';
import { ParserState, ParserAction } from 'common/parser/Constants';
import { Disposable } from 'common/Lifecycle';
import { IDisposable } from 'common/Types';
@@ -239,7 +239,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
// handler lookup containers
protected _printHandler: PrintHandlerType;
protected _executeHandlers: {[flag: number]: ExecuteHandlerType};
protected _executeHandlers: { [flag: number]: ExecuteHandlerType };
protected _csiHandlers: IHandlerCollection<CsiHandlerType>;
protected _escHandlers: IHandlerCollection<EscHandlerType>;
protected _oscParser: IOscParser;
@@ -253,6 +253,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
protected _escHandlerFb: EscFallbackHandlerType;
protected _errorHandlerFb: (state: IParsingState) => IParsingState;
// parser stack save for async handler support
protected _parseStack: IParserStackState = {
state: ParserStackType.NONE,
handlers: [],
handlerPos: 0,
transition: 0,
chunkPos: 0
};
constructor(
protected readonly _transitions: TransitionTable = VT500_TRANSITION_TABLE
) {
@@ -280,7 +289,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._errorHandler = this._errorHandlerFb;
// swallow 7bit ST (ESC+\)
this.registerEscHandler({final: '\\'}, () => true);
this.registerEscHandler({ final: '\\' }, () => true);
}
protected _identifier(id: IFunctionIdentifier, finalRange: number[] = [0x40, 0x7e]): number {
@@ -427,6 +436,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._errorHandler = this._errorHandlerFb;
}
/**
* Reset parser to initial values.
*
* This can also be used to lift the improper continuation error condition
* when dealing with async handlers. Use this only as a last resort to silence
* that error when the terminal has no pending data to be processed. Note that
* the interrupted async handler might continue its work in the future messing
* up the terminal state even further.
*/
public reset(): void {
this.currentState = this.initialState;
this._oscParser.reset();
@@ -435,9 +453,31 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._params.addParam(0); // ZDM
this._collect = 0;
this.precedingCodepoint = 0;
// abort pending continuation from async handler
// Here the RESET type indicates, that the next parse call will
// ignore any saved stack, instead continues sync with next codepoint from GROUND
if (this._parseStack.state !== ParserStackType.NONE) {
this._parseStack.state = ParserStackType.RESET;
this._parseStack.handlers = []; // also release handlers ref
}
}
/**
* Async parse support.
*/
protected _preserveStack(
state: ParserStackType,
handlers: ResumableHandlersType,
handlerPos: number,
transition: number,
chunkPos: number
): void {
this._parseStack.state = state;
this._parseStack.handlers = handlers;
this._parseStack.handlerPos = handlerPos;
this._parseStack.transition = transition;
this._parseStack.chunkPos = chunkPos;
}
/**
* Parse UTF32 codepoints in `data` up to `length`.
@@ -452,23 +492,141 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
* - DCS_PARAM:PARAM
* - OSC_STRING:OSC_PUT
* - DCS_PASSTHROUGH:DCS_PUT
*
* Note on asynchronous handler support:
* Any handler returning a promise will be treated as asynchronous.
* To keep the in-band blocking working for async handlers, `parse` pauses execution,
* creates a stack save and returns the promise to the caller.
* For proper continuation of the paused state it is important
* to await the promise resolving. On resolve the parse must be repeated
* with the same chunk of data and the resolved value in `promiseResult`
* until no promise is returned.
*
* Important: With only sync handlers defined, parsing is completely synchronous as well.
* As soon as an async handler is involved, synchronous parsing is not possible anymore.
*
* Boilerplate for proper parsing of multiple chunks with async handlers:
*
* ```typescript
* async function parseMultipleChunks(chunks: Uint32Array[]): Promise<void> {
* for (const chunk of chunks) {
* let result: void | Promise<boolean>;
* let prev: boolean | undefined;
* while (result = parser.parse(chunk, chunk.length, prev)) {
* prev = await result;
* }
* }
* // finished parsing all chunks...
* }
* ```
*/
public parse(data: Uint32Array, length: number): void {
public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise<boolean> {
let code = 0;
let transition = 0;
let currentState = this.currentState;
const osc = this._oscParser;
const dcs = this._dcsParser;
let collect = this._collect;
const params = this._params;
const table: Uint8Array = this._transitions.table;
let start = 0;
let handlerResult: void | boolean | Promise<boolean>;
// resume from async handler
if (this._parseStack.state) {
// allow sync parser reset even in continuation mode
// Note: can be used to recover parser from improper continuation error below
if (this._parseStack.state === ParserStackType.RESET) {
this._parseStack.state = ParserStackType.NONE;
start = this._parseStack.chunkPos + 1; // continue with next codepoint in GROUND
} else {
if (promiseResult === undefined || this._parseStack.state === ParserStackType.FAIL) {
/**
* Reject further parsing on improper continuation after pausing.
* This is a really bad condition with screwed up execution order and prolly messed up
* terminal state, therefore we exit hard with an exception and reject any further parsing.
*
* Note: With `Terminal.write` usage this exception should never occur, as the top level
* calls are guaranteed to handle async conditions properly. If you ever encounter this
* exception in your terminal integration it indicates, that you injected data chunks to
* `InputHandler.parse` or `EscapeSequenceParser.parse` synchronously without waiting for
* continuation of a running async handler.
*
* It is possible to get rid of this error by calling `reset`. But dont rely on that,
* as the pending async handler still might mess up the terminal later. Instead fix the faulty
* async handling, so this error will not be thrown anymore.
*/
this._parseStack.state = ParserStackType.FAIL;
throw new Error('improper continuation due to previous async handler, giving up parsing');
}
// we have to resume the old handler loop if:
// - return value of the promise was `false`
// - handlers are not exhausted yet
const handlers = this._parseStack.handlers;
let handlerPos = this._parseStack.handlerPos - 1;
switch (this._parseStack.state) {
case ParserStackType.CSI:
if (promiseResult === false && handlerPos > -1) {
for (; handlerPos >= 0; handlerPos--) {
handlerResult = (handlers as CsiHandlerType[])[handlerPos](this._params);
if (handlerResult === true) {
break;
} else if (handlerResult instanceof Promise) {
this._parseStack.handlerPos = handlerPos;
return handlerResult;
}
}
}
this._parseStack.handlers = [];
break;
case ParserStackType.ESC:
if (promiseResult === false && handlerPos > -1) {
for (; handlerPos >= 0; handlerPos--) {
handlerResult = (handlers as EscHandlerType[])[handlerPos]();
if (handlerResult === true) {
break;
} else if (handlerResult instanceof Promise) {
this._parseStack.handlerPos = handlerPos;
return handlerResult;
}
}
}
this._parseStack.handlers = [];
break;
case ParserStackType.DCS:
code = data[this._parseStack.chunkPos];
handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a, promiseResult);
if (handlerResult) {
return handlerResult;
}
if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;
this._params.reset();
this._params.addParam(0); // ZDM
this._collect = 0;
break;
case ParserStackType.OSC:
code = data[this._parseStack.chunkPos];
handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a, promiseResult);
if (handlerResult) {
return handlerResult;
}
if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;
this._params.reset();
this._params.addParam(0); // ZDM
this._collect = 0;
break;
}
// cleanup before continuing with the main sync loop
this._parseStack.state = ParserStackType.NONE;
start = this._parseStack.chunkPos + 1;
this.precedingCodepoint = 0;
this.currentState = this._parseStack.transition & TableAccess.TRANSITION_STATE_MASK;
}
}
// continue with main sync loop
// process input string
for (let i = 0; i < length; ++i) {
for (let i = start; i < length; ++i) {
code = data[i];
// normal transition & action lookup
transition = table[currentState << TableAccess.INDEX_STATE_SHIFT | (code < 0xa0 ? code : NON_ASCII_PRINTABLE)];
transition = this._transitions.table[this.currentState << TableAccess.INDEX_STATE_SHIFT | (code < 0xa0 ? code : NON_ASCII_PRINTABLE)];
switch (transition >> TableAccess.TRANSITION_ACTION_SHIFT) {
case ParserAction.PRINT:
// read ahead with loop unrolling
@@ -508,9 +666,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
{
position: i,
code,
currentState,
collect,
params,
currentState: this.currentState,
collect: this._collect,
params: this._params,
abort: false
});
if (inject.abort) return;
@@ -518,16 +676,21 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
break;
case ParserAction.CSI_DISPATCH:
// Trigger CSI Handler
const handlers = this._csiHandlers[collect << 8 | code];
const handlers = this._csiHandlers[this._collect << 8 | code];
let j = handlers ? handlers.length - 1 : -1;
for (; j >= 0; j--) {
// true means success and to stop bubbling
if (handlers[j](params)) {
// a promise indicates an async handler that needs to finish before progressing
handlerResult = handlers[j](this._params);
if (handlerResult === true) {
break;
} else if (handlerResult instanceof Promise) {
this._preserveStack(ParserStackType.CSI, handlers, j, transition, i);
return handlerResult;
}
}
if (j < 0) {
this._csiHandlerFb(collect << 8 | code, params);
this._csiHandlerFb(this._collect << 8 | code, this._params);
}
this.precedingCodepoint = 0;
break;
@@ -536,91 +699,98 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
do {
switch (code) {
case 0x3b:
params.addParam(0); // ZDM
this._params.addParam(0); // ZDM
break;
case 0x3a:
params.addSubParam(-1);
this._params.addSubParam(-1);
break;
default: // 0x30 - 0x39
params.addDigit(code - 48);
this._params.addDigit(code - 48);
}
} while (++i < length && (code = data[i]) > 0x2f && code < 0x3c);
i--;
break;
case ParserAction.COLLECT:
collect <<= 8;
collect |= code;
this._collect <<= 8;
this._collect |= code;
break;
case ParserAction.ESC_DISPATCH:
const handlersEsc = this._escHandlers[collect << 8 | code];
const handlersEsc = this._escHandlers[this._collect << 8 | code];
let jj = handlersEsc ? handlersEsc.length - 1 : -1;
for (; jj >= 0; jj--) {
// true means success and to stop bubbling
if (handlersEsc[jj]()) {
// a promise indicates an async handler that needs to finish before progressing
handlerResult = handlersEsc[jj]();
if (handlerResult === true) {
break;
} else if (handlerResult instanceof Promise) {
this._preserveStack(ParserStackType.ESC, handlersEsc, jj, transition, i);
return handlerResult;
}
}
if (jj < 0) {
this._escHandlerFb(collect << 8 | code);
this._escHandlerFb(this._collect << 8 | code);
}
this.precedingCodepoint = 0;
break;
case ParserAction.CLEAR:
params.reset();
params.addParam(0); // ZDM
collect = 0;
this._params.reset();
this._params.addParam(0); // ZDM
this._collect = 0;
break;
case ParserAction.DCS_HOOK:
dcs.hook(collect << 8 | code, params);
this._dcsParser.hook(this._collect << 8 | code, this._params);
break;
case ParserAction.DCS_PUT:
// inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f
// unhook triggered by: 0x1b, 0x9c (success) and 0x18, 0x1a (abort)
for (let j = i + 1; ; ++j) {
if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {
dcs.put(data, i, j);
this._dcsParser.put(data, i, j);
i = j - 1;
break;
}
}
break;
case ParserAction.DCS_UNHOOK:
dcs.unhook(code !== 0x18 && code !== 0x1a);
handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a);
if (handlerResult) {
this._preserveStack(ParserStackType.DCS, [], 0, transition, i);
return handlerResult;
}
if (code === 0x1b) transition |= ParserState.ESCAPE;
params.reset();
params.addParam(0); // ZDM
collect = 0;
this._params.reset();
this._params.addParam(0); // ZDM
this._collect = 0;
this.precedingCodepoint = 0;
break;
case ParserAction.OSC_START:
osc.start();
this._oscParser.start();
break;
case ParserAction.OSC_PUT:
// inner loop: 0x20 (SP) included, 0x7F (DEL) included
for (let j = i + 1; ; j++) {
if (j >= length || (code = data[j]) < 0x20 || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {
osc.put(data, i, j);
this._oscParser.put(data, i, j);
i = j - 1;
break;
}
}
break;
case ParserAction.OSC_END:
osc.end(code !== 0x18 && code !== 0x1a);
handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a);
if (handlerResult) {
this._preserveStack(ParserStackType.OSC, [], 0, transition, i);
return handlerResult;
}
if (code === 0x1b) transition |= ParserState.ESCAPE;
params.reset();
params.addParam(0); // ZDM
collect = 0;
this._params.reset();
this._params.addParam(0); // ZDM
this._collect = 0;
this.precedingCodepoint = 0;
break;
}
currentState = transition & TableAccess.TRANSITION_STATE_MASK;
this.currentState = transition & TableAccess.TRANSITION_STATE_MASK;
}
// save collected intermediates
this._collect = collect;
// save state
this.currentState = currentState;
}
}
+201
View File
@@ -250,3 +250,204 @@ describe('OscParser', () => {
});
});
});
class TestHandlerAsync implements IOscHandler {
constructor(public id: number, public output: any[], public msg: string, public returnFalse: boolean = false) {}
public start(): void {
this.output.push([this.msg, this.id, 'START']);
}
public put(data: Uint32Array, start: number, end: number): void {
this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]);
}
public async end(success: boolean): Promise<boolean> {
await new Promise(res => setTimeout(res, 20));
this.output.push([this.msg, this.id, 'END', success]);
if (this.returnFalse) {
return false;
}
return true;
}
}
async function endP(parser: OscParser, success: boolean): Promise<void> {
let result: void | Promise<boolean>;
let prev: boolean | undefined;
while (result = parser.end(success, prev)) {
prev = await result;
}
}
describe('OscParser - async tests', () => {
let parser: OscParser;
let reports: any[] = [];
beforeEach(() => {
reports = [];
parser = new OscParser();
parser.setHandlerFallback((id, action, data) => {
reports.push([id, action, data]);
});
});
describe('sync and async mixed', () => {
describe('sync | async | sync', () => {
it('first should run, cleanup action for others', async () => {
parser.registerHandler(1234, new TestHandler(1234, reports, 's1'));
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 'a1'));
parser.registerHandler(1234, new TestHandler(1234, reports, 's2'));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await endP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['s2', 1234, 'START'],
['a1', 1234, 'START'],
['s1', 1234, 'START'],
['s2', 1234, 'PUT', 'Here comes'],
['a1', 1234, 'PUT', 'Here comes'],
['s1', 1234, 'PUT', 'Here comes'],
['s2', 1234, 'PUT', 'the mouse!'],
['a1', 1234, 'PUT', 'the mouse!'],
['s1', 1234, 'PUT', 'the mouse!'],
['s2', 1234, 'END', true],
['a1', 1234, 'END', false],
['s1', 1234, 'END', false]
]);
});
it('all should run', async () => {
parser.registerHandler(1234, new TestHandler(1234, reports, 's1', true));
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 'a1', true));
parser.registerHandler(1234, new TestHandler(1234, reports, 's2', true));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await endP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['s2', 1234, 'START'],
['a1', 1234, 'START'],
['s1', 1234, 'START'],
['s2', 1234, 'PUT', 'Here comes'],
['a1', 1234, 'PUT', 'Here comes'],
['s1', 1234, 'PUT', 'Here comes'],
['s2', 1234, 'PUT', 'the mouse!'],
['a1', 1234, 'PUT', 'the mouse!'],
['s1', 1234, 'PUT', 'the mouse!'],
['s2', 1234, 'END', true],
['a1', 1234, 'END', true],
['s1', 1234, 'END', true]
]);
});
});
describe('async | sync | async', () => {
it('first should run, cleanup action for others', async () => {
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's1'));
parser.registerHandler(1234, new TestHandler(1234, reports, 'a1'));
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's2'));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await endP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['s2', 1234, 'START'],
['a1', 1234, 'START'],
['s1', 1234, 'START'],
['s2', 1234, 'PUT', 'Here comes'],
['a1', 1234, 'PUT', 'Here comes'],
['s1', 1234, 'PUT', 'Here comes'],
['s2', 1234, 'PUT', 'the mouse!'],
['a1', 1234, 'PUT', 'the mouse!'],
['s1', 1234, 'PUT', 'the mouse!'],
['s2', 1234, 'END', true],
['a1', 1234, 'END', false],
['s1', 1234, 'END', false]
]);
});
it('all should run', async () => {
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's1', true));
parser.registerHandler(1234, new TestHandler(1234, reports, 'a1', true));
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's2', true));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await endP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['s2', 1234, 'START'],
['a1', 1234, 'START'],
['s1', 1234, 'START'],
['s2', 1234, 'PUT', 'Here comes'],
['a1', 1234, 'PUT', 'Here comes'],
['s1', 1234, 'PUT', 'Here comes'],
['s2', 1234, 'PUT', 'the mouse!'],
['a1', 1234, 'PUT', 'the mouse!'],
['s1', 1234, 'PUT', 'the mouse!'],
['s2', 1234, 'END', true],
['a1', 1234, 'END', true],
['s1', 1234, 'END', true]
]);
});
});
describe('OscHandlerFactory', () => {
it('should be called once on end(true)', async () => {
parser.registerHandler(1234, new OscHandler(async data => { reports.push([1234, data]); return true; }));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
await endP(parser, true);
assert.deepEqual(reports, [[1234, 'Here comes the mouse!']]);
});
it('should not be called on end(false)', async () => {
parser.registerHandler(1234, new OscHandler(async data => { reports.push([1234, data]); return true; }));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await endP(parser, false);
assert.deepEqual(reports, []);
});
it('should be disposable', async () => {
parser.registerHandler(1234, new OscHandler(async data => { reports.push(['one', data]); return true; }));
const dispo = parser.registerHandler(1234, new OscHandler(async data => { reports.push(['two', data]); return true; }));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await endP(parser, true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!']]);
dispo.dispose();
parser.start();
data = toUtf32('1234;some other');
parser.put(data, 0, data.length);
data = toUtf32(' data');
parser.put(data, 0, data.length);
await endP(parser, true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]);
});
it('should respect return false', async () => {
parser.registerHandler(1234, new OscHandler(async data => { reports.push(['one', data]); return true; }));
parser.registerHandler(1234, new OscHandler(async data => { reports.push(['two', data]); return false; }));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await endP(parser, true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'Here comes the mouse!']]);
});
});
});
});
+66 -30
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser } from 'common/parser/Types';
import { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser, ISubParserStackState } from 'common/parser/Types';
import { OscState, PAYLOAD_LIMIT } from 'common/parser/Constants';
import { utf32ToString } from 'common/input/TextDecoder';
import { IDisposable } from 'common/Types';
@@ -16,6 +16,11 @@ export class OscParser implements IOscParser {
private _id = -1;
private _handlers: IHandlerCollection<IOscHandler> = Object.create(null);
private _handlerFb: OscFallbackHandlerType = () => { };
private _stack: ISubParserStackState = {
paused: false,
loopPosition: 0,
fallThrough: false
};
public registerHandler(ident: number, handler: IOscHandler): IDisposable {
if (this._handlers[ident] === undefined) {
@@ -41,15 +46,18 @@ export class OscParser implements IOscParser {
public dispose(): void {
this._handlers = Object.create(null);
this._handlerFb = () => {};
this._handlerFb = () => { };
this._active = EMPTY_HANDLERS;
}
public reset(): void {
// cleanup handlers if payload was already sent
// force cleanup handlers if payload was already sent
if (this._state === OscState.PAYLOAD) {
this.end(false);
for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {
this._active[j].end(false);
}
}
this._stack.paused = false;
this._active = EMPTY_HANDLERS;
this._id = -1;
this._state = OscState.START;
@@ -76,27 +84,6 @@ export class OscParser implements IOscParser {
}
}
private _end(success: boolean): void {
// other than the old code we always have to call .end
// to keep the bubbling we use `success` to indicate
// whether a handler should execute
if (!this._active.length) {
this._handlerFb(this._id, 'END', success);
} else {
let j = this._active.length - 1;
for (; j >= 0; j--) {
if (this._active[j].end(success)) {
break;
}
}
j--;
// cleanup left over handlers
for (; j >= 0; j--) {
this._active[j].end(false);
}
}
}
public start(): void {
// always reset leftover handlers
this.reset();
@@ -142,7 +129,7 @@ export class OscParser implements IOscParser {
* Whether the OSC got aborted or finished normally
* is indicated by `success`.
*/
public end(success: boolean): void {
public end(success: boolean, promiseResult: boolean = true): void | Promise<boolean> {
if (this._state === OscState.START) {
return;
}
@@ -154,7 +141,47 @@ export class OscParser implements IOscParser {
if (this._state === OscState.ID) {
this._start();
}
this._end(success);
if (!this._active.length) {
this._handlerFb(this._id, 'END', success);
} else {
let handlerResult: boolean | Promise<boolean> = false;
let j = this._active.length - 1;
let fallThrough = false;
if (this._stack.paused) {
j = this._stack.loopPosition - 1;
handlerResult = promiseResult;
fallThrough = this._stack.fallThrough;
this._stack.paused = false;
}
if (!fallThrough && handlerResult === false) {
for (; j >= 0; j--) {
handlerResult = this._active[j].end(success);
if (handlerResult === true) {
break;
} else if (handlerResult instanceof Promise) {
this._stack.paused = true;
this._stack.loopPosition = j;
this._stack.fallThrough = false;
return handlerResult;
}
}
j--;
}
// cleanup left over handlers
// we always have to call .end for proper cleanup,
// here we use `success` to indicate whether a handler should execute
for (; j >= 0; j--) {
handlerResult = this._active[j].end(false);
if (handlerResult instanceof Promise) {
this._stack.paused = true;
this._stack.loopPosition = j;
this._stack.fallThrough = true;
return handlerResult;
}
}
}
}
this._active = EMPTY_HANDLERS;
this._id = -1;
@@ -170,7 +197,7 @@ export class OscHandler implements IOscHandler {
private _data = '';
private _hitLimit: boolean = false;
constructor(private _handler: (data: string) => boolean) {}
constructor(private _handler: (data: string) => boolean | Promise<boolean>) { }
public start(): void {
this._data = '';
@@ -188,12 +215,21 @@ export class OscHandler implements IOscHandler {
}
}
public end(success: boolean): boolean {
let ret = false;
public end(success: boolean): boolean | Promise<boolean> {
let ret: boolean | Promise<boolean> = false;
if (this._hitLimit) {
ret = false;
} else if (success) {
ret = this._handler(this._data);
if (ret instanceof Promise) {
// need to hold data until `ret` got resolved
// dont care for errors, data will be freed anyway on next start
return ret.then(res => {
this._data = '';
this._hitLimit = false;
return res;
});
}
}
this._data = '';
this._hitLimit = false;
+42 -7
View File
@@ -6,6 +6,7 @@
import { IDisposable } from 'common/Types';
import { ParserState } from 'common/parser/Constants';
/** sequence params serialized to js arrays */
export type ParamsArray = (number | number[])[];
@@ -69,7 +70,7 @@ export interface IParsingState {
* CSI handler types.
* Note: `params` is borrowed.
*/
export type CsiHandlerType = (params: IParams) => boolean;
export type CsiHandlerType = (params: IParams) => boolean | Promise<boolean>;
export type CsiFallbackHandlerType = (ident: number, params: IParams) => void;
/**
@@ -93,14 +94,14 @@ export interface IDcsHandler {
* execution of the command should depend on `success`.
* To save memory also cleanup data structures here.
*/
unhook(success: boolean): boolean;
unhook(success: boolean): boolean | Promise<boolean>;
}
export type DcsFallbackHandlerType = (ident: number, action: 'HOOK' | 'PUT' | 'UNHOOK', payload?: any) => void;
/**
* ESC handler types.
*/
export type EscHandlerType = () => boolean;
export type EscHandlerType = () => boolean | Promise<boolean>;
export type EscFallbackHandlerType = (identifier: number) => void;
/**
@@ -129,7 +130,7 @@ export interface IOscHandler {
* execution of the command should depend on `success`.
* To save memory also cleanup data structures here.
*/
end(success: boolean): boolean;
end(success: boolean): boolean | Promise<boolean>;
}
export type OscFallbackHandlerType = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void;
@@ -160,7 +161,7 @@ export interface IEscapeSequenceParser extends IDisposable {
* Parse UTF32 codepoints in `data` up to `length`.
* @param data The data to parse.
*/
parse(data: Uint32Array, length: number): void;
parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise<boolean>;
/**
* Get string from numercial function identifier `ident`.
@@ -213,12 +214,12 @@ export interface ISubParser<T, U> extends IDisposable {
export interface IOscParser extends ISubParser<IOscHandler, OscFallbackHandlerType> {
start(): void;
end(success: boolean): void;
end(success: boolean, promiseResult?: boolean): void | Promise<boolean>;
}
export interface IDcsParser extends ISubParser<IDcsHandler, DcsFallbackHandlerType> {
hook(ident: number, params: IParams): void;
unhook(success: boolean): void;
unhook(success: boolean, promiseResult?: boolean): void | Promise<boolean>;
}
/**
@@ -237,3 +238,37 @@ export interface IFunctionIdentifier {
export interface IHandlerCollection<T> {
[key: string]: T[];
}
/**
* Types for async parser support.
*/
// type of saved stack state in parser
export const enum ParserStackType {
NONE = 0,
FAIL,
RESET,
CSI,
ESC,
OSC,
DCS
}
// aggregate of resumable handler lists
export type ResumableHandlersType = CsiHandlerType[] | EscHandlerType[];
// saved stack state of the parser
export interface IParserStackState {
state: ParserStackType;
handlers: ResumableHandlersType;
handlerPos: number;
transition: number;
chunkPos: number;
}
// saved stack state of subparser (OSC and DCS)
export interface ISubParserStackState {
paused: boolean;
loopPosition: number;
fallThrough: boolean;
}
+130
View File
@@ -8,6 +8,7 @@ import { BufferSet } from 'common/buffer/BufferSet';
import { IBufferSet, IBuffer } from 'common/buffer/Types';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
import { IAttributeData, IBufferLine, ScrollSource } from 'common/Types';
export const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars
export const MINIMUM_ROWS = 1;
@@ -23,9 +24,14 @@ export class BufferService extends Disposable implements IBufferService {
private _onResize = new EventEmitter<{ cols: number, rows: number }>();
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; }
private _onScroll = new EventEmitter<number>();
public get onScroll(): IEvent<number> { return this._onScroll.event; }
public get buffer(): IBuffer { return this.buffers.active; }
/** An IBufferline to clone/copy from for new blank lines */
private _cachedBlankLine: IBufferLine | undefined;
constructor(
@IOptionsService private _optionsService: IOptionsService
) {
@@ -52,4 +58,128 @@ export class BufferService extends Disposable implements IBufferService {
this.buffers.reset();
this.isUserScrolling = false;
}
/**
* Scroll the terminal down 1 row, creating a blank line.
* @param isWrapped Whether the new line is wrapped from the previous line.
*/
public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void {
const buffer = this.buffer;
let newLine: IBufferLine | undefined;
newLine = this._cachedBlankLine;
if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) {
newLine = buffer.getBlankLine(eraseAttr, isWrapped);
this._cachedBlankLine = newLine;
}
newLine.isWrapped = isWrapped;
const topRow = buffer.ybase + buffer.scrollTop;
const bottomRow = buffer.ybase + buffer.scrollBottom;
if (buffer.scrollTop === 0) {
// Determine whether the buffer is going to be trimmed after insertion.
const willBufferBeTrimmed = buffer.lines.isFull;
// Insert the line using the fastest method
if (bottomRow === buffer.lines.length - 1) {
if (willBufferBeTrimmed) {
buffer.lines.recycle().copyFrom(newLine);
} else {
buffer.lines.push(newLine.clone());
}
} else {
buffer.lines.splice(bottomRow + 1, 0, newLine.clone());
}
// Only adjust ybase and ydisp when the buffer is not trimmed
if (!willBufferBeTrimmed) {
buffer.ybase++;
// Only scroll the ydisp with ybase if the user has not scrolled up
if (!this.isUserScrolling) {
buffer.ydisp++;
}
} else {
// When the buffer is full and the user has scrolled up, keep the text
// stable unless ydisp is right at the top
if (this.isUserScrolling) {
buffer.ydisp = Math.max(buffer.ydisp - 1, 0);
}
}
} else {
// scrollTop is non-zero which means no line will be going to the
// scrollback, instead we can just shift them in-place.
const scrollRegionHeight = bottomRow - topRow + 1 /* as it's zero-based */;
buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1);
buffer.lines.set(bottomRow, newLine.clone());
}
// Move the viewport to the bottom of the buffer unless the user is
// scrolling.
if (!this.isUserScrolling) {
buffer.ydisp = buffer.ybase;
}
this._onScroll.fire(buffer.ydisp);
}
/**
* Scroll the display of the terminal
* @param disp The number of lines to scroll down (negative scroll up).
* @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used
* to avoid unwanted events being handled by the viewport when the event was triggered from the
* viewport originally.
*/
public scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void {
const buffer = this.buffer;
if (disp < 0) {
if (buffer.ydisp === 0) {
return;
}
this.isUserScrolling = true;
} else if (disp + buffer.ydisp >= buffer.ybase) {
this.isUserScrolling = false;
}
const oldYdisp = buffer.ydisp;
buffer.ydisp = Math.max(Math.min(buffer.ydisp + disp, buffer.ybase), 0);
// No change occurred, don't trigger scroll/refresh
if (oldYdisp === buffer.ydisp) {
return;
}
if (!suppressScrollEvent) {
this._onScroll.fire(buffer.ydisp);
}
}
/**
* Scroll the display of the terminal by a number of pages.
* @param pageCount The number of pages to scroll (negative scrolls up).
*/
public scrollPages(pageCount: number): void {
this.scrollLines(pageCount * (this.rows - 1));
}
/**
* Scrolls the display of the terminal to the top.
*/
public scrollToTop(): void {
this.scrollLines(-this.buffer.ydisp);
}
/**
* Scrolls the display of the terminal to the bottom.
*/
public scrollToBottom(): void {
this.scrollLines(this.buffer.ybase - this.buffer.ydisp);
}
public scrollToLine(line: number): void {
const scrollAmount = line - this.buffer.ydisp;
if (scrollAmount !== 0) {
this.scrollLines(scrollAmount);
}
}
}
@@ -42,6 +42,8 @@ export class ServiceCollection {
}
export class InstantiationService implements IInstantiationService {
public serviceBrand: undefined;
private readonly _services: ServiceCollection = new ServiceCollection();
constructor() {
+13 -22
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ILogService, IOptionsService } from 'common/services/Services';
import { ILogService, IOptionsService, LogLevelEnum } from 'common/services/Services';
type LogType = (message?: any, ...optionalParams: any[]) => void;
@@ -19,21 +19,12 @@ interface IConsole {
// module doesn't depend on them so we need to explicitly declare it.
declare const console: IConsole;
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
OFF = 4
}
const optionsKeyToLogLevel: { [key: string]: LogLevel } = {
debug: LogLevel.DEBUG,
info: LogLevel.INFO,
warn: LogLevel.WARN,
error: LogLevel.ERROR,
off: LogLevel.OFF
const optionsKeyToLogLevel: { [key: string]: LogLevelEnum } = {
debug: LogLevelEnum.DEBUG,
info: LogLevelEnum.INFO,
warn: LogLevelEnum.WARN,
error: LogLevelEnum.ERROR,
off: LogLevelEnum.OFF
};
const LOG_PREFIX = 'xterm.js: ';
@@ -41,7 +32,7 @@ const LOG_PREFIX = 'xterm.js: ';
export class LogService implements ILogService {
public serviceBrand: any;
private _logLevel!: LogLevel;
public logLevel: LogLevelEnum = LogLevelEnum.OFF;
constructor(
@IOptionsService private readonly _optionsService: IOptionsService
@@ -55,7 +46,7 @@ export class LogService implements ILogService {
}
private _updateLogLevel(): void {
this._logLevel = optionsKeyToLogLevel[this._optionsService.options.logLevel];
this.logLevel = optionsKeyToLogLevel[this._optionsService.options.logLevel];
}
private _evalLazyOptionalParams(optionalParams: any[]): void {
@@ -72,25 +63,25 @@ export class LogService implements ILogService {
}
public debug(message: string, ...optionalParams: any[]): void {
if (this._logLevel <= LogLevel.DEBUG) {
if (this.logLevel <= LogLevelEnum.DEBUG) {
this._log(console.log, message, optionalParams);
}
}
public info(message: string, ...optionalParams: any[]): void {
if (this._logLevel <= LogLevel.INFO) {
if (this.logLevel <= LogLevelEnum.INFO) {
this._log(console.info, message, optionalParams);
}
}
public warn(message: string, ...optionalParams: any[]): void {
if (this._logLevel <= LogLevel.WARN) {
if (this.logLevel <= LogLevelEnum.WARN) {
this._log(console.warn, message, optionalParams);
}
}
public error(message: string, ...optionalParams: any[]): void {
if (this._logLevel <= LogLevel.ERROR) {
if (this.logLevel <= LogLevelEnum.ERROR) {
this._log(console.error, message, optionalParams);
}
}
+19 -3
View File
@@ -5,7 +5,7 @@
import { IEvent } from 'common/EventEmitter';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes } from 'common/Types';
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource } from 'common/Types';
import { createDecorator } from 'common/services/ServiceRegistry';
export const IBufferService = createDecorator<IBufferService>('BufferService');
@@ -17,9 +17,14 @@ export interface IBufferService {
readonly buffer: IBuffer;
readonly buffers: IBufferSet;
isUserScrolling: boolean;
onResize: IEvent<{ cols: number, rows: number }>;
onScroll: IEvent<number>;
scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void;
scrollToBottom(): void;
scrollToTop(): void;
scrollToLine(line: number): void;
scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void;
scrollPages(pageCount: number): void;
resize(cols: number, rows: number): void;
reset(): void;
}
@@ -152,6 +157,8 @@ type GetLeadingNonServiceArgs<Args> =
export const IInstantiationService = createDecorator<IInstantiationService>('InstantiationService');
export interface IInstantiationService {
serviceBrand: undefined;
setService<T>(id: IServiceIdentifier<T>, instance: T): void;
getService<T>(id: IServiceIdentifier<T>): T | undefined;
createInstance<Ctor extends new (...args: any[]) => any, R extends InstanceType<Ctor>>(t: Ctor, ...args: GetLeadingNonServiceArgs<ConstructorParameters<Ctor>>): R;
@@ -161,6 +168,8 @@ export const ILogService = createDecorator<ILogService>('LogService');
export interface ILogService {
serviceBrand: undefined;
logLevel: LogLevelEnum;
debug(message: any, ...optionalParams: any[]): void;
info(message: any, ...optionalParams: any[]): void;
warn(message: any, ...optionalParams: any[]): void;
@@ -181,6 +190,13 @@ export interface IOptionsService {
export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number;
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'off';
export enum LogLevelEnum {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
OFF = 4
}
export type RendererType = 'dom' | 'canvas';
export interface IPartialTerminalOptions {
+1 -1
View File
@@ -6,7 +6,7 @@
import { pollFor, openTerminal, getBrowserType } from './TestUtils';
import { Browser, Page } from 'playwright';
const APP = 'http://127.0.0.1:3000/test';
const APP = 'http://127.0.0.1:3001/test';
let browser: Browser;
let page: Page;
+1 -1
View File
@@ -8,7 +8,7 @@ import { pollFor, openTerminal, getBrowserType } from './TestUtils';
import { Browser, Page } from 'playwright';
import { IRenderDimensions } from 'browser/renderer/Types';
const APP = 'http://127.0.0.1:3000/test';
const APP = 'http://127.0.0.1:3001/test';
let browser: Browser;
let page: Page;
+1 -1
View File
@@ -6,7 +6,7 @@
import { pollFor, writeSync, openTerminal, getBrowserType } from './TestUtils';
import { Browser, Page } from 'playwright';
const APP = 'http://127.0.0.1:3000/test';
const APP = 'http://127.0.0.1:3001/test';
let browser: Browser;
let page: Page;
+126 -21
View File
@@ -7,15 +7,15 @@ import { assert } from 'chai';
import { writeSync, openTerminal, getBrowserType } from './TestUtils';
import { Browser, Page } from 'playwright';
const APP = 'http://127.0.0.1:3000/test';
const APP = 'http://127.0.0.1:3001/test';
let browser: Browser;
let page: Page;
const width = 800;
const height = 600;
describe('Parser Integration Tests', function(): void {
before(async function(): Promise<any> {
describe('Parser Integration Tests', function (): void {
before(async function (): Promise<any> {
const browserType = getBrowserType();
browser = await browserType.launch({
headless: process.argv.indexOf('--headless') !== -1
@@ -28,84 +28,189 @@ describe('Parser Integration Tests', function(): void {
after(async () => browser.close());
describe('addCsiHandler', () => {
describe('registerCsiHandler', () => {
it('should call custom CSI handler with js array params', async () => {
await page.evaluate(`
window.term.reset();
window._customCsiHandlerParams = [];
const _customCsiHandler = window.term.parser.addCsiHandler({final: 'm'}, (params, collect) => {
const _customCsiHandler = window.term.parser.registerCsiHandler({final: 'm'}, (params, collect) => {
window._customCsiHandlerParams.push(params);
return false;
}, '');
`);
await writeSync(page, '\x1b[38;5;123mparams\x1b[38:2::50:100:150msubparams');
assert.deepEqual(await page.evaluate(`(() => window._customCsiHandlerParams)();`), [[38, 5, 123], [38, [2, -1, 50, 100, 150]]]);
assert.deepEqual(await page.evaluate(`window._customCsiHandlerParams`), [
[38, 5, 123],
[38, [2, -1, 50, 100, 150]]
]);
});
it('async', async () => {
await page.evaluate(`
window.term.reset();
window._customCsiHandlerCallStack = [];
const _customCsiHandlerA = window.term.parser.registerCsiHandler({intermediates:'+', final: 'Z'}, params => {
window._customCsiHandlerCallStack.push(['A', params]);
return false;
});
const _customCsiHandlerB = window.term.parser.registerCsiHandler({intermediates:'+', final: 'Z'}, async params => {
await new Promise(res => setTimeout(res, 50));
window._customCsiHandlerCallStack.push(['B', params]);
return false;
});
const _customCsiHandlerC = window.term.parser.registerCsiHandler({intermediates:'+', final: 'Z'}, params => {
window._customCsiHandlerCallStack.push(['C', params]);
return false;
});
`);
await writeSync(page, '\x1b[1;2+Z');
assert.deepEqual(await page.evaluate(`window._customCsiHandlerCallStack`), [
['C', [1, 2]],
['B', [1, 2]],
['A', [1, 2]]
]);
});
});
describe('addDcsHandler', () => {
describe('registerDcsHandler', () => {
it('should respects return value', async () => {
await page.evaluate(`
window.term.reset();
window._customDcsHandlerCallStack = [];
const _customDcsHandlerA = window.term.parser.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => {
const _customDcsHandlerA = window.term.parser.registerDcsHandler({intermediates:'+', final: 'p'}, (data, params) => {
window._customDcsHandlerCallStack.push(['A', params, data]);
return false;
});
const _customDcsHandlerB = window.term.parser.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => {
const _customDcsHandlerB = window.term.parser.registerDcsHandler({intermediates:'+', final: 'p'}, (data, params) => {
window._customDcsHandlerCallStack.push(['B', params, data]);
return true;
});
const _customDcsHandlerC = window.term.parser.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => {
const _customDcsHandlerC = window.term.parser.registerDcsHandler({intermediates:'+', final: 'p'}, (data, params) => {
window._customDcsHandlerCallStack.push(['C', params, data]);
return false;
});
`);
await writeSync(page, '\x1bP1;2+psome data\x1b\\\\');
assert.deepEqual(await page.evaluate(`(() => window._customDcsHandlerCallStack)();`), [['C', [1, 2], 'some data'], ['B', [1, 2], 'some data']]);
assert.deepEqual(await page.evaluate(`window._customDcsHandlerCallStack`), [
['C', [1, 2], 'some data'],
['B', [1, 2], 'some data']
]);
});
it('async', async () => {
await page.evaluate(`
window.term.reset();
window._customDcsHandlerCallStack = [];
const _customDcsHandlerA = window.term.parser.registerDcsHandler({intermediates:'+', final: 'q'}, (data, params) => {
window._customDcsHandlerCallStack.push(['A', params, data]);
return false;
});
const _customDcsHandlerB = window.term.parser.registerDcsHandler({intermediates:'+', final: 'q'}, async (data, params) => {
await new Promise(res => setTimeout(res, 50));
window._customDcsHandlerCallStack.push(['B', params, data]);
return false;
});
const _customDcsHandlerC = window.term.parser.registerDcsHandler({intermediates:'+', final: 'q'}, (data, params) => {
window._customDcsHandlerCallStack.push(['C', params, data]);
return false;
});
`);
await writeSync(page, '\x1bP1;2+qsome data\x1b\\\\');
assert.deepEqual(await page.evaluate(`window._customDcsHandlerCallStack`), [
['C', [1, 2], 'some data'],
['B', [1, 2], 'some data'],
['A', [1, 2], 'some data']
]);
});
});
describe('addEscHandler', () => {
describe('registerEscHandler', () => {
it('should respects return value', async () => {
await page.evaluate(`
window.term.reset();
window._customEscHandlerCallStack = [];
const _customEscHandlerA = window.term.parser.addEscHandler({intermediates:'(', final: 'B'}, () => {
const _customEscHandlerA = window.term.parser.registerEscHandler({intermediates:'(', final: 'B'}, () => {
window._customEscHandlerCallStack.push('A');
return false;
});
const _customEscHandlerB = window.term.parser.addEscHandler({intermediates:'(', final: 'B'}, () => {
const _customEscHandlerB = window.term.parser.registerEscHandler({intermediates:'(', final: 'B'}, () => {
window._customEscHandlerCallStack.push('B');
return true;
});
const _customEscHandlerC = window.term.parser.addEscHandler({intermediates:'(', final: 'B'}, () => {
const _customEscHandlerC = window.term.parser.registerEscHandler({intermediates:'(', final: 'B'}, () => {
window._customEscHandlerCallStack.push('C');
return false;
});
`);
await writeSync(page, '\x1b(B');
assert.deepEqual(await page.evaluate(`(() => window._customEscHandlerCallStack)();`), ['C', 'B']);
assert.deepEqual(await page.evaluate(`window._customEscHandlerCallStack`), ['C', 'B']);
});
it('async', async () => {
await page.evaluate(`
window.term.reset();
window._customEscHandlerCallStack = [];
const _customEscHandlerA = window.term.parser.registerEscHandler({intermediates:'(', final: 'Z'}, () => {
window._customEscHandlerCallStack.push('A');
return false;
});
const _customEscHandlerB = window.term.parser.registerEscHandler({intermediates:'(', final: 'Z'}, async () => {
await new Promise(res => setTimeout(res, 50));
window._customEscHandlerCallStack.push('B');
return false;
});
const _customEscHandlerC = window.term.parser.registerEscHandler({intermediates:'(', final: 'Z'}, () => {
window._customEscHandlerCallStack.push('C');
return false;
});
`);
await writeSync(page, '\x1b(Z');
assert.deepEqual(await page.evaluate(`window._customEscHandlerCallStack`), ['C', 'B', 'A']);
});
});
describe('addOscHandler', () => {
describe('registerOscHandler', () => {
it('should respects return value', async () => {
await page.evaluate(`
window.term.reset();
window._customOscHandlerCallStack = [];
const _customOscHandlerA = window.term.parser.addOscHandler(1234, data => {
const _customOscHandlerA = window.term.parser.registerOscHandler(1234, data => {
window._customOscHandlerCallStack.push(['A', data]);
return false;
});
const _customOscHandlerB = window.term.parser.addOscHandler(1234, data => {
const _customOscHandlerB = window.term.parser.registerOscHandler(1234, data => {
window._customOscHandlerCallStack.push(['B', data]);
return true;
});
const _customOscHandlerC = window.term.parser.addOscHandler(1234, data => {
const _customOscHandlerC = window.term.parser.registerOscHandler(1234, data => {
window._customOscHandlerCallStack.push(['C', data]);
return false;
});
`);
await writeSync(page, '\x1b]1234;some data\x07');
assert.deepEqual(await page.evaluate(`(() => window._customOscHandlerCallStack)();`), [['C', 'some data'], ['B', 'some data']]);
assert.deepEqual(await page.evaluate(`window._customOscHandlerCallStack`), [
['C', 'some data'],
['B', 'some data']
]);
});
it('async', async () => {
await page.evaluate(`
window.term.reset();
window._customOscHandlerCallStack = [];
const _customOscHandlerA = window.term.parser.registerOscHandler(666, data => {
window._customOscHandlerCallStack.push(['A', data]);
return false;
});
const _customOscHandlerB = window.term.parser.registerOscHandler(666, async data => {
await new Promise(res => setTimeout(res, 50));
window._customOscHandlerCallStack.push(['B', data]);
return false;
});
const _customOscHandlerC = window.term.parser.registerOscHandler(666, data => {
window._customOscHandlerCallStack.push(['C', data]);
return false;
});
`);
await writeSync(page, '\x1b]666;some data\x07');
assert.deepEqual(await page.evaluate(`window._customOscHandlerCallStack`), [
['C', 'some data'],
['B', 'some data'],
['A', 'some data']
]);
});
});
});
+13 -3
View File
@@ -7,7 +7,7 @@ import { assert } from 'chai';
import { pollFor, timeout, writeSync, openTerminal, getBrowserType } from './TestUtils';
import { Browser, Page } from 'playwright';
const APP = 'http://127.0.0.1:3000/test';
const APP = 'http://127.0.0.1:3001/test';
let browser: Browser;
let page: Page;
@@ -410,6 +410,16 @@ describe('API Integration Tests', function(): void {
await page.evaluate(`window.term.write('\\x1b]2;foo\\x9c')`);
await pollFor(page, `window.calls`, ['foo']);
});
it('onBell', async () => {
await openTerminal(page);
await page.evaluate(`
window.calls = [];
window.term.onBell(() => window.calls.push(true));
`);
await pollFor(page, `window.calls`, []);
await page.evaluate(`window.term.write('\\x07')`);
await pollFor(page, `window.calls`, [true]);
});
});
describe('buffer', () => {
@@ -625,11 +635,11 @@ describe('API Integration Tests', function(): void {
`);
const dims = await getDimensions();
await moveMouseCell(page, dims, 5, 1);
await pollFor(page, `window.calls`, ['provide 1', 'match', 'hover']);
await timeout(100);
await moveMouseCell(page, dims, 4, 1);
await pollFor(page, `window.calls`, ['provide 1', 'match', 'hover', 'leave' ]);
await moveMouseCell(page, dims, 7, 1);
await pollFor(page, `window.calls`, ['provide 1', 'match', 'hover', 'leave', 'hover']);
await timeout(100);
await moveMouseCell(page, dims, 8, 1);
await pollFor(page, `window.calls`, ['provide 1', 'match', 'hover', 'leave', 'hover', 'leave']);
await page.evaluate(`window.disposable.dispose()`);
+9 -2
View File
@@ -6,8 +6,9 @@
import * as playwright from 'playwright';
import deepEqual = require('deep-equal');
import { ITerminalOptions } from 'xterm';
import { deepStrictEqual, fail } from 'assert';
export async function pollFor<T>(page: playwright.Page, evalOrFn: string | (() => Promise<T>), val: T, preFn?: () => Promise<void>): Promise<void> {
export async function pollFor<T>(page: playwright.Page, evalOrFn: string | (() => Promise<T>), val: T, preFn?: () => Promise<void>, maxDuration?: number): Promise<void> {
if (preFn) {
await preFn();
}
@@ -18,8 +19,14 @@ export async function pollFor<T>(page: playwright.Page, evalOrFn: string | (() =
}
if (!deepEqual(result, val)) {
if (maxDuration === undefined) {
maxDuration = 2000;
}
if (maxDuration <= 0) {
deepStrictEqual(result, val, 'pollFor max duration exceeded');
}
return new Promise<void>(r => {
setTimeout(() => r(pollFor(page, evalOrFn, val, preFn)), 1);
setTimeout(() => r(pollFor(page, evalOrFn, val, preFn, maxDuration! - 10)), 10);
});
}
}
+7 -7
View File
@@ -29,7 +29,7 @@ perfContext('Terminal: ls -lR /usr/lib', () => {
chunks.push(data as unknown as Buffer);
length += data.length;
});
await new Promise(resolve => p.on('exit', () => resolve()));
await new Promise<void>(resolve => p.on('exit', () => resolve()));
contentUtf8 = Buffer.concat(chunks, length);
// translate to content string
const buffer = new Uint32Array(contentUtf8.length);
@@ -44,24 +44,24 @@ perfContext('Terminal: ls -lR /usr/lib', () => {
}
});
perfContext('write', () => {
perfContext('write/string/async', () => {
let terminal: Terminal;
before(() => {
terminal = new Terminal({cols: 80, rows: 25, scrollback: 1000});
});
new ThroughputRuntimeCase('', () => {
terminal.writeSync(content);
new ThroughputRuntimeCase('', async () => {
await new Promise<void>(res => terminal.write(content, res));
return {payloadSize: contentUtf8.length};
}, {fork: false}).showAverageThroughput();
});
perfContext('writeUtf8', () => {
perfContext('write/Utf8/async', () => {
let terminal: Terminal;
before(() => {
terminal = new Terminal({cols: 80, rows: 25, scrollback: 1000});
});
new ThroughputRuntimeCase('', () => {
terminal.writeSync(content);
new ThroughputRuntimeCase('', async () => {
await new Promise<void>(res => terminal.write(content, res));
return {payloadSize: contentUtf8.length};
}, {fork: false}).showAverageThroughput();
});
+35 -16
View File
@@ -709,6 +709,12 @@ declare module 'xterm' {
*/
onTitleChange: IEvent<string>;
/**
* Adds an event listener for when the bell is triggered.
* @returns an `IDisposable` to stop listening.
*/
onBell: IEvent<void>;
/**
* Unfocus the terminal.
*/
@@ -1501,6 +1507,23 @@ declare module 'xterm' {
/**
* Allows hooking into the parser for custom handling of escape sequences.
*
* Note on sync vs. async handlers:
* xterm.js implements all parser actions with synchronous handlers.
* In general custom handlers should also operate in sync mode wherever
* possible to keep the parser fast.
* Still the exposed interfaces allow to register async handlers by returning
* a `Promise<boolean>`. Here the parser will pause input processing until
* the promise got resolved or rejected (in-band blocking). This "full stop"
* on the input chain allows to implement backpressure from a certain async
* action while the terminal state will not progress any further from input.
* It does not mean that the terminal state will not change at all in between,
* as user actions like resize or reset are still processed immediately.
* It is an error to assume a stable terminal state while giving back control
* in between, e.g. by multiple chained `then` calls.
* Downside of an async handler is a rather bad throughput performance,
* thus use async handlers only as a last resort or for actions that have
* to rely on async interfaces itself.
*/
export interface IParser {
/**
@@ -1510,12 +1533,11 @@ declare module 'xterm' {
* @param callback The function to handle the sequence. The callback is
* called with the numerical params. If the sequence has subparams the
* array will contain subarrays with their numercial values.
* Return true if the sequence was handled; false if we should try
* a previous handler (set by addCsiHandler or setCsiHandler).
* The most recently added handler is tried first.
* Return `true` if the sequence was handled, `false` if the parser should try
* a previous handler. The most recently added handler is tried first.
* @return An IDisposable you can call to remove this handler.
*/
registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable;
registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise<boolean>): IDisposable;
/**
* Adds a handler for DCS escape sequences.
@@ -1529,12 +1551,11 @@ declare module 'xterm' {
* big payloads. Currently xterm.js limits DCS payload to 10 MB
* which should give enough room for most use cases.
* The function gets the payload and numerical parameters as arguments.
* Return true if the sequence was handled; false if we should try
* a previous handler (set by addDcsHandler or setDcsHandler).
* The most recently added handler is tried first.
* Return `true` if the sequence was handled, `false` if the parser should try
* a previous handler. The most recently added handler is tried first.
* @return An IDisposable you can call to remove this handler.
*/
registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable;
registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise<boolean>): IDisposable;
/**
* Adds a handler for ESC escape sequences.
@@ -1542,12 +1563,11 @@ declare module 'xterm' {
* gets registered, e.g. {intermediates: '%' final: 'G'} for
* default charset selection.
* @param callback The function to handle the sequence.
* Return true if the sequence was handled; false if we should try
* a previous handler (set by addEscHandler or setEscHandler).
* The most recently added handler is tried first.
* Return `true` if the sequence was handled, `false` if the parser should try
* a previous handler. The most recently added handler is tried first.
* @return An IDisposable you can call to remove this handler.
*/
registerEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable;
registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise<boolean>): IDisposable;
/**
* Adds a handler for OSC escape sequences.
@@ -1560,12 +1580,11 @@ declare module 'xterm' {
* big payloads. Currently xterm.js limits OSC payload to 10 MB
* which should give enough room for most use cases.
* The callback is called with OSC data string.
* Return true if the sequence was handled; false if we should try
* a previous handler (set by addOscHandler or setOscHandler).
* The most recently added handler is tried first.
* Return `true` if the sequence was handled, `false` if the parser should try
* a previous handler. The most recently added handler is tried first.
* @return An IDisposable you can call to remove this handler.
*/
registerOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
}
/**
+6 -6
View File
@@ -2928,9 +2928,9 @@ lodash.sortby@^4.7.0:
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
log-symbols@2.2.0:
version "2.2.0"
@@ -4820,9 +4820,9 @@ xterm-benchmark@^0.1.3:
typescript "^3.5.1"
y18n@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
version "4.0.1"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4"
integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==
yargs-parser@13.1.2, yargs-parser@^13.1.2:
version "13.1.2"