Compare commits

...
541 Commits
Author SHA1 Message Date
Daniel Imms b2bb848dfe Merge pull request #2299 from Tyriar/release/3.14-5
Pull bug fixes into release/3.14 and bump version
2019-07-10 11:43:59 -07:00
Daniel Imms 8005272aa3 v3.14.5 2019-07-10 11:37:36 -07:00
Daniel Imms c97972b9bb Fix setTheme NPE in 3.14
Fixes #2220
2019-07-10 11:37:18 -07:00
Max Risuhin 0734e97022 Reset isWrapped line flag. 2019-07-10 11:35:54 -07:00
Daniel Imms 2a6791881c Merge pull request #2266 from Tyriar/release/3.14_2263
Release/3.14 2263
2019-06-26 10:56:06 -07:00
Daniel Imms 41cfe868e4 v3.14.4
Messed up publish for .3
2019-06-26 10:55:41 -07:00
Daniel Imms 71a998de76 v3.14.3 2019-06-26 10:44:21 -07:00
Daniel Imms 63b25813e7 Fix soft reset breaking default attribute
Fixes #2196
2019-06-26 10:44:06 -07:00
Daniel Imms ebb21710c5 Merge pull request #2179 from Tyriar/release/3.14_2174
Fix NPE in open
2019-06-03 12:34:14 -07:00
Daniel Imms 367e23b64e v3.14.2 2019-06-03 12:29:25 -07:00
Daniel Imms 322ae131a1 Fix NPE in open
Fixes #2174
2019-06-03 12:28:44 -07:00
Daniel Imms 4f35c21355 v3.14.1 2019-05-31 09:30:49 -07:00
Daniel Imms 13133d699f Make sure xterm.css gets built to dist/ 2019-05-31 09:30:40 -07:00
Daniel Imms 0ed75dc100 Merge pull request #2145 from xtermjs/Tyriar-patch-1
v3.14.0
2019-05-30 16:37:12 -07:00
Daniel Imms b3a631750d v3.14.0 2019-05-30 16:26:37 -07:00
Daniel Imms 9643de4c01 Merge pull request #2141 from Tyriar/ts_3.5
typescript@3.5
2019-05-29 16:38:37 -07:00
Daniel Imms ab1263f669 typescript@3.5 2019-05-29 16:10:54 -07:00
Daniel Imms 9ebe094a2f Merge pull request #2140 from Tyriar/2136_fix_dom_renderer
Fix DOM renderer
2019-05-28 11:56:31 -07:00
Daniel Imms 0c3f2d7222 Revert "Revert "fix: don't use Math.floor in DomRenderer charWidth computation""
This reverts commit 1feb0a27e9.
2019-05-28 09:49:14 -07:00
Daniel Imms a39da0f0d5 Ensure DOM renderer has styles injected on ctor
Fixes #2136
2019-05-28 09:49:03 -07:00
Daniel Imms adac12eded Merge pull request #2134 from Tyriar/fix_underline
Draw underline over char glyph
2019-05-28 08:49:01 -07:00
Daniel Imms 030fb78712 Merge branch 'master' into fix_underline 2019-05-26 21:19:03 -07:00
Daniel Imms 96b3eb982b Draw underline over char glyph 2019-05-26 21:12:36 -07:00
Daniel Imms 5276ad8146 Merge pull request #2133 from Tyriar/remove_scripts
Remove release scripts
2019-05-26 13:17:19 -07:00
Daniel Imms 4c42393aa4 Remove release scripts
They're no longer used, azure-pipelines.yml does this now
2019-05-26 13:07:37 -07:00
Daniel Imms 1942b4ad51 Merge pull request #2130 from Tyriar/2117_J_tests
Tests for erase in display
2019-05-25 21:34:11 -07:00
Daniel Imms 05ca1cb7dc Up timeouts 2019-05-25 21:25:56 -07:00
Daniel Imms 2f4c5b93fa Merge pull request #2131 from Tyriar/72149_npe
Fix NPE in reflow
2019-05-25 21:23:19 -07:00
Daniel Imms de8d9a720a Fix NPE in reflow
See microsoft/vscode#72149
2019-05-25 21:17:36 -07:00
Daniel Imms 7c2bc70aac Tests for erase in display
Part of #2117
2019-05-25 21:13:47 -07:00
Daniel Imms 44e01ba006 Merge pull request #2128 from Tyriar/1965_optimize_a11y
Only update a11y announcements on animation frame
2019-05-25 19:03:22 -07:00
Daniel Imms 8ffdc14143 Only update a11y announcements on animation frame
Fixes #1965
2019-05-25 11:46:02 -07:00
Daniel Imms fda964b3ea Merge pull request #2127 from jerch/2126_fix
fix losing data after RIS command
2019-05-24 16:39:40 -07:00
Jörg Breitbart b748eca980 fix losing data behind RIS 2019-05-24 23:17:40 +02:00
jerch bbefedb6eb Merge pull request #2125 from jerch/2124_fix_colormode_switches
fix colormode switches
2019-05-24 21:13:59 +02:00
Jörg Breitbart c387180a47 add color mode transition tests 2019-05-24 21:09:11 +02:00
Jörg Breitbart 0e1704f563 cleanup 2019-05-24 20:50:25 +02:00
Daniel Imms b8f55f1c8f Merge pull request #2123 from Tyriar/revert_floor
Revert "fix: don't use Math.floor in DomRenderer charWidth computation"
2019-05-24 06:55:26 -07:00
Daniel Imms a428193424 Merge branch 'master' into revert_floor 2019-05-24 06:50:37 -07:00
Daniel Imms 1feb0a27e9 Revert "fix: don't use Math.floor in DomRenderer charWidth computation"
This reverts commit 23bdb2a6c2.
2019-05-24 06:48:48 -07:00
jerch a92e4adfe2 Merge pull request #2120 from jerch/2108_memory_exhaustion
trim processed chunks from write buffer
2019-05-22 22:28:01 +02:00
Jörg Breitbart 7716e9abda trim already processed chunks 2019-05-22 14:12:15 +02:00
Daniel Imms 6e196da59c Merge pull request #2118 from Tyriar/2117_csi_tests
Add tests for CSI @-I
2019-05-21 22:53:48 -07:00
Daniel Imms 69e8f62149 Make tests way faster by avoiding page reload 2019-05-21 22:43:02 -07:00
Daniel Imms b46757010f Boost timeout of xterm output tests 2019-05-21 22:34:04 -07:00
Daniel Imms a74173adf2 Add tests for CSI @-I
Part of #2117
2019-05-21 22:29:39 -07:00
Daniel Imms c10b18113e Merge pull request #2116 from Tyriar/2097_fix_addon_dispose
Fix new addon dispose
2019-05-21 16:35:28 -07:00
Daniel Imms 7786e59c77 Fix new addon dispose
Fixes #2097
2019-05-21 16:26:50 -07:00
Daniel Imms 9dfac7e674 Merge pull request #2114 from Tyriar/consolidate_api_test_timeout
Move API test timeout to top-level describe
2019-05-21 14:58:37 -07:00
Daniel Imms 922e594fcf Fix lint 2019-05-21 14:51:48 -07:00
Daniel Imms c73aa07135 Merge branch 'master' into consolidate_api_test_timeout 2019-05-21 14:48:22 -07:00
Daniel Imms acea979fe0 Move API test timeout to top-level describe
I learned a thing.
2019-05-21 14:48:03 -07:00
Daniel Imms e4f626e3b6 Merge pull request #2110 from Tyriar/split_out_integration
Split out integration tests into its own job
2019-05-21 14:39:35 -07:00
Daniel Imms 9a4f419820 Merge branch 'master' into split_out_integration 2019-05-21 14:33:27 -07:00
Daniel Imms 209c44898d Merge pull request #2113 from Tyriar/2111_automate_patch_release
Run release against builds on release/* branches
2019-05-21 14:32:12 -07:00
Daniel Imms d3ff32da72 Run release against builds on release/* branches
Fixes #2111
2019-05-21 14:26:28 -07:00
Daniel Imms f81d1cb1cf Increase integration test sleep 2019-05-21 14:20:58 -07:00
Daniel Imms 8f09f58119 Merge branch 'master' into split_out_integration 2019-05-21 12:39:22 -07:00
Daniel Imms ab0eedc513 Fix job name 2019-05-21 11:19:54 -07:00
Daniel Imms 8e460dfe55 Merge pull request #2109 from Tyriar/2107_cursor_report
Fix status reporting
2019-05-21 11:13:32 -07:00
Daniel Imms 37ed1e4796 Split out integration tests into its own job
This will make Linux finish faster and release trigger faster
2019-05-21 11:12:34 -07:00
Daniel Imms d0a3164e99 Fix lint 2019-05-21 11:01:40 -07:00
Daniel Imms 968dd93ba7 Fix status reporting
Fixes #2107
2019-05-21 10:57:26 -07:00
Daniel Imms 739723fe92 Merge pull request #2101 from Tyriar/bad_comment_
Remove old comment
2019-05-18 21:21:34 -07:00
Daniel Imms 2f08b87d95 Remove old comment 2019-05-18 21:21:02 -07:00
Daniel Imms eb70ddcba5 Merge pull request #2100 from Tyriar/2099_pausing
Support pausing in RenderCoordinator
2019-05-18 21:19:51 -07:00
Daniel Imms 43015f85f4 Move Renderer ownership to RenderCoordinator 2019-05-18 21:15:18 -07:00
Daniel Imms 87dca56dee Support pausing in RenderCoordinator
This actually removes the pause rendering functionality when the resize
and theme changes but simplifies it as a result, they also rarely happen
especially when off screen/hidden.

Fixes #2099
2019-05-18 20:20:46 -07:00
Daniel Imms 72e6863a7f Merge pull request #2098 from Tyriar/2093_logo_demo
Use logo in demo
2019-05-18 20:05:33 -07:00
Daniel Imms e83aa64794 Merge pull request #2096 from Tyriar/render_coordinator
Introduce RenderCoordinator
2019-05-18 20:03:20 -07:00
Daniel Imms c7416a6eaa Use logo in demo
Fixes #2093
2019-05-18 19:56:28 -07:00
Daniel Imms 9ed5920275 Add some basic fg/bg renderer tests 2019-05-18 19:51:44 -07:00
Daniel Imms 497d31bec3 onThemeChange -> setColors 2019-05-18 19:31:35 -07:00
Daniel Imms 24de302d2f onWindowResize -> onDevicePixelRatioChange 2019-05-18 19:27:06 -07:00
Daniel Imms 5f70b3f3be Move window resize listener to render coordinator 2019-05-18 19:22:51 -07:00
Daniel Imms b64cc0a9de Fix ui project build 2019-05-18 19:16:06 -07:00
Daniel Imms 3e4c1b5a21 Move ScreenDprMonitor to be owned by RenderCoordinator
(and a11y manager)
2019-05-18 19:12:37 -07:00
Daniel Imms f698cafccb Move the renderer events to RenderCoordinator 2019-05-18 19:02:42 -07:00
Daniel Imms 8792bb5f36 Introduce a RenderCoordinator that owns common Renderer logic
Part of #2005
2019-05-18 18:47:44 -07:00
Daniel Imms ac48ef977b Merge pull request #2094 from Tyriar/2005_colormanager
Move ColorManager to ui
2019-05-18 18:16:47 -07:00
Daniel Imms 4119c7d1d7 Move ColorManager ownership to Terminal 2019-05-18 18:10:04 -07:00
Daniel Imms 000fefdead Move ColorManager to ui
Part of #2005
Part of #1507
2019-05-18 17:49:48 -07:00
Daniel Imms 33955398e7 Merge pull request #2092 from Tyriar/layering
Move BufferReflow and Marker to core
2019-05-18 09:59:54 -07:00
Daniel Imms dbafc793da Move Marker to core 2019-05-17 23:41:26 -07:00
Daniel Imms 88c1ff6f11 Move BufferReflow to core 2019-05-17 23:37:44 -07:00
Daniel Imms 610c4c9726 Merge pull request #2088 from Tyriar/fix_coveralls
Fix what branch coverage is sent with
2019-05-16 10:47:34 -07:00
Daniel Imms e2214b0239 Fix what branch coverage is sent with 2019-05-16 10:39:53 -07:00
Daniel Imms 23d3a27a6f Merge pull request #2085 from Tyriar/a11y_fix
Add sanity check in AccessibilityManager
2019-05-16 09:58:02 -07:00
Daniel Imms d04016d51f Merge branch 'master' into a11y_fix 2019-05-16 09:52:54 -07:00
Daniel Imms fede301f5f Merge pull request #2081 from Tyriar/xterm-tests
Support debugging integration tests
2019-05-16 09:52:42 -07:00
Daniel Imms a62ec679bd Move readFileSync out of test 2019-05-15 12:49:13 -07:00
Daniel Imms da79fec5c2 Disable failing test on Linux 2019-05-15 11:57:32 -07:00
Daniel Imms 80025804b3 Add sanity check in AccessibilityManager
Fixes #2082
2019-05-15 11:47:15 -07:00
Daniel Imms 955c45906b Merge branch 'master' into xterm-tests 2019-05-13 21:30:33 -07:00
Daniel Imms f331269ef4 Support debugging integration tests 2019-05-13 21:11:56 -07:00
Daniel Imms 9cb6706e2d Merge pull request #2080 from jerch/demo_update
apply attach changes to demo
2019-05-13 21:07:48 -07:00
Daniel Imms b9cfbbea8f xterm-addon-attach@0.1.0-beta8 2019-05-13 20:34:14 -07:00
Jörg Breitbart 58d461bb90 optimize Buffer.concat 2019-05-13 15:33:54 +02:00
Jörg Breitbart 69411916cb make linter happy 2019-05-13 01:45:21 +02:00
Jörg Breitbart 1a596f9608 default to string transport 2019-05-13 01:41:51 +02:00
Jörg Breitbart 67be8c36e7 utf8 switch in server.js 2019-05-13 01:40:54 +02:00
Jörg Breitbart 0b50c06810 Merge branch 'master' into demo_update 2019-05-13 01:25:51 +02:00
Jörg Breitbart 53167df1a8 document utf8 switch in client.ts 2019-05-13 01:24:41 +02:00
jerch eef855601b Merge pull request #1904 from jerch/utf8_input
Support for raw UTF8 input
2019-05-13 01:23:39 +02:00
Jörg Breitbart 6fd5cdd8f1 apply attach changes to demo 2019-05-13 00:58:14 +02:00
Jörg Breitbart c0e9bbe87f Revert "change demo to utf8 input"
This reverts commit e6e5ecc0f2.
2019-05-12 22:17:06 +02:00
Jörg Breitbart 06c292c11d Merge branch 'master' into utf8_input 2019-05-12 22:16:59 +02:00
Jörg Breitbart bda068c5f0 docs 2019-05-12 22:07:14 +02:00
Daniel Imms 031a30b007 Merge pull request #2065 from Tyriar/1128_addons
New addon API
2019-05-12 12:54:45 -07:00
Daniel Imms 3475764946 Types/doc polish 2019-05-12 10:59:26 -07:00
Daniel Imms f90239377b Fix possible infinite loop if addon disposes terminal 2019-05-12 10:47:35 -07:00
Daniel Imms 1502fea431 Remove unused fullscreen addon from demo 2019-05-12 10:41:01 -07:00
Daniel Imms caf8f9d35e Update search addon 2019-05-12 10:39:39 -07:00
Daniel Imms d230f93bde Remove old search addon from demo 2019-05-12 10:25:41 -07:00
Daniel Imms bae125b789 Merge branch 'master' into utf8_input 2019-05-12 10:24:39 -07:00
Daniel Imms eda04bcb23 Add new search addon, move AddonManager to public
Because addons build on top of the API it needs to live in public, the main
reason for this is because the implementation of buffer differs on
public/Terminal and src/Terminal.
2019-05-12 10:22:29 -07:00
Jörg Breitbart 523d562f73 fix missing types 2019-05-12 18:49:23 +02:00
Daniel Imms 0c588a7333 Fix missed merge conflict 2019-05-12 09:45:09 -07:00
Daniel Imms f7612a49d2 Merge remote-tracking branch 'ups/master' into 1128_addons 2019-05-12 09:44:09 -07:00
Daniel Imms 34244248bf Merge pull request #2078 from Tyriar/1443_selection_api
select and getSelectionPosition APIs
2019-05-12 09:40:03 -07:00
Daniel Imms f654044e5e Fix search addon unit tests 2019-05-12 00:17:59 -07:00
Daniel Imms a7fa58cc60 Add API test for getSelectionPosition 2019-05-11 23:59:51 -07:00
Daniel Imms 9ca9f7296e Use new APIs in selection addon 2019-05-11 23:56:34 -07:00
Daniel Imms 5bbbbf7348 Expose getSelectionPosition and select APIs instead 2019-05-11 23:52:38 -07:00
Daniel Imms da7c39e526 Add API test 2019-05-11 23:37:42 -07:00
Daniel Imms 6674f05cc8 Implement setSelection API
Fixes #1443
2019-05-11 23:35:31 -07:00
Daniel Imms 161c7f9339 Use API clearSelection in search addon 2019-05-11 23:22:46 -07:00
Daniel Imms 780e924b21 Use stable API for buffer in search addon 2019-05-11 23:15:04 -07:00
Daniel Imms 1f000054c2 Merge remote-tracking branch 'ups/master' into 1128_addons 2019-05-11 22:48:49 -07:00
Daniel Imms c13d6e5eeb Make write and writeln consistent with writeUtf8 test 2019-05-11 22:29:01 -07:00
Daniel Imms e5dfc56030 Add api test for writeUtf8 2019-05-11 22:24:33 -07:00
Daniel Imms 00423407c1 Clean up 2019-05-11 22:18:18 -07:00
Daniel Imms 2340fcd071 Fix compile and update yarn.lock with utf8 2019-05-11 21:21:36 -07:00
Daniel Imms 59a1749d44 Merge branch 'master' into utf8_input 2019-05-11 21:16:05 -07:00
Daniel Imms 0fae9bb5a8 Merge pull request #2074 from Tyriar/1994_buffer_api
Buffer API
2019-05-11 21:15:55 -07:00
Daniel Imms 70ba349a85 Fix invalid index check, add api tests 2019-05-11 20:58:51 -07:00
Daniel Imms 49574be510 Add warnings to getCell and getLine 2019-05-11 20:49:48 -07:00
Daniel Imms f41e00ed97 Return undefined if cell or line doesn't exist 2019-05-11 20:46:55 -07:00
Jörg Breitbart ce079f399c fix docstring 2019-05-12 00:07:07 +02:00
Jörg Breitbart 7a567d2717 apply time-based limit 2019-05-11 23:10:18 +02:00
Daniel Imms cd3f2609d6 Make tests use buffer API over _core 2019-05-11 12:12:53 -07:00
Daniel Imms 8a929ed142 Add API tests for loadAddon 2019-05-11 11:02:15 -07:00
Daniel Imms 46995fdbc1 Add tests for buffer.getLine 2019-05-11 10:46:00 -07:00
Daniel Imms d694eae6b5 Add some buffer tests 2019-05-11 10:32:53 -07:00
Daniel Imms bce2eae66f Update yarn.lock 2019-05-11 10:15:29 -07:00
Daniel Imms 3554d3e075 Merge branch 'master' into 1128_addons 2019-05-11 10:14:48 -07:00
Daniel Imms eb6deb67f9 Fix build issues 2019-05-11 10:10:31 -07:00
Daniel Imms 7c55e1be9e Merge remote-tracking branch 'ups/master' into 1994_buffer_api 2019-05-11 10:05:31 -07:00
Daniel Imms 7549ca59be Merge branch 'master' into utf8_input 2019-05-11 10:02:14 -07:00
Daniel Imms af81acc50e Merge pull request #2068 from Tyriar/api_tests
API integration tests
2019-05-11 10:00:36 -07:00
Jörg Breitbart f033800554 fix imports 2019-05-11 17:56:26 +02:00
Daniel Imms 623dd56190 Merge pull request #2073 from jerch/2072_remove_instanceof
remove instanceof check
2019-05-11 08:46:22 -07:00
Jörg Breitbart 740604f035 remove instanceof check 2019-05-11 17:21:16 +02:00
Daniel Imms 3f68a60e73 Merge pull request #2067 from starpit/floor
fix: don't use Math.floor in DomRenderer charWidth computation
2019-05-11 00:50:20 -07:00
Daniel Imms 556c56ff94 Merge branch 'master' into api_tests 2019-05-11 00:10:33 -07:00
Daniel Imms dfb5d8e663 Add focus, blur test 2019-05-11 00:00:57 -07:00
Daniel Imms 7562948259 Add tests for all API events 2019-05-10 23:47:28 -07:00
Daniel Imms 34ac1bc502 Fix clearSelection not firing onSelectionChange
Fixes #2070
2019-05-10 23:38:45 -07:00
Daniel Imms 97b176e701 Add selection tests 2019-05-10 23:14:16 -07:00
Daniel Imms 8a5b6b0356 Move puppeteer to devDependencies 2019-05-10 22:58:23 -07:00
Daniel Imms 55d590348c API integration tests
Part of #1247
2019-05-10 22:57:27 -07:00
Nick Mitchell 23bdb2a6c2 fix: don't use Math.floor in DomRenderer charWidth computation
Fixes #2063
2019-05-11 00:59:59 -04:00
Daniel Imms 16aaf6c58c Fix build 2019-05-10 21:08:06 -07:00
Daniel Imms 22c9ad7145 Merge remote-tracking branch 'origin/master' into 1994_buffer_api 2019-05-10 21:04:26 -07:00
Daniel Imms 41a7cabbc2 Merge branch 'master' into 1128_addons 2019-05-10 21:02:55 -07:00
Daniel Imms 724baf22f9 Merge pull request #2066 from Tyriar/lib_import
Fix bad import from lib
2019-05-10 20:52:20 -07:00
Daniel Imms c04ebe974f Fix bad import from lib
Related #1996
2019-05-10 20:51:18 -07:00
Daniel Imms fcee890819 Merge branch 'master' into 1128_addons 2019-05-10 20:47:17 -07:00
Daniel Imms d696046921 Simplify and mark new addon API as experimental 2019-05-10 20:38:08 -07:00
Daniel Imms 518e2734ba Re-introduce original attach and webLinks addons back
We could release this in v3 if we don't break addons like this
2019-05-10 20:30:47 -07:00
Daniel Imms eedcbccb15 Merge remote-tracking branch 'origin/master' into 1128_addons 2019-05-10 20:24:43 -07:00
Daniel Imms d7e6cb17a5 Merge pull request #2062 from Tyriar/windowsMode_typings
Add typings for windowsMode
2019-05-10 19:41:49 -07:00
Daniel Imms b658baf921 Add typings for windowsMode 2019-05-10 16:26:53 -07:00
Daniel Imms 10c6e900ad Merge pull request #2059 from Tyriar/buffer_core
Move BufferLine into core
2019-05-10 15:17:14 -07:00
Daniel Imms b608c9d0ef Merge branch 'master' into buffer_core 2019-05-10 14:57:32 -07:00
Daniel Imms 0e0bcde957 Merge pull request #2061 from Tyriar/fix_beta_off_patch
Make sure beta builds are always on patch version 0
2019-05-10 07:57:33 -07:00
Daniel Imms d735196a30 Make sure beta builds are always on patch version 0 2019-05-10 07:39:42 -07:00
Daniel Imms 59aec69379 Add BufferLine into core and strict null check 2019-05-09 22:19:33 -07:00
Daniel Imms 228ec7b818 Move BufferLine into core 2019-05-09 22:15:32 -07:00
Daniel Imms 7b7fc53157 Move more BufferLine dependencies into core 2019-05-09 22:10:07 -07:00
Daniel Imms f381cdfc33 Move a bunch of types to core 2019-05-09 21:57:33 -07:00
Daniel Imms ac6bced6e7 Merge pull request #2056 from xtermjs/Tyriar-patch-1
Add missing space to typings
2019-05-09 10:12:29 -07:00
Daniel Imms 1a61401cbe Merge pull request #2057 from Tyriar/fix_cd_after_release
Fix beta CD for new major/minor beta versions
2019-05-09 10:12:11 -07:00
Daniel Imms 6d55ab23fd Fix beta CD for new major/minor beta versions 2019-05-09 10:11:25 -07:00
Daniel Imms 3abc090412 Add missing space to typings 2019-05-09 10:02:42 -07:00
Daniel Imms 21f1498977 Merge pull request #2053 from xtermjs/Tyriar-patch-1
Update year in README
2019-05-09 10:00:09 -07:00
Daniel Imms 16c0550d7c Merge pull request #2051 from Tyriar/2019_prepublish
Replace prepublish
2019-05-09 09:59:58 -07:00
Daniel Imms 517d5cfd0e Merge pull request #2050 from Tyriar/2048_demo_line_height
Update terminal dimensions after line height changes
2019-05-09 09:59:49 -07:00
Daniel Imms d363416fb4 Merge pull request #2046 from NizamLZ/fix/2045
Allow + char in URI path
2019-05-09 09:59:36 -07:00
Daniel Imms ffc868a4b8 Merge pull request #2055 from xtermjs/Tyriar-patch-2
v3.13.0
2019-05-09 09:42:56 -07:00
Daniel Imms e40f924913 v3.13.0 2019-05-09 09:37:07 -07:00
Daniel Imms ca1c647442 Merge pull request #2054 from Tyriar/fix_beta_cd
Actually fix beta CD
2019-05-09 08:20:58 -07:00
Daniel Imms 79e182d63f Actually fix beta CD
Part of #2044
2019-05-09 08:20:19 -07:00
Daniel Imms c1b56722d3 Merge pull request #2052 from jflatow/unit-separator
Send the unit separator for C-_, like some other keyboards
2019-05-09 07:56:30 -07:00
Daniel Imms b959fc8d6a Update year in README 2019-05-09 07:49:50 -07:00
Khaja Nizamuddin 48753625f3 Merge branch 'master' into fix/2045 2019-05-09 12:43:25 +05:30
Jared Flatow dc236aeb20 Send the unit separator for C-_, like some other keyboards 2019-05-09 00:06:32 -07:00
Daniel Imms 71886167be Replace prepublish
On prepublishOnly we want to do a full build, on prepare we want
to only build the ts projects so the .d.ts files are avilable.

Fixes #2019
2019-05-08 23:08:39 -07:00
Daniel Imms 4b1c54d634 Update terminal dimensions after line height changes
Fixes #2048
2019-05-08 20:53:24 -07:00
Daniel Imms 2d0937ac1d Merge pull request #2047 from Tyriar/2044_cd_fix
Base the next beta version on the correct current beta
2019-05-08 18:08:18 -07:00
Daniel Imms 0d5cd9582f Base the next beta version on the correct current beta
Fixes #2044
2019-05-08 18:03:06 -07:00
Khaja Nizamuddin e831bdf1ef Allow + char in URI path 2019-05-08 21:06:22 +05:30
Daniel Imms 4729bfaa2d Merge pull request #2043 from cubiclesoft/add_php_app_server
Added PHP App Server to Real-world uses section.
2019-05-07 18:30:04 -07:00
cubiclesoft a88469aac6 Added PHP App Server to Real-world uses section. 2019-05-07 12:07:00 -07:00
jerch d6da010c7e Merge pull request #2035 from jerch/2028_handle_joined_cells
cell representation for joined cells
2019-05-06 22:42:05 +02:00
Daniel Imms c6138edec2 Merge branch 'master' into 2028_handle_joined_cells 2019-05-05 18:55:53 -07:00
Daniel Imms 15a6c9846d Merge pull request #2040 from Tyriar/devcontainer
Add a dev container
2019-05-05 16:06:55 -07:00
Jörg Breitbart ca193ed00d Merge branch 'master' into 2028_handle_joined_cells 2019-05-05 21:54:16 +02:00
Jörg Breitbart ffcdba8d5b simplify JoinedCellData 2019-05-05 21:48:35 +02:00
Daniel Imms d68c441b86 Merge pull request #2038 from roottool/fix-that-webLinks-not-interpreted#1954
Fix that webLinks not interpreted
2019-05-03 21:13:00 -07:00
Daniel Imms 613f877954 Add a dev container
This lets users using VS Code/Docker avoid installing node and the C++
dependencies necessary for running xterm.js.
2019-05-03 21:08:25 -07:00
Daniel Imms a1353a8fb4 Merge pull request #2039 from roottool/refacter-the-css-code-of-cursor-blink
Refactored the CSS code of cursor blink
2019-05-03 12:59:04 -07:00
roottool 4269777f1c Fix: Refactored the css code of keyframes blink 2019-05-04 02:54:38 +09:00
roottool 23eed1b3ea Fix: Changed pathClause 2019-05-04 02:02:26 +09:00
Daniel Imms ebc0083531 Merge pull request #2036 from Tyriar/protect_markers
Make Terminal.markers readonly
2019-05-02 09:45:15 -07:00
Daniel Imms 7d3e2d7da4 Merge branch 'master' into protect_markers 2019-05-02 09:33:24 -07:00
Daniel Imms ea09c6af09 Fix build 2019-05-02 09:23:13 -07:00
Daniel Imms 267357c4de Merge pull request #2032 from Tyriar/task
Add build and test task
2019-05-02 07:54:42 -07:00
Daniel Imms f416bd155a Make compound task the default 2019-05-02 07:53:28 -07:00
Daniel Imms c6a3fb3b42 Merge branch 'master' into task 2019-05-02 07:47:29 -07:00
Daniel Imms cb33c897d2 Merge branch 'master' into protect_markers 2019-04-30 12:20:30 -07:00
Daniel Imms 469d5158a0 Make Terminal.markers readonly 2019-04-30 19:19:41 +00:00
Daniel Imms 766b1745d3 Merge pull request #2027 from Tyriar/auto_release
Automatically release stable builds
2019-04-29 22:50:16 -07:00
Daniel Imms 68182ed162 Merge branch 'master' into auto_release 2019-04-29 22:42:20 -07:00
Daniel Imms d261862ccb Merge pull request #2026 from Tyriar/ui_project
Create UI project, strict null check some more files
2019-04-29 22:41:54 -07:00
Daniel Imms 8d6682a2bc Merge branch 'master' into ui_project 2019-04-29 22:26:28 -07:00
Daniel Imms 63833b59ed Fix lint 2019-04-29 22:24:44 -07:00
Jörg Breitbart 4fd9b3f249 jix joined cell representation as overloaded CellData type 2019-04-30 00:58:18 +02:00
jerch 3f145b8a0c Merge pull request #2034 from jerch/2031_defaultcolor_handling
apply default fg color for uncached chars and underline in canvas renderer
2019-04-29 23:40:18 +02:00
Jörg Breitbart 1f42e7c06a fix fg color for underline 2019-04-29 23:19:16 +02:00
Jörg Breitbart 42b1dca7db add handling for default fg color 2019-04-29 22:33:01 +02:00
Daniel Imms f0c3bab156 Add build and test task 2019-04-29 11:35:42 -07:00
Daniel Imms 4453a1cbf7 Automatically release stable builds
Part of #1921
2019-04-28 03:52:43 +00:00
Daniel Imms 362152004b Move files not in ui project back to root 2019-04-28 02:28:20 +00:00
Daniel Imms 88d5cf46f8 Strict null check ScreenDprMonitor 2019-04-28 02:16:28 +00:00
Daniel Imms ebfa57b71c Strict null check RenderDebouncer 2019-04-28 02:03:41 +00:00
Daniel Imms 4c3082c924 Remove ITerminal dependency in RenderDebouncer 2019-04-28 01:59:40 +00:00
Daniel Imms 672db9437f Create ui project, add Lifecycle 2019-04-28 01:55:56 +00:00
Daniel Imms eb10652e46 Merge branch 'master' into utf8_input 2019-04-27 18:49:46 -07:00
Daniel Imms f4bbfc30f3 Merge pull request #1872 from Tyriar/cd
Setup continuous deployment beta builds
2019-04-27 18:33:46 -07:00
Daniel Imms 65eaea39d3 Base the beta build off of the next minor version 2019-04-28 00:45:51 +00:00
Daniel Imms 70db48b533 Add notice about beta builds in readme 2019-04-28 00:34:44 +00:00
Daniel Imms 779ebe87e5 Merge branch 'master' into cd 2019-04-27 16:52:09 -07:00
Daniel Imms 6237eaa86f Merge pull request #2025 from thebradbain/fix-terminal-resize
Clear line cache on terminal resize
2019-04-27 16:42:30 -07:00
Bradley Bain 736bd53e49 Clear line cache on terminal resize 2019-04-25 21:24:07 -07:00
Daniel Imms 69d43d4a40 Merge branch 'master' into cd 2019-04-14 16:04:49 -07:00
Daniel Imms 017a5b1218 Merge branch 'master' into utf8_input 2019-04-14 12:54:26 -07:00
Daniel Imms c6f785f51a Merge pull request #2014 from starpit/underline
feat: add underline support to DOM renderer
2019-04-14 11:24:25 -07:00
Nick Mitchell 8718d3a4ca feat: add underline support to DOM renderer
part of #1896
2019-04-14 13:47:58 -04:00
Daniel Imms 7aaa1ad0bb Merge pull request #2009 from Tyriar/single_quote
Add vscode setting for single quote style
2019-04-12 09:36:35 -07:00
Daniel Imms fa70aa0983 Merge pull request #2008 from Tyriar/event_emitter_2
Explicit event API
2019-04-11 09:08:15 -07:00
Daniel Imms 89a3931a68 Merge branch 'master' into event_emitter_2 2019-04-11 08:15:19 -07:00
Daniel Imms a0ee09a7bc Add vscode setting for single quote style
This will alwayts use single quotes when TS server does auto imports,
implement interface, etc.
2019-04-11 08:14:18 -07:00
Daniel Imms 94415a227d Merge pull request #1866 from ntchjb/master
Fix search addons: Fix problem of skipping some results and incorrect text selection after resize window
2019-04-10 03:45:58 -04:00
Daniel Imms 0b4ff4bb6b Merge branch 'master' into event_emitter_2 2019-04-10 03:34:37 -04:00
Daniel Imms 4be376d397 Merge branch 'master' into master 2019-04-10 03:31:40 -04:00
Daniel Imms 5362ea195c Add first API test 2019-04-10 00:24:50 -07:00
Daniel Imms b55bedc6af Add terminal API interface to Types.ts
This is needed since the API/public buffer now differs from the TerminalCore's
2019-04-09 21:29:08 -07:00
Daniel Imms cf4b59567b Add tests for new events 2019-04-09 21:01:55 -07:00
Daniel Imms b9bc219ecc Make events in d.ts alphabetical 2019-04-09 20:52:18 -07:00
Daniel Imms f4c7e3fd7d Remove extends EventEmitter from Linkifier 2019-04-09 20:43:42 -07:00
Daniel Imms 36567e60df Remove extends EventEmitter from more classes 2019-04-09 20:36:08 -07:00
Daniel Imms 2e0e24c382 Remove extends EventEmitter from SelectinoManager 2019-04-09 20:26:18 -07:00
Daniel Imms 290bde6163 Convert refresh to EventEmitter2 2019-04-09 20:16:26 -07:00
Daniel Imms db78b75e8b Convert selection to EventEmitter2 2019-04-09 20:05:13 -07:00
Daniel Imms 87328fe293 Convert data to EventEmitter2 2019-04-09 19:53:02 -07:00
Daniel Imms 2c96c262f4 Convert scroll to EventEmitter2 2019-04-09 19:42:53 -07:00
Daniel Imms f111aa27dc Convert linefeed to EventEmitter2 2019-04-09 19:35:41 -07:00
Daniel Imms 8e5d372b81 Convert cursormove to EventEmitter2 2019-04-09 19:31:59 -07:00
Daniel Imms 8b5b64605e Start find previous from the current viewport 2019-04-09 19:53:22 -04:00
Daniel Imms 0c388c69bc Merge branch 'master' into pr/jerch/1904 2019-04-09 19:38:39 -04:00
Daniel Imms 660466d244 Merge pull request #1923 from jerch/fix_1879
signal sequence was finally handled
2019-04-09 11:15:02 -04:00
Daniel Imms dee4429151 Remove unneeded interfaces 2019-04-09 11:11:16 -04:00
Daniel Imms c3d1b0703c Merge branch 'master' into fix_1879 2019-04-09 11:10:17 -04:00
Daniel Imms 71827f7981 Merge branch 'master' into master 2019-04-09 11:01:02 -04:00
Daniel Imms 2d80abe801 Merge branch 'master' into 1128_addons 2019-04-09 10:57:32 -04:00
Daniel Imms 6b0f2170ea Merge branch 'master' into 1994_buffer_api 2019-04-09 10:57:22 -04:00
Daniel Imms a86c2943b8 Merge branch 'master' into event_emitter_2 2019-04-09 10:57:11 -04:00
Daniel Imms cb591f212a Merge pull request #2006 from skavanagh/patch-1
Added Bastillion to Real-world uses
2019-04-09 10:53:08 -04:00
Sean Kavanagh e5f94391ae Update README.md 2019-04-09 09:10:29 -04:00
Daniel Imms 5476f4cb93 Merge pull request #2002 from Tyriar/1932_broken_reflow
Fix reflow case where wrapped line portions ended in \t
2019-04-08 18:59:21 -04:00
Daniel Imms aaf8b80292 Merge branch 'master' into 1932_broken_reflow 2019-04-08 18:51:02 -04:00
Daniel Imms 9ab3d319d1 Merge pull request #2004 from Tyriar/dim_dom_renderer_test
Add a test for dim flag in dom renderer
2019-04-08 10:56:15 -04:00
Daniel Imms efeca49705 Mostly implemented buffer API
Part of #1994
2019-04-07 15:29:37 -04:00
Daniel Imms 267071ead6 Add deprecation message to applyAddon 2019-04-07 15:00:27 -04:00
Daniel Imms 26a80d0d35 Convert to much simpler model 2019-04-07 14:53:09 -04:00
Daniel Imms 49f58f5d25 Merge branch 'master' into 1128_addons 2019-04-07 14:24:29 -04:00
Daniel Imms a0b4ee56f6 Add a test for dim flag in dom renderer 2019-04-07 14:17:38 -04:00
Daniel Imms 6ff81c43f6 Convert marker dispose, selection manager newselection 2019-04-07 14:09:31 -04:00
Daniel Imms 625e168e1b Convert title and resize events 2019-04-07 14:02:11 -04:00
Daniel Imms b2ab89ddaa Merge branch 'master' into event_emitter_2 2019-04-07 13:56:50 -04:00
Daniel Imms 4ace81d994 Merge branch 'master' into 1932_broken_reflow 2019-04-07 13:53:23 -04:00
Daniel Imms 25745777c6 Merge pull request #2003 from Tyriar/fix_merge_problem
Fix compile error from conflicting PRs
2019-04-07 13:53:09 -04:00
Daniel Imms 74b68ac1c7 Fix compile error from conflicting PRs 2019-04-07 13:49:56 -04:00
Daniel Imms 2a4208cf3e Merge branch 'master' into 1932_broken_reflow 2019-04-07 13:48:31 -04:00
Daniel Imms e58033bbac Merge pull request #1895 from jerch/true_color
True color support
2019-04-07 13:45:08 -04:00
Daniel Imms bd92aaf4dc Merge branch 'master' into 1932_broken_reflow 2019-04-07 13:44:29 -04:00
Daniel Imms 3ac313f8e1 Fix reflow larger case
Fixes #1932
2019-04-07 13:18:05 -04:00
Daniel Imms ab44650d87 Fix reflow smaller case related to ended \t chars
Part of #1932
2019-04-07 13:16:20 -04:00
Daniel Imms 11ab4dbd5b Merge pull request #2000 from starpit/dim
feat: add DIM support to DomRenderer
2019-04-07 09:47:51 -04:00
Daniel Imms 92c435a16d Use same dim opacity as canvas renderer 2019-04-07 09:45:54 -04:00
Nick Mitchell ddd02b4968 feat: add DIM support to DomRenderer
part of #1896
2019-04-06 16:04:50 -04:00
Daniel Imms 7e36bf47c6 Have undefined | true mean success 2019-04-06 15:21:20 -04:00
Daniel Imms d6daf40250 Merge branch 'master' into fix_1879 2019-04-06 15:08:58 -04:00
Daniel Imms 09a5fd4e61 Fix capitalization in color mode 2019-04-06 15:06:42 -04:00
Daniel Imms 171b9337cb Make BufferLine enums const 2019-04-06 14:56:15 -04:00
Daniel Imms bb324ba9c1 Move width shift back into enum 2019-04-06 14:55:43 -04:00
Daniel Imms 27283e45e9 Merge branch 'master' into true_color 2019-04-06 14:13:14 -04:00
Daniel Imms 23ce6ce19e Merge pull request #1999 from asadm/master
add CodeInterview.io in README
2019-04-05 11:12:03 -04:00
Asad Memon e8a9994583 add CodeInterview.io in README 2019-04-04 11:58:19 -07:00
Daniel Imms 46f3b3fa62 Merge branch 'master' into true_color 2019-04-04 01:06:26 -04:00
Daniel Imms 2f8d49c74d Convert internal key event usages 2019-04-04 00:45:57 -04:00
Daniel Imms 629238ae70 Use onResize in demo 2019-04-04 00:25:03 -04:00
Daniel Imms 3f9d90ecf6 Remove unnecessary paste listener from demo 2019-04-04 00:23:12 -04:00
Daniel Imms f35dfb496a Use onKey in demo 2019-04-04 00:22:34 -04:00
Daniel Imms 4e6f4be6cc Convert Listener to an interface 2019-04-04 00:14:04 -04:00
Daniel Imms 798cf8abc3 Merge branch 'master' into event_emitter_2 2019-04-04 00:12:07 -04:00
Daniel Imms b591db8c02 Merge pull request #1997 from Tyriar/1989_backspace_demo
Fix backspace on demo
2019-04-04 00:06:09 -04:00
Daniel Imms b18829963c Fix backspace on demo
buffer.x will probably be public API soon (#1994)

Fixes #1989
2019-04-04 00:01:43 -04:00
Daniel Imms e402dba635 Merge pull request #1995 from termius/fix_csi_scroll_down
Fix CSI scroll down handler
2019-04-03 23:57:01 -04:00
Daniel Imms 2023b5b677 Merge pull request #1992 from Tyriar/1990_layering_libs
Ensure DOM APIs cannot be used in common and core
2019-04-03 18:28:53 -04:00
Vadim Zakondyrin 68e3d7fa16 Fix CSI scroll down handler
The issue has been introduced in cd8477a942
2019-04-03 12:10:32 +06:00
Daniel Imms 3f993d675f Fix conflicts with new stuff 2019-04-02 20:17:33 -04:00
Daniel Imms 7ddcf1a2a2 Merge branch 'master' into 1128_addons 2019-04-02 20:16:09 -04:00
Daniel Imms e6333bde2d Merge branch 'master' into true_color 2019-04-02 17:13:21 -07:00
Daniel Imms 51c7337099 Merge branch 'master' into 1990_layering_libs 2019-04-02 17:10:49 -07:00
Daniel Imms 6476429077 Merge pull request #1993 from Tyriar/windows_mode
Replace winptyCompat addon with windowsMode option
2019-04-02 17:08:31 -07:00
Daniel Imms 7b9a718af0 Fix some imports to point at ts files, not lib/ 2019-04-02 16:45:43 -07:00
Daniel Imms 60113fa155 Replace EventEmitter usage in CharMeasure 2019-04-02 11:38:55 -07:00
Daniel Imms 60c2b20d68 Replace EventEmitter usage in BufferSet 2019-04-02 11:35:20 -07:00
Daniel Imms f8fb4da9f4 Replace EventEmitter with EventEmitter2 in CircularList 2019-04-02 11:30:47 -07:00
Daniel Imms 5f014be057 Add onCursorMove, replace .on usage in addons 2019-04-02 11:12:57 -07:00
Daniel Imms b12d2c218e Remove reference to winptyCompat tsconfig 2019-04-02 11:03:24 -07:00
Daniel Imms ba4662ac14 Introduce new onEvent APIs and deprecate on/off/etc.
Part of #1505
2019-04-02 11:01:16 -07:00
Daniel Imms ca1f7f1392 Add EventEmitter2 2019-04-02 10:11:59 -07:00
Daniel Imms fa505111a2 Replace winptyCompat addon with windowsMode option 2019-04-02 08:51:23 -07:00
Daniel Imms 2e10c8c25e Clean up 2019-04-02 08:04:39 -07:00
Daniel Imms 8d4da3c2ac Merge branch 'master' into pr/jerch/1895 2019-04-02 07:53:22 -07:00
Daniel Imms 81dda13218 Merge branch 'master' into 1990_layering_libs 2019-04-02 06:35:59 -07:00
Daniel Imms 0a8d57ce74 Merge pull request #1878 from jerch/utf32_buffer
Utf32 buffer
2019-04-01 17:50:09 -07:00
Daniel Imms 5c8c680dac Fix feedback 2019-04-01 00:01:41 -07:00
Daniel Imms 69f8700557 Merge branch 'master' into pr/jerch/1878 2019-03-31 23:34:46 -07:00
Daniel Imms 4a3609f805 Move platform to common and remove dom dependence in common
Because common/ imports 'xterm', it also imported dom accidentally.

Fixes #1990
2019-03-31 23:24:29 -07:00
Daniel Imms fa2cbb0aca Merge branch 'master' into fix_1879 2019-03-30 15:38:53 -07:00
Daniel Imms b05c66b132 Merge pull request #1987 from Tyriar/1986_localhost
Recommend 127.0.0.1:3000 on mac and linux too
2019-03-30 15:36:42 -07:00
Daniel Imms 75046235af Merge pull request #1984 from Tyriar/project_references
Build using TypeScript project references
2019-03-30 15:09:11 -07:00
Daniel Imms bab94ebbdc Merge pull request #1958 from nikonso/1947-weblinks-with-selection
WebLinks plugin don't follow link on selection
2019-03-30 15:07:34 -07:00
Daniel Imms 4d008c66f1 Recommend 127.0.0.1:3000 on mac and linux too
Fixes #1986
2019-03-30 15:06:25 -07:00
Daniel Imms bf9d879efa Fix typo 2019-03-30 14:59:31 -07:00
Daniel Imms 636afa11d6 Merge branch 'master' into 1947-weblinks-with-selection 2019-03-30 14:57:24 -07:00
Daniel Imms 1283123d4d Merge pull request #1985 from Tyriar/fix_demo_non_win
Fix demo on non-Windows
2019-03-30 14:55:00 -07:00
Daniel Imms e182e3d4e4 Fix demo on non-Windows
Broke in #1978
2019-03-30 14:52:48 -07:00
Daniel Imms 51033596ef Merge branch 'master' into 1947-weblinks-with-selection 2019-03-30 14:47:46 -07:00
Daniel Imms 0960a7ae14 Merge branch 'master' into project_references 2019-03-30 14:43:11 -07:00
Daniel Imms 84d5963e7e Merge pull request #1976 from Tyriar/1975_renderer_pause_fix
Fix renderer pausing to not full refresh every time
2019-03-30 14:42:38 -07:00
Daniel Imms 10b7b467b4 Merge pull request #1978 from Tyriar/move_windows_check
Let consumers decide whether winptyCompat should be active
2019-03-30 14:41:58 -07:00
Daniel Imms 2317a319d0 Merge pull request #1982 from Tyriar/1981_prevent_scroll_on_focus
Prevent scroll on focus
2019-03-30 14:40:26 -07:00
Daniel Imms f80924fb8d Remove zmodem demo 2019-03-30 14:12:15 -07:00
Daniel Imms 18f4dc6b3e Remove concurrently 2019-03-30 14:09:51 -07:00
Daniel Imms b400f9ca3b Don't clear terminal on yarn watch 2019-03-30 14:08:57 -07:00
Daniel Imms 0b78011fa3 Adopt project references
Recent versions of TypeScript has improved the performance of project
references so they are now viable to switch over to.
2019-03-30 14:05:24 -07:00
Daniel Imms 2be0a5819b Merge branch 'master' into 1981_prevent_scroll_on_focus 2019-03-29 19:57:33 -07:00
Daniel Imms e346f8bba3 Prevent scroll on focus
Fixes #1981
2019-03-29 19:46:24 -07:00
Daniel Imms 1451bcb6ae Merge pull request #1961 from nikonso:1773-take-drawBoldTextInBrightColors-into-account
Fix #1773 - drawBoldTextInBrightColors is not taken into account with DOM renderer
2019-03-29 12:59:20 -07:00
Daniel Imms f1ef9abc77 Merge pull request #1979 from JesseStolwijk:master
#1977 Add blinking cursor to DomRenderer
2019-03-29 12:51:49 -07:00
Jesse Stolwijk f9df863ee0 Add blinking cursor to DomRenderer 2019-03-25 22:17:16 +01:00
Daniel Imms e6ac5634bb Merge branch 'master' into move_windows_check 2019-03-23 11:38:19 -07:00
Daniel Imms 4dbe8ec1db Let consumers decide whether winptyCompat should be active 2019-03-23 10:58:26 -07:00
Daniel Imms 8b6d564598 Merge pull request #1968 from nikonso/new-feature
Use new vscode serverReadyAction
2019-03-23 09:37:15 -07:00
Daniel Imms 131e4f78bd Fix renderer pausing to not full refresh every time
Fixes #1975
2019-03-23 09:08:55 -07:00
Daniel Imms 99beeb2270 Merge pull request #1973 from zhaojh329/master
Modify uses
2019-03-19 10:46:23 -07:00
Jianhui Zhao 0d63cecc6b Modify uses
Signed-off-by: Jianhui Zhao <jianhuizhao329@gmail.com>
2019-03-19 14:04:16 +08:00
ntchjb 3c444ca39a Fixed coding format 2019-03-15 19:20:23 +07:00
ntchjb 03e648a0fb Fixed the case that, if findPrevious is run without any selection, then start scan at the last row 2019-03-15 19:15:41 +07:00
Max 48a98fa29f Merge pull request #1 from xtermjs/master
update branch
2019-03-15 17:26:55 +07:00
Bruno Ribeito 27ebe8c353 Use new vscode serverReadyAction 2019-03-11 20:58:42 +00:00
Bruno Ribeito 62cfa642cb Better aproach, check i a selection is being performed 2019-03-11 20:54:17 +00:00
Bruno Ribeito c16e3e54e8 Merge branch 'master' of https://github.com/xtermjs/xterm.js into 1947-weblinks-with-selection 2019-03-11 20:41:23 +00:00
Daniel Imms bc86a61a0c Merge pull request #1966 from turtle0x1/master
Add LxdMosaic to list of applications that use xterm.js
2019-03-11 10:29:22 -07:00
turtle0x1 19d36f9228 Update readme with link for lxdmosaic 2019-03-11 13:47:25 +00:00
Bruno Ribeito b4bef0986c Remove circular dependency 2019-03-10 22:03:38 +00:00
Daniel Imms f63bd3d218 Merge branch 'master' into master 2019-03-08 11:47:41 -08:00
Daniel Imms 5cf2a21ddd Merge branch 'master' into cd 2019-03-08 11:44:55 -08:00
Daniel Imms 7e1b5cc7e9 Merge pull request #1952 from NickShaffner/master
Fix for issue #812: Xterm.js's encoding of mouse coordinate
2019-03-08 09:50:24 -08:00
Daniel Imms 4046d682c9 v3.12.0 2019-03-08 09:21:01 -08:00
jerch 127a2ca27a Merge branch 'master' into master 2019-03-07 23:28:26 +01:00
Bruno Ribeito 525af9c36a Fix #1773 2019-03-07 21:30:32 +00:00
Bruno Ribeito 5878aa099d Cleaner aproach 2019-03-07 21:00:07 +00:00
Daniel Imms 9597ad20d3 Merge pull request #1950 from Tyriar/1943_winpty_compat_reflow
Disable reflow when winptyCompat is on
2019-03-07 12:38:30 -08:00
Daniel Imms 6deb3e13d2 Merge pull request #1938 from Tyriar/1937_uncached_char_top_fix
Align y draw coord with how cache draws it
2019-03-07 12:38:00 -08:00
Daniel Imms ecb7f429bc Merge branch 'master' into 1937_uncached_char_top_fix 2019-03-07 12:34:57 -08:00
Daniel Imms ce09ae4ff8 Merge pull request #1959 from nikonso/1908-leave-callback
Fix #1908 - leaveCallback does not fire when links are on the edge of the terminal
2019-03-07 11:17:49 -08:00
Bruno Ribeito bc41cc7d27 Fix #1908 2019-03-07 00:15:03 +00:00
Bruno Ribeito 51e2cdfafb WIP: First draft 2019-03-06 21:45:53 +00:00
Daniel Imms f41146fb40 Merge pull request #1934 from ahtshamraziq/fix-docker-compose-variable-substitution
Compose file: fix variable substitution
2019-03-05 08:31:59 -08:00
Daniel Imms 8fa7f177a6 Merge pull request #1956 from JesseStolwijk/master
Replace array shift with offset
2019-03-05 08:18:06 -08:00
Jesse Stolwijk 07430a4892 Replace array shift with offset (#1955) 2019-03-05 00:00:54 +01:00
Daniel Imms ce1c45fb18 Merge pull request #1818 from juancampa/perf3
Use a time-based limit in Terminal._innerWrite
2019-03-04 10:50:27 -08:00
Daniel Imms 55f3aeeff5 Merge branch 'master' into perf3 2019-03-04 10:39:00 -08:00
Daniel Imms a99364eae9 Merge branch 'master' into fix_1879 2019-03-04 10:37:51 -08:00
Daniel Imms 7b41b002c6 Merge branch 'master' into master 2019-03-04 10:05:18 -08:00
Daniel Imms 32e157bfaa Remove unnecessary else 2019-03-04 10:04:52 -08:00
Daniel Imms 655c595011 Merge pull request #1927 from pfitzseb/sp/mousehandlerfix
*actually* fix mouse handler before term attached
2019-03-04 10:01:42 -08:00
Daniel Imms 064a372c71 Merge branch 'master' into sp/mousehandlerfix 2019-03-04 10:00:38 -08:00
Daniel Imms c63f15a9b2 Fix lint 2019-03-04 10:00:04 -08:00
Daniel Imms 1aa40eb58b Merge branch 'master' into fix-docker-compose-variable-substitution 2019-03-04 09:57:33 -08:00
Daniel Imms d8b973b126 Merge pull request #1929 from coderaiser/patch-1
fix: Renderer: IntersectionObserver can produce more then 1 entry
2019-03-04 09:54:53 -08:00
Daniel Imms 05b162eefd Merge branch 'master' into patch-1 2019-03-04 09:53:12 -08:00
Daniel Imms 46aa6a40f9 Merge branch 'master' into fix-docker-compose-variable-substitution 2019-03-04 09:49:12 -08:00
Daniel Imms 9f2aec1944 Merge branch 'master' into 1937_uncached_char_top_fix 2019-03-04 09:47:33 -08:00
Daniel Imms 42b66d98b2 Merge branch 'master' into 1943_winpty_compat_reflow 2019-03-04 09:46:15 -08:00
Daniel Imms 1eee182860 Merge pull request #1930 from Tyriar/1926_reflow_larger_fix
Make sure the viewport is filled when reflowing a row change
2019-03-04 09:45:43 -08:00
Daniel Imms bd602fb078 Merge branch 'master' into 1926_reflow_larger_fix 2019-03-04 07:23:06 -08:00
Daniel Imms 8ea254bdc7 Merge pull request #1953 from cherouvim/patch-1
docs: style improvements
2019-03-01 11:26:06 -08:00
Ioannis Cherouvim 09754fddf8 xterm.js references to the library should not be backticked 2019-03-01 21:15:20 +02:00
Ioannis Cherouvim a8a0344d1a docs: style improvements
- Added a trailing dot at the end of each list line.
- Consistent usage of `xterm.js` in "Real-world uses".
2019-03-01 14:51:19 +02:00
Ioannis Cherouvim 4e479b455a docs: Consistent style on lists.
Added some missing dots, capitalized a couple of lines.
2019-03-01 14:40:21 +02:00
Nick Shaffner e178139907 Fix for issue #812: Xterm.js's encoding of mouse coordinate
See: https://github.com/xtermjs/xterm.js/issues/812
Changed the utf-8 mouse encoding to match iTerm
2019-02-27 22:37:22 -08:00
Sebastian Pfitzner 0854f84653 actually fix mouse handler before term attached 2019-02-26 11:31:22 +01:00
Daniel Imms a35cd43395 Merge branch 'master' into 1943_winpty_compat_reflow 2019-02-20 10:02:54 -08:00
Daniel Imms 3285374618 Disable reflow when winptyCompat is on
Fixes #1943
2019-02-20 07:03:40 -08:00
Daniel Imms 6ad6a01c3f Merge pull request #1949 from liuzheng/master
Update README.md
2019-02-20 06:21:08 -08:00
夏虫 e1c1c7a4f2 Update README.md
move it to the bottom
2019-02-20 14:57:16 +08:00
Daniel Imms 437a59d4cd Merge branch 'master' into 1937_uncached_char_top_fix 2019-02-19 07:19:33 -08:00
夏虫 9f29eeed23 Update README.md
(Jumpserver)[https://github.com/jumpserver/] use xterm.js
2019-02-19 17:19:19 +08:00
coderaiser b0f01beb0f Merge branch 'master' into patch-1 2019-02-15 16:57:58 +02:00
Daniel Imms 857ae4b702 Merge pull request #1936 from Tyriar/1935_remove_font_family
Remove font-family from .css file
2019-02-11 07:24:11 -08:00
Daniel Imms c934200c86 Also set font size 2019-02-11 06:07:31 -08:00
Daniel Imms 78426d8a12 Make the composition view use the same font as the terminal 2019-02-11 06:05:14 -08:00
Daniel Imms 817401bbcd Align y draw coord with how cache draws it
Fixes #1937
2019-02-11 05:32:47 -08:00
Daniel Imms 84d7bfeacc Remove font-family from .css file
Fixes #1935
2019-02-11 05:14:55 -08:00
Ahtsham Raziq 92be01dd81 Compose file: fix variable substitution 2019-02-09 23:24:32 +05:00
Daniel Imms d17dfbc73f Cover a case when resizing smaller making y go out of bounds 2019-02-08 12:13:00 -08:00
Daniel Imms ba9ed43acd Merge branch 'master' into utf32_buffer 2019-02-05 11:39:08 -08:00
Daniel Imms b88230db8b Make sure the viewport is filled when reflowing a row change
Fixes #1926
2019-02-05 08:20:16 -08:00
coderaiser 69d3a4667f fix: Renderer: IntersectionObserver can produce more then 1 entry 2019-02-04 13:28:14 +02:00
Daniel Imms abf6a9fa58 Merge pull request #1925 from xtermjs/Tyriar-patch-1
Add roadmap wiki link to README
2019-02-03 15:37:50 -08:00
Daniel Imms ea9d1d7ef0 Merge pull request #1928 from xtermjs/Tyriar-patch-2
Update license year
2019-02-03 15:37:40 -08:00
Daniel Imms f3aac10bbd Update license year 2019-02-03 10:46:20 -08:00
Daniel Imms f30012b985 Merge branch 'master' into fix_1879 2019-02-01 20:03:40 -08:00
Daniel Imms db9aa5c88b Merge pull request #1916 from skyway777:master
Clear selection state if selection doesn't exists
2019-02-01 19:39:25 -08:00
Daniel Imms 1829cb7609 Add roadmap wiki link to README 2019-02-01 19:28:07 -08:00
Jörg Breitbart 65dbf88460 apply bright shift for uncached draws 2019-02-01 21:44:51 +01:00
Jörg Breitbart 0c54b1a29e Merge branch 'master' into true_color 2019-02-01 20:59:31 +01:00
Jörg Breitbart 179e5d5892 Merge branch 'master' into utf32_buffer 2019-02-01 20:59:02 +01:00
Jörg Breitbart 6993daef15 Merge branch 'master' into fix_1879 2019-02-01 20:38:45 +01:00
Jörg Breitbart 801db3831d type add methods 2019-02-01 20:32:07 +01:00
Jörg Breitbart e813ae02fc return true from inputhandler methods to signal sequence was handled 2019-02-01 20:28:45 +01:00
Jörg Breitbart c2446df966 Merge branch 'utf32_buffer' into true_color 2019-02-01 01:30:29 +01:00
Jörg Breitbart 2de39024be Merge branch 'utf32_buffer' into utf8_input 2019-02-01 01:11:58 +01:00
Jörg Breitbart 079729f1b4 change property getter into methods 2019-02-01 01:10:20 +01:00
Jörg Breitbart ec3a95f727 Merge branch 'utf32_buffer' into utf8_input 2019-01-31 23:32:48 +01:00
Jörg Breitbart c4a5e0862c Merge branch 'utf32_buffer' into true_color 2019-01-31 23:16:23 +01:00
Jörg Breitbart 594797e05b Merge branch 'master' into utf32_buffer 2019-01-31 23:15:50 +01:00
Jörg Breitbart 7da40b9d56 Merge branch 'utf32_buffer' into true_color 2019-01-31 22:13:33 +01:00
Jörg Breitbart a1b61e05f9 Merge branch 'master' into utf32_buffer 2019-01-31 21:51:58 +01:00
Nikita Chuklinov fc1692b3eb clear state if selection doesn't exists 2019-01-30 20:34:16 +03:00
Daniel Imms d48982b60e Merge branch 'master' into cd 2019-01-28 07:25:55 -08:00
Daniel Imms 9c72254d3a Bump to 3.11.0 2019-01-28 07:23:58 -08:00
Daniel Imms af6544d6d9 Improve package.json modifications 2019-01-28 07:23:46 -08:00
Jörg Breitbart 39fd6c427d name polishing, docs 2019-01-28 10:42:44 +01:00
Jörg Breitbart c60f9b1468 code formatting 2019-01-27 20:06:58 +01:00
Jörg Breitbart e77e36cbb7 fix typo in HAS_CONTENT 2019-01-27 19:59:13 +01:00
Jörg Breitbart b20730ee4d add more docs 2019-01-27 19:40:28 +01:00
Jörg Breitbart acea73c8ac Merge branch 'master' into utf32_buffer 2019-01-27 19:02:09 +01:00
Daniel Imms 8b6327b563 Merge branch 'master' into master 2019-01-27 08:18:40 -08:00
Daniel Imms 007cac22d2 Merge branch 'master' into cd 2019-01-27 07:29:34 -08:00
Daniel Imms b047045949 Replace hardcoded version 2019-01-27 07:28:04 -08:00
Daniel Imms 7e3b315d91 Merge branch 'master' into perf3 2019-01-26 13:20:32 -08:00
Daniel Imms 273cc45357 Merge branch 'master' into cd 2019-01-26 13:12:40 -08:00
Jörg Breitbart e6447bc91f Merge branch 'master' into true_color 2019-01-25 02:43:52 +01:00
Jörg Breitbart b587c2f30f Merge branch 'master' into utf8_input 2019-01-25 02:43:07 +01:00
Jörg Breitbart 4f531f8490 Merge branch 'master' into utf32_buffer 2019-01-25 02:42:40 +01:00
Jörg Breitbart e6e5ecc0f2 change demo to utf8 input 2019-01-20 23:06:51 +01:00
Jörg Breitbart 3a2076805c Merge branch 'master' into utf8_input 2019-01-20 22:44:15 +01:00
Jörg Breitbart d7ea0edfcf add utf8 input to terminal 2019-01-20 22:19:50 +01:00
Jörg Breitbart 6bdd380890 add utf8 decoder 2019-01-20 21:54:58 +01:00
Jörg Breitbart b13b737a07 Merge branch 'master' into true_color 2019-01-17 02:09:44 +01:00
Jörg Breitbart 200f7bf33d Merge branch 'master' into utf32_buffer 2019-01-17 02:09:19 +01:00
Jörg Breitbart 2370038523 always draw RGB uncached 2019-01-17 01:41:48 +01:00
Jörg Breitbart df739ab814 remove old color and flag shims 2019-01-17 01:31:59 +01:00
Jörg Breitbart 8d850d6c8b apply CellData to canvas renderer 2019-01-17 01:25:16 +01:00
Jörg Breitbart ae50d84c5a fix characterjoiner to support AttrData 2019-01-17 00:07:25 +01:00
Jörg Breitbart 1536e79542 remove printf remnant 2019-01-15 03:24:47 +01:00
Jörg Breitbart 94c19fb92a preliminarly RGB support in canvas renderer 2019-01-15 03:13:44 +01:00
Jörg Breitbart 9cf479bcfa cleanup rgb channel conversion 2019-01-14 08:51:18 +01:00
Jörg Breitbart 0802582c9b test cases for DOM renderer style changes 2019-01-14 01:00:07 +01:00
Jörg Breitbart 3515c0b220 DOM renderer with RGB support 2019-01-14 00:46:44 +01:00
Jörg Breitbart 70ded42bee follow naming scheme 2019-01-13 23:50:58 +01:00
Jörg Breitbart c444eebd89 RGB support in buffer and attributes 2019-01-13 23:19:56 +01:00
Jörg Breitbart 7c6dad5805 test cases 2019-01-12 19:06:09 +01:00
Jörg Breitbart 40b231edd1 tests for CellData 2019-01-12 18:44:10 +01:00
Jörg Breitbart 35a6aa8326 some docs 2019-01-12 18:04:52 +01:00
Jörg Breitbart 711ae65947 remove get CharData from codebase and deprecate method 2019-01-12 17:50:19 +01:00
Jörg Breitbart ece7192db7 remove set with CharData from codebase and deprecate method 2019-01-12 16:15:24 +01:00
Jörg Breitbart 3e456971b1 change buffer line ctor to new interface 2019-01-12 15:47:50 +01:00
Jörg Breitbart 60a591e8c8 use getNullChar in InputHandler.print 2019-01-12 15:37:20 +01:00
Jörg Breitbart 56dec2849f change resize, fill to new interface, remove _cell on buffer line 2019-01-12 15:21:55 +01:00
Jörg Breitbart fe6919b52a change replaceCells to new interface 2019-01-12 15:05:52 +01:00
Jörg Breitbart 4bf7f3eb89 change deleteCells to new interface 2019-01-12 14:58:30 +01:00
Jörg Breitbart 523ff6e5fc add null and whitespace placeholder cells to buffer 2019-01-12 14:54:54 +01:00
Jörg Breitbart 88a037b309 change insertCells to new interface 2019-01-12 14:34:29 +01:00
Jörg Breitbart ad67f47acb Merge branch 'typedarray_parser' into utf32_buffer 2019-01-12 14:08:53 +01:00
Jörg Breitbart 907a6b9732 Merge branch 'master' into utf32_buffer 2019-01-12 13:43:11 +01:00
ntchjb 9236934586 _findInLine() function should not run multiple times on the same row 2019-01-09 07:01:34 +07:00
Daniel Imms 932d5c7c21 Merge branch 'master' into master 2019-01-08 10:19:38 -08:00
Jörg Breitbart 62dfa84d5d remove get CharData calls from codebase (beside tests) 2019-01-05 01:01:10 +01:00
Jörg Breitbart b93b774971 direct cell attrs getter 2019-01-04 19:14:56 +01:00
Jörg Breitbart 6552a023c1 remove get from linkifier 2019-01-04 18:46:45 +01:00
Jörg Breitbart 13ffed392d remove get calls from renderer 2019-01-04 18:36:07 +01:00
Jörg Breitbart d7e5977d9a remove leftover 2019-01-04 16:19:01 +01:00
Daniel Imms 7ba17ca770 Convert web links and attach to modules 2019-01-03 22:19:30 -08:00
Daniel Imms 290394b401 New addon prototype
Part of #1128
2019-01-03 20:22:53 -08:00
Jörg Breitbart 802543f7cf replace set(CharData) with setCell 2019-01-04 02:58:15 +01:00
Jörg Breitbart f3619ab61c fix linter error 2019-01-04 01:33:49 +01:00
Jörg Breitbart 2f66efa49a first try to optimize read access 2019-01-04 01:32:56 +01:00
Jörg Breitbart 75fbda1fe6 further opt for InputHandler.print 2019-01-04 01:32:20 +01:00
Jörg Breitbart b0eecea45a partially apply fast data access in InputHandler.print 2019-01-03 21:40:34 +01:00
Jörg Breitbart f63223f8d8 Merge branch 'master' into utf32_buffer 2019-01-03 21:10:40 +01:00
Jörg Breitbart a2a8c3d47f extend buffer line with more direct access methods 2019-01-03 21:08:18 +01:00
Jörg Breitbart ba1582cf3c apply utf32 buffer layout 2019-01-03 19:01:43 +01:00
Max b4b6bab000 Merge branch 'master' into master 2019-01-03 16:12:19 +07:00
ntchjb c784a012c7 Fix: update findingRow and cumulativeCols before running _findInLine
to be able to scan unwrapped line at the last row
2019-01-03 15:55:27 +07:00
ntchjb efe22d8cf3 Revert "Support wide characters and combined characters"
This reverts commit f961f90bc8.
2019-01-03 15:32:25 +07:00
Daniel Imms 4db95f253e Build to dist dir in release build 2019-01-02 13:40:06 -08:00
Daniel Imms 805c02fb18 Merge branch 'master' into cd 2019-01-02 12:56:07 -08:00
Daniel Imms d120ef5301 Re-enable condition and dependsOn 2019-01-02 12:55:44 -08:00
Daniel Imms 68fbb2f8c9 Tweak more 2019-01-02 11:53:51 -08:00
Daniel Imms 515cf7878e Tweak var 2019-01-02 11:49:20 -08:00
Daniel Imms a35ad6aa81 Inherit exit code from publish call 2019-01-02 11:39:44 -08:00
Daniel Imms aa2d28e619 Disable dependsOn 2019-01-02 11:37:34 -08:00
Daniel Imms a6c3f94860 Publish to npm as beta 2019-01-02 11:24:06 -08:00
Daniel Imms ff81ce078b Add a release pipelines job 2019-01-02 09:38:40 -08:00
ntchjb f961f90bc8 Support wide characters and combined characters
- Convert buffer index to string index before searching
- Convert string index to buffer index after searching
- Fix bug when search selection skipped some result
2019-01-01 16:39:23 +07:00
ntchjb d94c897754 Fix search addons: be able to search wrapped lines from unwrapped line
by managing column range that is needed to be scanned.
2018-12-29 13:55:14 +07:00
Max 1b68682b79 Merge branch 'master' into master 2018-12-29 10:12:17 +07:00
ntchjb 565cba9c1a Fix search addons:
- Changed to use `this._terminal.cols` for buffer line length instead.
- Let `_findInLine` check on wrapped line
- Added conditions in `_findInLine`
  - For reverse search, if there is no  selection at given row, then start scan at the end of the string
2018-12-29 09:38:17 +07:00
Daniel Imms 60bc1bb873 Merge branch 'master' into perf3 2018-12-27 13:53:01 -08:00
Daniel Imms dc5c2381a8 Merge branch 'master' into perf3 2018-12-12 11:24:34 -08:00
Juan Campa 3d2ae2b01e Removing redundant condition. Clearer variable name 2018-12-09 17:58:44 -05:00
Juan Campa fd34caee23 Use a time-based limit to Terminal._innerWrite
The idea is that it should run for a bit and then let the renderer draw
a frame so that the terminal look responsive. The existing approach
limits the work done using a fixed number elements from the write
buffer so the duration of a frame can vary widely.

This approach looks at the clock to determine when to stop, we basically
allocate an amount of time each frame to write, while the rest can be
used for rendering.

From my tests this change makes the terminal feel a lot smoother.
2018-12-08 00:44:05 -05:00
137 changed files with 6136 additions and 3088 deletions
+22
View File
@@ -0,0 +1,22 @@
FROM node:10
# Configure apt
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get -y install --no-install-recommends apt-utils 2>&1
# Verify git and process tools are installed
RUN apt-get install -y git procps
# Install yarn
RUN apt-get install -y curl apt-transport-https lsb-release \
&& curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \
&& echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
&& apt-get update \
&& apt-get -y install --no-install-recommends yarn
# Clean up
RUN apt-get autoremove -y \
&& apt-get clean -y \
&& rm -rf /var/lib/apt/lists/*
ENV DEBIAN_FRONTEND=dialog
+9
View File
@@ -0,0 +1,9 @@
{
"name": "xterm.js",
"dockerFile": "Dockerfile",
"appPort": 3000,
"extensions": [
"editorconfig.editorconfig",
"ms-vscode.vscode-typescript-tslint-plugin"
]
}
+1
View File
@@ -42,6 +42,7 @@ lib/test/
docs/
/.idea/
.vscode/
bin/
build/
fixtures/
coverage/
+25 -6
View File
@@ -13,7 +13,25 @@
"runtimeArgs": [
"--colors",
"--recursive",
"${workspaceRoot}/lib"
"${workspaceRoot}/lib/**/*.test.js"
],
"sourceMaps": true,
"outFiles": [ "${workspaceRoot}/lib/**/*.js" ],
"internalConsoleOptions": "openOnSessionStart"
},
{
"type": "node",
"request": "launch",
"name": "Integration Tests",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha.cmd"
},
"runtimeArgs": [
"--colors",
"--recursive",
"${workspaceRoot}/lib/**/*.api.js"
],
"sourceMaps": true,
"outFiles": [ "${workspaceRoot}/lib/**/*.js" ],
@@ -23,10 +41,7 @@
"type": "chrome",
"request": "launch",
"name": "Demo Client",
"url": "http://0.0.0.0:3000",
"windows": {
"url": "http://127.0.0.1:3000"
},
"url": "http://127.0.0.1:3000",
"webRoot": "${workspaceFolder}/"
},
{
@@ -38,7 +53,11 @@
"run",
"start-debug"
],
"port": 9229
"port": 9229,
"serverReadyAction": {
"action": "openExternally",
"pattern": "App listening to (http://.*?:[0-9]+)"
}
}
]
}
+3
View File
@@ -0,0 +1,3 @@
{
"typescript.preferences.quoteStyle": "single"
}
+51
View File
@@ -0,0 +1,51 @@
{
"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"
}
}
]
}
+6 -6
View File
@@ -33,21 +33,21 @@ opening an issue, read these pointers.
You can find issues to work on by looking at the [help wanted](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) or [good first issue](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) issues. It's a good idea to comment on the issue saying that you're taking it, just in case someone else comes along and you duplicate work. Once you have your issue, here are the steps to contribute:
- Fork [xterm.js](https://github.com/sourcelair/xterm.js/)
([how to fork a repo](https://help.github.com/articles/fork-a-repo))
- Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running
- Make your changes
([how to fork a repo](https://help.github.com/articles/fork-a-repo)).
- Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running.
- Make your changes.
- If your changes are easy to test or likely to regress, add tests. Tests go into `test`, directory.
- Follow the general code style of the rest of the project (see below).
- Submit a pull request
([how to create a pull request](https://help.github.com/articles/fork-a-repo)).
Don't put more than one feature/fix in a single pull request.
By contributing code to xterm.js you
By contributing code to xterm.js you:
- agree to license the contributed code under xterm.js' [MIT
- Agree to license the contributed code under xterm.js' [MIT
license](LICENSE).
- confirm that you have the right to contribute and license the code
- Confirm that you have the right to contribute and license the code
in question. (Either you hold all rights on the code, or the rights
holder has explicitly granted the right to use it like this,
through a compatible open source license or through a direct
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2017-2018, The xterm.js authors (https://github.com/xtermjs/xterm.js)
Copyright (c) 2017-2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com)
Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)
+39 -25
View File
@@ -7,17 +7,17 @@ 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
- **Perfomant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer
- **Rich unicode support**: Supports CJK, emojis and IMEs
- **Self-contained**: Requires zero dependencies to work
- **Accessible**: Screen reader support can be turned on using the `screenReaderMode` option
- **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.
- **Perfomant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer.
- **Rich unicode support**: Supports CJK, emojis and IMEs.
- **Self-contained**: Requires zero dependencies to work.
- **Accessible**: Screen reader support can be turned on using the `screenReaderMode` option.
- **And much more**: Links, theming, addons, well documented API, etc.
## What xterm.js is not
- Xterm.js is not a terminal application that you can download and use on your computer
- Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output)
- Xterm.js is not a terminal application that you can download and use on your computer.
- Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output).
## Getting Started
@@ -41,7 +41,7 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
<script>
var term = new Terminal();
term.open(document.getElementById('terminal'));
      term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
</script>
</body>
</html>
@@ -106,17 +106,17 @@ Note that some APIs are marked *experimental*, these are added to enable experim
## 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
- [**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
- [**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.
- [**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.
- [**Codenvy**](http://www.codenvy.com): Cloud workspaces for development teams.
- [**CoderPad**](https://coderpad.io): Online interviewing platform for programmers. Run code in many programming languages, with results displayed by `xterm.js`.
- [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using `xterm.js`, socket.io, and ssh2.
- [**CoderPad**](https://coderpad.io): Online interviewing platform for programmers. Run code in many programming languages, with results displayed by xterm.js.
- [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using xterm.js, socket.io, and ssh2.
- [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE.
- [**Cloud Commander**](https://cloudcmd.io "Cloud Commander"): Orthodox web file manager with console and editor.
- [**Codevolve**](https://www.codevolve.com "Codevolve"): Online platform for interactive coding and web development courses. Live container-backed terminal uses `xterm.js`.
- [**Codevolve**](https://www.codevolve.com "Codevolve"): 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.
@@ -124,9 +124,8 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**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.
- [**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.
- [**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.
- [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript.
- [**Opshell**](https://github.com/ricktbaker/opshell) Ops Helper tool to make life easier working with AWS instances across multiple organizations.
- [**Proxmox VE**](https://www.proxmox.com/en/proxmox-ve): Proxmox VE is a complete open-source platform for enterprise virtualization. It uses xterm.js for container terminals and the host shell.
@@ -137,23 +136,28 @@ computational environment for Jupyter, supporting interactive data science and s
- [**Kubebox**](https://github.com/astefanutti/kubebox): Terminal console for Kubernetes clusters.
- [**Azure Cloud Shell**](https://shell.azure.com): Azure Cloud Shell is a Microsoft-managed admin machine built on Azure, for Azure.
- [**atom-xterm**](https://atom.io/packages/atom-xterm): Atom plugin for providing terminals inside your Atom workspace.
- [**rtty**](https://github.com/zhaojh329/rtty): A reverse proxy WebTTY. It is composed of the client and the server.
- [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS
- [**rtty**](https://github.com/zhaojh329/rtty): Access your terminals from anywhere via the web.
- [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS.
- [**abstruse**](https://github.com/bleenco/abstruse): Abstruse CI is a continuous integration platform based on Node.JS and Docker.
- [**Azure Data Studio**](https://github.com/Microsoft/azuredatastudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux.
- [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users
- [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users.
- [**Fluent Terminal**](https://github.com/felixse/FluentTerminal): A terminal emulator based on UWP and web technologies.
- [**Hyper**](https://hyper.is): A terminal built on web technologies
- [**Hyper**](https://hyper.is): A terminal built on web technologies.
- [**Diag**](https://diag.ai): A better way to troubleshoot problems faster. Capture, share and reapply troubleshooting knowledge so you can focus on solving problems that matter.
- [**GoTTY**](https://github.com/yudai/gotty): A simple command line tool that shares your terminal as a web application based on xterm.js.
- [**genact**](https://github.com/svenstaro/genact): A nonsense activity generator.
- [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice.
- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js
- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js.
- [**SSH Web Client**](https://github.com/roke22/PHP-SSH2-Web-Client): SSH Web Client with PHP.
- [**Shellvault**](https://www.shellvault.io): The cloud-based SSH terminal you can access from anywhere.
- [**Juno**](http://junolab.org/): A flexible Julia IDE, based on Atom.
- [**webssh**](https://github.com/huashengdun/webssh): Web based ssh client.
- [**info-beamer hosted**](https://info-beamer.com): Uses Xterm.js to manage digital signage devices from the web dashboard.
- [**info-beamer hosted**](https://info-beamer.com): Uses xterm.js to manage digital signage devices from the web dashboard.
- [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use xterm.js for web terminal emulation.
- [**LxdMosaic**](https://github.com/turtle0x1/LxdMosaic): Uses xterm.js to give terminal access to containers through LXD
- [**CodeInterview.io**](https://codeinterview.io): A coding interview platform in 25+ languages and many web frameworks. Uses xterm.js to provide shell access.
- [**Bastillion**](https://www.bastillion.io): Bastillion is an open-source web-based SSH console that centrally manages administrative access to systems.
- [**PHP App Server**](https://github.com/cubiclesoft/php-app-server/): Create lightweight, installable almost-native applications for desktop OSes. ExecTerminal (nicely wraps the xterm.js Terminal), TerminalManager, and RunProcessSDK are self-contained, reusable ES5+ compliant Javascript components.
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
@@ -163,7 +167,17 @@ Do you use xterm.js in your application as well? Please [open a Pull Request](ht
Xterm.js follows a monthly release cycle roughly.
All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), while a rough roadmap is available by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones).
All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones).
### Beta builds
Our CI releases beta builds to npm for every change that goes into master, install the latest beta build with:
```
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.
## Contributing
@@ -173,6 +187,6 @@ You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Co
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.
Copyright (c) 2017-2018, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)<br>
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>
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
+51 -4
View File
@@ -21,7 +21,7 @@ jobs:
displayName: 'Install dependencies and build'
- script: |
yarn mocha
displayName: 'Test'
displayName: 'Unit tests'
- script: |
yarn lint
displayName: 'Lint'
@@ -39,13 +39,13 @@ jobs:
displayName: 'Install dependencies and build'
- script: |
yarn mocha
displayName: 'Test'
displayName: 'Unit tests'
- script: |
yarn lint
displayName: 'Lint'
- script: |
yarn test-coverage
export COVERALLS_GIT_BRANCH=$BUILD_SOURCEBRANCH
export COVERALLS_GIT_BRANCH=$BUILD_SOURCEBRANCHNAME
yarn coveralls
displayName: 'Generate and publish coverage'
@@ -62,7 +62,54 @@ jobs:
displayName: 'Install dependencies and build'
- script: |
yarn mocha
displayName: 'Test'
displayName: 'Unit tests'
- script: |
yarn lint
displayName: 'Lint'
- job: IntegrationTests
pool:
vmImage: 'ubuntu-16.04'
steps:
- task: NodeTool@0
inputs:
versionSpec: '8.x'
displayName: 'Install Node.js'
- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2
inputs:
versionSpec: "1.9.4"
displayName: 'Install Yarn'
- script: |
yarn
displayName: 'Install dependencies and build'
- script: |
yarn start &
sleep 10
yarn test-api --headless
displayName: 'Integration tests'
- job: Release
dependsOn:
- Linux
- macOS
- Windows
- IntegrationTests
condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['Build.SourceBranch'], 'refs/heads/release/*')))
pool:
vmImage: 'ubuntu-16.04'
steps:
- task: NodeTool@0
inputs:
versionSpec: '8.x'
displayName: 'Install Node.js'
- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2
inputs:
versionSpec: "1.9.4"
displayName: 'Install Yarn'
- script: |
yarn
BUILD_DIR=dist npm run build
displayName: 'Install dependencies and build'
- script: |
NPM_AUTH_TOKEN="$(NPM_AUTH_TOKEN)" node ./bin/publish.js
displayName: 'Publish to npm'
-24
View File
@@ -1,24 +0,0 @@
#! /usr/bin/env sh
# Usage: ./bin/prepare-release x.y.z
# x.y.z should be semver (e.g. 1.0.0)
set -e
NEW_VERSION=$1
CURRENT_PACKAGE_JSON_VERSION=$(cat package.json \
| grep version \
| head -1 \
| awk -F: '{ print $2 }' \
| sed 's/[",]//g' \
| tr -d '[[:space:]]')
# Build xterm.js into `dist`
export BUILD_DIR=dist
npm run build
# Update version in package.json
sed -i "s/\"version\": \"$CURRENT_PACKAGE_JSON_VERSION\"/\"version\": \"$NEW_VERSION\"/g" package.json
git commit -S -s -a -m "Bump version to $NEW_VERSION"
git tag $NEW_VERSION
+63
View File
@@ -0,0 +1,63 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
const cp = require('child_process');
const fs = require('fs');
const path = require('path');
const packageJson = require('../package.json');
// Setup auth
fs.writeFileSync(`${process.env['HOME']}/.npmrc`, `//registry.npmjs.org/:_authToken=${process.env['NPM_AUTH_TOKEN']}`);
// Determine if this is a stable or beta release
const publishedVersions = getPublishedVersions();
const isStableRelease = publishedVersions.indexOf(packageJson.version) === -1;
// Get the next version
let nextVersion = isStableRelease ? packageJson.version : getNextBetaVersion();
console.log(`Publishing version: ${nextVersion}`);
// Set the version in package.json
const packageJsonFile = path.resolve(__dirname, '..', 'package.json');
packageJson.version = nextVersion;
fs.writeFileSync(packageJsonFile, JSON.stringify(packageJson, null, 2));
// Publish
const args = ['publish'];
if (!isStableRelease) {
args.push('--tag', 'beta');
}
const result = cp.spawn('npm', args, { stdio: 'inherit' });
result.on('exit', code => process.exit(code));
function getNextBetaVersion() {
if (!/^[0-9]+\.[0-9]+\.[0-9]+$/.exec(packageJson.version)) {
console.error('The package.json version must be of the form x.y.z');
process.exit(1);
}
const tag = 'beta';
const stableVersion = packageJson.version.split('.');
const nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.0`;
const publishedVersions = getPublishedVersions(nextStableVersion, tag);
if (publishedVersions.length === 0) {
return `${nextStableVersion}-${tag}1`;
}
const latestPublishedVersion = publishedVersions.sort((a, b) => {
const aVersion = parseInt(a.substr(a.search(/[0-9]+$/)));
const bVersion = parseInt(b.substr(b.search(/[0-9]+$/)));
return aVersion > bVersion ? -1 : 1;
})[0];
const latestTagVersion = parseInt(latestPublishedVersion.substr(latestPublishedVersion.search(/[0-9]+$/)), 10);
return `${nextStableVersion}-${tag}${latestTagVersion + 1}`;
}
function getPublishedVersions(version, tag) {
const versionsProcess = cp.spawnSync('npm', ['view', packageJson.name, 'versions', '--json']);
const versionsJson = JSON.parse(versionsProcess.stdout);
if (tag) {
return versionsJson.filter(v => !v.search(new RegExp(`${version}-${tag}[0-9]+`)));
}
return versionsJson;
}
-22
View File
@@ -1,22 +0,0 @@
#! /usr/bin/env sh
# Usage: ./bin/release x.y.z
# x.y.z should be semver (e.g. 1.0.0)
set -e
if [ -z "$1" ];
then
echo "No version supplied. Please a version argument\n"
echo "Usage: $0 VERSION\n"
echo "Example: $0 1.0.0"
exit
fi
NEW_VERSION=$1
./bin/prepare-release $NEW_VERSION
git push && \
git push --tags && \
npm publish
+48 -35
View File
@@ -8,32 +8,26 @@
/// <reference path="../typings/xterm.d.ts"/>
import { Terminal } from '../lib/public/Terminal';
import * as attach from '../lib/addons/attach/attach';
import { AttachAddon } from 'xterm-addon-attach';
import { SearchAddon, ISearchOptions } from 'xterm-addon-search';
import { WebLinksAddon } from 'xterm-addon-web-links';
import * as fit from '../lib/addons/fit/fit';
import * as fullscreen from '../lib/addons/fullscreen/fullscreen';
import * as search from '../lib/addons/search/search';
import * as webLinks from '../lib/addons/webLinks/webLinks';
import * as winptyCompat from '../lib/addons/winptyCompat/winptyCompat';
import { ISearchOptions } from '../lib/addons/search/Interfaces';
// Pulling in the module's types relies on the <reference> above, it's looks a
// little weird here as we're importing "this" module
import { Terminal as TerminalType } from 'xterm';
import { Terminal as TerminalType, ITerminalOptions } from 'xterm';
export interface IWindowWithTerminal extends Window {
term: TerminalType;
Terminal?: typeof TerminalType;
}
declare let window: IWindowWithTerminal;
Terminal.applyAddon(attach);
Terminal.applyAddon(fit);
Terminal.applyAddon(fullscreen);
Terminal.applyAddon(search);
Terminal.applyAddon(webLinks);
Terminal.applyAddon(winptyCompat);
let term;
let searchAddon: SearchAddon;
let protocol;
let socketURL;
let socket;
@@ -59,8 +53,6 @@ function getSearchOptions(): ISearchOptions {
};
}
createTerminal();
const disposeRecreateButtonHandler = () => {
// If the terminal exists dispose of it, otherwise recreate it
if (term) {
@@ -76,16 +68,32 @@ const disposeRecreateButtonHandler = () => {
}
};
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
if (document.location.pathname === '/test') {
window.Terminal = Terminal;
} else {
createTerminal();
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
}
function createTerminal(): void {
// Clean terminal
while (terminalContainer.children.length) {
terminalContainer.removeChild(terminalContainer.children[0]);
}
term = new Terminal({});
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
term = new Terminal({
windowsMode: isWindows
} as ITerminalOptions);
// Load addons
const typedTerm = term as TerminalType;
typedTerm.loadAddon(new WebLinksAddon());
searchAddon = new SearchAddon();
typedTerm.loadAddon(searchAddon);
window.term = term; // Expose `term` to window for debugging purposes
term.on('resize', (size: { cols: number, rows: number }) => {
term.onResize((size: { cols: number, rows: number }) => {
if (!pid) {
return;
}
@@ -99,8 +107,6 @@ function createTerminal(): void {
socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/';
term.open(terminalContainer);
term.winptyCompatInit();
term.webLinksInit();
term.fit();
term.focus();
@@ -109,12 +115,12 @@ function createTerminal(): void {
addDomListener(actionElements.findNext, 'keyup', (e) => {
const searchOptions = getSearchOptions();
searchOptions.incremental = e.key !== `Enter`;
term.findNext(actionElements.findNext.value, searchOptions);
searchAddon.findNext(actionElements.findNext.value, searchOptions);
});
addDomListener(actionElements.findPrevious, 'keyup', (e) => {
if (e.key === `Enter`) {
term.findPrevious(actionElements.findPrevious.value, getSearchOptions());
searchAddon.findPrevious(actionElements.findPrevious.value, getSearchOptions());
}
});
@@ -143,7 +149,14 @@ function createTerminal(): void {
}
function runRealTerminal(): void {
term.attach(socket);
/**
* The demo defaults to string transport by default.
* To run it with UTF8 binary transport, swap comment on
* the lines below. (Must also be switched in server.js)
*/
term.loadAddon(new AttachAddon(socket));
// term.loadAddon(new AttachAddon(socket, {inputUtf8: true}));
term._initialized = true;
}
@@ -164,24 +177,21 @@ function runFakeTerminal(): void {
term.writeln('');
term.prompt();
term._core.register(term.addDisposableListener('key', (key, ev) => {
const printable = !ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey;
term.onKey((e: { key: string, domEvent: KeyboardEvent }) => {
const ev = e.domEvent;
const printable = !ev.altKey && !ev.ctrlKey && !ev.metaKey;
if (ev.keyCode === 13) {
term.prompt();
} else if (ev.keyCode === 8) {
// Do not delete the prompt
if (term.x > 2) {
if (term._core.buffer.x > 2) {
term.write('\b \b');
}
} else if (printable) {
term.write(key);
term.write(e.key);
}
}));
term._core.register(term.addDisposableListener('paste', (data, ev) => {
term.write(data);
}));
});
}
function initOptions(term: TerminalType): void {
@@ -261,8 +271,11 @@ function initOptions(term: TerminalType): void {
console.log('change', o, input.value);
if (o === 'cols' || o === 'rows') {
updateTerminalSize();
} else if (o === 'lineHeight') {
term.setOption(o, parseFloat(input.value));
updateTerminalSize();
} else {
term.setOption(o, o === 'lineHeight' ? parseFloat(input.value) : parseInt(input.value, 10));
term.setOption(o, parseInt(input.value));
}
});
});
@@ -283,8 +296,8 @@ function addDomListener(element: HTMLElement, type: string, handler: (...args: a
function updateTerminalSize(): void {
const cols = parseInt((<HTMLInputElement>document.getElementById(`opt-cols`)).value, 10);
const rows = parseInt((<HTMLInputElement>document.getElementById(`opt-rows`)).value, 10);
const width = (cols * term._core.renderer.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px';
const height = (rows * term._core.renderer.dimensions.actualCellHeight).toString() + 'px';
const width = (cols * term._core._renderCoordinator.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px';
const height = (rows * term._core._renderCoordinator.dimensions.actualCellHeight).toString() + 'px';
terminalContainer.style.width = width;
terminalContainer.style.height = height;
term.fit();
+3 -3
View File
@@ -2,8 +2,8 @@
<html>
<head>
<title>xterm.js demo</title>
<link rel="stylesheet" href="/build/xterm.css" />
<link rel="stylesheet" href="/build/addons/fullscreen/fullscreen.css" />
<link rel="shortcut icon" type="image/png" href="/logo.png">
<link rel="stylesheet" href="/src/xterm.css" />
<link rel="stylesheet" href="style.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
@@ -16,7 +16,7 @@
<p>
<label>Find next <input id="find-next"/></label>
<label>Find previous <input id="find-previous"/></label>
<label>Use regex<input type="checkbox" id="regex"/></label>
<label>Use regex<input type="checkbox" id="regex"/></label>
<label>Case sensitive<input type="checkbox" id="case-sensitive"/></label>
<label>Whole word<input type="checkbox" id="whole-word"/></label>
</p>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

+36 -4
View File
@@ -3,6 +3,13 @@ var expressWs = require('express-ws');
var os = require('os');
var pty = require('node-pty');
/**
* Whether to use UTF8 binary transport.
* (Must also be switched in client.ts)
*/
const USE_BINARY_UTF8 = false;
function startServer() {
var app = express();
expressWs(app);
@@ -10,12 +17,17 @@ function startServer() {
var terminals = {},
logs = {};
app.use('/build', express.static(__dirname + '/../build'));
app.use('/src', express.static(__dirname + '/../src'));
app.get('/logo.png', (req, res) => res.sendFile(__dirname + '/logo.png'));
app.get('/', function(req, res){
res.sendFile(__dirname + '/index.html');
});
app.get('/test', function(req, res){
res.sendFile(__dirname + '/test.html');
});
app.get('/style.css', function(req, res){
res.sendFile(__dirname + '/style.css');
});
@@ -32,7 +44,8 @@ function startServer() {
cols: cols || 80,
rows: rows || 24,
cwd: process.env.PWD,
env: process.env
env: process.env,
encoding: USE_BINARY_UTF8 ? null : 'utf8'
});
console.log('Created terminal with PID: ' + term.pid);
@@ -61,6 +74,7 @@ function startServer() {
console.log('Connected to terminal ' + term.pid);
ws.send(logs[term.pid]);
// string message buffering
function buffer(socket, timeout) {
let s = '';
let sender = null;
@@ -75,7 +89,25 @@ function startServer() {
}
};
}
const send = buffer(ws, 5);
// binary message buffering
function bufferUtf8(socket, timeout) {
let buffer = [];
let sender = null;
let length = 0;
return (data) => {
buffer.push(data);
length += data.length;
if (!sender) {
sender = setTimeout(() => {
socket.send(Buffer.concat(buffer, length));
buffer = [];
sender = null;
length = 0;
}, timeout);
}
};
}
const send = USE_BINARY_UTF8 ? bufferUtf8(ws, 5) : buffer(ws, 5);
term.on('data', function(data) {
try {
@@ -99,7 +131,7 @@ function startServer() {
var port = process.env.PORT || 3000,
host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0';
console.log('App listening to http://' + host + ':' + port);
console.log('App listening to http://127.0.0.1:' + port);
app.listen(port, host);
}
+3 -1
View File
@@ -25,11 +25,13 @@ const clientConfig = {
{
test: /\.js$/,
use: ["source-map-loader"],
enforce: "pre"
enforce: "pre",
exclude: /node_modules/
}
]
},
resolve: {
modules: [path.resolve(__dirname, '..'), 'node_modules'],
extensions: [ '.tsx', '.ts', '.js' ]
},
output: {
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html>
<head>
<title>xterm.js integration test fixture</title>
<link rel="stylesheet" href="/src/xterm.css" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div id="terminal-container"></div>
<script src="dist/client-bundle.js" defer ></script>
</body>
</html>
-87
View File
@@ -1,87 +0,0 @@
var express = require('express');
var app = express();
var expressWs = require('express-ws')(app);
var os = require('os');
var pty = require('node-pty');
var terminals = {},
logs = {};
app.use('/build', express.static(__dirname + '/../../build'));
app.use('/demo', express.static(__dirname + '/../../demo'));
app.use('/zmodemjs', express.static(__dirname + '/../../node_modules/zmodem.js/dist'));
app.get('/', function(req, res){
res.sendFile(__dirname + '/index.html');
});
app.get('/style.css', function(req, res){
res.sendFile(__dirname + '../style.css');
});
app.get('/main.js', function(req, res){
res.sendFile(__dirname + '/main.js');
});
app.post('/terminals', function (req, res) {
var cols = parseInt(req.query.cols),
rows = parseInt(req.query.rows),
term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], {
encoding: null,
name: 'xterm-color',
cols: cols || 80,
rows: rows || 24,
cwd: process.env.PWD,
env: process.env
});
console.log('Created terminal with PID: ' + term.pid);
terminals[term.pid] = term;
logs[term.pid] = '';
term.on('data', function(data) {
logs[term.pid] += data;
});
res.send(term.pid.toString());
res.end();
});
app.post('/terminals/:pid/size', function (req, res) {
var pid = parseInt(req.params.pid),
cols = parseInt(req.query.cols),
rows = parseInt(req.query.rows),
term = terminals[pid];
term.resize(cols, rows);
console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.');
res.end();
});
app.ws('/terminals/:pid', function (ws, req) {
var term = terminals[parseInt(req.params.pid)];
console.log('Connected to terminal ' + term.pid);
ws.send(logs[term.pid]);
term.on('data', function(data) {
try {
ws.send(data);
} catch (ex) {
// The WebSocket is not open, ignore
}
});
ws.on('message', function(msg) {
term.write(msg);
});
ws.on('close', function () {
term.kill();
console.log('Closed terminal ' + term.pid);
// Clean things up
delete terminals[term.pid];
delete logs[term.pid];
});
});
var port = process.env.PORT || 3000,
host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0';
console.log('App listening to http://' + host + ':' + port);
app.listen(port, host);
-128
View File
@@ -1,128 +0,0 @@
<!doctype html>
<html>
<head>
<title>xterm.js demo</title>
<link rel="stylesheet" href="/build/xterm.css" />
<link rel="stylesheet" href="/build/addons/fullscreen/fullscreen.css" />
<link rel="stylesheet" href="/demo/style.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
<script src="/build/xterm.js" ></script>
<script src="/build/addons/attach/attach.js" ></script>
<script src="/zmodemjs/zmodem.js"></script>
<script src="/build/addons/zmodem/zmodem.js" ></script>
<script src="/build/addons/fit/fit.js" ></script>
<script src="/build/addons/fullscreen/fullscreen.js" ></script>
<script src="/build/addons/search/search.js" ></script>
</head>
<body>
<h1>xterm.js: xterm, in the browser</h1>
<div id="terminal-container"></div>
<div id="zmodem_controls">
<form id="zm_start" style="display: none" action="javascript:void(0)">
ZMODEM detected: Start ZMODEM session?
<label><input id="zmstart_yes" name="zmstart" type=radio checked value="1"> Yes</label>
&nbsp;
<label><input name="zmstart" type=radio value=""> No</label>
<button type="submit">Submit</button>
</form>
<form id="zm_offer" style="display: none" action="javascript:void(0)">
<p>ZMODEM File offered!</p>
<label><input id="zmaccept_yes" name="zmaccept" type=radio checked value="1"> Accept</label>
&nbsp;
<label><input name="zmaccept" type=radio value=""> Skip</label>
<button type="submit">Submit</button>
</form>
<div id="zm_file" style="display: none">
<div>Name: <span id="name"></span></div>
<div>Size: <span id="size"></span></div>
<div>Last modified: <span id="mtime"></span></div>
<div>Mode: <span id="mode"></span></div>
<br>
<div>Conversion: <span id="zfile_conversion"></span></div>
<div>Management: <span id="zfile_management"></span></div>
<div>Transport: <span id="zfile_transport"></span></div>
<div>Sparse? <span id="zfile_sparse"></span></div>
<br>
<div>Files remaining in batch: <span id="files_remaining"></span></div>
<div>Bytes remaining in batch: <span id="bytes_remaining"></span></div>
</div>
<form id="zm_progress" style="display: none" action="javascript:void(0)">
<div><span id="percent_received"></span>% (<span id="bytes_received"></span> bytes) received</div>
<button id="zm_progress_skipper" type="button" onclick="skip_current_file();">Skip File</button>
</form>
<form id="zm_choose" style="display: none" action="javascript:void(0)">
<label>Choose file(s): <input id="zm_files" type="file" multiple></label>
</form>
</div>
<div>
<h2>Actions</h2>
<p>
<label>Find next <input id="find-next"/></label>
<label>Find previous <input id="find-previous"/></label>
</p>
</div>
<div>
<h2>Options</h2>
<p>
<label><input type="checkbox" id="option-cursor-blink"> cursorBlink</label>
</p>
<p>
<label><input type="checkbox" checked id="zmodem-auto"> Accept all ZMODEM prompts<sup>*</sup></label>
</p>
<p>
<label>
cursorStyle
<select id="option-cursor-style">
<option value="block">block</option>
<option value="underline">underline</option>
<option value="bar">bar</option>
</select>
</label>
</p>
<p>
<label>
bellStyle
<select id="option-bell-style">
<option value="">none</option>
<option value="sound">sound</option>
<option value="visual">visual</option>
<option value="both">both</option>
</select>
</label>
</p>
<p>
<label>scrollback <input type="number" id="option-scrollback" value="1000" /></label>
</p>
<p>
<label>tabStopWidth <input type="number" id="option-tabstopwidth" value="8" /></label>
</p>
<div>
<h3>Size</h3>
<div>
<div style="display: inline-block; margin-right: 16px;">
<label for="cols">Columns</label>
<input type="number" id="cols" />
</div>
<div style="display: inline-block; margin-right: 16px;">
<label for="rows">Rows</label>
<input type="number" id="rows" />
</div>
</div>
</div>
</div>
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for xterm.js evaluation purposes only. Exposing the demo to the public as is would introduce security risks for the host.</p>
<p><sup>*</sup> ZMODEM file transfers are supported via an addon. To try it out, install <a href="https://ohse.de/uwe/software/lrzsz.html"><code>lrzsz</code></a> onto the remote peer, then run <code>rz</code> to send from your browser or <code>sz &lt;file&gt;</code> to send from the remote peer.</p>
<script src="main.js" defer ></script>
</body>
</html>
-388
View File
@@ -1,388 +0,0 @@
"use strict";
var term,
protocol,
socketURL,
socket,
pid;
Terminal.applyAddon(fit);
Terminal.applyAddon(attach);
Terminal.applyAddon(zmodem);
Terminal.applyAddon(search);
var terminalContainer = document.getElementById('terminal-container'),
actionElements = {
findNext: document.querySelector('#find-next'),
findPrevious: document.querySelector('#find-previous')
},
optionElements = {
cursorBlink: document.querySelector('#option-cursor-blink'),
cursorStyle: document.querySelector('#option-cursor-style'),
scrollback: document.querySelector('#option-scrollback'),
tabstopwidth: document.querySelector('#option-tabstopwidth'),
bellStyle: document.querySelector('#option-bell-style')
},
colsElement = document.getElementById('cols'),
rowsElement = document.getElementById('rows');
function setTerminalSize() {
var cols = parseInt(colsElement.value, 10);
var rows = parseInt(rowsElement.value, 10);
var viewportElement = document.querySelector('.xterm-viewport');
var scrollBarWidth = viewportElement.offsetWidth - viewportElement.clientWidth;
var width = (cols * term.charMeasure.width + 20 /*room for scrollbar*/).toString() + 'px';
var height = (rows * term.charMeasure.height).toString() + 'px';
terminalContainer.style.width = width;
terminalContainer.style.height = height;
term.resize(cols, rows);
}
colsElement.addEventListener('change', setTerminalSize);
rowsElement.addEventListener('change', setTerminalSize);
actionElements.findNext.addEventListener('keypress', function (e) {
if (e.key === "Enter") {
e.preventDefault();
term.findNext(actionElements.findNext.value);
}
});
actionElements.findPrevious.addEventListener('keypress', function (e) {
if (e.key === "Enter") {
e.preventDefault();
term.findPrevious(actionElements.findPrevious.value);
}
});
optionElements.cursorBlink.addEventListener('change', function () {
term.setOption('cursorBlink', optionElements.cursorBlink.checked);
});
optionElements.cursorStyle.addEventListener('change', function () {
term.setOption('cursorStyle', optionElements.cursorStyle.value);
});
optionElements.bellStyle.addEventListener('change', function () {
term.setOption('bellStyle', optionElements.bellStyle.value);
});
optionElements.scrollback.addEventListener('change', function () {
term.setOption('scrollback', parseInt(optionElements.scrollback.value, 10));
});
optionElements.tabstopwidth.addEventListener('change', function () {
term.setOption('tabStopWidth', parseInt(optionElements.tabstopwidth.value, 10));
});
createTerminal();
function createTerminal() {
// Clean terminal
while (terminalContainer.children.length) {
terminalContainer.removeChild(terminalContainer.children[0]);
}
term = new Terminal({
cursorBlink: optionElements.cursorBlink.checked,
scrollback: parseInt(optionElements.scrollback.value, 10),
tabStopWidth: parseInt(optionElements.tabstopwidth.value, 10)
});
term.on('resize', function (size) {
if (!pid) {
return;
}
var cols = size.cols,
rows = size.rows,
url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows;
fetch(url, {method: 'POST'});
});
protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://';
socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/';
term.open(terminalContainer);
term.fit();
// fit is called within a setTimeout, cols and rows need this.
setTimeout(function () {
colsElement.value = term.cols;
rowsElement.value = term.rows;
// Set terminal size again to set the specific dimensions on the demo
setTerminalSize();
fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) {
res.text().then(function (pid) {
window.pid = pid;
socketURL += pid;
socket = new WebSocket(socketURL);
socket.onopen = runRealTerminal;
socket.onclose = runFakeTerminal;
socket.onerror = runFakeTerminal;
term.zmodemAttach(socket, {
noTerminalWriteOutsideSession: true,
} );
term.on("zmodemRetract", () => {
start_form.style.display = "none";
start_form.onsubmit = null;
});
term.on("zmodemDetect", (detection) => {
function do_zmodem() {
term.detach();
let zsession = detection.confirm();
var promise;
if (zsession.type === "receive") {
promise = _handle_receive_session(zsession);
}
else {
promise = _handle_send_session(zsession);
}
promise.catch( console.error.bind(console) ).then( () => {
term.attach(socket);
} );
}
if (_auto_zmodem()) {
do_zmodem();
}
else {
start_form.style.display = "";
start_form.onsubmit = function(e) {
start_form.style.display = "none";
if (document.getElementById("zmstart_yes").checked) {
do_zmodem();
}
else {
detection.deny();
}
};
}
});
});
});
}, 0);
}
//----------------------------------------------------------------------
// UI STUFF
function _show_file_info(xfer) {
var file_info = xfer.get_details();
document.getElementById("name").textContent = file_info.name;
document.getElementById("size").textContent = file_info.size;
document.getElementById("mtime").textContent = file_info.mtime;
document.getElementById("files_remaining").textContent = file_info.files_remaining;
document.getElementById("bytes_remaining").textContent = file_info.bytes_remaining;
document.getElementById("mode").textContent = "0" + file_info.mode.toString(8);
var xfer_opts = xfer.get_options();
["conversion", "management", "transport", "sparse"].forEach( (lbl) => {
document.getElementById(`zfile_${lbl}`).textContent = xfer_opts[lbl];
} );
document.getElementById("zm_file").style.display = "";
}
function _hide_file_info() {
document.getElementById("zm_file").style.display = "none";
}
function _save_to_disk(xfer, buffer) {
return Zmodem.Browser.save_to_disk(buffer, xfer.get_details().name);
}
var skipper_button = document.getElementById("zm_progress_skipper");
var skipper_button_orig_text = skipper_button.textContent;
function _show_progress() {
skipper_button.disabled = false;
skipper_button.textContent = skipper_button_orig_text;
document.getElementById("bytes_received").textContent = 0;
document.getElementById("percent_received").textContent = 0;
document.getElementById("zm_progress").style.display = "";
}
function _update_progress(xfer) {
var total_in = xfer.get_offset();
document.getElementById("bytes_received").textContent = total_in;
var percent_received = 100 * total_in / xfer.get_details().size;
document.getElementById("percent_received").textContent = percent_received.toFixed(2);
}
function _hide_progress() {
document.getElementById("zm_progress").style.display = "none";
}
var start_form = document.getElementById("zm_start");
function _auto_zmodem() {
return document.getElementById("zmodem-auto").checked;
}
// END UI STUFF
//----------------------------------------------------------------------
function _handle_receive_session(zsession) {
zsession.on("offer", function(xfer) {
current_receive_xfer = xfer;
_show_file_info(xfer);
var offer_form = document.getElementById("zm_offer");
function on_form_submit() {
offer_form.style.display = "none";
//START
//if (offer_form.zmaccept.value) {
if (_auto_zmodem() || document.getElementById("zmaccept_yes").checked) {
_show_progress();
var FILE_BUFFER = [];
xfer.on("input", (payload) => {
_update_progress(xfer);
FILE_BUFFER.push( new Uint8Array(payload) );
});
xfer.accept().then(
() => {
_save_to_disk(xfer, FILE_BUFFER);
},
console.error.bind(console)
);
}
else {
xfer.skip();
}
//END
}
if (_auto_zmodem()) {
on_form_submit();
}
else {
offer_form.onsubmit = on_form_submit;
offer_form.style.display = "";
}
} );
var promise = new Promise( (res) => {
zsession.on("session_end", () => {
_hide_file_info();
_hide_progress();
res();
} );
} );
zsession.start();
return promise;
}
function _handle_send_session(zsession) {
var choose_form = document.getElementById("zm_choose");
choose_form.style.display = "";
var file_el = document.getElementById("zm_files");
var promise = new Promise( (res) => {
file_el.onchange = function(e) {
choose_form.style.display = "none";
var files_obj = file_el.files;
Zmodem.Browser.send_files(
zsession,
files_obj,
{
on_offer_response(obj, xfer) {
if (xfer) _show_progress();
//console.log("offer", xfer ? "accepted" : "skipped");
},
on_progress(obj, xfer) {
_update_progress(xfer);
},
on_file_complete(obj) {
//console.log("COMPLETE", obj);
_hide_progress();
},
}
).then(_hide_progress).then(
zsession.close.bind(zsession),
console.error.bind(console)
).then( () => {
_hide_file_info();
_hide_progress();
res();
} );
};
} );
return promise;
}
//This is here to allow canceling of an in-progress ZMODEM transfer.
var current_receive_xfer;
//Called from HTML directly.
function skip_current_file() {
current_receive_xfer.skip();
skipper_button.disabled = true;
skipper_button.textContent = "Waiting for server to acknowledge skip …";
}
function runRealTerminal() {
term.attach(socket);
term._initialized = true;
}
function runFakeTerminal() {
if (term._initialized) {
return;
}
term._initialized = true;
var shellprompt = '$ ';
term.prompt = function () {
term.write('\r\n' + shellprompt);
};
term.writeln('Welcome to xterm.js');
term.writeln('This is a local terminal emulation, without a real terminal in the back-end.');
term.writeln('Type some keys and commands to play around.');
term.writeln('');
term.prompt();
term.on('key', function (key, ev) {
var printable = (
!ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey
);
if (ev.keyCode == 13) {
term.prompt();
} else if (ev.keyCode == 8) {
// Do not delete the prompt
if (term.x > 2) {
term.write('\b \b');
}
} else if (printable) {
term.write(key);
}
});
term.on('paste', function (data, ev) {
term.write(data);
});
}
+1 -1
View File
@@ -7,7 +7,7 @@ services:
volumes:
- ./:/usr/src/app
ports:
- ${XTERMJS_PORT:3000}:3000
- ${XTERMJS_PORT:-3000}:3000
command: ["npm", "start"]
watch:
+5 -3
View File
@@ -21,7 +21,7 @@ namespace constructor {
'disableStdin': false,
'rows': 1,
'scrollback': 10,
'tabStopWidth': 2,
'tabStopWidth': 2
});
}
}
@@ -119,8 +119,8 @@ namespace methods_core {
const t: Terminal = new Terminal();
t.attachCustomKeyEventHandler((e: KeyboardEvent) => true);
t.attachCustomKeyEventHandler((e: KeyboardEvent) => false);
const d1: IDisposable = t.addCsiHandler("x",
(params: number[], collect: string): boolean => params[0]===1);
const d1: IDisposable = t.addCsiHandler('x',
(params: number[], collect: string): boolean => params[0] === 1);
d1.dispose();
const d2: IDisposable = t.addOscHandler(199,
(data: string): boolean => true);
@@ -155,6 +155,7 @@ namespace methods_core {
const r25: string = t.getOption('fontWeightBold');
const r26: boolean = t.getOption('allowTransparency');
const r27: boolean = t.getOption('rightClickSelectsWord');
const r28: boolean = t.getOption('windowsMode');
}
{
const t: Terminal = new Terminal();
@@ -177,6 +178,7 @@ namespace methods_core {
t.setOption('useFlowControl', true);
t.setOption('allowTransparency', true);
t.setOption('visualBell', true);
t.setOption('windowsMode', true);
t.setOption('colors', ['a', 'b']);
t.setOption('letterSpacing', 1);
t.setOption('cols', 1);
+9 -5
View File
@@ -16,9 +16,9 @@ const ts = require('gulp-typescript');
const util = require('gulp-util');
const buildDir = process.env.BUILD_DIR || 'build';
const tsProject = ts.createProject('tsconfig.json');
let srcDir = tsProject.config.compilerOptions.rootDir;
let outDir = tsProject.config.compilerOptions.outDir;
const tsProject = ts.createProject('src/tsconfig.json');
let srcDir = './src';
let outDir = './lib';
const addons = fs.readdirSync(`${__dirname}/src/addons`);
@@ -39,6 +39,10 @@ gulp.task('css', function() {
return gulp.src(`${srcDir}/**/*.css`).pipe(gulp.dest(outDir));
});
gulp.task('css-build', function() {
return gulp.src(`${srcDir}/**/*.css`).pipe(gulp.dest(buildDir));
});
gulp.task('watch-css', function() {
return gulp.watch(`${srcDir}/**/*.css`, ['css']);
});
@@ -61,7 +65,7 @@ gulp.task('browserify', function() {
};
let bundleStream = browserify(browserifyOptions)
.bundle()
.pipe(source('xterm.js'))
.pipe(source(`xterm.js`))
.pipe(buffer())
.pipe(sourcemaps.init({loadMaps: true, sourceRoot: '..'}))
.pipe(sourcemaps.write('./'))
@@ -136,6 +140,6 @@ gulp.task('sorcery-addons', ['browserify-addons'], function () {
})
});
gulp.task('build', ['sorcery', 'sorcery-addons']);
gulp.task('build', ['css', 'css-build', 'sorcery', 'sorcery-addons']);
gulp.task('test', ['mocha']);
gulp.task('default', ['build']);
+14 -10
View File
@@ -1,7 +1,7 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
"version": "3.11.0",
"version": "3.14.5",
"main": "lib/public/Terminal.js",
"types": "typings/xterm.d.ts",
"repository": "https://github.com/xtermjs/xterm.js",
@@ -12,10 +12,11 @@
"@types/jsdom": "11.0.1",
"@types/mocha": "^2.2.33",
"@types/node": "6.0.108",
"@types/puppeteer": "^1.12.4",
"@types/utf8": "^2.1.6",
"@types/webpack": "^4.4.11",
"browserify": "^13.3.0",
"chai": "3.5.0",
"concurrently": "^3.5.1",
"coveralls": "^3.0.1",
"express": "4.13.4",
"express-ws": "2.0.0-rc.1",
@@ -33,16 +34,21 @@
"node-pty": "0.7.6",
"nodemon": "1.10.2",
"nyc": "^11.8.0",
"puppeteer": "^1.15.0",
"sorcery": "^0.10.0",
"source-map-loader": "^0.2.4",
"ts-loader": "^4.5.0",
"tslint": "^5.9.1",
"tslint-consistent-codestyle": "^1.13.0",
"typescript": "3.1",
"typescript": "3.5",
"utf8": "^3.0.0",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"webpack": "^4.17.1",
"webpack-cli": "^3.1.0",
"xterm-addon-attach": "0.1.0-beta8",
"xterm-addon-search": "0.1.0-beta4",
"xterm-addon-web-links": "0.1.0-beta6",
"zmodem.js": "^0.1.5"
},
"scripts": {
@@ -50,20 +56,18 @@
"start-debug": "node --inspect-brk demo/start",
"start-zmodem": "node demo/zmodem/app",
"lint": "tslint 'src/**/*.ts' './demo/**/*.ts'",
"pretest": "npm run layering",
"test": "npm run mocha",
"posttest": "npm run lint",
"test-debug": "node --inspect-brk node_modules/.bin/gulp test",
"test-suite": "gulp mocha-suite --test",
"test-coverage": "nyc -x gulpfile.js -x '**/*test*' npm run mocha",
"test-api": "mocha \"**/*.api.js\"",
"mocha": "gulp test",
"tsc": "tsc",
"prebuild": "concurrently --kill-others-on-fail --names \"lib,attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem,css\" \"tsc\" \"tsc -p ./src/addons/attach\" \"tsc -p ./src/addons/fit\" \"tsc -p ./src/addons/fullscreen\" \"tsc -p ./src/addons/search\" \"tsc -p ./src/addons/terminado\" \"tsc -p ./src/addons/webLinks\" \"tsc -p ./src/addons/winptyCompat\" \"tsc -p ./src/addons/zmodem\" \"gulp css\"",
"prebuild": "tsc -b ./src/tsconfig.all.json",
"build": "gulp build",
"prepublish": "npm run build",
"prepare": "npm run prebuild",
"prepublishOnly": "npm run build",
"coveralls": "nyc report --reporter=text-lcov | coveralls",
"watch": "concurrently --kill-others-on-fail --names \"lib,css\" \"tsc -w\" \"gulp watch-css\"",
"watch-addons": "concurrently --kill-others-on-fail --names \"attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem\" \"tsc -w -p ./src/addons/attach\" \"tsc -w -p ./src/addons/fit\" \"tsc -w -p ./src/addons/fullscreen\" \"tsc -w -p ./src/addons/search\" \"tsc -w -p ./src/addons/terminado\" \"tsc -w -p ./src/addons/webLinks\" \"tsc -w -p ./src/addons/winptyCompat\" \"tsc -w -p ./src/addons/zmodem\"",
"layering": "concurrently --kill-others-on-fail --names \"common,core\" \"tsc -p ./src/common\" \"tsc -p ./src/core\""
"watch": "tsc -b -w ./src/tsconfig.all.json --preserveWatchOutput"
}
}
+43 -30
View File
@@ -5,10 +5,12 @@
import * as Strings from './Strings';
import { ITerminal, IBuffer } from './Types';
import { isMac } from './core/Platform';
import { isMac } from './common/Platform';
import { RenderDebouncer } from './ui/RenderDebouncer';
import { addDisposableDomListener } from './ui/Lifecycle';
import { Disposable } from './common/Lifecycle';
import { ScreenDprMonitor } from './ui/ScreenDprMonitor';
import { IRenderDimensions } from './renderer/Types';
const MAX_ROWS_TO_READ = 20;
@@ -25,6 +27,7 @@ export class AccessibilityManager extends Disposable {
private _liveRegionLineCount: number = 0;
private _renderRowsDebouncer: RenderDebouncer;
private _screenDprMonitor: ScreenDprMonitor;
private _topBoundaryFocusListener: (e: FocusEvent) => void;
private _bottomBoundaryFocusListener: (e: FocusEvent) => void;
@@ -40,7 +43,12 @@ export class AccessibilityManager extends Disposable {
*/
private _charsToConsume: string[] = [];
constructor(private _terminal: ITerminal) {
private _charsToAnnounce: string = '';
constructor(
private _terminal: ITerminal,
private _dimensions: IRenderDimensions
) {
super();
this._accessibilityTreeRoot = document.createElement('div');
this._accessibilityTreeRoot.classList.add('xterm-accessibility');
@@ -61,7 +69,7 @@ export class AccessibilityManager extends Disposable {
this._refreshRowsDimensions();
this._accessibilityTreeRoot.appendChild(this._rowContainer);
this._renderRowsDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this));
this._renderRowsDebouncer = new RenderDebouncer(this._renderRows.bind(this));
this._refreshRows();
this._liveRegion = document.createElement('div');
@@ -72,22 +80,21 @@ export class AccessibilityManager extends Disposable {
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
this.register(this._renderRowsDebouncer);
this.register(this._terminal.addDisposableListener('resize', data => this._onResize(data.rows)));
this.register(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
this.register(this._terminal.addDisposableListener('scroll', data => this._refreshRows()));
this.register(this._terminal.onResize(e => this._onResize(e.rows)));
this.register(this._terminal.onRender(e => this._refreshRows(e.start, e.end)));
this.register(this._terminal.onScroll(() => this._refreshRows()));
// Line feed is an issue as the prompt won't be read out after a command is run
this.register(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char)));
this.register(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n')));
this.register(this._terminal.onLineFeed(() => this._onChar('\n')));
this.register(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount)));
this.register(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar)));
this.register(this._terminal.onKey(e => this._onKey(e.key)));
this.register(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion()));
// TODO: Maybe renderer should fire an event on terminal when the characters change and that
// should be listened to instead? That would mean that the order of events are always
// guarenteed
this.register(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions()));
this.register(this._terminal.renderer.addDisposableListener('resize', () => this._refreshRowsDimensions()));
this._screenDprMonitor = new ScreenDprMonitor();
this.register(this._screenDprMonitor);
this._screenDprMonitor.setListener(() => this._refreshRowsDimensions());
// This shouldn't be needed on modern browsers but is present in case the
// media query that drives the dprchange event isn't supported
// media query that drives the ScreenDprMonitor isn't supported
this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions()));
}
@@ -197,10 +204,10 @@ export class AccessibilityManager extends Disposable {
// Have the screen reader ignore the char if it was just input
const shiftedChar = this._charsToConsume.shift();
if (shiftedChar !== char) {
this._announceCharacter(char);
this._charsToAnnounce += char;
}
} else {
this._announceCharacter(char);
this._charsToAnnounce += char;
}
if (char === '\n') {
@@ -239,7 +246,7 @@ export class AccessibilityManager extends Disposable {
}
private _refreshRows(start?: number, end?: number): void {
this._renderRowsDebouncer.refresh(start, end);
this._renderRowsDebouncer.refresh(start, end, this._terminal.rows);
}
private _renderRows(start: number, end: number): void {
@@ -249,14 +256,17 @@ export class AccessibilityManager extends Disposable {
const lineData = buffer.translateBufferLineToString(buffer.ydisp + i, true);
const posInSet = (buffer.ydisp + i + 1).toString();
const element = this._rowElements[i];
element.textContent = lineData.length === 0 ? Strings.blankLine : lineData;
element.setAttribute('aria-posinset', posInSet);
element.setAttribute('aria-setsize', setSize);
if (element) {
element.textContent = lineData.length === 0 ? Strings.blankLine : lineData;
element.setAttribute('aria-posinset', posInSet);
element.setAttribute('aria-setsize', setSize);
}
}
this._announceCharacters();
}
private _refreshRowsDimensions(): void {
if (!this._terminal.renderer.dimensions.actualCellHeight) {
if (!this._dimensions.actualCellHeight) {
return;
}
if (this._rowElements.length !== this._terminal.rows) {
@@ -267,17 +277,20 @@ export class AccessibilityManager extends Disposable {
}
}
private _refreshRowDimensions(element: HTMLElement): void {
element.style.height = `${this._terminal.renderer.dimensions.actualCellHeight}px`;
public setDimensions(dimensions: IRenderDimensions): void {
this._dimensions = dimensions;
this._refreshRowsDimensions();
}
private _announceCharacter(char: string): void {
if (char === ' ') {
// Always use nbsp for spaces in order to preserve the space between characters in
// voiceover's caption window
this._liveRegion.innerHTML += '&nbsp;';
} else {
this._liveRegion.textContent += char;
private _refreshRowDimensions(element: HTMLElement): void {
element.style.height = `${this._dimensions.actualCellHeight}px`;
}
private _announceCharacters(): void {
if (this._charsToAnnounce.length === 0) {
return;
}
this._liveRegion.textContent += this._charsToAnnounce;
this._charsToAnnounce = '';
}
}
+88 -39
View File
@@ -5,10 +5,10 @@
import { assert, expect } from 'chai';
import { ITerminal } from './Types';
import { Buffer, DEFAULT_ATTR, CHAR_DATA_CHAR_INDEX } from './Buffer';
import { Buffer } from './Buffer';
import { CircularList } from './common/CircularList';
import { MockTerminal, TestTerminal } from './ui/TestUtils.test';
import { BufferLine } from './BufferLine';
import { MockTerminal, TestTerminal } from './TestUtils.test';
import { BufferLine, CellData, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine';
const INIT_COLS = 80;
const INIT_ROWS = 24;
@@ -37,13 +37,13 @@ describe('Buffer', () => {
describe('fillViewportRows', () => {
it('should fill the buffer with blank lines based on the size of the viewport', () => {
const blankLineChar = buffer.getBlankLine(DEFAULT_ATTR).get(0);
const blankLineChar = buffer.getBlankLine(DEFAULT_ATTR_DATA).loadCell(0, new CellData()).getAsCharData();
buffer.fillViewportRows();
assert.equal(buffer.lines.length, INIT_ROWS);
for (let y = 0; y < INIT_ROWS; y++) {
assert.equal(buffer.lines.get(y).length, INIT_COLS);
for (let x = 0; x < INIT_COLS; x++) {
assert.deepEqual(buffer.lines.get(y).get(x), blankLineChar);
assert.deepEqual(buffer.lines.get(y).loadCell(x, new CellData()).getAsCharData(), blankLineChar);
}
}
});
@@ -155,15 +155,15 @@ describe('Buffer', () => {
assert.equal(buffer.lines.maxLength, INIT_ROWS);
buffer.y = INIT_ROWS - 1;
buffer.fillViewportRows();
let chData = buffer.lines.get(5).get(0);
let chData = buffer.lines.get(5).loadCell(0, new CellData()).getAsCharData();
chData[1] = 'a';
buffer.lines.get(5).set(0, chData);
chData = buffer.lines.get(INIT_ROWS - 1).get(0);
buffer.lines.get(5).setCell(0, CellData.fromCharData(chData));
chData = buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).getAsCharData();
chData[1] = 'b';
buffer.lines.get(INIT_ROWS - 1).set(0, chData);
buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData(chData));
buffer.resize(INIT_COLS, INIT_ROWS - 5);
assert.equal(buffer.lines.get(0).get(0)[1], 'a');
assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5).get(0)[1], 'b');
assert.equal(buffer.lines.get(0).loadCell(0, new CellData()).getAsCharData()[1], 'a');
assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5).loadCell(0, new CellData()).getAsCharData()[1], 'b');
});
});
});
@@ -184,7 +184,7 @@ describe('Buffer', () => {
buffer.fillViewportRows();
// Create 10 extra blank lines
for (let i = 0; i < 10; i++) {
buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR));
buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR_DATA));
}
// Set cursor to the bottom of the buffer
buffer.y = INIT_ROWS - 1;
@@ -204,7 +204,7 @@ describe('Buffer', () => {
buffer.fillViewportRows();
// Create 10 extra blank lines
for (let i = 0; i < 10; i++) {
buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR));
buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR_DATA));
}
// Set cursor to the bottom of the buffer
buffer.y = INIT_ROWS - 1;
@@ -518,6 +518,29 @@ describe('Buffer', () => {
assert.equal(secondMarker.line, 1, 'second marker should be restored');
assert.equal(thirdMarker.line, 2, 'third marker should be restored');
});
it('should correctly reflow wrapped lines that end in null space (via tab char)', () => {
buffer.fillViewportRows();
buffer.resize(4, 10);
buffer.y = 2;
buffer.lines.get(0).set(0, [null, 'a', 1, 'a'.charCodeAt(0)]);
buffer.lines.get(0).set(1, [null, 'b', 1, 'b'.charCodeAt(0)]);
buffer.lines.get(1).set(0, [null, 'c', 1, 'c'.charCodeAt(0)]);
buffer.lines.get(1).set(1, [null, 'd', 1, 'd'.charCodeAt(0)]);
buffer.lines.get(1).isWrapped = true;
// Buffer:
// "ab " (wrapped)
// "cd"
buffer.resize(5, 10);
assert.equal(buffer.ybase, 0);
assert.equal(buffer.lines.length, 10);
assert.equal(buffer.lines.get(0).translateToString(true), 'ab c');
assert.equal(buffer.lines.get(1).translateToString(false), 'd ');
buffer.resize(6, 10);
assert.equal(buffer.ybase, 0);
assert.equal(buffer.lines.length, 10);
assert.equal(buffer.lines.get(0).translateToString(true), 'ab cd');
assert.equal(buffer.lines.get(1).translateToString(false), ' ');
});
it('should wrap wide characters correctly when reflowing larger', () => {
buffer.fillViewportRows();
buffer.resize(12, 10);
@@ -553,6 +576,32 @@ describe('Buffer', () => {
assert.equal(buffer.lines.get(1).translateToString(true), '语汉语汉语');
assert.equal(buffer.lines.get(1).translateToString(false), '语汉语汉语 ');
});
it('should correctly reflow wrapped lines that end in null space (via tab char)', () => {
buffer.fillViewportRows();
buffer.resize(4, 10);
buffer.y = 2;
buffer.lines.get(0).set(0, [null, 'a', 1, 'a'.charCodeAt(0)]);
buffer.lines.get(0).set(1, [null, 'b', 1, 'b'.charCodeAt(0)]);
buffer.lines.get(1).set(0, [null, 'c', 1, 'c'.charCodeAt(0)]);
buffer.lines.get(1).set(1, [null, 'd', 1, 'd'.charCodeAt(0)]);
buffer.lines.get(1).isWrapped = true;
// Buffer:
// "ab " (wrapped)
// "cd"
buffer.resize(3, 10);
assert.equal(buffer.y, 2);
assert.equal(buffer.ybase, 0);
assert.equal(buffer.lines.length, 10);
assert.equal(buffer.lines.get(0).translateToString(false), 'ab ');
assert.equal(buffer.lines.get(1).translateToString(false), ' cd');
buffer.resize(2, 10);
assert.equal(buffer.y, 3);
assert.equal(buffer.ybase, 0);
assert.equal(buffer.lines.length, 10);
assert.equal(buffer.lines.get(0).translateToString(false), 'ab');
assert.equal(buffer.lines.get(1).translateToString(false), ' ');
assert.equal(buffer.lines.get(2).translateToString(false), 'cd');
});
it('should wrap wide characters correctly when reflowing smaller', () => {
buffer.fillViewportRows();
buffer.resize(12, 10);
@@ -683,7 +732,7 @@ describe('Buffer', () => {
beforeEach(() => {
// Add 10 empty rows to start
for (let i = 0; i < 10; i++) {
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR));
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
}
buffer.ybase = 10;
});
@@ -742,7 +791,7 @@ describe('Buffer', () => {
terminal.options.scrollback = 10;
// Add 10 empty rows to start
for (let i = 0; i < 10; i++) {
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR));
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
}
buffer.y = 9;
buffer.ybase = 10;
@@ -877,7 +926,7 @@ describe('Buffer', () => {
beforeEach(() => {
// Add 10 empty rows to start
for (let i = 0; i < 10; i++) {
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR));
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
}
buffer.ybase = 10;
});
@@ -940,7 +989,7 @@ describe('Buffer', () => {
terminal.options.scrollback = 10;
// Add 10 empty rows to start
for (let i = 0; i < 10; i++) {
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR));
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
}
buffer.ybase = 10;
});
@@ -1025,7 +1074,7 @@ describe('Buffer', () => {
buffer.fillViewportRows();
const marker = buffer.addMarker(buffer.lines.length - 1);
assert.equal(marker.line, buffer.lines.length - 1);
buffer.lines.emit('trim', 1);
buffer.lines.onTrimEmitter.fire(1);
assert.equal(marker.line, buffer.lines.length - 2);
});
it('should dispose of a marker if it is trimmed off the buffer', () => {
@@ -1036,7 +1085,7 @@ describe('Buffer', () => {
const marker = buffer.addMarker(0);
assert.equal(marker.isDisposed, false);
assert.equal(buffer.markers.length, 1);
buffer.lines.emit('trim', 1);
buffer.lines.onTrimEmitter.fire(1);
assert.equal(marker.isDisposed, true);
assert.equal(buffer.markers.length, 0);
});
@@ -1045,10 +1094,10 @@ describe('Buffer', () => {
describe ('translateBufferLineToString', () => {
it('should handle selecting a section of ascii text', () => {
const line = new BufferLine(4);
line.set(0, [ null, 'a', 1, 'a'.charCodeAt(0)]);
line.set(1, [ null, 'b', 1, 'b'.charCodeAt(0)]);
line.set(2, [ null, 'c', 1, 'c'.charCodeAt(0)]);
line.set(3, [ null, 'd', 1, 'd'.charCodeAt(0)]);
line.setCell(0, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(1, CellData.fromCharData([ null, 'b', 1, 'b'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([ null, 'c', 1, 'c'.charCodeAt(0)]));
line.setCell(3, CellData.fromCharData([ null, 'd', 1, 'd'.charCodeAt(0)]));
buffer.lines.set(0, line);
const str = buffer.translateBufferLineToString(0, true, 0, 2);
@@ -1057,9 +1106,9 @@ describe('Buffer', () => {
it('should handle a cut-off double width character by including it', () => {
const line = new BufferLine(3);
line.set(0, [ null, '語', 2, 35486 ]);
line.set(1, [ null, '', 0, null]);
line.set(2, [ null, 'a', 1, 'a'.charCodeAt(0)]);
line.setCell(0, CellData.fromCharData([ null, '語', 2, 35486 ]));
line.setCell(1, CellData.fromCharData([ null, '', 0, null]));
line.setCell(2, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)]));
buffer.lines.set(0, line);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
@@ -1068,9 +1117,9 @@ describe('Buffer', () => {
it('should handle a zero width character in the middle of the string by not including it', () => {
const line = new BufferLine(3);
line.set(0, [ null, '語', 2, '語'.charCodeAt(0) ]);
line.set(1, [ null, '', 0, null]);
line.set(2, [ null, 'a', 1, 'a'.charCodeAt(0)]);
line.setCell(0, CellData.fromCharData([ null, '語', 2, '語'.charCodeAt(0) ]));
line.setCell(1, CellData.fromCharData([ null, '', 0, null]));
line.setCell(2, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)]));
buffer.lines.set(0, line);
const str0 = buffer.translateBufferLineToString(0, true, 0, 1);
@@ -1085,8 +1134,8 @@ describe('Buffer', () => {
it('should handle single width emojis', () => {
const line = new BufferLine(2);
line.set(0, [ null, '😁', 1, '😁'.charCodeAt(0) ]);
line.set(1, [ null, 'a', 1, 'a'.charCodeAt(0)]);
line.setCell(0, CellData.fromCharData([ null, '😁', 1, '😁'.charCodeAt(0) ]));
line.setCell(1, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)]));
buffer.lines.set(0, line);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
@@ -1098,8 +1147,8 @@ describe('Buffer', () => {
it('should handle double width emojis', () => {
const line = new BufferLine(2);
line.set(0, [ null, '😁', 2, '😁'.charCodeAt(0) ]);
line.set(1, [ null, '', 0, null]);
line.setCell(0, CellData.fromCharData([ null, '😁', 2, '😁'.charCodeAt(0) ]));
line.setCell(1, CellData.fromCharData([ null, '', 0, null]));
buffer.lines.set(0, line);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
@@ -1109,9 +1158,9 @@ describe('Buffer', () => {
assert.equal(str2, '😁');
const line2 = new BufferLine(3);
line2.set(0, [ null, '😁', 2, '😁'.charCodeAt(0) ]);
line2.set(1, [ null, '', 0, null]);
line2.set(2, [ null, 'a', 1, 'a'.charCodeAt(0)]);
line2.setCell(0, CellData.fromCharData([ null, '😁', 2, '😁'.charCodeAt(0) ]));
line2.setCell(1, CellData.fromCharData([ null, '', 0, null]));
line2.setCell(2, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)]));
buffer.lines.set(0, line2);
const str3 = buffer.translateBufferLineToString(0, true, 0, 3);
@@ -1264,7 +1313,7 @@ describe('Buffer', () => {
assert.equal(input, s);
const stringIndex = s.match(/😃/).index;
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex);
assert(terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX], '😃');
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)', () => {
@@ -1291,7 +1340,7 @@ describe('Buffer', () => {
assert.equal(input, s);
for (let i = 0; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true);
assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]);
assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars());
}
});
@@ -1309,7 +1358,7 @@ describe('Buffer', () => {
: (i % 3 === 1)
? input.substr(i, 2)
: input.substr(i - 1, 2),
terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]);
terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars());
}
});
+68 -77
View File
@@ -3,31 +3,15 @@
* @license MIT
*/
import { IMarker } from 'xterm';
import { BufferLine } from './BufferLine';
import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow';
import { CircularList, IDeleteEvent, IInsertEvent } from './common/CircularList';
import { EventEmitter } from './common/EventEmitter';
import { DEFAULT_COLOR } from './renderer/atlas/Types';
import { BufferIndex, CharData, IBuffer, IBufferLine, IBufferStringIterator, IBufferStringIteratorResult, ITerminal } from './Types';
import { CircularList, IInsertEvent } from './common/CircularList';
import { ITerminal, IBuffer, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from './Types';
import { IBufferLine, ICellData, IAttributeData } from './core/Types';
import { BufferLine, CellData, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine';
import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths, getWrappedLineTrimmedLength } from './core/buffer/BufferReflow';
import { Marker } from './core/buffer/Marker';
export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0);
export const CHAR_DATA_ATTR_INDEX = 0;
export const CHAR_DATA_CHAR_INDEX = 1;
export const CHAR_DATA_WIDTH_INDEX = 2;
export const CHAR_DATA_CODE_INDEX = 3;
export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
export const NULL_CELL_CHAR = '';
export const NULL_CELL_WIDTH = 1;
export const NULL_CELL_CODE = 0;
export const WHITESPACE_CELL_CHAR = ' ';
export const WHITESPACE_CELL_WIDTH = 1;
export const WHITESPACE_CELL_CODE = 32;
export const FILL_CHAR_DATA: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
/**
* This class represents a terminal buffer (an internal state of the terminal), where the
* following information is stored (in high-level):
@@ -46,8 +30,10 @@ export class Buffer implements IBuffer {
public tabs: any;
public savedY: number;
public savedX: number;
public savedCurAttr: number;
public savedCurAttrData = DEFAULT_ATTR_DATA.clone();
public markers: Marker[] = [];
private _nullCell: ICellData = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
private _whitespaceCell: ICellData = CellData.fromCharData([0, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE]);
private _cols: number;
private _rows: number;
@@ -66,9 +52,30 @@ export class Buffer implements IBuffer {
this.clear();
}
public getBlankLine(attr: number, isWrapped?: boolean): IBufferLine {
const fillCharData: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
return new BufferLine(this._cols, fillCharData, isWrapped);
public getNullCell(attr?: IAttributeData): ICellData {
if (attr) {
this._nullCell.fg = attr.fg;
this._nullCell.bg = attr.bg;
} else {
this._nullCell.fg = 0;
this._nullCell.bg = 0;
}
return this._nullCell;
}
public getWhitespaceCell(attr?: IAttributeData): ICellData {
if (attr) {
this._whitespaceCell.fg = attr.fg;
this._whitespaceCell.bg = attr.bg;
} else {
this._whitespaceCell.fg = 0;
this._whitespaceCell.bg = 0;
}
return this._whitespaceCell;
}
public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine {
return new BufferLine(this._terminal.cols, this.getNullCell(attr), isWrapped);
}
public get hasScrollback(): boolean {
@@ -99,10 +106,10 @@ export class Buffer implements IBuffer {
/**
* Fills the buffer's viewport with blank lines.
*/
public fillViewportRows(fillAttr?: number): void {
public fillViewportRows(fillAttr?: IAttributeData): void {
if (this.lines.length === 0) {
if (fillAttr === undefined) {
fillAttr = DEFAULT_ATTR;
fillAttr = DEFAULT_ATTR_DATA;
}
let i = this._rows;
while (i--) {
@@ -131,6 +138,9 @@ export class Buffer implements IBuffer {
* @param newRows The new number of rows.
*/
public resize(newCols: number, newRows: number): void {
// store reference to null cell with default attrs
const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);
// Increase max length if needed before adjustments to allow space to fill
// as required.
const newMaxLength = this._getCorrectBufferLength(newRows);
@@ -144,7 +154,7 @@ export class Buffer implements IBuffer {
// Deal with columns increasing (reducing needs to happen after reflow)
if (this._cols < newCols) {
for (let i = 0; i < this.lines.length; i++) {
this.lines.get(i).resize(newCols, FILL_CHAR_DATA);
this.lines.get(i).resize(newCols, nullCell);
}
}
@@ -165,7 +175,7 @@ export class Buffer implements IBuffer {
} else {
// Add a blank line if there is no buffer left at the top to scroll to, or if there
// are blank lines after the cursor
this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA));
this.lines.push(new BufferLine(newCols, nullCell));
}
}
}
@@ -211,13 +221,13 @@ export class Buffer implements IBuffer {
this.scrollBottom = newRows - 1;
if (this._hasScrollback) {
if (this._isReflowEnabled) {
this._reflow(newCols, newRows);
// Trim the end of the line off if cols shrunk
if (this._cols > newCols) {
for (let i = 0; i < this.lines.length; i++) {
this.lines.get(i).resize(newCols, FILL_CHAR_DATA);
this.lines.get(i).resize(newCols, nullCell);
}
}
}
@@ -226,6 +236,10 @@ export class Buffer implements IBuffer {
this._rows = newRows;
}
private get _isReflowEnabled(): boolean {
return this._hasScrollback && !this._terminal.options.windowsMode;
}
private _reflow(newCols: number, newRows: number): void {
if (this._cols === newCols) {
return;
@@ -233,22 +247,23 @@ export class Buffer implements IBuffer {
// Iterate through rows, ignore the last one as it cannot be wrapped
if (newCols > this._cols) {
this._reflowLarger(newCols);
this._reflowLarger(newCols, newRows);
} else {
this._reflowSmaller(newCols, newRows);
}
}
private _reflowLarger(newCols: number): void {
const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, newCols, this.ybase + this.y);
private _reflowLarger(newCols: number, newRows: number): void {
const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, this._cols, newCols, this.ybase + this.y, this.getNullCell(DEFAULT_ATTR_DATA));
if (toRemove.length > 0) {
const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove);
reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout);
this._reflowLargerAdjustViewport(newCols, newLayoutResult.countRemoved);
this._reflowLargerAdjustViewport(newCols, newRows, newLayoutResult.countRemoved);
}
}
private _reflowLargerAdjustViewport(newCols: number, countRemoved: number): void {
private _reflowLargerAdjustViewport(newCols: number, newRows: number, countRemoved: number): void {
const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);
// Adjust viewport based on number of items removed
let viewportAdjustments = countRemoved;
while (viewportAdjustments-- > 0) {
@@ -256,9 +271,9 @@ export class Buffer implements IBuffer {
if (this.y > 0) {
this.y--;
}
if (this.lines.length < this._rows) {
if (this.lines.length < newRows) {
// Add an extra row at the bottom of the viewport
this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA));
this.lines.push(new BufferLine(newCols, nullCell));
}
} else {
if (this.ydisp === this.ybase) {
@@ -270,6 +285,7 @@ export class Buffer implements IBuffer {
}
private _reflowSmaller(newCols: number, newRows: number): void {
const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);
// Gather all BufferLines that need to be inserted into the Buffer here so that they can be
// batched up and only committed once
const toInsert = [];
@@ -278,7 +294,7 @@ export class Buffer implements IBuffer {
for (let y = this.lines.length - 1; y >= 0; y--) {
// Check whether this line is a problem
let nextLine = this.lines.get(y) as BufferLine;
if (!nextLine.isWrapped && nextLine.getTrimmedLength() <= newCols) {
if (!nextLine || !nextLine.isWrapped && nextLine.getTrimmedLength() <= newCols) {
continue;
}
@@ -310,7 +326,7 @@ export class Buffer implements IBuffer {
// Add the new lines
const newLines: BufferLine[] = [];
for (let i = 0; i < linesToAdd; i++) {
const newLine = this.getBlankLine(DEFAULT_ATTR, true) as BufferLine;
const newLine = this.getBlankLine(DEFAULT_ATTR_DATA, true) as BufferLine;
newLines.push(newLine);
}
if (newLines.length > 0) {
@@ -344,15 +360,15 @@ export class Buffer implements IBuffer {
srcCol -= cellsToCopy;
if (srcCol === 0) {
srcLineIndex--;
// TODO: srcCol shoudl take trimmed length into account
srcCol = wrappedLines[Math.max(srcLineIndex, 0)].getTrimmedLength(); // this._cols;
const wrappedLinesIndex = Math.max(srcLineIndex, 0);
srcCol = getWrappedLineTrimmedLength(wrappedLines, wrappedLinesIndex, this._cols);
}
}
// Null out the end of the line ends if a wide character wrapped to the following line
for (let i = 0; i < wrappedLines.length; i++) {
if (destLineLengths[i] < newCols) {
wrappedLines[i].set(destLineLengths[i], FILL_CHAR_DATA);
wrappedLines[i].setCell(destLineLengths[i], nullCell);
}
}
@@ -360,7 +376,7 @@ export class Buffer implements IBuffer {
let viewportAdjustments = linesToAdd - trimmedLines;
while (viewportAdjustments-- > 0) {
if (this.ybase === 0) {
if (this.y < this._rows - 1) {
if (this.y < newRows - 1) {
this.y++;
this.lines.pop();
} else {
@@ -411,7 +427,7 @@ export class Buffer implements IBuffer {
insertEvents.push({
index: originalLineIndex + 1,
amount: nextToInsert.newLines.length
} as IInsertEvent);
});
countInsertedSoFar += nextToInsert.newLines.length;
nextToInsert = toInsert[++nextToInsertIndex];
@@ -424,12 +440,12 @@ export class Buffer implements IBuffer {
let insertCountEmitted = 0;
for (let i = insertEvents.length - 1; i >= 0; i--) {
insertEvents[i].index += insertCountEmitted;
this.lines.emit('insert', insertEvents[i]);
this.lines.onInsertEmitter.fire(insertEvents[i]);
insertCountEmitted += insertEvents[i].amount;
}
const amountToTrim = Math.max(0, originalLinesLength + countToInsert - this.lines.maxLength);
if (amountToTrim > 0) {
this.lines.emitMayRemoveListeners('trim', amountToTrim);
this.lines.onTrimEmitter.fire(amountToTrim);
}
}
}
@@ -549,19 +565,19 @@ export class Buffer implements IBuffer {
public addMarker(y: number): Marker {
const marker = new Marker(y);
this.markers.push(marker);
marker.register(this.lines.addDisposableListener('trim', amount => {
marker.register(this.lines.onTrim(amount => {
marker.line -= amount;
// The marker should be disposed when the line is trimmed from the buffer
if (marker.line < 0) {
marker.dispose();
}
}));
marker.register(this.lines.addDisposableListener('insert', (event: IInsertEvent) => {
marker.register(this.lines.onInsert(event => {
if (marker.line >= event.index) {
marker.line += event.amount;
}
}));
marker.register(this.lines.addDisposableListener('delete', (event: IDeleteEvent) => {
marker.register(this.lines.onDelete(event => {
// Delete the marker if it's within the range
if (marker.line >= event.index && marker.line < event.index + event.amount) {
marker.dispose();
@@ -572,7 +588,7 @@ export class Buffer implements IBuffer {
marker.line -= event.amount;
}
}));
marker.register(marker.addDisposableListener('dispose', () => this._removeMarker(marker)));
marker.register(marker.onDispose(() => this._removeMarker(marker)));
return marker;
}
@@ -585,31 +601,6 @@ export class Buffer implements IBuffer {
}
}
export class Marker extends EventEmitter implements IMarker {
private static _nextId = 1;
private _id: number = Marker._nextId++;
public isDisposed: boolean = false;
public get id(): number { return this._id; }
constructor(
public line: number
) {
super();
}
public dispose(): void {
if (this.isDisposed) {
return;
}
this.isDisposed = true;
// Emit before super.dispose such that dispose listeners get a change to react
this.emit('dispose');
super.dispose();
}
}
/**
* Iterator to get unwrapped content strings from the buffer.
* The iterator returns at least the string data between the borders
-305
View File
@@ -1,305 +0,0 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import * as chai from 'chai';
import { BufferLine } from './BufferLine';
import { CharData, IBufferLine } from './Types';
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from './Buffer';
class TestBufferLine extends BufferLine {
public get combined(): {[index: number]: string} {
return this._combined;
}
public toArray(): CharData[] {
const result = [];
for (let i = 0; i < this.length; ++i) {
result.push(this.get(i));
}
return result;
}
}
describe('BufferLine', function(): void {
it('ctor', function(): void {
let line: IBufferLine = new TestBufferLine(0);
chai.expect(line.length).equals(0);
chai.expect(line.isWrapped).equals(false);
line = new TestBufferLine(10);
chai.expect(line.length).equals(10);
chai.expect(line.get(0)).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
chai.expect(line.isWrapped).equals(false);
line = new TestBufferLine(10, null, true);
chai.expect(line.length).equals(10);
chai.expect(line.get(0)).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
chai.expect(line.isWrapped).equals(true);
line = new TestBufferLine(10, [123, 'a', 456, 'a'.charCodeAt(0)], true);
chai.expect(line.length).equals(10);
chai.expect(line.get(0)).eql([123, 'a', 456, 'a'.charCodeAt(0)]);
chai.expect(line.isWrapped).equals(true);
});
it('insertCells', function(): void {
const line = new TestBufferLine(3);
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
line.insertCells(1, 3, [4, 'd', 0, 'd'.charCodeAt(0)]);
chai.expect(line.toArray()).eql([
[1, 'a', 0, 'a'.charCodeAt(0)],
[4, 'd', 0, 'd'.charCodeAt(0)],
[4, 'd', 0, 'd'.charCodeAt(0)]
]);
});
it('deleteCells', function(): void {
const line = new TestBufferLine(5);
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]);
line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]);
line.deleteCells(1, 2, [6, 'f', 0, 'f'.charCodeAt(0)]);
chai.expect(line.toArray()).eql([
[1, 'a', 0, 'a'.charCodeAt(0)],
[4, 'd', 0, 'd'.charCodeAt(0)],
[5, 'e', 0, 'e'.charCodeAt(0)],
[6, 'f', 0, 'f'.charCodeAt(0)],
[6, 'f', 0, 'f'.charCodeAt(0)]
]);
});
it('replaceCells', function(): void {
const line = new TestBufferLine(5);
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]);
line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]);
line.replaceCells(2, 4, [6, 'f', 0, 'f'.charCodeAt(0)]);
chai.expect(line.toArray()).eql([
[1, 'a', 0, 'a'.charCodeAt(0)],
[2, 'b', 0, 'b'.charCodeAt(0)],
[6, 'f', 0, 'f'.charCodeAt(0)],
[6, 'f', 0, 'f'.charCodeAt(0)],
[5, 'e', 0, 'e'.charCodeAt(0)]
]);
});
it('fill', function(): void {
const line = new TestBufferLine(5);
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]);
line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]);
line.fill([123, 'z', 0, 'z'.charCodeAt(0)]);
chai.expect(line.toArray()).eql([
[123, 'z', 0, 'z'.charCodeAt(0)],
[123, 'z', 0, 'z'.charCodeAt(0)],
[123, 'z', 0, 'z'.charCodeAt(0)],
[123, 'z', 0, 'z'.charCodeAt(0)],
[123, 'z', 0, 'z'.charCodeAt(0)]
]);
});
it('clone', function(): void {
const line = new TestBufferLine(5, null, true);
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]);
line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]);
const line2 = line.clone();
chai.expect(TestBufferLine.prototype.toArray.apply(line2)).eql(line.toArray());
chai.expect(line2.length).equals(line.length);
chai.expect(line2.isWrapped).equals(line.isWrapped);
});
it('copyFrom', function(): void {
const line = new TestBufferLine(5);
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]);
line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]);
const line2 = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], true);
line2.copyFrom(line);
chai.expect(line2.toArray()).eql(line.toArray());
chai.expect(line2.length).equals(line.length);
chai.expect(line2.isWrapped).equals(line.isWrapped);
});
it('should support combining chars', function(): void {
// CHAR_DATA_CODE_INDEX resembles current behavior in InputHandler.print
// --> set code to the last charCodeAt value of the string
// Note: needs to be fixed once the string pointer is in place
const line = new TestBufferLine(2, [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]);
chai.expect(line.toArray()).eql([[1, 'e\u0301', 0, '\u0301'.charCodeAt(0)], [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]]);
const line2 = new TestBufferLine(5, [1, 'a', 0, '\u0301'.charCodeAt(0)], true);
line2.copyFrom(line);
chai.expect(line2.toArray()).eql(line.toArray());
const line3 = line.clone();
chai.expect(TestBufferLine.prototype.toArray.apply(line3)).eql(line.toArray());
});
describe('resize', function(): void {
it('enlarge(false)', function(): void {
const line = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('enlarge(true)', function(): void {
const line = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(true) - should apply new size', function(): void {
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.toArray()).eql(Array(5).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink to 0 length', function(): void {
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.toArray()).eql(Array(0).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('should remove combining data on replaced cells after shrinking then enlarging', () => {
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.set(2, [ null, '😁', 1, '😁'.charCodeAt(0) ]);
line.set(9, [ null, '😁', 1, '😁'.charCodeAt(0) ]);
chai.expect(line.translateToString()).eql('aa😁aaaaaa😁');
chai.expect(Object.keys(line.combined).length).eql(2);
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.translateToString()).eql('aa😁aa');
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.translateToString()).eql('aa😁aaaaaaa');
chai.expect(Object.keys(line.combined).length).eql(1);
});
});
describe('getTrimLength', function(): void {
it('empty line', function(): void {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
chai.expect(line.getTrimmedLength()).equal(0);
});
it('ASCII', function(): void {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(2, [1, 'a', 1, 'a'.charCodeAt(0)]);
chai.expect(line.getTrimmedLength()).equal(3);
});
it('surrogate', function(): void {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(2, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]);
chai.expect(line.getTrimmedLength()).equal(3);
});
it('combining', function(): void {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(2, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
chai.expect(line.getTrimmedLength()).equal(3);
});
it('fullwidth', function(): void {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(2, [1, '1', 2, '1'.charCodeAt(0)]);
line.set(3, [0, '', 0, undefined]);
chai.expect(line.getTrimmedLength()).equal(4); // also counts null cell after fullwidth
});
});
describe('translateToString with and w\'o trimming', function(): void {
it('empty line', function(): void {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
chai.expect(line.translateToString(false)).equal(' ');
chai.expect(line.translateToString(true)).equal('');
});
it('ASCII', function(): void {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(2, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(4, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(5, [1, 'a', 1, 'a'.charCodeAt(0)]);
chai.expect(line.translateToString(false)).equal('a a aa ');
chai.expect(line.translateToString(true)).equal('a a aa');
chai.expect(line.translateToString(false, 0, 5)).equal('a a a');
chai.expect(line.translateToString(false, 0, 4)).equal('a a ');
chai.expect(line.translateToString(false, 0, 3)).equal('a a');
chai.expect(line.translateToString(true, 0, 5)).equal('a a a');
chai.expect(line.translateToString(true, 0, 4)).equal('a a ');
chai.expect(line.translateToString(true, 0, 3)).equal('a a');
});
it('surrogate', function(): void {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(2, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]);
line.set(4, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]);
line.set(5, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]);
chai.expect(line.translateToString(false)).equal('a 𝄞 𝄞𝄞 ');
chai.expect(line.translateToString(true)).equal('a 𝄞 𝄞𝄞');
chai.expect(line.translateToString(false, 0, 5)).equal('a 𝄞 𝄞');
chai.expect(line.translateToString(false, 0, 4)).equal('a 𝄞 ');
chai.expect(line.translateToString(false, 0, 3)).equal('a 𝄞');
chai.expect(line.translateToString(true, 0, 5)).equal('a 𝄞 𝄞');
chai.expect(line.translateToString(true, 0, 4)).equal('a 𝄞 ');
chai.expect(line.translateToString(true, 0, 3)).equal('a 𝄞');
});
it('combining', function(): void {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(2, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
line.set(4, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
line.set(5, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
chai.expect(line.translateToString(false)).equal('a e\u0301 e\u0301e\u0301 ');
chai.expect(line.translateToString(true)).equal('a e\u0301 e\u0301e\u0301');
chai.expect(line.translateToString(false, 0, 5)).equal('a e\u0301 e\u0301');
chai.expect(line.translateToString(false, 0, 4)).equal('a e\u0301 ');
chai.expect(line.translateToString(false, 0, 3)).equal('a e\u0301');
chai.expect(line.translateToString(true, 0, 5)).equal('a e\u0301 e\u0301');
chai.expect(line.translateToString(true, 0, 4)).equal('a e\u0301 ');
chai.expect(line.translateToString(true, 0, 3)).equal('a e\u0301');
});
it('fullwidth', function(): void {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(2, [1, '1', 2, '1'.charCodeAt(0)]);
line.set(3, [0, '', 0, undefined]);
line.set(5, [1, '1', 2, '1'.charCodeAt(0)]);
line.set(6, [0, '', 0, undefined]);
line.set(7, [1, '1', 2, '1'.charCodeAt(0)]);
line.set(8, [0, '', 0, undefined]);
chai.expect(line.translateToString(false)).equal('a 1 11 ');
chai.expect(line.translateToString(true)).equal('a 1 11');
chai.expect(line.translateToString(false, 0, 7)).equal('a 1 1');
chai.expect(line.translateToString(false, 0, 6)).equal('a 1 1');
chai.expect(line.translateToString(false, 0, 5)).equal('a 1 ');
chai.expect(line.translateToString(false, 0, 4)).equal('a 1');
chai.expect(line.translateToString(false, 0, 3)).equal('a 1');
chai.expect(line.translateToString(false, 0, 2)).equal('a ');
chai.expect(line.translateToString(true, 0, 7)).equal('a 1 1');
chai.expect(line.translateToString(true, 0, 6)).equal('a 1 1');
chai.expect(line.translateToString(true, 0, 5)).equal('a 1 ');
chai.expect(line.translateToString(true, 0, 4)).equal('a 1');
chai.expect(line.translateToString(true, 0, 3)).equal('a 1');
chai.expect(line.translateToString(true, 0, 2)).equal('a ');
});
it('space at end', function(): void {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(2, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(4, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(5, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(6, [1, ' ', 1, ' '.charCodeAt(0)]);
chai.expect(line.translateToString(false)).equal('a a aa ');
chai.expect(line.translateToString(true)).equal('a a aa ');
});
it('should always return some sane value', function(): void {
// sanity check - broken line with invalid out of bound null width cells
// this can atm happen with deleting/inserting chars in inputhandler by "breaking"
// fullwidth pairs --> needs to be fixed after settling BufferLine impl
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE], false);
chai.expect(line.translateToString(false)).equal(' ');
chai.expect(line.translateToString(true)).equal('');
});
it('should work with endCol=0', () => {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE], false);
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
chai.expect(line.translateToString(true, 0, 0)).equal('');
});
});
});
-233
View File
@@ -1,233 +0,0 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { CharData, IBufferLine } from './Types';
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, WHITESPACE_CELL_CHAR } from './Buffer';
/** typed array slots taken by one cell */
const CELL_SIZE = 3;
/** cell member indices */
const enum Cell {
FLAGS = 0,
STRING = 1,
WIDTH = 2
}
/** single vs. combined char distinction */
const IS_COMBINED_BIT_MASK = 0x80000000;
/**
* Typed array based bufferline implementation.
*/
export class BufferLine implements IBufferLine {
protected _data: Uint32Array | null = null;
protected _combined: {[index: number]: string} = {};
public length: number;
constructor(cols: number, fillCharData?: CharData, public isWrapped: boolean = false) {
if (!fillCharData) {
fillCharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
}
if (cols) {
this._data = new Uint32Array(cols * CELL_SIZE);
for (let i = 0; i < cols; ++i) {
this.set(i, fillCharData);
}
}
this.length = cols;
}
public get(index: number): CharData {
const stringData = this._data[index * CELL_SIZE + Cell.STRING];
return [
this._data[index * CELL_SIZE + Cell.FLAGS],
(stringData & IS_COMBINED_BIT_MASK)
? this._combined[index]
: (stringData) ? String.fromCharCode(stringData) : '',
this._data[index * CELL_SIZE + Cell.WIDTH],
(stringData & IS_COMBINED_BIT_MASK)
? this._combined[index].charCodeAt(this._combined[index].length - 1)
: stringData
];
}
public getWidth(index: number): number {
return this._data[index * CELL_SIZE + Cell.WIDTH];
}
public set(index: number, value: CharData): void {
this._data[index * CELL_SIZE + Cell.FLAGS] = value[0];
if (value[1].length > 1) {
this._combined[index] = value[1];
this._data[index * CELL_SIZE + Cell.STRING] = index | IS_COMBINED_BIT_MASK;
} else {
this._data[index * CELL_SIZE + Cell.STRING] = value[1].charCodeAt(0);
}
this._data[index * CELL_SIZE + Cell.WIDTH] = value[2];
}
public insertCells(pos: number, n: number, fillCharData: CharData): void {
pos %= this.length;
if (n < this.length - pos) {
for (let i = this.length - pos - n - 1; i >= 0; --i) {
this.set(pos + n + i, this.get(pos + i));
}
for (let i = 0; i < n; ++i) {
this.set(pos + i, fillCharData);
}
} else {
for (let i = pos; i < this.length; ++i) {
this.set(i, fillCharData);
}
}
}
public deleteCells(pos: number, n: number, fillCharData: CharData): void {
pos %= this.length;
if (n < this.length - pos) {
for (let i = 0; i < this.length - pos - n; ++i) {
this.set(pos + i, this.get(pos + n + i));
}
for (let i = this.length - n; i < this.length; ++i) {
this.set(i, fillCharData);
}
} else {
for (let i = pos; i < this.length; ++i) {
this.set(i, fillCharData);
}
}
}
public replaceCells(start: number, end: number, fillCharData: CharData): void {
while (start < end && start < this.length) {
this.set(start++, fillCharData);
}
}
public resize(cols: number, fillCharData: CharData): void {
if (cols === this.length) {
return;
}
if (cols > this.length) {
const data = new Uint32Array(cols * CELL_SIZE);
if (this.length) {
if (cols * CELL_SIZE < this._data.length) {
data.set(this._data.subarray(0, cols * CELL_SIZE));
} else {
data.set(this._data);
}
}
this._data = data;
for (let i = this.length; i < cols; ++i) {
this.set(i, fillCharData);
}
} else {
if (cols) {
const data = new Uint32Array(cols * CELL_SIZE);
data.set(this._data.subarray(0, cols * CELL_SIZE));
this._data = data;
// Remove any cut off combined data
const keys = Object.keys(this._combined);
for (let i = 0; i < keys.length; i++) {
const key = parseInt(keys[i], 10);
if (key >= cols) {
delete this._combined[key];
}
}
} else {
this._data = null;
this._combined = {};
}
}
this.length = cols;
}
/** fill a line with fillCharData */
public fill(fillCharData: CharData): void {
this._combined = {};
for (let i = 0; i < this.length; ++i) {
this.set(i, fillCharData);
}
}
/** alter to a full copy of line */
public copyFrom(line: BufferLine): void {
if (this.length !== line.length) {
this._data = new Uint32Array(line._data);
} else {
// use high speed copy if lengths are equal
this._data.set(line._data);
}
this.length = line.length;
this._combined = {};
for (const el in line._combined) {
this._combined[el] = line._combined[el];
}
this.isWrapped = line.isWrapped;
}
/** create a new clone */
public clone(): IBufferLine {
const newLine = new BufferLine(0);
// creation of new typed array from another is actually pretty slow :(
// still faster than copying values one by one
newLine._data = new Uint32Array(this._data);
newLine.length = this.length;
for (const el in this._combined) {
newLine._combined[el] = this._combined[el];
}
newLine.isWrapped = this.isWrapped;
return newLine;
}
public getTrimmedLength(): number {
for (let i = this.length - 1; i >= 0; --i) {
if (this._data[i * CELL_SIZE + Cell.STRING] !== 0) { // 0 ==> ''.charCodeAt(0) ==> NaN ==> 0
return i + this._data[i * CELL_SIZE + Cell.WIDTH];
}
}
return 0;
}
public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void {
const srcData = src._data;
if (applyInReverse) {
for (let cell = length - 1; cell >= 0; cell--) {
for (let i = 0; i < CELL_SIZE; i++) {
this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i];
}
}
} else {
for (let cell = 0; cell < length; cell++) {
for (let i = 0; i < CELL_SIZE; i++) {
this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i];
}
}
}
// Move any combined data over as needed
const srcCombinedKeys = Object.keys(src._combined);
for (let i = 0; i < srcCombinedKeys.length; i++) {
const key = parseInt(srcCombinedKeys[i], 10);
if (key >= srcCol) {
this._combined[key - srcCol + destCol] = src._combined[key];
}
}
}
public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string {
if (trimRight) {
endCol = Math.min(endCol, this.getTrimmedLength());
}
let result = '';
while (startCol < endCol) {
const stringData = this._data[startCol * CELL_SIZE + Cell.STRING];
result += (stringData & IS_COMBINED_BIT_MASK) ? this._combined[startCol] : (stringData) ? String.fromCharCode(stringData) : WHITESPACE_CELL_CHAR;
startCol += this._data[startCol * CELL_SIZE + Cell.WIDTH] || 1;
}
return result;
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ import { assert } from 'chai';
import { ITerminal } from './Types';
import { BufferSet } from './BufferSet';
import { Buffer } from './Buffer';
import { MockTerminal } from './ui/TestUtils.test';
import { MockTerminal } from './TestUtils.test';
describe('BufferSet', () => {
let terminal: ITerminal;
+11 -7
View File
@@ -3,25 +3,29 @@
* @license MIT
*/
import { ITerminal, IBufferSet } from './Types';
import { ITerminal, IBufferSet, IBuffer } from './Types';
import { IAttributeData } from './core/Types';
import { Buffer } from './Buffer';
import { EventEmitter } from './common/EventEmitter';
import { EventEmitter2, IEvent } from './common/EventEmitter2';
/**
* The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and
* provides also utilities for working with them.
*/
export class BufferSet extends EventEmitter implements IBufferSet {
export class BufferSet implements IBufferSet {
private _normal: Buffer;
private _alt: Buffer;
private _activeBuffer: Buffer;
private _onBufferActivate = new EventEmitter2<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}>();
public get onBufferActivate(): IEvent<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}> { return this._onBufferActivate.event; }
/**
* Create a new BufferSet for the given terminal.
* @param _terminal - The terminal the BufferSet will belong to
*/
constructor(private _terminal: ITerminal) {
super();
this._normal = new Buffer(this._terminal, true);
this._normal.fillViewportRows();
@@ -68,7 +72,7 @@ export class BufferSet extends EventEmitter implements IBufferSet {
// when activated.
this._alt.clear();
this._activeBuffer = this._normal;
this.emit('activate', {
this._onBufferActivate.fire({
activeBuffer: this._normal,
inactiveBuffer: this._alt
});
@@ -77,7 +81,7 @@ export class BufferSet extends EventEmitter implements IBufferSet {
/**
* Sets the alt Buffer of the BufferSet as its currently active Buffer
*/
public activateAltBuffer(fillAttr?: number): void {
public activateAltBuffer(fillAttr?: IAttributeData): void {
if (this._activeBuffer === this._alt) {
return;
}
@@ -87,7 +91,7 @@ export class BufferSet extends EventEmitter implements IBufferSet {
this._alt.x = this._normal.x;
this._alt.y = this._normal.y;
this._activeBuffer = this._alt;
this.emit('activate', {
this._onBufferActivate.fire({
activeBuffer: this._alt,
inactiveBuffer: this._normal
});
@@ -4,7 +4,7 @@
*/
import jsdom = require('jsdom');
import { ICharMeasure } from '../Types';
import { ICharMeasure } from './Types';
import { assert } from 'chai';
import { CharMeasure } from './CharMeasure';
+7 -5
View File
@@ -3,23 +3,25 @@
* @license MIT
*/
import { ICharMeasure, ITerminalOptions } from '../Types';
import { EventEmitter } from '../common/EventEmitter';
import { ICharMeasure, ITerminalOptions } from './Types';
import { EventEmitter2, IEvent } from './common/EventEmitter2';
/**
* Utility class that measures the size of a character. Measurements are done in
* the DOM rather than with a canvas context because support for extracting the
* height of characters is patchy across browsers.
*/
export class CharMeasure extends EventEmitter implements ICharMeasure {
export class CharMeasure implements ICharMeasure {
private _document: Document;
private _parentElement: HTMLElement;
private _measureElement: HTMLElement;
private _width: number;
private _height: number;
private _onCharSizeChanged = new EventEmitter2<void>();
public get onCharSizeChanged(): IEvent<void> { return this._onCharSizeChanged.event; }
constructor(document: Document, parentElement: HTMLElement) {
super();
this._document = document;
this._parentElement = parentElement;
this._measureElement = this._document.createElement('span');
@@ -50,7 +52,7 @@ export class CharMeasure extends EventEmitter implements ICharMeasure {
if (this._width !== geometry.width || this._height !== adjustedHeight) {
this._width = geometry.width;
this._height = adjustedHeight;
this.emit('charsizechanged');
this._onCharSizeChanged.fire();
}
}
}
+3 -3
View File
@@ -3,11 +3,11 @@
* @license MIT
*/
import { TestTerminal } from './ui/TestUtils.test';
import { TestTerminal } from './TestUtils.test';
import { assert } from 'chai';
import { getStringCellWidth, wcwidth } from './CharWidth';
import { IBuffer } from './Types';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
import { CellData, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './core/buffer/BufferLine';
describe('getStringCellWidth', function(): void {
@@ -22,7 +22,7 @@ describe('getStringCellWidth', function(): void {
for (let i = start; i < end; ++i) {
const line = buffer.lines.get(i);
for (let j = 0; j < line.length; ++j) { // TODO: change to trimBorder with multiline
const ch = line.get(j);
const ch = line.loadCell(j, new CellData()).getAsCharData();
result += ch[CHAR_DATA_WIDTH_INDEX];
// return on sentinel
if (ch[CHAR_DATA_CHAR_INDEX] === sentinel) {
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ITerminal, ISelectionManager } from '../Types';
import { ITerminal, ISelectionManager } from './Types';
interface IWindow extends Window {
clipboardData?: {
+2
View File
@@ -203,6 +203,8 @@ export class CompositionHelper {
this._compositionView.style.top = cursorTop + 'px';
this._compositionView.style.height = cellHeight + 'px';
this._compositionView.style.lineHeight = cellHeight + 'px';
this._compositionView.style.fontFamily = this._terminal.options.fontFamily;
this._compositionView.style.fontSize = this._terminal.options.fontSize + 'px';
// Sync the textarea to the exact position of the composition view so the IME knows where the
// text is.
const compositionViewBounds = this._compositionView.getBoundingClientRect();
+5 -3
View File
@@ -353,7 +353,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
if (this._oscHandlers[ident] === undefined) {
this._oscHandlers[ident] = [];
}
const handlerList = this._oscHandlers[ident];
const handlerList = this._oscHandlers[ident];
handlerList.push(callback);
return {
dispose: () => {
@@ -505,7 +505,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
const handlers = this._csiHandlers[code];
let j = handlers ? handlers.length - 1 : -1;
for (; j >= 0; j--) {
if (handlers[j](params, collect)) {
// undefined or true means success and to stop bubbling
if (handlers[j](params, collect) !== false) {
break;
}
}
@@ -590,7 +591,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
const handlers = this._oscHandlers[identifier];
let j = handlers ? handlers.length - 1 : -1;
for (; j >= 0; j--) {
if (handlers[j](content)) {
// undefined or true means success and to stop bubbling
if (handlers[j](content) !== false) {
break;
}
}
+251
View File
@@ -0,0 +1,251 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import * as puppeteer from 'puppeteer';
import { assert } from 'chai';
import { ITerminalOptions } from './Types';
const APP = 'http://127.0.0.1:3000/test';
let browser: puppeteer.Browser;
let page: puppeteer.Page;
const width = 800;
const height = 600;
describe('InputHandler Integration Tests', function(): void {
this.timeout(20000);
before(async function(): Promise<any> {
browser = await puppeteer.launch({
headless: process.argv.indexOf('--headless') !== -1,
slowMo: 80,
args: [`--window-size=${width},${height}`]
});
page = (await browser.pages())[0];
await page.setViewport({ width, height });
await page.goto(APP);
await openTerminal();
});
after(() => {
browser.close();
});
describe('CSI', () => {
beforeEach(async () => {
await page.evaluate(`window.term.reset()`);
});
it('ICH: Insert Ps (Blank) Character(s) (default = 1) - CSI Ps @', async function(): Promise<any> {
await page.evaluate(`
// Default
window.term.write('foo\\x1b[3D\\x1b[@\\n\\r')
// Explicit
window.term.write('bar\\x1b[3D\\x1b[4@')
`);
assert.deepEqual(await getLinesAsArray(2), [' foo', ' bar']);
});
it('CUU: Cursor Up Ps Times (default = 1) - CSI Ps A', async function(): Promise<any> {
await page.evaluate(`
// Default
window.term.write('\\n\\n\\n\\n\x1b[Aa')
// Explicit
window.term.write('\x1b[2Ab')
`);
assert.deepEqual(await getLinesAsArray(4), ['', ' b', '', 'a']);
});
it('CUD: Cursor Down Ps Times (default = 1) - CSI Ps B', async function(): Promise<any> {
await page.evaluate(`
// Default
window.term.write('\x1b[Ba')
// Explicit
window.term.write('\x1b[2Bb')
`);
assert.deepEqual(await getLinesAsArray(4), ['', 'a', '', ' b']);
});
it('CUF: Cursor Forward Ps Times (default = 1) - CSI Ps C', async function(): Promise<any> {
await page.evaluate(`
// Default
window.term.write('\x1b[Ca')
// Explicit
window.term.write('\x1b[2Cb')
`);
assert.deepEqual(await getLinesAsArray(1), [' a b']);
});
it('CUB: Cursor Backward Ps Times (default = 1) - CSI Ps D', async function(): Promise<any> {
await page.evaluate(`
// Default
window.term.write('foo\x1b[Da')
// Explicit
window.term.write('\x1b[2Db')
`);
assert.deepEqual(await getLinesAsArray(1), ['fba']);
});
it('CNL: Cursor Next Line Ps Times (default = 1) - CSI Ps E', async function(): Promise<any> {
await page.evaluate(`
// Default
window.term.write('\x1b[Ea')
// Explicit
window.term.write('\x1b[2Eb')
`);
assert.deepEqual(await getLinesAsArray(4), ['', 'a', '', 'b']);
});
it('CPL: Cursor Preceding Line Ps Times (default = 1) - CSI Ps F', async function(): Promise<any> {
await page.evaluate(`
// Default
window.term.write('\\n\\n\\n\\n\x1b[Fa')
// Explicit
window.term.write('\x1b[2Fb')
`);
assert.deepEqual(await getLinesAsArray(5), ['', 'b', '', 'a', '']);
});
it('CHA: Cursor Character Absolute [column] (default = [row,1]) - CSI Ps G', async function(): Promise<any> {
await page.evaluate(`
// Default
window.term.write('foo\x1b[Ga')
// Explicit
window.term.write('\x1b[10Gb')
`);
assert.deepEqual(await getLinesAsArray(1), ['aoo b']);
});
it('CUP: Cursor Position [row;column] (default = [1,1]) - CSI Ps ; Ps H', async function(): Promise<any> {
await page.evaluate(`
// Default
window.term.write('foo\x1b[Ha')
// Explicit
window.term.write('\x1b[3;3Hb')
`);
assert.deepEqual(await getLinesAsArray(3), ['aoo', '', ' b']);
});
it('CHT: Cursor Forward Tabulation Ps tab stops (default = 1) - CSI Ps I', async function(): Promise<any> {
await page.evaluate(`
// Default
window.term.write('\x1b[Ia')
// Explicit
window.term.write('\\n\\r\x1b[2Ib')
`);
assert.deepEqual(await getLinesAsArray(2), [' a', ' b']);
});
it('ED: Erase in Display, VT100 - CSI Ps J', async function(): Promise<any> {
const fixture = 'abc\\n\\rdef\\n\\rghi\x1b[2;2H';
await page.evaluate(`
// Default: Erase Below
window.term.resize(5, 5);
window.term.write('${fixture}\x1b[J')
`);
assert.deepEqual(await getLinesAsArray(3), ['abc', 'd', '']);
await page.evaluate(`
// 0: Erase Below
window.term.reset()
window.term.write('${fixture}\x1b[0J')
`);
assert.deepEqual(await getLinesAsArray(3), ['abc', 'd', '']);
await page.evaluate(`
// 1: Erase Above
window.term.reset()
window.term.write('${fixture}\x1b[1J')
`);
assert.deepEqual(await getLinesAsArray(3), ['', ' f', 'ghi']);
await page.evaluate(`
// 2: Erase Saved Lines (scrollback)
window.term.reset()
window.term.write('1\\n2\\n3\\n4\\n5${fixture}\x1b[3J')
`);
assert.equal(await page.evaluate(`window.term.buffer.length`), 5);
assert.deepEqual(await getLinesAsArray(5), [' 4', ' 5', 'abc', 'def', 'ghi']);
});
it('DECSED: Erase in Display, VT220 - CSI ? Ps J', async function(): Promise<any> {
const fixture = 'abc\\n\\rdef\\n\\rghi\x1b[2;2H';
await page.evaluate(`
// Default: Erase Below
window.term.resize(5, 5);
window.term.write('${fixture}\x1b[?J')
`);
assert.deepEqual(await getLinesAsArray(3), ['abc', 'd', '']);
await page.evaluate(`
// 0: Erase Below
window.term.reset()
window.term.write('${fixture}\x1b[?0J')
`);
assert.deepEqual(await getLinesAsArray(3), ['abc', 'd', '']);
await page.evaluate(`
// 1: Erase Above
window.term.reset()
window.term.write('${fixture}\x1b[?1J')
`);
assert.deepEqual(await getLinesAsArray(3), ['', ' f', 'ghi']);
await page.evaluate(`
// 2: Erase Saved Lines (scrollback)
window.term.reset()
window.term.write('1\\n2\\n3\\n4\\n5${fixture}\x1b[?3J')
`);
assert.equal(await page.evaluate(`window.term.buffer.length`), 5);
assert.deepEqual(await getLinesAsArray(5), [' 4', ' 5', 'abc', 'def', 'ghi']);
});
describe('DSR: Device Status Report', () => {
it('Status Report - CSI 5 n', async function(): Promise<any> {
await page.evaluate(`
window.term.onData(e => window.result = e);
window.term.write('\\x1b[5n');
`);
assert.equal(await page.evaluate(`window.result`), '\x1b[0n');
});
it('Report Cursor Position (CPR) - CSI 6 n', async function(): Promise<any> {
await page.evaluate(`window.term.write('\\n\\nfoo')`);
assert.deepEqual(await page.evaluate(`
[window.term.buffer.cursorY, window.term.buffer.cursorX]
`), [2, 3]);
await page.evaluate(`
window.term.onData(e => window.result = e);
window.term.write('\\x1b[6n');
`);
assert.equal(await page.evaluate(`window.result`), '\x1b[3;4R');
});
it('Report Cursor Position (DECXCPR) - CSI ? 6 n', async function(): Promise<any> {
await page.evaluate(`window.term.write('\\n\\nfoo')`);
assert.deepEqual(await page.evaluate(`
[window.term.buffer.cursorY, window.term.buffer.cursorX]
`), [2, 3]);
await page.evaluate(`
window.term.onData(e => window.result = e);
window.term.write('\\x1b[?6n');
`);
assert.equal(await page.evaluate(`window.result`), '\x1b[?3;4R');
});
});
});
});
async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`);
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
if (options.rendererType === 'dom') {
await page.waitForSelector('.xterm-rows');
} else {
await page.waitForSelector('.xterm-text-layer');
}
}
async function getLinesAsArray(count: number, start: number = 0): Promise<string[]> {
let text = '';
for (let i = start; i < start + count; i++) {
text += `window.term.buffer.getLine(${i}).translateToString(true),`;
}
return await page.evaluate(`[${text}]`);
}
+159 -15
View File
@@ -5,32 +5,32 @@
import { assert, expect } from 'chai';
import { InputHandler } from './InputHandler';
import { MockInputHandlingTerminal } from './ui/TestUtils.test';
import { CHAR_DATA_ATTR_INDEX, DEFAULT_ATTR } from './Buffer';
import { MockInputHandlingTerminal, TestTerminal } from './TestUtils.test';
import { Terminal } from './Terminal';
import { IBufferLine } from './Types';
import { IBufferLine } from './core/Types';
import { CellData, Attributes, AttributeData, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine';
describe('InputHandler', () => {
describe('save and restore cursor', () => {
const terminal = new MockInputHandlingTerminal();
terminal.buffer.x = 1;
terminal.buffer.y = 2;
terminal.curAttr = 3;
terminal.curAttrData.fg = 3;
const inputHandler = new InputHandler(terminal);
// Save cursor position
inputHandler.saveCursor([]);
assert.equal(terminal.buffer.x, 1);
assert.equal(terminal.buffer.y, 2);
assert.equal(terminal.curAttr, 3);
assert.equal(terminal.curAttrData.fg, 3);
// Change cursor position
terminal.buffer.x = 10;
terminal.buffer.y = 20;
terminal.curAttr = 30;
terminal.curAttrData.fg = 30;
// Restore cursor position
inputHandler.restoreCursor([]);
assert.equal(terminal.buffer.x, 1);
assert.equal(terminal.buffer.y, 2);
assert.equal(terminal.curAttr, 3);
assert.equal(terminal.curAttrData.fg, 3);
});
describe('setCursorStyle', () => {
it('should call Terminal.setOption with correct params', () => {
@@ -356,45 +356,189 @@ describe('InputHandler', () => {
expect(term.buffer.translateBufferLineToString(0, true)).to.equal('');
expect(term.buffer.translateBufferLineToString(1, true)).to.equal(' TEST');
// Text color of 'TEST' should be red
expect((term.buffer.lines.get(1).get(4)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1);
expect((term.buffer.lines.get(1).loadCell(4, new CellData()).getFgColor())).to.equal(1);
});
it('should handle DECSET/DECRST 1047 (alt screen buffer)', () => {
handler.parse('\x1b[?1047h\r\n\x1b[31mJUNK\x1b[?1047lTEST');
expect(term.buffer.translateBufferLineToString(0, true)).to.equal('');
expect(term.buffer.translateBufferLineToString(1, true)).to.equal(' TEST');
// Text color of 'TEST' should be red
expect((term.buffer.lines.get(1).get(4)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1);
expect((term.buffer.lines.get(1).loadCell(4, new CellData()).getFgColor())).to.equal(1);
});
it('should handle DECSET/DECRST 1048 (alt screen cursor)', () => {
handler.parse('\x1b[?1048h\r\n\x1b[31mJUNK\x1b[?1048lTEST');
expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST');
expect(term.buffer.translateBufferLineToString(1, true)).to.equal('JUNK');
// Text color of 'TEST' should be default
expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR);
expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg);
// Text color of 'JUNK' should be red
expect((term.buffer.lines.get(1).get(0)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1);
expect((term.buffer.lines.get(1).loadCell(0, new CellData()).getFgColor())).to.equal(1);
});
it('should handle DECSET/DECRST 1049 (alt screen buffer+cursor)', () => {
handler.parse('\x1b[?1049h\r\n\x1b[31mJUNK\x1b[?1049lTEST');
expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST');
expect(term.buffer.translateBufferLineToString(1, true)).to.equal('');
// Text color of 'TEST' should be default
expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR);
expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg);
});
it('should handle DECSET/DECRST 1049 - maintains saved cursor for alt buffer', () => {
handler.parse('\x1b[?1049h\r\n\x1b[31m\x1b[s\x1b[?1049lTEST');
expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST');
// Text color of 'TEST' should be default
expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR);
expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg);
handler.parse('\x1b[?1049h\x1b[uTEST');
expect(term.buffer.translateBufferLineToString(1, true)).to.equal('TEST');
// Text color of 'TEST' should be red
expect((term.buffer.lines.get(1).get(0)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1);
expect((term.buffer.lines.get(1).loadCell(0, new CellData()).getFgColor())).to.equal(1);
});
it('should handle DECSET/DECRST 1049 - clears alt buffer with erase attributes', () => {
handler.parse('\x1b[42m\x1b[?1049h');
// Buffer should be filled with green background
expect(term.buffer.lines.get(20).get(10)[CHAR_DATA_ATTR_INDEX] & 0x1ff).to.equal(2);
expect(term.buffer.lines.get(20).loadCell(10, new CellData()).getBgColor()).to.equal(2);
});
});
describe('text attributes', () => {
let term: TestTerminal;
beforeEach(() => {
term = new TestTerminal();
});
it('bold', () => {
term.writeSync('\x1b[1m');
assert.equal(!!term.curAttrData.isBold(), true);
term.writeSync('\x1b[22m');
assert.equal(!!term.curAttrData.isBold(), false);
});
it('dim', () => {
term.writeSync('\x1b[2m');
assert.equal(!!term.curAttrData.isDim(), true);
term.writeSync('\x1b[22m');
assert.equal(!!term.curAttrData.isDim(), false);
});
it('italic', () => {
term.writeSync('\x1b[3m');
assert.equal(!!term.curAttrData.isItalic(), true);
term.writeSync('\x1b[23m');
assert.equal(!!term.curAttrData.isItalic(), false);
});
it('underline', () => {
term.writeSync('\x1b[4m');
assert.equal(!!term.curAttrData.isUnderline(), true);
term.writeSync('\x1b[24m');
assert.equal(!!term.curAttrData.isUnderline(), false);
});
it('blink', () => {
term.writeSync('\x1b[5m');
assert.equal(!!term.curAttrData.isBlink(), true);
term.writeSync('\x1b[25m');
assert.equal(!!term.curAttrData.isBlink(), false);
});
it('inverse', () => {
term.writeSync('\x1b[7m');
assert.equal(!!term.curAttrData.isInverse(), true);
term.writeSync('\x1b[27m');
assert.equal(!!term.curAttrData.isInverse(), false);
});
it('invisible', () => {
term.writeSync('\x1b[8m');
assert.equal(!!term.curAttrData.isInvisible(), true);
term.writeSync('\x1b[28m');
assert.equal(!!term.curAttrData.isInvisible(), false);
});
it('colormode palette 16', () => {
assert.equal(term.curAttrData.getFgColorMode(), 0); // DEFAULT
assert.equal(term.curAttrData.getBgColorMode(), 0); // DEFAULT
// lower 8 colors
for (let i = 0; i < 8; ++i) {
term.writeSync(`\x1b[${i + 30};${i + 40}m`);
assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P16);
assert.equal(term.curAttrData.getFgColor(), i);
assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P16);
assert.equal(term.curAttrData.getBgColor(), i);
}
// reset to DEFAULT
term.writeSync(`\x1b[39;49m`);
assert.equal(term.curAttrData.getFgColorMode(), 0);
assert.equal(term.curAttrData.getBgColorMode(), 0);
});
it('colormode palette 256', () => {
assert.equal(term.curAttrData.getFgColorMode(), 0); // DEFAULT
assert.equal(term.curAttrData.getBgColorMode(), 0); // DEFAULT
// lower 8 colors
for (let i = 0; i < 256; ++i) {
term.writeSync(`\x1b[38;5;${i};48;5;${i}m`);
assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P256);
assert.equal(term.curAttrData.getFgColor(), i);
assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P256);
assert.equal(term.curAttrData.getBgColor(), i);
}
// reset to DEFAULT
term.writeSync(`\x1b[39;49m`);
assert.equal(term.curAttrData.getFgColorMode(), 0);
assert.equal(term.curAttrData.getFgColor(), -1);
assert.equal(term.curAttrData.getBgColorMode(), 0);
assert.equal(term.curAttrData.getBgColor(), -1);
});
it('colormode RGB', () => {
assert.equal(term.curAttrData.getFgColorMode(), 0); // DEFAULT
assert.equal(term.curAttrData.getBgColorMode(), 0); // DEFAULT
term.writeSync(`\x1b[38;2;1;2;3;48;2;4;5;6m`);
assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_RGB);
assert.equal(term.curAttrData.getFgColor(), 1 << 16 | 2 << 8 | 3);
assert.deepEqual(AttributeData.toColorRGB(term.curAttrData.getFgColor()), [1, 2, 3]);
assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_RGB);
assert.deepEqual(AttributeData.toColorRGB(term.curAttrData.getBgColor()), [4, 5, 6]);
// reset to DEFAULT
term.writeSync(`\x1b[39;49m`);
assert.equal(term.curAttrData.getFgColorMode(), 0);
assert.equal(term.curAttrData.getFgColor(), -1);
assert.equal(term.curAttrData.getBgColorMode(), 0);
assert.equal(term.curAttrData.getBgColor(), -1);
});
it('colormode transition RGB to 256', () => {
// enter RGB for FG and BG
term.writeSync(`\x1b[38;2;1;2;3;48;2;4;5;6m`);
// enter 256 for FG and BG
term.writeSync(`\x1b[38;5;255;48;5;255m`);
assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P256);
assert.equal(term.curAttrData.getFgColor(), 255);
assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P256);
assert.equal(term.curAttrData.getBgColor(), 255);
});
it('colormode transition RGB to 16', () => {
// enter RGB for FG and BG
term.writeSync(`\x1b[38;2;1;2;3;48;2;4;5;6m`);
// enter 16 for FG and BG
term.writeSync(`\x1b[37;47m`);
assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P16);
assert.equal(term.curAttrData.getFgColor(), 7);
assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P16);
assert.equal(term.curAttrData.getBgColor(), 7);
});
it('colormode transition 16 to 256', () => {
// enter 16 for FG and BG
term.writeSync(`\x1b[37;47m`);
// enter 256 for FG and BG
term.writeSync(`\x1b[38;5;255;48;5;255m`);
assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P256);
assert.equal(term.curAttrData.getFgColor(), 255);
assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P256);
assert.equal(term.curAttrData.getBgColor(), 255);
});
it('colormode transition 256 to 16', () => {
// enter 256 for FG and BG
term.writeSync(`\x1b[38;5;255;48;5;255m`);
// enter 16 for FG and BG
term.writeSync(`\x1b[37;47m`);
assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P16);
assert.equal(term.curAttrData.getFgColor(), 7);
assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P16);
assert.equal(term.curAttrData.getBgColor(), 7);
});
it('should zero missing RGB values', () => {
term.writeSync(`\x1b[38;2;1;2;3m`);
term.writeSync(`\x1b[38;2;5m`);
assert.deepEqual(AttributeData.toColorRGB(term.curAttrData.getFgColor()), [5, 0, 0]);
});
});
});
+160 -125
View File
@@ -4,18 +4,17 @@
* @license MIT
*/
import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types';
import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IInputHandlingTerminal } from './Types';
import { C0, C1 } from './common/data/EscapeSequences';
import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer';
import { FLAGS } from './renderer/Types';
import { wcwidth } from './CharWidth';
import { EscapeSequenceParser } from './EscapeSequenceParser';
import { ICharset } from './core/Types';
import { IDisposable } from 'xterm';
import { Disposable } from './common/Lifecycle';
import { concat } from './common/TypedArrayUtils';
import { StringToUtf32, stringFromCodePoint, utf32ToString } from './core/input/TextDecoder';
import { StringToUtf32, stringFromCodePoint, utf32ToString, Utf8ToUtf32 } from './core/input/TextDecoder';
import { CellData, Attributes, FgFlags, BgFlags, AttributeData, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine';
import { EventEmitter2, IEvent } from './common/EventEmitter2';
/**
* Map collect to glevel. Used in `selectCharset`.
@@ -105,6 +104,17 @@ class DECRQSS implements IDcsHandler {
export class InputHandler extends Disposable implements IInputHandler {
private _parseBuffer: Uint32Array = new Uint32Array(4096);
private _stringDecoder: StringToUtf32 = new StringToUtf32();
private _utf8Decoder: Utf8ToUtf32 = new Utf8ToUtf32();
private _workCell: CellData = new CellData();
private _onCursorMove = new EventEmitter2<void>();
public get onCursorMove(): IEvent<void> { return this._onCursorMove.event; }
private _onData = new EventEmitter2<string>();
public get onData(): IEvent<string> { return this._onData.event; }
private _onLineFeed = new EventEmitter2<void>();
public get onLineFeed(): IEvent<void> { return this._onLineFeed.event; }
private _onScroll = new EventEmitter2<number>();
public get onScroll(): IEvent<number> { return this._onScroll.event; }
constructor(
protected _terminal: IInputHandlingTerminal,
@@ -301,11 +311,34 @@ export class InputHandler extends Disposable implements IInputHandler {
if (this._parseBuffer.length < data.length) {
this._parseBuffer = new Uint32Array(data.length);
}
for (let i = 0; i < data.length; ++i) {
this._parseBuffer[i] = data.charCodeAt(i);
}
this._parser.parse(this._parseBuffer, this._stringDecoder.decode(data, this._parseBuffer));
buffer = this._terminal.buffer;
if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) {
this._onCursorMove.fire();
}
}
public parseUtf8(data: Uint8Array): void {
// Ensure the terminal is not disposed
if (!this._terminal) {
return;
}
let buffer = this._terminal.buffer;
const cursorStartX = buffer.x;
const cursorStartY = buffer.y;
// TODO: Consolidate debug/logging #1560
if ((<any>this._terminal).debug) {
this._terminal.log('data: ' + data);
}
if (this._parseBuffer.length < data.length) {
this._parseBuffer = new Uint32Array(data.length);
}
this._parser.parse(this._parseBuffer, this._utf8Decoder.decode(data, this._parseBuffer));
buffer = this._terminal.buffer;
if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) {
this._terminal.emit('cursormove');
@@ -314,39 +347,36 @@ export class InputHandler extends Disposable implements IInputHandler {
public print(data: Uint32Array, start: number, end: number): void {
let code: number;
let char: string;
let chWidth: number;
const buffer: IBuffer = this._terminal.buffer;
const charset: ICharset = this._terminal.charset;
const screenReaderMode: boolean = this._terminal.options.screenReaderMode;
const cols: number = this._terminal.cols;
const wraparoundMode: boolean = this._terminal.wraparoundMode;
const insertMode: boolean = this._terminal.insertMode;
const curAttr: number = this._terminal.curAttr;
const buffer = this._terminal.buffer;
const charset = this._terminal.charset;
const screenReaderMode = this._terminal.options.screenReaderMode;
const cols = this._terminal.cols;
const wraparoundMode = this._terminal.wraparoundMode;
const insertMode = this._terminal.insertMode;
const curAttr = this._terminal.curAttrData;
let bufferRow = buffer.lines.get(buffer.y + buffer.ybase);
this._terminal.updateRange(buffer.y);
for (let pos = start; pos < end; ++pos) {
code = data[pos];
char = stringFromCodePoint(code);
// calculate print space
// expensive call, therefore we save width in line buffer
chWidth = wcwidth(code);
// get charset replacement character
// charset are only defined for ASCII, therefore we only
// charset is only defined for ASCII, therefore we only
// search for an replacement char if code < 127
if (code < 127 && charset) {
const ch = charset[char];
const ch = charset[String.fromCharCode(code)];
if (ch) {
code = ch.charCodeAt(0);
char = ch;
}
}
if (screenReaderMode) {
this._terminal.emit('a11y.char', char);
this._terminal.emit('a11y.char', stringFromCodePoint(code));
}
// insert combining char at last cursor position
@@ -355,23 +385,13 @@ export class InputHandler extends Disposable implements IInputHandler {
// since they always follow a cell consuming char
// therefore we can test for buffer.x to avoid overflow left
if (!chWidth && buffer.x) {
const chMinusOne = bufferRow.get(buffer.x - 1);
if (chMinusOne) {
if (!chMinusOne[CHAR_DATA_WIDTH_INDEX]) {
// found empty cell after fullwidth, need to go 2 cells back
// it is save to step 2 cells back here
// since an empty cell is only set by fullwidth chars
const chMinusTwo = bufferRow.get(buffer.x - 2);
if (chMinusTwo) {
chMinusTwo[CHAR_DATA_CHAR_INDEX] += char;
chMinusTwo[CHAR_DATA_CODE_INDEX] = code;
bufferRow.set(buffer.x - 2, chMinusTwo); // must be set explicitly now
}
} else {
chMinusOne[CHAR_DATA_CHAR_INDEX] += char;
chMinusOne[CHAR_DATA_CODE_INDEX] = code;
bufferRow.set(buffer.x - 1, chMinusOne); // must be set explicitly now
}
if (!bufferRow.getWidth(buffer.x - 1)) {
// found empty cell after fullwidth, need to go 2 cells back
// it is save to step 2 cells back here
// since an empty cell is only set by fullwidth chars
bufferRow.addCodepointToCell(buffer.x - 2, code);
} else {
bufferRow.addCodepointToCell(buffer.x - 1, code);
}
continue;
}
@@ -410,35 +430,42 @@ export class InputHandler extends Disposable implements IInputHandler {
// insert mode: move characters to right
if (insertMode) {
// right shift cells according to the width
bufferRow.insertCells(buffer.x, chWidth, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
bufferRow.insertCells(buffer.x, chWidth, buffer.getNullCell(curAttr));
// test last cell - since the last cell has only room for
// a halfwidth char any fullwidth shifted there is lost
// and will be set to eraseChar
const lastCell = bufferRow.get(cols - 1);
if (lastCell[CHAR_DATA_WIDTH_INDEX] === 2) {
bufferRow.set(cols - 1, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
// and will be set to empty cell
if (bufferRow.getWidth(cols - 1) === 2) {
bufferRow.setCellFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr.fg, curAttr.bg);
}
}
// write current char to buffer and advance cursor
bufferRow.set(buffer.x++, [curAttr, char, chWidth, code]);
bufferRow.setCellFromCodePoint(buffer.x++, code, chWidth, curAttr.fg, curAttr.bg);
// fullwidth char - also set next cell to placeholder stub and advance cursor
// for graphemes bigger than fullwidth we can simply loop to zero
// we already made sure above, that buffer.x + chWidth will not overflow right
if (chWidth > 0) {
while (--chWidth) {
bufferRow.set(buffer.x++, [curAttr, '', 0, undefined]);
// other than a regular empty cell a cell following a wide char has no width
bufferRow.setCellFromCodePoint(buffer.x++, 0, 0, curAttr.fg, curAttr.bg);
}
}
}
this._terminal.updateRange(buffer.y);
}
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
/**
* Forward addCsiHandler from parser.
*/
public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
return this._parser.addCsiHandler(flag, callback);
}
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
/**
* Forward addOscHandler from parser.
*/
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
return this._parser.addOscHandler(ident, callback);
}
@@ -470,12 +497,8 @@ export class InputHandler extends Disposable implements IInputHandler {
if (buffer.x >= this._terminal.cols) {
buffer.x--;
}
/**
* This event is emitted whenever the terminal outputs a LF or NL.
*
* @event linefeed
*/
this._terminal.emit('linefeed');
this._onLineFeed.fire();
}
/**
@@ -534,7 +557,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).insertCells(
this._terminal.buffer.x,
params[0] || 1,
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
this._terminal.buffer.getNullCell(this._terminal.eraseAttrData())
);
this._terminal.updateRange(this._terminal.buffer.y);
}
@@ -708,7 +731,7 @@ export class InputHandler extends Disposable implements IInputHandler {
line.replaceCells(
start,
end,
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
this._terminal.buffer.getNullCell(this._terminal.eraseAttrData())
);
if (clearWrap) {
line.isWrapped = false;
@@ -778,7 +801,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._terminal.buffer.ybase = Math.max(this._terminal.buffer.ybase - scrollBackSize, 0);
this._terminal.buffer.ydisp = Math.max(this._terminal.buffer.ydisp - scrollBackSize, 0);
// Force a scroll event to refresh viewport
this._terminal.emit('scroll', 0);
this._onScroll.fire(0);
}
break;
}
@@ -831,7 +854,7 @@ export class InputHandler extends Disposable implements IInputHandler {
// test: echo -e '\e[44m\e[1L\e[0m'
// blankLine(true) - xterm/linux behavior
buffer.lines.splice(scrollBottomAbsolute - 1, 1);
buffer.lines.splice(row, 0, buffer.getBlankLine(this._terminal.eraseAttr()));
buffer.lines.splice(row, 0, buffer.getBlankLine(this._terminal.eraseAttrData()));
}
// this.maxRange();
@@ -861,7 +884,7 @@ export class InputHandler extends Disposable implements IInputHandler {
// test: echo -e '\e[44m\e[1M\e[0m'
// blankLine(true) - xterm/linux behavior
buffer.lines.splice(row, 1);
buffer.lines.splice(j, 0, buffer.getBlankLine(this._terminal.eraseAttr()));
buffer.lines.splice(j, 0, buffer.getBlankLine(this._terminal.eraseAttrData()));
}
// this.maxRange();
@@ -877,7 +900,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).deleteCells(
this._terminal.buffer.x,
params[0] || 1,
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
this._terminal.buffer.getNullCell(this._terminal.eraseAttrData())
);
this._terminal.updateRange(this._terminal.buffer.y);
}
@@ -893,7 +916,7 @@ export class InputHandler extends Disposable implements IInputHandler {
while (param--) {
buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1);
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR));
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
}
// this.maxRange();
this._terminal.updateRange(buffer.scrollTop);
@@ -912,7 +935,7 @@ export class InputHandler extends Disposable implements IInputHandler {
while (param--) {
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1);
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR));
buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
}
// this.maxRange();
this._terminal.updateRange(buffer.scrollTop);
@@ -928,7 +951,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).replaceCells(
this._terminal.buffer.x,
this._terminal.buffer.x + (params[0] || 1),
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
this._terminal.buffer.getNullCell(this._terminal.eraseAttrData())
);
}
@@ -984,9 +1007,10 @@ export class InputHandler extends Disposable implements IInputHandler {
// make buffer local for faster access
const buffer = this._terminal.buffer;
const line = buffer.lines.get(buffer.ybase + buffer.y);
line.loadCell(buffer.x - 1, this._workCell);
line.replaceCells(buffer.x,
buffer.x + (params[0] || 1),
line.get(buffer.x - 1) || [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
(this._workCell.content !== undefined) ? this._workCell : buffer.getNullCell(DEFAULT_ATTR_DATA)
);
// FIXME: no updateRange here?
}
@@ -1284,7 +1308,9 @@ export class InputHandler extends Disposable implements IInputHandler {
if (this._terminal.element) {
this._terminal.element.classList.add('enable-mouse-events');
}
this._terminal.selectionManager.disable();
if (this._terminal.selectionManager) {
this._terminal.selectionManager.disable();
}
this._terminal.log('Binding to mouse events.');
break;
case 1004: // send focusin/focusout events
@@ -1322,7 +1348,7 @@ export class InputHandler extends Disposable implements IInputHandler {
// FALL-THROUGH
case 47: // alt screen buffer
case 1047: // alt screen buffer
this._terminal.buffers.activateAltBuffer(this._terminal.eraseAttr());
this._terminal.buffers.activateAltBuffer(this._terminal.eraseAttrData());
this._terminal.refresh(0, this._terminal.rows - 1);
if (this._terminal.viewport) {
this._terminal.viewport.syncScrollArea();
@@ -1474,7 +1500,9 @@ export class InputHandler extends Disposable implements IInputHandler {
if (this._terminal.element) {
this._terminal.element.classList.remove('enable-mouse-events');
}
this._terminal.selectionManager.enable();
if (this._terminal.selectionManager) {
this._terminal.selectionManager.enable();
}
break;
case 1004: // send focusin/focusout events
this._terminal.sendFocus = false;
@@ -1584,127 +1612,124 @@ export class InputHandler extends Disposable implements IInputHandler {
public charAttributes(params: number[]): void {
// Optimize a single SGR0.
if (params.length === 1 && params[0] === 0) {
this._terminal.curAttr = DEFAULT_ATTR;
this._terminal.curAttrData.fg = DEFAULT_ATTR_DATA.fg;
this._terminal.curAttrData.bg = DEFAULT_ATTR_DATA.bg;
return;
}
const l = params.length;
let flags = this._terminal.curAttr >> 18;
let fg = (this._terminal.curAttr >> 9) & 0x1ff;
let bg = this._terminal.curAttr & 0x1ff;
let p;
const attr = this._terminal.curAttrData;
for (let i = 0; i < l; i++) {
p = params[i];
if (p >= 30 && p <= 37) {
// fg color 8
fg = p - 30;
attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK);
attr.fg |= Attributes.CM_P16 | (p - 30);
} else if (p >= 40 && p <= 47) {
// bg color 8
bg = p - 40;
attr.bg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK);
attr.bg |= Attributes.CM_P16 | (p - 40);
} else if (p >= 90 && p <= 97) {
// fg color 16
p += 8;
fg = p - 90;
attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK);
attr.fg |= Attributes.CM_P16 | (p - 90) | 8;
} else if (p >= 100 && p <= 107) {
// bg color 16
p += 8;
bg = p - 100;
attr.bg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK);
attr.bg |= Attributes.CM_P16 | (p - 100) | 8;
} else if (p === 0) {
// default
flags = DEFAULT_ATTR >> 18;
fg = (DEFAULT_ATTR >> 9) & 0x1ff;
bg = DEFAULT_ATTR & 0x1ff;
// flags = 0;
// fg = 0x1ff;
// bg = 0x1ff;
attr.fg = DEFAULT_ATTR_DATA.fg;
attr.bg = DEFAULT_ATTR_DATA.bg;
} else if (p === 1) {
// bold text
flags |= FLAGS.BOLD;
attr.fg |= FgFlags.BOLD;
} else if (p === 3) {
// italic text
flags |= FLAGS.ITALIC;
attr.bg |= BgFlags.ITALIC;
} else if (p === 4) {
// underlined text
flags |= FLAGS.UNDERLINE;
attr.fg |= FgFlags.UNDERLINE;
} else if (p === 5) {
// blink
flags |= FLAGS.BLINK;
attr.fg |= FgFlags.BLINK;
} else if (p === 7) {
// inverse and positive
// test with: echo -e '\e[31m\e[42mhello\e[7mworld\e[27mhi\e[m'
flags |= FLAGS.INVERSE;
attr.fg |= FgFlags.INVERSE;
} else if (p === 8) {
// invisible
flags |= FLAGS.INVISIBLE;
attr.fg |= FgFlags.INVISIBLE;
} else if (p === 2) {
// dimmed text
flags |= FLAGS.DIM;
attr.bg |= BgFlags.DIM;
} else if (p === 22) {
// not bold nor faint
flags &= ~FLAGS.BOLD;
flags &= ~FLAGS.DIM;
attr.fg &= ~FgFlags.BOLD;
attr.bg &= ~BgFlags.DIM;
} else if (p === 23) {
// not italic
flags &= ~FLAGS.ITALIC;
attr.bg &= ~BgFlags.ITALIC;
} else if (p === 24) {
// not underlined
flags &= ~FLAGS.UNDERLINE;
attr.fg &= ~FgFlags.UNDERLINE;
} else if (p === 25) {
// not blink
flags &= ~FLAGS.BLINK;
attr.fg &= ~FgFlags.BLINK;
} else if (p === 27) {
// not inverse
flags &= ~FLAGS.INVERSE;
attr.fg &= ~FgFlags.INVERSE;
} else if (p === 28) {
// not invisible
flags &= ~FLAGS.INVISIBLE;
attr.fg &= ~FgFlags.INVISIBLE;
} else if (p === 39) {
// reset fg
fg = (DEFAULT_ATTR >> 9) & 0x1ff;
attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
attr.fg |= DEFAULT_ATTR_DATA.fg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK);
} else if (p === 49) {
// reset bg
bg = DEFAULT_ATTR & 0x1ff;
attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
attr.bg |= DEFAULT_ATTR_DATA.bg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK);
} else if (p === 38) {
// fg color 256
// fg color 256 and RGB
if (params[i + 1] === 2) {
i += 2;
fg = this._terminal.matchColor(
params[i] & 0xff,
params[i + 1] & 0xff,
params[i + 2] & 0xff);
if (fg === -1) fg = 0x1ff;
attr.fg |= Attributes.CM_RGB;
attr.fg &= ~Attributes.RGB_MASK;
attr.fg |= AttributeData.fromColorRGB([params[i], params[i + 1], params[i + 2]]);
i += 2;
} else if (params[i + 1] === 5) {
i += 2;
p = params[i] & 0xff;
fg = p;
attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK);
attr.fg |= Attributes.CM_P256 | p;
}
} else if (p === 48) {
// bg color 256
// bg color 256 and RGB
if (params[i + 1] === 2) {
i += 2;
bg = this._terminal.matchColor(
params[i] & 0xff,
params[i + 1] & 0xff,
params[i + 2] & 0xff);
if (bg === -1) bg = 0x1ff;
attr.bg |= Attributes.CM_RGB;
attr.bg &= ~Attributes.RGB_MASK;
attr.bg |= AttributeData.fromColorRGB([params[i], params[i + 1], params[i + 2]]);
i += 2;
} else if (params[i + 1] === 5) {
i += 2;
p = params[i] & 0xff;
bg = p;
attr.bg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK);
attr.bg |= Attributes.CM_P256 | p;
}
} else if (p === 100) {
// reset fg/bg
fg = (DEFAULT_ATTR >> 9) & 0x1ff;
bg = DEFAULT_ATTR & 0x1ff;
attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
attr.fg |= DEFAULT_ATTR_DATA.fg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK);
attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
attr.bg |= DEFAULT_ATTR_DATA.bg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK);
} else {
this._terminal.error('Unknown SGR attribute: %d.', p);
}
}
this._terminal.curAttr = (flags << 18) | (fg << 9) | bg;
}
/**
@@ -1735,13 +1760,13 @@ export class InputHandler extends Disposable implements IInputHandler {
switch (params[0]) {
case 5:
// status report
this._terminal.emit('data', `${C0.ESC}[0n`);
this._onData.fire(`${C0.ESC}[0n`);
break;
case 6:
// cursor position
const y = this._terminal.buffer.y + 1;
const x = this._terminal.buffer.x + 1;
this._terminal.emit('data', `${C0.ESC}[${y};${x}R`);
this._onData.fire(`${C0.ESC}[${y};${x}R`);
break;
}
} else if (collect === '?') {
@@ -1752,7 +1777,7 @@ export class InputHandler extends Disposable implements IInputHandler {
// cursor position
const y = this._terminal.buffer.y + 1;
const x = this._terminal.buffer.x + 1;
this._terminal.emit('data', `${C0.ESC}[?${y};${x}R`);
this._onData.fire(`${C0.ESC}[?${y};${x}R`);
break;
case 15:
// no printer
@@ -1791,7 +1816,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._terminal.applicationCursor = false;
this._terminal.buffer.scrollTop = 0;
this._terminal.buffer.scrollBottom = this._terminal.rows - 1;
this._terminal.curAttr = DEFAULT_ATTR;
this._terminal.curAttrData = DEFAULT_ATTR_DATA.clone();
this._terminal.buffer.x = this._terminal.buffer.y = 0; // ?
this._terminal.charset = null;
this._terminal.glevel = 0; // ??
@@ -1838,7 +1863,9 @@ export class InputHandler extends Disposable implements IInputHandler {
* CSI ? Pm r
*/
public setScrollRegion(params: number[], collect?: string): void {
if (collect) return;
if (collect) {
return;
}
this._terminal.buffer.scrollTop = (params[0] || 1) - 1;
this._terminal.buffer.scrollBottom = (params[1] && params[1] <= this._terminal.rows ? params[1] : this._terminal.rows) - 1;
this._terminal.buffer.x = 0;
@@ -1854,7 +1881,8 @@ export class InputHandler extends Disposable implements IInputHandler {
public saveCursor(params: number[]): void {
this._terminal.buffer.savedX = this._terminal.buffer.x;
this._terminal.buffer.savedY = this._terminal.buffer.y;
this._terminal.buffer.savedCurAttr = this._terminal.curAttr;
this._terminal.buffer.savedCurAttrData.fg = this._terminal.curAttrData.fg;
this._terminal.buffer.savedCurAttrData.bg = this._terminal.curAttrData.bg;
}
@@ -1866,7 +1894,8 @@ export class InputHandler extends Disposable implements IInputHandler {
public restoreCursor(params: number[]): void {
this._terminal.buffer.x = this._terminal.buffer.savedX || 0;
this._terminal.buffer.y = this._terminal.buffer.savedY || 0;
this._terminal.curAttr = this._terminal.buffer.savedCurAttr || DEFAULT_ATTR;
this._terminal.curAttrData.fg = this._terminal.buffer.savedCurAttrData.fg;
this._terminal.curAttrData.bg = this._terminal.buffer.savedCurAttrData.bg;
}
@@ -1944,9 +1973,15 @@ export class InputHandler extends Disposable implements IInputHandler {
* Designate G3 Character Set (VT300). C = A -> ISO Latin-1 Supplemental. - Supported?
*/
public selectCharset(collectAndFlag: string): void {
if (collectAndFlag.length !== 2) return this.selectDefaultCharset();
if (collectAndFlag[0] === '/') return; // TODO: Is this supported?
if (collectAndFlag.length !== 2) {
this.selectDefaultCharset();
return;
}
if (collectAndFlag[0] === '/') {
return; // TODO: Is this supported?
}
this._terminal.setgCharset(GLEVEL[collectAndFlag[0]], CHARSETS[collectAndFlag[1]] || DEFAULT_CHARSET);
return;
}
/**
+5 -5
View File
@@ -4,12 +4,12 @@
*/
import { assert } from 'chai';
import { IMouseZoneManager, IMouseZone } from './ui/Types';
import { ILinkMatcher, ITerminal, IBufferLine } from './Types';
import { IMouseZoneManager, IMouseZone, ILinkMatcher, ITerminal } from './Types';
import { IBufferLine } from './core/Types';
import { Linkifier } from './Linkifier';
import { MockBuffer, MockTerminal, TestTerminal } from './ui/TestUtils.test';
import { MockBuffer, MockTerminal, TestTerminal } from './TestUtils.test';
import { CircularList } from './common/CircularList';
import { BufferLine } from './BufferLine';
import { BufferLine, CellData } from './core/buffer/BufferLine';
class TestLinkifier extends Linkifier {
constructor(terminal: ITerminal) {
@@ -53,7 +53,7 @@ describe('Linkifier', () => {
function stringToRow(text: string): IBufferLine {
const result = new BufferLine(text.length);
for (let i = 0; i < text.length; i++) {
result.set(i, [0, text.charAt(i), 1, text.charCodeAt(i)]);
result.setCell(i, CellData.fromCharData([0, text.charAt(i), 1, text.charCodeAt(i)]));
}
return result;
}
+18 -15
View File
@@ -3,17 +3,15 @@
* @license MIT
*/
import { IMouseZoneManager } from './ui/Types';
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult } from './Types';
import { MouseZone } from './ui/MouseZoneManager';
import { EventEmitter } from './common/EventEmitter';
import { CHAR_DATA_ATTR_INDEX } from './Buffer';
import { ILinkifierEvent, ILinkMatcher, LinkMatcherHandler, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult, IMouseZoneManager } from './Types';
import { MouseZone } from './MouseZoneManager';
import { getStringCellWidth } from './CharWidth';
import { EventEmitter2, IEvent } from './common/EventEmitter2';
/**
* The Linkifier applies links to rows shortly after they have been refreshed.
*/
export class Linkifier extends EventEmitter implements ILinkifier {
export class Linkifier implements ILinkifier {
/**
* The time to wait after a row is changed before it is linkified. This prevents
* the costly operation of searching every row multiple times, potentially a
@@ -35,10 +33,16 @@ export class Linkifier extends EventEmitter implements ILinkifier {
private _nextLinkMatcherId = 0;
private _rowsToLinkify: { start: number, end: number };
private _onLinkHover = new EventEmitter2<ILinkifierEvent>();
public get onLinkHover(): IEvent<ILinkifierEvent> { return this._onLinkHover.event; }
private _onLinkLeave = new EventEmitter2<ILinkifierEvent>();
public get onLinkLeave(): IEvent<ILinkifierEvent> { return this._onLinkLeave.event; }
private _onLinkTooltip = new EventEmitter2<ILinkifierEvent>();
public get onLinkTooltip(): IEvent<ILinkifierEvent> { return this._onLinkTooltip.event; }
constructor(
protected _terminal: ITerminal
) {
super();
this._rowsToLinkify = {
start: null,
end: null
@@ -232,10 +236,9 @@ export class Linkifier extends EventEmitter implements ILinkifier {
}
const line = this._terminal.buffer.lines.get(bufferIndex[0]);
const char = line.get(bufferIndex[1]);
const attr = line.getFg(bufferIndex[1]);
let fg: number | undefined;
if (char) {
const attr: number = char[CHAR_DATA_ATTR_INDEX];
if (attr) {
fg = (attr >> 9) & 0x1ff;
}
@@ -285,18 +288,18 @@ export class Linkifier extends EventEmitter implements ILinkifier {
}
window.open(uri, '_blank');
},
e => {
this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
() => {
this._onLinkHover.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
this._terminal.element.classList.add('xterm-cursor-pointer');
},
e => {
this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
this._onLinkTooltip.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
if (matcher.hoverTooltipCallback) {
matcher.hoverTooltipCallback(e, uri);
}
},
() => {
this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
this._onLinkLeave.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
this._terminal.element.classList.remove('xterm-cursor-pointer');
if (matcher.hoverLeaveCallback) {
matcher.hoverLeaveCallback();
@@ -311,7 +314,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
));
}
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number): ILinkHoverEvent {
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number): ILinkifierEvent {
return { x1, y1, x2, y2, cols: this._terminal.cols, fg };
}
}
@@ -31,7 +31,7 @@ describe('MouseHelper.getCoords', () => {
actualCellWidth: CHAR_WIDTH,
actualCellHeight: CHAR_HEIGHT
};
mouseHelper = new MouseHelper(renderer);
mouseHelper = new MouseHelper(renderer as any);
});
describe('when charMeasure is not initialized', () => {
+7 -8
View File
@@ -3,14 +3,13 @@
* @license MIT
*/
import { ICharMeasure, IMouseHelper } from '../Types';
import { IRenderer } from '../renderer/Types';
import { ICharMeasure, IMouseHelper } from './Types';
import { RenderCoordinator } from './renderer/RenderCoordinator';
export class MouseHelper implements IMouseHelper {
constructor(private _renderer: IRenderer) {}
public setRenderer(renderer: IRenderer): void {
this._renderer = renderer;
constructor(
private _renderCoordinator: RenderCoordinator
) {
}
public static getCoordsRelativeToElement(event: {clientX: number, clientY: number}, element: HTMLElement): [number, number] {
@@ -42,8 +41,8 @@ export class MouseHelper implements IMouseHelper {
return null;
}
coords[0] = Math.ceil((coords[0] + (isSelection ? this._renderer.dimensions.actualCellWidth / 2 : 0)) / this._renderer.dimensions.actualCellWidth);
coords[1] = Math.ceil(coords[1] / this._renderer.dimensions.actualCellHeight);
coords[0] = Math.ceil((coords[0] + (isSelection ? this._renderCoordinator.dimensions.actualCellWidth / 2 : 0)) / this._renderCoordinator.dimensions.actualCellWidth);
coords[1] = Math.ceil(coords[1] / this._renderCoordinator.dimensions.actualCellHeight);
// Ensure coordinates are within the terminal viewport. Note that selections
// need an addition point of precision to cover the end point (as characters
@@ -3,10 +3,9 @@
* @license MIT
*/
import { ITerminal } from '../Types';
import { IMouseZoneManager, IMouseZone } from './Types';
import { Disposable } from '../common/Lifecycle';
import { addDisposableDomListener } from './Lifecycle';
import { ITerminal, IMouseZoneManager, IMouseZone } from './Types';
import { Disposable } from './common/Lifecycle';
import { addDisposableDomListener } from './ui/Lifecycle';
const HOVER_DURATION = 500;
@@ -23,11 +22,13 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
private _areZonesActive: boolean = false;
private _mouseMoveListener: (e: MouseEvent) => any;
private _mouseLeaveListener: (e: MouseEvent) => any;
private _clickListener: (e: MouseEvent) => any;
private _tooltipTimeout: number = null;
private _currentZone: IMouseZone = null;
private _lastHoverCoords: [number, number] = [null, null];
private _initialSelectionLength: number;
constructor(
private _terminal: ITerminal
@@ -38,6 +39,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
// These events are expensive, only listen to it when mouse zones are active
this._mouseMoveListener = e => this._onMouseMove(e);
this._mouseLeaveListener = e => this._onMouseLeave(e);
this._clickListener = e => this._onClick(e);
}
@@ -89,6 +91,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
if (!this._areZonesActive) {
this._areZonesActive = true;
this._terminal.element.addEventListener('mousemove', this._mouseMoveListener);
this._terminal.element.addEventListener('mouseleave', this._mouseLeaveListener);
this._terminal.element.addEventListener('click', this._clickListener);
}
}
@@ -97,6 +100,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
if (this._areZonesActive) {
this._areZonesActive = false;
this._terminal.element.removeEventListener('mousemove', this._mouseMoveListener);
this._terminal.element.removeEventListener('mouseleave', this._mouseLeaveListener);
this._terminal.element.removeEventListener('click', this._clickListener);
}
}
@@ -153,6 +157,10 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
}
private _onMouseDown(e: MouseEvent): void {
// Store current terminal selection length, to check if we're performing
// a selection operation
this._initialSelectionLength = this._terminal.getSelection().length;
// Ignore the event if there are no zones active
if (!this._areZonesActive) {
return;
@@ -169,10 +177,25 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
}
}
private _onMouseLeave(e: MouseEvent): void {
// Fire the hover end callback and cancel any existing timer if the mouse
// leaves the terminal element
if (this._currentZone) {
this._currentZone.leaveCallback();
this._currentZone = null;
if (this._tooltipTimeout) {
clearTimeout(this._tooltipTimeout);
}
}
}
private _onClick(e: MouseEvent): void {
// Find the active zone and click it if found
// Find the active zone and click it if found and no selection was
// being performed
const zone = this._findZoneEventAt(e);
if (zone) {
const currentSelectionLength = this._terminal.getSelection().length;
if (zone && currentSelectionLength === this._initialSelectionLength) {
zone.clickCallback(e);
e.preventDefault();
e.stopImmediatePropagation();
+8 -7
View File
@@ -4,13 +4,14 @@
*/
import { assert } from 'chai';
import { CharMeasure } from './ui/CharMeasure';
import { CharMeasure } from './CharMeasure';
import { SelectionManager, SelectionMode } from './SelectionManager';
import { SelectionModel } from './SelectionModel';
import { BufferSet } from './BufferSet';
import { ITerminal, IBuffer, IBufferLine } from './Types';
import { MockTerminal } from './ui/TestUtils.test';
import { BufferLine } from './BufferLine';
import { ITerminal, IBuffer } from './Types';
import { IBufferLine } from './core/Types';
import { MockTerminal } from './TestUtils.test';
import { BufferLine, CellData } from './core/buffer/BufferLine';
class TestMockTerminal extends MockTerminal {
emit(event: string, data: any): void {}
@@ -57,14 +58,14 @@ describe('SelectionManager', () => {
function stringToRow(text: string): IBufferLine {
const result = new BufferLine(text.length);
for (let i = 0; i < text.length; i++) {
result.set(i, [0, text.charAt(i), 1, text.charCodeAt(i)]);
result.setCell(i, CellData.fromCharData([0, text.charAt(i), 1, text.charCodeAt(i)]));
}
return result;
}
function stringArrayToRow(chars: string[]): IBufferLine {
const line = new BufferLine(chars.length);
chars.map((c, idx) => line.set(idx, [0, c, 1, c.charCodeAt(0)]));
chars.map((c, idx) => line.setCell(idx, CellData.fromCharData([0, c, 1, c.charCodeAt(0)])));
return line;
}
@@ -119,7 +120,7 @@ describe('SelectionManager', () => {
[null, 'o', 1, 'o'.charCodeAt(0)]
];
const line = new BufferLine(data.length);
for (let i = 0; i < data.length; ++i) line.set(i, data[i]);
for (let i = 0; i < data.length; ++i) line.setCell(i, CellData.fromCharData(data[i]));
buffer.lines.set(0, line);
// Ensure wide characters take up 2 columns
selectionManager.selectWordAt([0, 0]);
+68 -58
View File
@@ -3,15 +3,16 @@
* @license MIT
*/
import { ITerminal, ISelectionManager, IBuffer, CharData, IBufferLine } from './Types';
import { XtermListener } from './common/Types';
import { MouseHelper } from './ui/MouseHelper';
import * as Browser from './core/Platform';
import { CharMeasure } from './ui/CharMeasure';
import { EventEmitter } from './common/EventEmitter';
import { ITerminal, ISelectionManager, IBuffer, ISelectionRedrawRequestEvent } from './Types';
import { IBufferLine } from './core/Types';
import { MouseHelper } from './MouseHelper';
import * as Browser from './common/Platform';
import { CharMeasure } from './CharMeasure';
import { SelectionModel } from './SelectionModel';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer';
import { AltClickHandler } from './handlers/AltClickHandler';
import { CellData } from './core/buffer/BufferLine';
import { IDisposable } from 'xterm';
import { EventEmitter2, IEvent } from './common/EventEmitter2';
/**
* The number of pixels the mouse needs to be above or below the viewport in
@@ -67,10 +68,10 @@ export const enum SelectionMode {
* SelectionModel, SelectionManager handles with all logic associated with
* dealing with the selection, including handling mouse interaction, wide
* characters and fetching the actual text within the selection. Rendering is
* not handled by the SelectionManager but a 'refresh' event is fired when the
* selection is ready to be redrawn.
* not handled by the SelectionManager but the onRedrawRequest event is fired
* when the selection is ready to be redrawn (on an animation frame).
*/
export class SelectionManager extends EventEmitter implements ISelectionManager {
export class SelectionManager implements ISelectionManager {
protected _model: SelectionModel;
/**
@@ -102,15 +103,22 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
private _mouseMoveListener: EventListener;
private _mouseUpListener: EventListener;
private _trimListener: XtermListener;
private _trimListener: IDisposable;
private _workCell: CellData = new CellData();
private _mouseDownTimeStamp: number;
private _onLinuxMouseSelection = new EventEmitter2<string>();
public get onLinuxMouseSelection(): IEvent<string> { return this._onLinuxMouseSelection.event; }
private _onRedrawRequest = new EventEmitter2<ISelectionRedrawRequestEvent>();
public get onRedrawRequest(): IEvent<ISelectionRedrawRequestEvent> { return this._onRedrawRequest.event; }
private _onSelectionChange = new EventEmitter2<void>();
public get onSelectionChange(): IEvent<void> { return this._onSelectionChange.event; }
constructor(
private _terminal: ITerminal,
private _charMeasure: CharMeasure
) {
super();
this._initListeners();
this.enable();
@@ -119,7 +127,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
}
public dispose(): void {
super.dispose();
this._removeMouseDownListeners();
}
@@ -133,14 +140,13 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
private _initListeners(): void {
this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event);
this._mouseUpListener = event => this._onMouseUp(<MouseEvent>event);
this._trimListener = (amount: number) => this._onTrim(amount);
this.initBuffersListeners();
}
public initBuffersListeners(): void {
this._terminal.buffer.lines.on('trim', this._trimListener);
this._terminal.buffers.on('activate', e => this._onBufferActivate(e));
this._trimListener = this._terminal.buffer.lines.onTrim(amount => this._onTrim(amount));
this._terminal.buffers.onBufferActivate(e => this._onBufferActivate(e));
}
/**
@@ -240,14 +246,15 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._model.clearSelection();
this._removeMouseDownListeners();
this.refresh();
this._onSelectionChange.fire();
}
/**
* Queues a refresh, redrawing the selection on the next opportunity.
* @param isNewSelection Whether the selection should be registered as a new
* @param isLinuxMouseSelection Whether the selection should be registered as a new
* selection on Linux.
*/
public refresh(isNewSelection?: boolean): void {
public refresh(isLinuxMouseSelection?: boolean): void {
// Queue the refresh for the renderer
if (!this._refreshAnimationFrame) {
this._refreshAnimationFrame = window.requestAnimationFrame(() => this._refresh());
@@ -255,10 +262,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
// If the platform is Linux and the refresh call comes from a mouse event,
// we need to update the selection for middle click to paste selection.
if (Browser.isLinux && isNewSelection) {
if (Browser.isLinux && isLinuxMouseSelection) {
const selectionText = this.selectionText;
if (selectionText.length) {
this.emit('newselection', this.selectionText);
this._onLinuxMouseSelection.fire(this.selectionText);
}
}
}
@@ -269,7 +276,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
*/
private _refresh(): void {
this._refreshAnimationFrame = null;
this.emit('refresh', {
this._onRedrawRequest.fire({
start: this._model.finalSelectionStart,
end: this._model.finalSelectionEnd,
columnSelectMode: this._activeSelectionMode === SelectionMode.COLUMN
@@ -318,7 +325,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
public selectAll(): void {
this._model.isSelectAllActive = true;
this.refresh();
this._terminal.emit('selection');
this._onSelectionChange.fire();
}
public selectLines(start: number, end: number): void {
@@ -328,7 +335,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._model.selectionStart = [0, start];
this._model.selectionEnd = [this._terminal.cols, end];
this.refresh();
this._terminal.emit('selection');
this._onSelectionChange.fire();
}
/**
@@ -506,8 +513,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
// If the mouse is over the second half of a wide character, adjust the
// selection to cover the whole character
const char = line.get(this._model.selectionStart[0]);
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
if (line.hasWidth(this._model.selectionStart[0]) === 0) {
this._model.selectionStart[0]++;
}
}
@@ -596,8 +602,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
// selection. Note that selections at the very end of the line will never
// have a character.
if (this._model.selectionEnd[1] < this._buffer.lines.length) {
const char = this._buffer.lines.get(this._model.selectionEnd[1]).get(this._model.selectionEnd[0]);
if (char && char[CHAR_DATA_WIDTH_INDEX] === 0) {
if (this._buffer.lines.get(this._model.selectionEnd[1]).hasWidth(this._model.selectionEnd[0]) === 0) {
this._model.selectionEnd[0]++;
}
}
@@ -648,7 +653,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME) {
(new AltClickHandler(event, this._terminal)).move();
} else if (this.hasSelection) {
this._terminal.emit('selection');
this._onSelectionChange.fire();
}
}
@@ -658,8 +663,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
// reverseIndex) and delete in a splice is only ever used when the same
// number of elements was just added. Given this is could actually be
// beneficial to leave the selection as is for these cases.
e.inactiveBuffer.lines.off('trim', this._trimListener);
e.activeBuffer.lines.on('trim', this._trimListener);
if (this._trimListener) {
this._trimListener.dispose();
}
this._trimListener = e.activeBuffer.lines.onTrim(amount => this._onTrim(amount));
}
/**
@@ -670,16 +677,16 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
private _convertViewportColToCharacterIndex(bufferLine: IBufferLine, coords: [number, number]): number {
let charIndex = coords[0];
for (let i = 0; coords[0] >= i; i++) {
const char = bufferLine.get(i);
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
const length = bufferLine.loadCell(i, this._workCell).getChars().length;
if (this._workCell.getWidth() === 0) {
// Wide characters aren't included in the line string so decrement the
// index so the index is back on the wide character.
charIndex--;
} else if (char[CHAR_DATA_CHAR_INDEX].length > 1 && coords[0] !== i) {
} else if (length > 1 && coords[0] !== i) {
// Emojis take up multiple characters, so adjust accordingly. For these
// we don't want ot include the character at the column as we're
// returning the start index in the string, not the end index.
charIndex += char[CHAR_DATA_CHAR_INDEX].length - 1;
charIndex += length - 1;
}
}
return charIndex;
@@ -739,48 +746,51 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
// Consider the initial position, skip it and increment the wide char
// variable
if (bufferLine.get(startCol)[CHAR_DATA_WIDTH_INDEX] === 0) {
if (bufferLine.getWidth(startCol) === 0) {
leftWideCharCount++;
startCol--;
}
if (bufferLine.get(endCol)[CHAR_DATA_WIDTH_INDEX] === 2) {
if (bufferLine.getWidth(endCol) === 2) {
rightWideCharCount++;
endCol++;
}
// Adjust the end index for characters whose length are > 1 (emojis)
if (bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length > 1) {
rightLongCharOffset += bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length - 1;
endIndex += bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length - 1;
const length = bufferLine.getString(endCol).length;
if (length > 1) {
rightLongCharOffset += length - 1;
endIndex += length - 1;
}
// Expand the string in both directions until a space is hit
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.get(startCol - 1))) {
const char = bufferLine.get(startCol - 1);
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.loadCell(startCol - 1, this._workCell))) {
bufferLine.loadCell(startCol - 1, this._workCell);
const length = this._workCell.getChars().length;
if (this._workCell.getWidth() === 0) {
// If the next character is a wide char, record it and skip the column
leftWideCharCount++;
startCol--;
} else if (char[CHAR_DATA_CHAR_INDEX].length > 1) {
} else if (length > 1) {
// If the next character's string is longer than 1 char (eg. emoji),
// adjust the index
leftLongCharOffset += char[CHAR_DATA_CHAR_INDEX].length - 1;
startIndex -= char[CHAR_DATA_CHAR_INDEX].length - 1;
leftLongCharOffset += length - 1;
startIndex -= length - 1;
}
startIndex--;
startCol--;
}
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.get(endCol + 1))) {
const char = bufferLine.get(endCol + 1);
if (char[CHAR_DATA_WIDTH_INDEX] === 2) {
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.loadCell(endCol + 1, this._workCell))) {
bufferLine.loadCell(endCol + 1, this._workCell);
const length = this._workCell.getChars().length;
if (this._workCell.getWidth() === 2) {
// If the next character is a wide char, record it and skip the column
rightWideCharCount++;
endCol++;
} else if (char[CHAR_DATA_CHAR_INDEX].length > 1) {
} else if (length > 1) {
// If the next character's string is longer than 1 char (eg. emoji),
// adjust the index
rightLongCharOffset += char[CHAR_DATA_CHAR_INDEX].length - 1;
endIndex += char[CHAR_DATA_CHAR_INDEX].length - 1;
rightLongCharOffset += length - 1;
endIndex += length - 1;
}
endIndex++;
endCol++;
@@ -814,9 +824,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
// Recurse upwards if the line is wrapped and the word wraps to the above line
if (followWrappedLinesAbove) {
if (start === 0 && bufferLine.get(0)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
if (start === 0 && bufferLine.getCodePoint(0) !== 32 /*' '*/) {
const previousBufferLine = this._buffer.lines.get(coords[1] - 1);
if (previousBufferLine && bufferLine.isWrapped && previousBufferLine.get(this._terminal.cols - 1)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
if (previousBufferLine && bufferLine.isWrapped && previousBufferLine.getCodePoint(this._terminal.cols - 1) !== 32 /*' '*/) {
const previousLineWordPosition = this._getWordAt([this._terminal.cols - 1, coords[1] - 1], false, true, false);
if (previousLineWordPosition) {
const offset = this._terminal.cols - previousLineWordPosition.start;
@@ -829,9 +839,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
// Recurse downwards if the line is wrapped and the word wraps to the next line
if (followWrappedLinesBelow) {
if (start + length === this._terminal.cols && bufferLine.get(this._terminal.cols - 1)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
if (start + length === this._terminal.cols && bufferLine.getCodePoint(this._terminal.cols - 1) !== 32 /*' '*/) {
const nextBufferLine = this._buffer.lines.get(coords[1] + 1);
if (nextBufferLine && nextBufferLine.isWrapped && nextBufferLine.get(0)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
if (nextBufferLine && nextBufferLine.isWrapped && nextBufferLine.getCodePoint(0) !== 32 /*' '*/) {
const nextLineWordPosition = this._getWordAt([0, coords[1] + 1], false, false, true);
if (nextLineWordPosition) {
length += nextLineWordPosition.length;
@@ -894,13 +904,13 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* word logic.
* @param char The character to check.
*/
private _isCharWordSeparator(charData: CharData): boolean {
private _isCharWordSeparator(cell: CellData): boolean {
// Zero width characters are never separators as they are always to the
// right of wide characters
if (charData[CHAR_DATA_WIDTH_INDEX] === 0) {
if (cell.getWidth() === 0) {
return false;
}
return WORD_SEPARATORS.indexOf(charData[CHAR_DATA_CHAR_INDEX]) >= 0;
return WORD_SEPARATORS.indexOf(cell.getChars()) >= 0;
}
/**
+1 -1
View File
@@ -7,7 +7,7 @@ import { assert } from 'chai';
import { ITerminal } from './Types';
import { SelectionModel } from './SelectionModel';
import { BufferSet } from './BufferSet';
import { MockTerminal } from './ui/TestUtils.test';
import { MockTerminal } from './TestUtils.test';
class TestSelectionModel extends SelectionModel {
constructor(
+6 -5
View File
@@ -13,8 +13,8 @@ import * as path from 'path';
import * as pty from 'node-pty';
import { assert } from 'chai';
import { Terminal } from './Terminal';
import { CHAR_DATA_CHAR_INDEX, WHITESPACE_CELL_CHAR } from './Buffer';
import { IViewport } from './Types';
import { CellData, WHITESPACE_CELL_CHAR } from './core/buffer/BufferLine';
class TestTerminal extends Terminal {
innerWrite(): void { this._innerWrite(); }
@@ -67,7 +67,7 @@ function terminalToString(term: Terminal): string {
for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
lineText = '';
for (let cell = 0; cell < term.cols; ++cell) {
lineText += term.buffer.lines.get(line).get(cell)[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR;
lineText += term.buffer.lines.get(line).loadCell(cell, new CellData()).getChars() || WHITESPACE_CELL_CHAR;
}
// rtrim empty cells as xterm does
lineText = lineText.replace(/\s+$/, '');
@@ -91,7 +91,8 @@ if (os.platform() !== 'win32') {
primitivePty = (<any>pty).native.open(cols, rows);
/** tests */
describe('xterm output comparison', () => {
describe('xterm output comparison', function(): void {
this.timeout(10000);
let xterm: TestTerminal;
beforeEach(() => {
@@ -113,8 +114,8 @@ if (os.platform() !== 'win32') {
51, 52, 54, 55, 56, 57, 58, 59, 60, 61,
63, 68
];
// These are failing on macOS only
if (os.platform() === 'darwin') {
// These are failing on macOS only
skip.push(3, 7, 11, 67);
}
for (let i = 0; i < files.length; i++) {
@@ -122,9 +123,9 @@ if (os.platform() !== 'win32') {
continue;
}
((filename: string) => {
const inFile = fs.readFileSync(filename, 'utf8');
it(filename.split('/').slice(-1)[0], done => {
ptyReset(() => {
const inFile = fs.readFileSync(filename, 'utf8');
ptyWriteRead(inFile, fromPty => {
// uncomment this to get log from terminal
// console.log = function(){};
+328 -208
View File
@@ -5,8 +5,8 @@
import { assert, expect } from 'chai';
import { Terminal } from './Terminal';
import { MockViewport, MockCompositionHelper, MockRenderer } from './ui/TestUtils.test';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, DEFAULT_ATTR } from './Buffer';
import { MockViewport, MockCompositionHelper, MockRenderer } from './TestUtils.test';
import { CellData, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine';
const INIT_COLS = 80;
const INIT_ROWS = 24;
@@ -16,7 +16,7 @@ class TestTerminal extends Terminal {
public keyPress(ev: any): boolean { return this._keyPress(ev); }
}
describe('term.js addons', () => {
describe('Terminal', () => {
let term: TestTerminal;
const termOptions = {
cols: INIT_COLS,
@@ -67,6 +67,81 @@ describe('term.js addons', () => {
});
});
describe('events', () => {
it('should fire the onData evnet', (done) => {
term.onData(() => done());
term.handler('fake');
});
it('should fire the onCursorMove event', (done) => {
term.on('cursormove', () => done());
term.write('foo');
});
it('should fire the onLineFeed event', (done) => {
term.on('linefeed', () => done());
term.write('\n');
});
it('should fire a scroll event when scrollback is created', (done) => {
term.on('scroll', () => done());
term.write('\n'.repeat(INIT_ROWS));
});
it('should fire a scroll event when scrollback is cleared', (done) => {
term.write('\n'.repeat(INIT_ROWS));
term.on('scroll', () => done());
term.clear();
});
it('should fire a key event after a keypress DOM event', (done) => {
term.onKey(e => {
assert.equal(typeof e.key, 'string');
expect(e.domEvent).to.be.an.instanceof(Object);
done();
});
const evKeyPress = <KeyboardEvent>{
preventDefault: () => { },
stopPropagation: () => { },
type: 'keypress',
keyCode: 13
};
term.keyPress(evKeyPress);
});
it('should fire a key event after a keydown DOM event', (done) => {
term.onKey(e => {
assert.equal(typeof e.key, 'string');
expect(e.domEvent).to.be.an.instanceof(Object);
done();
});
const evKeyDown = <KeyboardEvent>{
preventDefault: () => { },
stopPropagation: () => { },
type: 'keydown',
keyCode: 13
};
term.keyDown(evKeyDown);
});
it('should fire the onResize event', (done) => {
term.onResize(e => {
expect(e).to.have.keys(['cols', 'rows']);
assert.equal(typeof e.cols, 'number');
assert.equal(typeof e.rows, 'number');
done();
});
term.resize(1, 1);
});
it('should fire the onScroll event', (done) => {
term.onScroll(e => {
assert.equal(typeof e, 'number');
done();
});
term.scroll();
});
it('should fire the onTitleChange event', (done) => {
term.onTitleChange(e => {
assert.equal(e, 'title');
done();
});
term.handleTitle('title');
});
});
describe('on', () => {
beforeEach(() => {
term.on('key', () => { });
@@ -84,6 +159,40 @@ describe('term.js addons', () => {
});
});
describe('cursormove', () => {
it('should emit a cursormove event', (done) => {
term.on('cursormove', () => {
done();
});
term.write('foo');
});
});
describe('linefeed', () => {
it('should emit a linefeed event', (done) => {
term.on('linefeed', () => {
done();
});
term.write('\n');
});
});
describe('scroll', () => {
it('should emit a scroll event when scrollback is created', (done) => {
term.on('scroll', () => {
done();
});
term.write('\n'.repeat(INIT_ROWS));
});
it('should emit a scroll event when scrollback is cleared', (done) => {
term.write('\n'.repeat(INIT_ROWS));
term.on('scroll', () => {
done();
});
term.clear();
});
});
describe(`keypress (including 'key' event)`, () => {
it('should receive a string and event object', (done) => {
let steps = 0;
@@ -227,7 +336,7 @@ describe('term.js addons', () => {
});
describe('setOption', () => {
it('should set the option correctly', () => {
it('should set option correctly', () => {
term.setOption('cursorBlink', true);
assert.equal(term.options.cursorBlink, true);
term.setOption('cursorBlink', false);
@@ -259,7 +368,7 @@ describe('term.js addons', () => {
assert.equal(term.buffer.lines.length, term.rows);
assert.deepEqual(term.buffer.lines.get(0), promptLine);
for (let i = 1; i < term.rows; i++) {
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR));
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA));
}
});
it('should clear a buffer larger than rows', () => {
@@ -276,7 +385,7 @@ describe('term.js addons', () => {
assert.equal(term.buffer.lines.length, term.rows);
assert.deepEqual(term.buffer.lines.get(0), promptLine);
for (let i = 1; i < term.rows; i++) {
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR));
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA));
}
});
it('should not break the prompt when cleared twice', () => {
@@ -289,7 +398,7 @@ describe('term.js addons', () => {
assert.equal(term.buffer.lines.length, term.rows);
assert.deepEqual(term.buffer.lines.get(0), promptLine);
for (let i = 1; i < term.rows; i++) {
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR));
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA));
}
});
});
@@ -455,62 +564,62 @@ describe('term.js addons', () => {
describe('scroll() function', () => {
describe('when scrollback > 0', () => {
it('should create a new line and scroll', () => {
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
term.buffer.lines.get(INIT_ROWS - 1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
term.buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
assert.equal(term.buffer.lines.get(INIT_ROWS).get(0)[CHAR_DATA_CHAR_INDEX], '');
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).getChars(), 'b');
assert.equal(term.buffer.lines.get(INIT_ROWS).loadCell(0, new CellData()).getChars(), '');
});
it('should properly scroll inside a scroll region (scrollTop set)', () => {
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]);
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c');
});
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]);
term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]);
term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]);
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
term.buffer.y = 3;
term.buffer.scrollBottom = 3;
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a', '\'a\' should be pushed to the scrollback');
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index');
assert.equal(term.buffer.lines.get(5).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a', '\'a\' should be pushed to the scrollback');
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'b');
assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'c');
assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), 'd');
assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index');
assert.equal(term.buffer.lines.get(5).loadCell(0, new CellData()).getChars(), 'e');
});
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]);
term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]);
term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]);
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.buffer.scrollBottom = 3;
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index');
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer');
assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'd');
assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index');
assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), 'e');
});
});
@@ -521,65 +630,65 @@ describe('term.js addons', () => {
});
it('should create a new line and shift everything up', () => {
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
term.buffer.lines.get(INIT_ROWS - 1).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]);
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
term.buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
assert.equal(term.buffer.lines.length, INIT_ROWS);
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS);
// 'a' gets pushed out of buffer
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], '');
assert.equal(term.buffer.lines.get(INIT_ROWS - 2).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], '');
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'b');
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), '');
assert.equal(term.buffer.lines.get(INIT_ROWS - 2).loadCell(0, new CellData()).getChars(), 'c');
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).getChars(), '');
});
it('should properly scroll inside a scroll region (scrollTop set)', () => {
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]);
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c');
});
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]);
term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]);
term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]);
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
term.buffer.y = 3;
term.buffer.scrollBottom = 3;
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index');
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'b');
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c');
assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'd');
assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index');
assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), 'e');
});
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]);
term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]);
term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]);
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.buffer.scrollBottom = 3;
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index');
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer');
assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'd');
assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index');
assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), 'e');
});
});
});
@@ -770,116 +879,126 @@ describe('term.js addons', () => {
it('2 characters per cell', function (): 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.write(high + String.fromCharCode(i));
const tchar = term.buffer.lines.get(0).get(0);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
const tchar = term.buffer.lines.get(0).loadCell(0, cell);
expect(tchar.getChars()).eql(high + String.fromCharCode(i));
expect(tchar.getChars().length).eql(2);
expect(tchar.getWidth()).eql(1);
expect(term.buffer.lines.get(0).loadCell(1, cell).getChars()).eql('');
term.reset();
}
});
it('2 characters at last cell', () => {
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1;
term.write(high + String.fromCharCode(i));
expect(term.buffer.lines.get(0).get(term.buffer.x - 1)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
expect(term.buffer.lines.get(0).get(term.buffer.x - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql('');
expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).getChars()).eql(high + String.fromCharCode(i));
expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).getChars().length).eql(2);
expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql('');
term.reset();
}
});
it('2 characters per cell over line end with autowrap', () => {
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1;
term.wraparoundMode = true;
term.write('a' + high + String.fromCharCode(i));
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a');
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql('a');
expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql(high + String.fromCharCode(i));
expect(term.buffer.lines.get(1).loadCell(0, cell).getChars().length).eql(2);
expect(term.buffer.lines.get(1).loadCell(1, cell).getChars()).eql('');
term.reset();
}
});
it('2 characters per cell over line end without autowrap', () => {
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1;
term.wraparoundMode = false;
term.write('a' + high + String.fromCharCode(i));
// auto wraparound mode should cut off the rest of the line
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a');
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(1);
expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql('a');
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars().length).eql(1);
expect(term.buffer.lines.get(1).loadCell(1, cell).getChars()).eql('');
term.reset();
}
});
it('splitted surrogates', () => {
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.write(high);
term.write(String.fromCharCode(i));
const tchar = term.buffer.lines.get(0).get(0);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
const tchar = term.buffer.lines.get(0).loadCell(0, cell);
expect(tchar.getChars()).eql(high + String.fromCharCode(i));
expect(tchar.getChars().length).eql(2);
expect(tchar.getWidth()).eql(1);
expect(term.buffer.lines.get(0).loadCell(1, cell).getChars()).eql('');
term.reset();
}
});
});
describe('unicode - combining characters', () => {
const cell = new CellData();
it('café', () => {
term.write('cafe\u0301');
expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_WIDTH_INDEX]).eql(1);
term.buffer.lines.get(0).loadCell(3, cell);
expect(cell.getChars()).eql('e\u0301');
expect(cell.getChars().length).eql(2);
expect(cell.getWidth()).eql(1);
});
it('café - end of line', () => {
term.buffer.x = term.cols - 1 - 3;
term.write('cafe\u0301');
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_WIDTH_INDEX]).eql(1);
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX].length).eql(0);
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_WIDTH_INDEX]).eql(1);
term.buffer.lines.get(0).loadCell(term.cols - 1, cell);
expect(cell.getChars()).eql('e\u0301');
expect(cell.getChars().length).eql(2);
expect(cell.getWidth()).eql(1);
term.buffer.lines.get(0).loadCell(1, cell);
expect(cell.getChars()).eql('');
expect(cell.getChars().length).eql(0);
expect(cell.getWidth()).eql(1);
});
it('multiple combined é', () => {
term.wraparoundMode = true;
term.write(Array(100).join('e\u0301'));
for (let i = 0; i < term.cols; ++i) {
const tchar = term.buffer.lines.get(0).get(i);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
term.buffer.lines.get(0).loadCell(i, cell);
expect(cell.getChars()).eql('e\u0301');
expect(cell.getChars().length).eql(2);
expect(cell.getWidth()).eql(1);
}
const tchar = term.buffer.lines.get(1).get(0);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
term.buffer.lines.get(1).loadCell(0, cell);
expect(cell.getChars()).eql('e\u0301');
expect(cell.getChars().length).eql(2);
expect(cell.getWidth()).eql(1);
});
it('multiple surrogate with combined', () => {
term.wraparoundMode = true;
term.write(Array(100).join('\uD800\uDC00\u0301'));
for (let i = 0; i < term.cols; ++i) {
const tchar = term.buffer.lines.get(0).get(i);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
term.buffer.lines.get(0).loadCell(i, cell);
expect(cell.getChars()).eql('\uD800\uDC00\u0301');
expect(cell.getChars().length).eql(3);
expect(cell.getWidth()).eql(1);
}
const tchar = term.buffer.lines.get(1).get(0);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
term.buffer.lines.get(1).loadCell(0, cell);
expect(cell.getChars()).eql('\uD800\uDC00\u0301');
expect(cell.getChars().length).eql(3);
expect(cell.getWidth()).eql(1);
});
});
describe('unicode - fullwidth characters', () => {
const cell = new CellData();
it('cursor movement even', () => {
expect(term.buffer.x).eql(0);
term.write('¥');
@@ -895,140 +1014,141 @@ describe('term.js addons', () => {
term.wraparoundMode = true;
term.write(Array(50).join('¥'));
for (let i = 0; i < term.cols; ++i) {
const tchar = term.buffer.lines.get(0).get(i);
term.buffer.lines.get(0).loadCell(i, cell);
if (i % 2) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
expect(cell.getChars()).eql('');
expect(cell.getChars().length).eql(0);
expect(cell.getWidth()).eql(0);
} else {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
expect(cell.getChars()).eql('¥');
expect(cell.getChars().length).eql(1);
expect(cell.getWidth()).eql(2);
}
}
const tchar = term.buffer.lines.get(1).get(0);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
term.buffer.lines.get(1).loadCell(0, cell);
expect(cell.getChars()).eql('¥');
expect(cell.getChars().length).eql(1);
expect(cell.getWidth()).eql(2);
});
it('line of ¥ odd', () => {
term.wraparoundMode = true;
term.buffer.x = 1;
term.write(Array(50).join('¥'));
for (let i = 1; i < term.cols - 1; ++i) {
const tchar = term.buffer.lines.get(0).get(i);
term.buffer.lines.get(0).loadCell(i, cell);
if (!(i % 2)) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
expect(cell.getChars()).eql('');
expect(cell.getChars().length).eql(0);
expect(cell.getWidth()).eql(0);
} else {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
expect(cell.getChars()).eql('¥');
expect(cell.getChars().length).eql(1);
expect(cell.getWidth()).eql(2);
}
}
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
tchar = term.buffer.lines.get(1).get(0);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
term.buffer.lines.get(0).loadCell(term.cols - 1, cell);
expect(cell.getChars()).eql('');
expect(cell.getChars().length).eql(0);
expect(cell.getWidth()).eql(1);
term.buffer.lines.get(1).loadCell(0, cell);
expect(cell.getChars()).eql('¥');
expect(cell.getChars().length).eql(1);
expect(cell.getWidth()).eql(2);
});
it('line of ¥ with combining odd', () => {
term.wraparoundMode = true;
term.buffer.x = 1;
term.write(Array(50).join('¥\u0301'));
for (let i = 1; i < term.cols - 1; ++i) {
const tchar = term.buffer.lines.get(0).get(i);
term.buffer.lines.get(0).loadCell(i, cell);
if (!(i % 2)) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
expect(cell.getChars()).eql('');
expect(cell.getChars().length).eql(0);
expect(cell.getWidth()).eql(0);
} else {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
expect(cell.getChars()).eql('¥\u0301');
expect(cell.getChars().length).eql(2);
expect(cell.getWidth()).eql(2);
}
}
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
tchar = term.buffer.lines.get(1).get(0);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
term.buffer.lines.get(0).loadCell(term.cols - 1, cell);
expect(cell.getChars()).eql('');
expect(cell.getChars().length).eql(0);
expect(cell.getWidth()).eql(1);
term.buffer.lines.get(1).loadCell(0, cell);
expect(cell.getChars()).eql('¥\u0301');
expect(cell.getChars().length).eql(2);
expect(cell.getWidth()).eql(2);
});
it('line of ¥ with combining even', () => {
term.wraparoundMode = true;
term.write(Array(50).join('¥\u0301'));
for (let i = 0; i < term.cols; ++i) {
const tchar = term.buffer.lines.get(0).get(i);
term.buffer.lines.get(0).loadCell(i, cell);
if (i % 2) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
expect(cell.getChars()).eql('');
expect(cell.getChars().length).eql(0);
expect(cell.getWidth()).eql(0);
} else {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
expect(cell.getChars()).eql('¥\u0301');
expect(cell.getChars().length).eql(2);
expect(cell.getWidth()).eql(2);
}
}
const tchar = term.buffer.lines.get(1).get(0);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
term.buffer.lines.get(1).loadCell(0, cell);
expect(cell.getChars()).eql('¥\u0301');
expect(cell.getChars().length).eql(2);
expect(cell.getWidth()).eql(2);
});
it('line of surrogate fullwidth with combining odd', () => {
term.wraparoundMode = true;
term.buffer.x = 1;
term.write(Array(50).join('\ud843\ude6d\u0301'));
for (let i = 1; i < term.cols - 1; ++i) {
const tchar = term.buffer.lines.get(0).get(i);
term.buffer.lines.get(0).loadCell(i, cell);
if (!(i % 2)) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
expect(cell.getChars()).eql('');
expect(cell.getChars().length).eql(0);
expect(cell.getWidth()).eql(0);
} else {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
expect(cell.getChars()).eql('\ud843\ude6d\u0301');
expect(cell.getChars().length).eql(3);
expect(cell.getWidth()).eql(2);
}
}
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
tchar = term.buffer.lines.get(1).get(0);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
term.buffer.lines.get(0).loadCell(term.cols - 1, cell);
expect(cell.getChars()).eql('');
expect(cell.getChars().length).eql(0);
expect(cell.getWidth()).eql(1);
term.buffer.lines.get(1).loadCell(0, cell);
expect(cell.getChars()).eql('\ud843\ude6d\u0301');
expect(cell.getChars().length).eql(3);
expect(cell.getWidth()).eql(2);
});
it('line of surrogate fullwidth with combining even', () => {
term.wraparoundMode = true;
term.write(Array(50).join('\ud843\ude6d\u0301'));
for (let i = 0; i < term.cols; ++i) {
const tchar = term.buffer.lines.get(0).get(i);
term.buffer.lines.get(0).loadCell(i, cell);
if (i % 2) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
expect(cell.getChars()).eql('');
expect(cell.getChars().length).eql(0);
expect(cell.getWidth()).eql(0);
} else {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
expect(cell.getChars()).eql('\ud843\ude6d\u0301');
expect(cell.getChars().length).eql(3);
expect(cell.getWidth()).eql(2);
}
}
const tchar = term.buffer.lines.get(1).get(0);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
term.buffer.lines.get(1).loadCell(0, cell);
expect(cell.getChars()).eql('\ud843\ude6d\u0301');
expect(cell.getChars().length).eql(3);
expect(cell.getWidth()).eql(2);
});
});
describe('insert mode', () => {
const cell = new CellData();
it('halfwidth - all', () => {
term.write(Array(9).join('0123456789').slice(-80));
term.buffer.x = 10;
@@ -1036,10 +1156,10 @@ describe('term.js addons', () => {
term.insertMode = true;
term.write('abcde');
expect(term.buffer.lines.get(0).length).eql(term.cols);
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('a');
expect(term.buffer.lines.get(0).get(14)[CHAR_DATA_CHAR_INDEX]).eql('e');
expect(term.buffer.lines.get(0).get(15)[CHAR_DATA_CHAR_INDEX]).eql('0');
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql('4');
expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('a');
expect(term.buffer.lines.get(0).loadCell(14, cell).getChars()).eql('e');
expect(term.buffer.lines.get(0).loadCell(15, cell).getChars()).eql('0');
expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql('4');
});
it('fullwidth - insert', () => {
term.write(Array(9).join('0123456789').slice(-80));
@@ -1048,11 +1168,11 @@ describe('term.js addons', () => {
term.insertMode = true;
term.write('¥¥¥');
expect(term.buffer.lines.get(0).length).eql(term.cols);
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('¥');
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('');
expect(term.buffer.lines.get(0).get(14)[CHAR_DATA_CHAR_INDEX]).eql('¥');
expect(term.buffer.lines.get(0).get(15)[CHAR_DATA_CHAR_INDEX]).eql('');
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql('3');
expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('¥');
expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('');
expect(term.buffer.lines.get(0).loadCell(14, cell).getChars()).eql('¥');
expect(term.buffer.lines.get(0).loadCell(15, cell).getChars()).eql('');
expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql('3');
});
it('fullwidth - right border', () => {
term.write(Array(41).join('¥'));
@@ -1061,14 +1181,14 @@ describe('term.js addons', () => {
term.insertMode = true;
term.write('a');
expect(term.buffer.lines.get(0).length).eql(term.cols);
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('a');
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('¥');
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(''); // fullwidth char got replaced
expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('a');
expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('¥');
expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql(''); // fullwidth char got replaced
term.write('b');
expect(term.buffer.lines.get(0).length).eql(term.cols);
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('b');
expect(term.buffer.lines.get(0).get(12)[CHAR_DATA_CHAR_INDEX]).eql('¥');
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(''); // empty cell after fullwidth
expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('b');
expect(term.buffer.lines.get(0).loadCell(12, cell).getChars()).eql('¥');
expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql(''); // empty cell after fullwidth
});
});
});
+316 -198
View File
@@ -21,37 +21,39 @@
* http://linux.die.net/man/7/urxvt
*/
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types';
import { IMouseZoneManager } from './ui/Types';
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharacterJoinerHandler, IMouseZoneManager } from './Types';
import { IRenderer } from './renderer/Types';
import { BufferSet } from './BufferSet';
import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_ATTR_INDEX } from './Buffer';
import { Buffer, MAX_BUFFER_SIZE } from './Buffer';
import { CompositionHelper } from './CompositionHelper';
import { EventEmitter } from './common/EventEmitter';
import { Viewport } from './Viewport';
import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './ui/Clipboard';
import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './Clipboard';
import { C0 } from './common/data/EscapeSequences';
import { InputHandler } from './InputHandler';
import { Renderer } from './renderer/Renderer';
import { Linkifier } from './Linkifier';
import { SelectionManager } from './SelectionManager';
import { CharMeasure } from './ui/CharMeasure';
import * as Browser from './core/Platform';
import { CharMeasure } from './CharMeasure';
import * as Browser from './common/Platform';
import { addDisposableDomListener } from './ui/Lifecycle';
import * as Strings from './Strings';
import { MouseHelper } from './ui/MouseHelper';
import { MouseHelper } from './MouseHelper';
import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager';
import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager';
import { MouseZoneManager } from './ui/MouseZoneManager';
import { MouseZoneManager } from './MouseZoneManager';
import { AccessibilityManager } from './AccessibilityManager';
import { ScreenDprMonitor } from './ui/ScreenDprMonitor';
import { ITheme, IMarker, IDisposable } from 'xterm';
import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm';
import { removeTerminalFromCache } from './renderer/atlas/CharAtlasCache';
import { DomRenderer } from './renderer/dom/DomRenderer';
import { IKeyboardEvent } from './common/Types';
import { evaluateKeyboardEvent } from './core/input/Keyboard';
import { KeyboardResultType, ICharset } from './core/Types';
import { KeyboardResultType, ICharset, IBufferLine, IAttributeData } from './core/Types';
import { clone } from './common/Clone';
import { EventEmitter2, IEvent } from './common/EventEmitter2';
import { Attributes, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine';
import { applyWindowsMode } from './WindowsMode';
import { ColorManager } from './ui/ColorManager';
import { RenderCoordinator } from './renderer/RenderCoordinator';
// Let it work inside Node.js for automated testing purposes.
const document = (typeof window !== 'undefined') ? window.document : null;
@@ -64,10 +66,13 @@ const document = (typeof window !== 'undefined') ? window.document : null;
const WRITE_BUFFER_PAUSE_THRESHOLD = 5;
/**
* The number of writes to perform in a single batch before allowing the
* renderer to catch up with a 0ms setTimeout.
* The max number of ms to spend on writes before allowing the renderer to
* catch up with a 0ms setTimeout. A value of < 33 to keep us close to
* 30fps, and a value of < 16 to try to run at 60fps. Of course, the real FPS
* depends on the time it takes for the renderer to draw the frame.
*/
const WRITE_BATCH_SIZE = 300;
const WRITE_TIMEOUT_MS = 12;
const WRITE_BUFFER_LENGTH_THRESHOLD = 50;
const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars
const MINIMUM_ROWS = 1;
@@ -106,9 +111,10 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
useFlowControl: false,
allowTransparency: false,
tabStopWidth: 8,
theme: null,
theme: undefined,
rightClickSelectsWord: Browser.isMac,
rendererType: 'canvas'
rendererType: 'canvas',
windowsMode: false
};
export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal {
@@ -171,13 +177,15 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
private _refreshEnd: number;
public savedCols: number;
public curAttr: number;
public curAttrData: IAttributeData;
private _eraseAttrData: IAttributeData;
public params: (string | number)[];
public currentParam: string | number;
// user input states
public writeBuffer: string[];
public writeBufferUtf8: Uint8Array[];
private _writeInProgress: boolean;
/**
@@ -196,7 +204,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
private _inputHandler: InputHandler;
public soundManager: SoundManager;
public renderer: IRenderer;
private _renderCoordinator: RenderCoordinator;
public selectionManager: SelectionManager;
public linkifier: ILinkifier;
public buffers: BufferSet;
@@ -206,8 +214,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
private _mouseZoneManager: IMouseZoneManager;
public mouseHelper: MouseHelper;
private _accessibilityManager: AccessibilityManager;
private _screenDprMonitor: ScreenDprMonitor;
private _colorManager: ColorManager;
private _theme: ITheme;
private _windowsMode: IDisposable | undefined;
// bufferline to clone/copy from for new blank lines
private _blankLine: IBufferLine = null;
@@ -215,6 +224,25 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
public cols: number;
public rows: number;
private _onCursorMove = new EventEmitter2<void>();
public get onCursorMove(): IEvent<void> { return this._onCursorMove.event; }
private _onData = new EventEmitter2<string>();
public get onData(): IEvent<string> { return this._onData.event; }
private _onKey = new EventEmitter2<{ key: string, domEvent: KeyboardEvent }>();
public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._onKey.event; }
private _onLineFeed = new EventEmitter2<void>();
public get onLineFeed(): IEvent<void> { return this._onLineFeed.event; }
private _onRender = new EventEmitter2<{ start: number, end: number }>();
public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; }
private _onResize = new EventEmitter2<{ cols: number, rows: number }>();
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; }
private _onScroll = new EventEmitter2<number>();
public get onScroll(): IEvent<number> { return this._onScroll.event; }
private _onSelectionChange = new EventEmitter2<void>();
public get onSelectionChange(): IEvent<void> { return this._onSelectionChange.event; }
private _onTitleChange = new EventEmitter2<string>();
public get onTitleChange(): IEvent<string> { return this._onTitleChange.event; }
/**
* Creates a new `Terminal` object.
*
@@ -233,10 +261,26 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
super();
this.options = clone(options);
this._setup();
// TODO: Remove these in v4
// Fire old style events from new emitters
this.onCursorMove(() => this.emit('cursormove'));
this.onData(e => this.emit('data', e));
this.onKey(e => this.emit('key', e.key, e.domEvent));
this.onLineFeed(() => this.emit('linefeed'));
this.onRender(e => this.emit('refresh', e));
this.onResize(e => this.emit('resize', e));
this.onSelectionChange(() => this.emit('selection'));
this.onScroll(e => this.emit('scroll', e));
this.onTitleChange(e => this.emit('title', e));
}
public dispose(): void {
super.dispose();
if (this._windowsMode) {
this._windowsMode.dispose();
this._windowsMode = undefined;
}
this._customKeyEventHandler = null;
removeTerminalFromCache(this);
this.handler = () => {};
@@ -269,7 +313,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.rows = Math.max(this.options.rows, MINIMUM_ROWS);
if (this.options.handler) {
this.on('data', this.options.handler);
this.onData(this.options.handler);
}
this.cursorState = 0;
@@ -291,23 +335,28 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// TODO: Can this be just []?
this.charsets = [null];
this.curAttr = DEFAULT_ATTR;
this.curAttrData = DEFAULT_ATTR_DATA.clone();
this._eraseAttrData = DEFAULT_ATTR_DATA.clone();
this.params = [];
this.currentParam = 0;
// user input states
this.writeBuffer = [];
this.writeBufferUtf8 = [];
this._writeInProgress = false;
this._xoffSentToCatchUp = false;
// this._writeStopped = false;
this._userScrolling = false;
// Register input handler and refire/handle events
this._inputHandler = new InputHandler(this);
this._inputHandler.onCursorMove(() => this._onCursorMove.fire());
this._inputHandler.onLineFeed(() => this._onLineFeed.fire());
this._inputHandler.onData(e => this._onData.fire(e));
this.register(this._inputHandler);
// Reuse renderer if the Terminal is being recreated via a reset call.
this.renderer = this.renderer || null;
this.selectionManager = this.selectionManager || null;
this.linkifier = this.linkifier || new Linkifier(this);
this._mouseZoneManager = this._mouseZoneManager || null;
@@ -319,6 +368,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.selectionManager.clearSelection();
this.selectionManager.initBuffersListeners();
}
if (this.options.windowsMode) {
this._windowsMode = applyWindowsMode(this);
}
}
/**
@@ -331,9 +384,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* back_color_erase feature for xterm.
*/
public eraseAttr(): number {
// if (this.is('screen')) return DEFAULT_ATTR;
return (DEFAULT_ATTR & ~0x1ff) | (this.curAttr & 0x1ff);
public eraseAttrData(): IAttributeData {
this._eraseAttrData.bg &= ~(Attributes.CM_MASK | 0xFFFFFF);
this._eraseAttrData.bg |= this.curAttrData.bg & ~0xFC000000;
return this._eraseAttrData;
}
/**
@@ -341,7 +395,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
*/
public focus(): void {
if (this.textarea) {
this.textarea.focus();
this.textarea.focus({ preventScroll: true });
}
}
@@ -414,12 +468,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
break;
case 'theme':
// If open has been called we do not want to set options.theme as the
// source of truth is owned by the renderer.
if (this.renderer) {
this._setTheme(<ITheme>value);
return;
}
this._setTheme(<ITheme>value);
break;
case 'scrollback':
value = Math.min(value, MAX_BUFFER_SIZE);
@@ -448,8 +497,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
case 'fontFamily':
case 'fontSize':
// When the font changes the size of the cells may change which requires a renderer clear
if (this.renderer) {
this.renderer.clear();
if (this._renderCoordinator) {
this._renderCoordinator.clear();
this.charMeasure.measure(this.options);
}
break;
@@ -461,24 +510,16 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
case 'fontWeight':
case 'fontWeightBold':
// When the font changes the size of the cells may change which requires a renderer clear
if (this.renderer) {
this.renderer.clear();
this.renderer.onResize(this.cols, this.rows);
if (this._renderCoordinator) {
this._renderCoordinator.clear();
this._renderCoordinator.onResize(this.cols, this.rows);
this.refresh(0, this.rows - 1);
}
break;
case 'rendererType':
if (this.renderer) {
this.unregister(this.renderer);
this.renderer.dispose();
this.renderer = null;
if (this._renderCoordinator) {
this._renderCoordinator.setRenderer(this._createRenderer());
}
this._setupRenderer();
this.renderer.onCharSizeChanged();
if (this._theme) {
this.renderer.setTheme(this._theme);
}
this.mouseHelper.setRenderer(this.renderer);
break;
case 'scrollback':
this.buffers.resize(this.cols, this.rows);
@@ -488,8 +529,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
break;
case 'screenReaderMode':
if (value) {
if (!this._accessibilityManager) {
this._accessibilityManager = new AccessibilityManager(this);
if (!this._accessibilityManager && this._renderCoordinator) {
this._accessibilityManager = new AccessibilityManager(this, this._renderCoordinator.dimensions);
}
} else {
if (this._accessibilityManager) {
@@ -499,10 +540,22 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
break;
case 'tabStopWidth': this.buffers.setupTabStops(); break;
case 'windowsMode':
if (value) {
if (!this._windowsMode) {
this._windowsMode = applyWindowsMode(this);
}
} else {
if (this._windowsMode) {
this._windowsMode.dispose();
this._windowsMode = undefined;
}
}
break;
}
// Inform renderer of changes
if (this.renderer) {
this.renderer.onOptionsChanged();
if (this._renderCoordinator) {
this._renderCoordinator.onOptionsChanged();
}
}
@@ -621,8 +674,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.register(addDisposableDomListener(this.textarea, 'compositionstart', () => this._compositionHelper.compositionstart()));
this.register(addDisposableDomListener(this.textarea, 'compositionupdate', (e: CompositionEvent) => this._compositionHelper.compositionupdate(e)));
this.register(addDisposableDomListener(this.textarea, 'compositionend', () => this._compositionHelper.compositionend()));
this.register(this.addDisposableListener('refresh', () => this._compositionHelper.updateCompositionElements()));
this.register(this.addDisposableListener('refresh', (data) => this._queueLinkification(data.start, data.end)));
this.register(this.onRender(() => this._compositionHelper.updateCompositionElements()));
this.register(this.onRender(e => this._queueLinkification(e.start, e.end)));
}
/**
@@ -641,10 +694,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this._context = this._parent.ownerDocument.defaultView;
this._document = this._parent.ownerDocument;
this._screenDprMonitor = new ScreenDprMonitor();
this._screenDprMonitor.setListener(() => this.emit('dprchange', window.devicePixelRatio));
this.register(this._screenDprMonitor);
// Create main element container
this.element = this._document.createElement('div');
this.element.dir = 'ltr'; // xterm.css assumes LTR
@@ -674,7 +723,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this._mouseZoneManager = new MouseZoneManager(this);
this.register(this._mouseZoneManager);
this.register(this.addDisposableListener('scroll', () => this._mouseZoneManager.clearAll()));
this.register(this.onScroll(() => this._mouseZoneManager.clearAll()));
this.linkifier.attachToDom(this._mouseZoneManager);
this.textarea = document.createElement('textarea');
@@ -700,28 +749,31 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// Performance: Add viewport and helper elements from the fragment
this.element.appendChild(fragment);
this._setupRenderer();
this._theme = this.options.theme;
this.options.theme = null;
this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure);
this.viewport.onThemeChanged(this.renderer.colorManager.colors);
this._colorManager = new ColorManager(document, this.options.allowTransparency);
this._colorManager.setTheme(this._theme);
const renderer = this._createRenderer();
this._renderCoordinator = new RenderCoordinator(renderer, this.rows, this.screenElement);
this._renderCoordinator.onRender(e => this._onRender.fire(e));
this.onResize(e => this._renderCoordinator.resize(e.cols, e.rows));
this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure, this._renderCoordinator.dimensions);
this.viewport.onThemeChange(this._colorManager.colors);
this.register(this.viewport);
this.register(this.addDisposableListener('cursormove', () => this.renderer.onCursorMove()));
this.register(this.addDisposableListener('resize', () => this.renderer.onResize(this.cols, this.rows)));
this.register(this.addDisposableListener('blur', () => this.renderer.onBlur()));
this.register(this.addDisposableListener('focus', () => this.renderer.onFocus()));
this.register(this.addDisposableListener('dprchange', () => this.renderer.onWindowResize(window.devicePixelRatio)));
// dprchange should handle this case, we need this as well for browsers that don't support the
// matchMedia query.
this.register(addDisposableDomListener(window, 'resize', () => this.renderer.onWindowResize(window.devicePixelRatio)));
this.register(this.charMeasure.addDisposableListener('charsizechanged', () => this.renderer.onCharSizeChanged()));
this.register(this.renderer.addDisposableListener('resize', (dimensions) => this.viewport.syncScrollArea()));
this.register(this.onCursorMove(() => this._renderCoordinator.onCursorMove()));
this.register(this.onResize(() => this._renderCoordinator.onResize(this.cols, this.rows)));
this.register(this.addDisposableListener('blur', () => this._renderCoordinator.onBlur()));
this.register(this.addDisposableListener('focus', () => this._renderCoordinator.onFocus()));
this.register(this.charMeasure.onCharSizeChanged(() => this._renderCoordinator.onCharSizeChanged()));
this.register(this._renderCoordinator.onDimensionsChange(() => this.viewport.syncScrollArea()));
this.selectionManager = new SelectionManager(this, this.charMeasure);
this.register(this.selectionManager.onSelectionChange(() => this._onSelectionChange.fire()));
this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e)));
this.register(this.selectionManager.addDisposableListener('refresh', data => this.renderer.onSelectionChanged(data.start, data.end, data.columnSelectMode)));
this.register(this.selectionManager.addDisposableListener('newselection', text => {
this.register(this.selectionManager.onRedrawRequest(e => this._renderCoordinator.onSelectionChanged(e.start, e.end, e.columnSelectMode)));
this.register(this.selectionManager.onLinuxMouseSelection(text => {
// If there's a new selection, put it into the textarea, focus and select it
// in order to register it as a selection on the OS. This event is fired
// only on Linux to enable middle click to paste selection.
@@ -729,20 +781,26 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.textarea.focus();
this.textarea.select();
}));
this.register(this.addDisposableListener('scroll', () => {
this.register(this.onScroll(() => {
this.viewport.syncScrollArea();
this.selectionManager.refresh();
}));
this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this.selectionManager.refresh()));
this.mouseHelper = new MouseHelper(this.renderer);
this.mouseHelper = new MouseHelper(this._renderCoordinator);
// apply mouse event classes set by escape codes before terminal was attached
this.element.classList.toggle('enable-mouse-events', this.mouseEvents);
if (this.mouseEvents) {
this.selectionManager.disable();
} else {
this.selectionManager.enable();
}
if (this.options.screenReaderMode) {
// Note that this must be done *after* the renderer is created in order to
// ensure the correct order of the dprchange event
this._accessibilityManager = new AccessibilityManager(this);
this._accessibilityManager = new AccessibilityManager(this, this._renderCoordinator.dimensions);
this._accessibilityManager.register(this._renderCoordinator.onDimensionsChange(e => this._accessibilityManager.setDimensions(e)));
}
// Measure the character size
@@ -760,13 +818,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
private _setupRenderer(): void {
private _createRenderer(): IRenderer {
switch (this.options.rendererType) {
case 'canvas': this.renderer = new Renderer(this, this.options.theme); break;
case 'dom': this.renderer = new DomRenderer(this, this.options.theme); break;
case 'canvas': return new Renderer(this, this._colorManager.colors); break;
case 'dom': return new DomRenderer(this, this._colorManager.colors); break;
default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`);
}
this.register(this.renderer);
}
/**
@@ -775,9 +832,14 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
*/
private _setTheme(theme: ITheme): void {
this._theme = theme;
const colors = this.renderer.setTheme(theme);
if (this._colorManager) {
this._colorManager.setTheme(theme);
}
if (this._renderCoordinator) {
this._renderCoordinator.setColors(this._colorManager.colors);
}
if (this.viewport) {
this.viewport.onThemeChanged(colors);
this.viewport.onThemeChange(this._colorManager.colors);
}
}
@@ -854,17 +916,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
if (ch > 127) ch = 127;
data.push(ch);
} else {
if (ch === 2047) {
data.push(0);
if (ch > 2047) {
data.push(2047);
return;
}
if (ch < 127) {
data.push(ch);
} else {
if (ch > 2047) ch = 2047;
data.push(0xC0 | (ch >> 6));
data.push(0x80 | (ch & 0x3F));
}
data.push(ch);
}
}
@@ -1136,8 +1192,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
* @param end The row to end at (between start and this.rows - 1).
*/
public refresh(start: number, end: number): void {
if (this.renderer) {
this.renderer.refreshRows(start, end);
if (this._renderCoordinator) {
this._renderCoordinator.refreshRows(start, end);
}
}
@@ -1180,8 +1236,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
public scroll(isWrapped: boolean = false): void {
let newLine: IBufferLine;
newLine = this._blankLine;
if (!newLine || newLine.length !== this.cols || newLine.get(0)[CHAR_DATA_ATTR_INDEX] !== this.eraseAttr()) {
newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped);
const eraseAttr = this.eraseAttrData();
if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) {
newLine = this.buffer.getBlankLine(eraseAttr, isWrapped);
this._blankLine = newLine;
}
newLine.isWrapped = isWrapped;
@@ -1236,13 +1293,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.updateRange(this.buffer.scrollTop);
this.updateRange(this.buffer.scrollBottom);
/**
* This event is emitted whenever the terminal is scrolled.
* The one parameter passed is the new y display position.
*
* @event scroll
*/
this.emit('scroll', this.buffer.ydisp);
this._onScroll.fire(this.buffer.ydisp);
}
/**
@@ -1271,7 +1322,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
if (!suppressScrollEvent) {
this.emit('scroll', this.buffer.ydisp);
this._onScroll.fire(this.buffer.ydisp);
}
this.refresh(0, this.rows - 1);
@@ -1306,6 +1357,93 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
}
/**
* Writes raw utf8 bytes to the terminal.
* @param data UintArray with UTF8 bytes to write to the terminal.
*/
public writeUtf8(data: Uint8Array): void {
// Ensure the terminal isn't disposed
if (this._isDisposed) {
return;
}
// Ignore falsy data values
if (!data) {
return;
}
this.writeBufferUtf8.push(data);
// Send XOFF to pause the pty process if the write buffer becomes too large so
// xterm.js can catch up before more data is sent. This is necessary in order
// to keep signals such as ^C responsive.
if (this.options.useFlowControl && !this._xoffSentToCatchUp && this.writeBufferUtf8.length >= WRITE_BUFFER_PAUSE_THRESHOLD) {
// XOFF - stop pty pipe
// XON will be triggered by emulator before processing data chunk
this.handler(C0.DC3);
this._xoffSentToCatchUp = true;
}
if (!this._writeInProgress && this.writeBufferUtf8.length > 0) {
// Kick off a write which will write all data in sequence recursively
this._writeInProgress = true;
// Kick off an async innerWrite so more writes can come in while processing data
setTimeout(() => {
this._innerWriteUtf8();
});
}
}
protected _innerWriteUtf8(bufferOffset: number = 0): void {
// Ensure the terminal isn't disposed
if (this._isDisposed) {
this.writeBufferUtf8 = [];
}
const startTime = Date.now();
while (this.writeBufferUtf8.length > bufferOffset) {
const data = this.writeBufferUtf8[bufferOffset];
bufferOffset++;
// If XOFF was sent in order to catch up with the pty process, resume it if
// we reached the end of the writeBuffer to allow more data to come in.
if (this._xoffSentToCatchUp && this.writeBufferUtf8.length === bufferOffset) {
this.handler(C0.DC1);
this._xoffSentToCatchUp = false;
}
this._refreshStart = this.buffer.y;
this._refreshEnd = this.buffer.y;
// HACK: Set the parser state based on it's state at the time of return.
// This works around the bug #662 which saw the parser state reset in the
// middle of parsing escape sequence in two chunks. For some reason the
// state of the parser resets to 0 after exiting parser.parse. This change
// just sets the state back based on the correct return statement.
this._inputHandler.parseUtf8(data);
this.updateRange(this.buffer.y);
this.refresh(this._refreshStart, this._refreshEnd);
if (Date.now() - startTime >= WRITE_TIMEOUT_MS) {
break;
}
}
if (this.writeBufferUtf8.length > bufferOffset) {
// Allow renderer to catch up before processing the next batch
// trim already processed chunks if we are above threshold
if (bufferOffset > WRITE_BUFFER_LENGTH_THRESHOLD) {
this.writeBufferUtf8 = this.writeBufferUtf8.slice(bufferOffset);
bufferOffset = 0;
}
setTimeout(() => this._innerWriteUtf8(bufferOffset), 0);
} else {
this._writeInProgress = false;
this.writeBufferUtf8 = [];
}
}
/**
* Writes text to the terminal.
* @param data The text to write to the terminal.
@@ -1343,19 +1481,20 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
}
protected _innerWrite(): void {
protected _innerWrite(bufferOffset: number = 0): void {
// Ensure the terminal isn't disposed
if (this._isDisposed) {
this.writeBuffer = [];
}
const writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE);
while (writeBatch.length > 0) {
const data = writeBatch.shift();
const startTime = Date.now();
while (this.writeBuffer.length > bufferOffset) {
const data = this.writeBuffer[bufferOffset];
bufferOffset++;
// If XOFF was sent in order to catch up with the pty process, resume it if
// the writeBuffer is empty to allow more data to come in.
if (this._xoffSentToCatchUp && writeBatch.length === 0 && this.writeBuffer.length === 0) {
// we reached the end of the writeBuffer to allow more data to come in.
if (this._xoffSentToCatchUp && this.writeBuffer.length === bufferOffset) {
this.handler(C0.DC1);
this._xoffSentToCatchUp = false;
}
@@ -1373,12 +1512,22 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.updateRange(this.buffer.y);
this.refresh(this._refreshStart, this._refreshEnd);
if (Date.now() - startTime >= WRITE_TIMEOUT_MS) {
break;
}
}
if (this.writeBuffer.length > 0) {
if (this.writeBuffer.length > bufferOffset) {
// Allow renderer to catch up before processing the next batch
setTimeout(() => this._innerWrite(), 0);
// trim already processed chunks if we are above threshold
if (bufferOffset > WRITE_BUFFER_LENGTH_THRESHOLD) {
this.writeBuffer = this.writeBuffer.slice(bufferOffset);
bufferOffset = 0;
}
setTimeout(() => this._innerWrite(bufferOffset), 0);
} else {
this._writeInProgress = false;
this.writeBuffer = [];
}
}
@@ -1439,13 +1588,13 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
const joinerId = this.renderer.registerCharacterJoiner(handler);
const joinerId = this._renderCoordinator.registerCharacterJoiner(handler);
this.refresh(0, this.rows - 1);
return joinerId;
}
public deregisterCharacterJoiner(joinerId: number): void {
if (this.renderer.deregisterCharacterJoiner(joinerId)) {
if (this._renderCoordinator.deregisterCharacterJoiner(joinerId)) {
this.refresh(0, this.rows - 1);
}
}
@@ -1470,6 +1619,16 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
return this.selectionManager ? this.selectionManager.hasSelection : false;
}
/**
* Selects text within the terminal.
* @param column The column the selection starts at..
* @param row The row the selection starts at.
* @param length The length of the selection.
*/
public select(column: number, row: number, length: number): void {
this.selectionManager.setSelection(column, row, length);
}
/**
* Gets the terminal's current selection, this is useful for implementing copy
* behavior outside of xterm.js.
@@ -1478,6 +1637,19 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
return this.selectionManager ? this.selectionManager.selectionText : '';
}
public getSelectionPosition(): ISelectionPosition | undefined {
if (!this.selectionManager.hasSelection) {
return undefined;
}
return {
startColumn: this.selectionManager.selectionStart[0],
startRow: this.selectionManager.selectionStart[1],
endColumn: this.selectionManager.selectionEnd[0],
endRow: this.selectionManager.selectionEnd[1]
};
}
/**
* Clears the current terminal selection.
*/
@@ -1554,7 +1726,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
this.emit('keydown', event);
this.emit('key', result.key, event);
this._onKey.fire({ key: result.key, domEvent: event });
this.showCursor();
this.handler(result.key);
@@ -1633,7 +1805,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
key = String.fromCharCode(key);
this.emit('keypress', key, ev);
this.emit('key', key, ev);
this._onKey.fire({ key, domEvent: ev });
this.showCursor();
this.handler(key);
@@ -1710,7 +1882,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
this.refresh(0, this.rows - 1);
this.emit('resize', {cols: x, rows: y});
this._onResize.fire({ cols: x, rows: y });
}
/**
@@ -1750,21 +1922,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.buffer.ybase = 0;
this.buffer.y = 0;
for (let i = 1; i < this.rows; i++) {
this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR));
this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR_DATA));
}
this.refresh(0, this.rows - 1);
this.emit('scroll', this.buffer.ydisp);
}
/**
* If cur return the back color xterm feature attribute. Else return default attribute.
* @param cur
*/
public ch(cur?: boolean): CharData {
if (cur) {
return [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
}
return [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
this._onScroll.fire(this.buffer.ydisp);
}
/**
@@ -1776,7 +1937,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
/**
* Emit the 'data' event and populate the given data.
* Emit the data event and populate the given data.
* @param data The data to populate in the event.
*/
public handler(data: string): void {
@@ -1794,7 +1955,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
if (this.buffer.ybase !== this.buffer.ydisp) {
this.scrollToBottom();
}
this.emit('data', data);
this._onData.fire(data);
}
/**
@@ -1802,13 +1963,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
* @param title The title to populate in the event.
*/
public handleTitle(title: string): void {
/**
* This event is emitted when the title of the terminal is changed
* from inside the terminal. The parameter is the new title.
*
* @event title
*/
this.emit('title', title);
this._onTitleChange.fire(title);
}
/**
@@ -1842,7 +1997,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// blankLine(true) is xterm/linux behavior
const scrollRegionHeight = this.buffer.scrollBottom - this.buffer.scrollTop;
this.buffer.lines.shiftElements(this.buffer.y + this.buffer.ybase, scrollRegionHeight, 1);
this.buffer.lines.set(this.buffer.y + this.buffer.ybase, this.buffer.getBlankLine(this.eraseAttr()));
this.buffer.lines.set(this.buffer.y + this.buffer.ybase, this.buffer.getBlankLine(this.eraseAttrData()));
this.updateRange(this.buffer.scrollTop);
this.updateRange(this.buffer.scrollBottom);
} else {
@@ -1854,15 +2009,34 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
* ESC c Full Reset (RIS).
*/
public reset(): void {
/**
* Since _setup handles a full terminal creation, we have to carry forward
* a few things that should not reset.
*/
this.options.rows = this.rows;
this.options.cols = this.cols;
const customKeyEventHandler = this._customKeyEventHandler;
const inputHandler = this._inputHandler;
const cursorState = this.cursorState;
const writeBuffer = this.writeBuffer;
const writeBufferUtf8 = this.writeBufferUtf8;
const writeInProgress = this._writeInProgress;
const xoffSentToCatchUp = this._xoffSentToCatchUp;
const userScrolling = this._userScrolling;
this._setup();
// reattach
this._customKeyEventHandler = customKeyEventHandler;
this._inputHandler = inputHandler;
this.cursorState = cursorState;
this.writeBuffer = writeBuffer;
this.writeBufferUtf8 = writeBufferUtf8;
this._writeInProgress = writeInProgress;
this._xoffSentToCatchUp = xoffSentToCatchUp;
this._userScrolling = userScrolling;
// do a full screen refresh
this.refresh(0, this.rows - 1);
if (this.viewport) {
this.viewport.syncScrollArea();
@@ -1887,46 +2061,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
return false;
}
// TODO: Remove when true color is implemented
public matchColor(r1: number, g1: number, b1: number): number {
const hash = (r1 << 16) | (g1 << 8) | b1;
if (matchColorCache[hash] !== null && matchColorCache[hash] !== undefined) {
return matchColorCache[hash];
}
let ldiff = Infinity;
let li = -1;
let i = 0;
let c: number;
let r2: number;
let g2: number;
let b2: number;
let diff: number;
for (; i < DEFAULT_ANSI_COLORS.length; i++) {
c = DEFAULT_ANSI_COLORS[i].rgba;
r2 = c >>> 24;
g2 = c >>> 16 & 0xFF;
b2 = c >>> 8 & 0xFF;
// assume that alpha is 0xFF
diff = matchColorDistance(r1, g1, b1, r2, g2, b2);
if (diff === 0) {
li = i;
break;
}
if (diff < ldiff) {
ldiff = diff;
li = i;
}
}
return matchColorCache[hash] = li;
}
private _visualBell(): boolean {
return false;
// return this.options.bellStyle === 'visual' ||
@@ -1949,19 +2083,3 @@ function wasModifierKeyOnlyEvent(ev: KeyboardEvent): boolean {
ev.keyCode === 17 || // Ctrl
ev.keyCode === 18; // Alt
}
/**
* TODO:
* The below color-related code can be removed when true color is implemented.
* It's only purpose is to match true color requests with the closest matching
* ANSI color code.
*/
const matchColorCache: {[colorRGBHash: number]: number} = {};
// http://stackoverflow.com/questions/1633828
function matchColorDistance(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number {
return Math.pow(30 * (r1 - r2), 2)
+ Math.pow(59 * (g1 - g2), 2)
+ Math.pow(11 * (b1 - b2), 2);
}
@@ -3,13 +3,16 @@
* @license MIT
*/
import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types';
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferLine, IBufferStringIterator } from '../Types';
import { ICircularList, XtermListener } from '../common/Types';
import { Buffer } from '../Buffer';
import * as Browser from '../core/Platform';
import { ITheme, IDisposable, IMarker } from 'xterm';
import { Terminal } from '../Terminal';
import { IRenderer, IRenderDimensions } from './renderer/Types';
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferStringIterator } from './Types';
import { IBufferLine, ICellData, IAttributeData } from './core/Types';
import { ICircularList, XtermListener } from './common/Types';
import { Buffer } from './Buffer';
import * as Browser from './common/Platform';
import { IDisposable, IMarker, IEvent, ISelectionPosition } from 'xterm';
import { Terminal } from './Terminal';
import { AttributeData } from './core/buffer/BufferLine';
import { IColorManager, IColorSet } from './ui/Types';
export class TestTerminal extends Terminal {
writeSync(data: string): void {
@@ -19,6 +22,15 @@ export class TestTerminal extends Terminal {
}
export class MockTerminal implements ITerminal {
onCursorMove: IEvent<void>;
onLineFeed: IEvent<void>;
onSelectionChange: IEvent<void>;
onData: IEvent<string>;
onTitleChange: IEvent<string>;
onScroll: IEvent<number>;
onKey: IEvent<{ key: string; domEvent: KeyboardEvent; }>;
onRender: IEvent<{ start: number; end: number; }>;
onResize: IEvent<{ cols: number; rows: number; }>;
markers: IMarker[];
addMarker(cursorYOffset: number): IMarker {
throw new Error('Method not implemented.');
@@ -72,9 +84,15 @@ export class MockTerminal implements ITerminal {
getSelection(): string {
throw new Error('Method not implemented.');
}
getSelectionPosition(): ISelectionPosition | undefined {
throw new Error('Method not implemented.');
}
clearSelection(): void {
throw new Error('Method not implemented.');
}
select(column: number, row: number, length: number): void {
throw new Error('Method not implemented.');
}
selectAll(): void {
throw new Error('Method not implemented.');
}
@@ -99,6 +117,9 @@ export class MockTerminal implements ITerminal {
write(data: string): void {
throw new Error('Method not implemented.');
}
writeUtf8(data: Uint8Array): void {
throw new Error('Method not implemented.');
}
bracketedPasteMode: boolean;
mouseHelper: IMouseHelper;
renderer: IRenderer;
@@ -165,6 +186,7 @@ export class MockTerminal implements ITerminal {
}
export class MockCharMeasure implements ICharMeasure {
onCharSizeChanged: IEvent<void>;
width: number;
height: number;
measure(options: ITerminalOptions): void {
@@ -187,7 +209,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
insertMode: boolean;
wraparoundMode: boolean;
bracketedPasteMode: boolean;
curAttr: number;
curAttrData = new AttributeData();
savedCols: number;
x10Mouse: boolean;
vt200Mouse: boolean;
@@ -222,7 +244,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
setgLevel(g: number): void {
throw new Error('Method not implemented.');
}
eraseAttr(): number {
eraseAttrData(): IAttributeData {
throw new Error('Method not implemented.');
}
eraseRight(x: number, y: number): void {
@@ -309,7 +331,7 @@ export class MockBuffer implements IBuffer {
scrollTop: number;
savedY: number;
savedX: number;
savedCurAttr: number;
savedCurAttrData = new AttributeData();
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string {
return Buffer.prototype.translateBufferLineToString.apply(this, arguments);
}
@@ -325,7 +347,7 @@ export class MockBuffer implements IBuffer {
setLines(lines: ICircularList<IBufferLine>): void {
this.lines = lines;
}
getBlankLine(attr: number, isWrapped?: boolean): IBufferLine {
getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine {
return Buffer.prototype.getBlankLine.apply(this, arguments);
}
stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[] {
@@ -334,9 +356,17 @@ export class MockBuffer implements IBuffer {
iterator(trimRight: boolean, startIndex?: number, endIndex?: number): IBufferStringIterator {
return Buffer.prototype.iterator.apply(this, arguments);
}
getNullCell(attr?: IAttributeData): ICellData {
throw new Error('Method not implemented.');
}
getWhitespaceCell(attr?: IAttributeData): ICellData {
throw new Error('Method not implemented.');
}
}
export class MockRenderer implements IRenderer {
onCanvasResize: IEvent<{ width: number; height: number; }>;
onRender: IEvent<{ start: number; end: number; }>;
dispose(): void {
throw new Error('Method not implemented.');
}
@@ -354,7 +384,9 @@ export class MockRenderer implements IRenderer {
throw new Error('Method not implemented.');
}
dimensions: IRenderDimensions;
setTheme(theme: ITheme): IColorSet { return <IColorSet>{}; }
setColors(colors: IColorSet): void {
throw new Error('Method not implemented.');
}
onResize(cols: number, rows: number): void {}
onCharSizeChanged(): void {}
onBlur(): void {}
@@ -362,9 +394,9 @@ export class MockRenderer implements IRenderer {
onSelectionChanged(start: [number, number], end: [number, number]): void {}
onCursorMove(): void {}
onOptionsChanged(): void {}
onWindowResize(devicePixelRatio: number): void {}
onDevicePixelRatioChange(): void {}
clear(): void {}
refreshRows(start: number, end: number): void {}
renderRows(start: number, end: number): void {}
registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; }
deregisterCharacterJoiner(): boolean { return true; }
}
@@ -374,7 +406,7 @@ export class MockViewport implements IViewport {
throw new Error('Method not implemented.');
}
scrollBarWidth: number = 0;
onThemeChanged(colors: IColorSet): void {
onThemeChange(colors: IColorSet): void {
throw new Error('Method not implemented.');
}
onWheel(ev: WheelEvent): void {
+99 -44
View File
@@ -3,15 +3,14 @@
* @license MIT
*/
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable } from 'xterm';
import { IColorSet, IRenderer } from './renderer/Types';
import { IMouseZoneManager } from './ui/Types';
import { ICharset } from './core/Types';
import { ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable, IMarker, ISelectionPosition } from 'xterm';
import { ICharset, IAttributeData, ICellData, IBufferLine, CharData } from './core/Types';
import { ICircularList } from './common/Types';
import { IEvent } from './common/EventEmitter2';
import { IColorSet } from './ui/Types';
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
export type CharData = [number, string, number, number];
export type LineData = CharData[];
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
@@ -22,12 +21,6 @@ export type CharacterJoinerHandler = (text: string) => [number, number][];
// BufferIndex denotes a position in the buffer: [rowIndex, colIndex]
export type BufferIndex = [number, number];
export const enum LinkHoverEventTypes {
HOVER = 'linkhover',
TOOLTIP = 'linktooltip',
LEAVE = 'linkleave'
}
/**
* This interface encapsulates everything needed from the Terminal by the
* InputHandler. This cleanly separates the large amount of methods needed by
@@ -48,7 +41,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
insertMode: boolean;
wraparoundMode: boolean;
bracketedPasteMode: boolean;
curAttr: number;
curAttrData: IAttributeData;
savedCols: number;
x10Mouse: boolean;
vt200Mouse: boolean;
@@ -70,7 +63,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
updateRange(y: number): void;
scroll(isWrapped?: boolean): void;
setgLevel(g: number): void;
eraseAttr(): number;
eraseAttrData(): IAttributeData;
is(term: string): boolean;
setgCharset(g: number, charset: ICharset): void;
resize(x: number, y: number): void;
@@ -78,7 +71,6 @@ export interface IInputHandlingTerminal extends IEventEmitter {
reset(): void;
showCursor(): void;
refresh(start: number, end: number): void;
matchColor(r1: number, g1: number, b1: number): number;
error(text: string, data?: any): void;
setOption(key: string, value: any): void;
tabSet(): void;
@@ -95,7 +87,7 @@ export interface IViewport extends IDisposable {
onWheel(ev: WheelEvent): void;
onTouchStart(ev: TouchEvent): void;
onTouchMove(ev: TouchEvent): void;
onThemeChanged(colors: IColorSet): void;
onThemeChange(colors: IColorSet): void;
}
export interface ICompositionHelper {
@@ -111,6 +103,7 @@ export interface ICompositionHelper {
*/
export interface IInputHandler {
parse(data: string): void;
parseUtf8(data: Uint8Array): void;
print(data: Uint32Array, start: number, end: number): void;
/** C0 BEL */ bell(): void;
@@ -194,7 +187,7 @@ export interface ILinkMatcher {
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
}
export interface ILinkHoverEvent {
export interface ILinkifierEvent {
x1: number;
y1: number;
x2: number;
@@ -203,11 +196,10 @@ export interface ILinkHoverEvent {
fg: number;
}
export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
screenElement: HTMLElement;
selectionManager: ISelectionManager;
charMeasure: ICharMeasure;
renderer: IRenderer;
browser: IBrowser;
writeBuffer: string[];
cursorHidden: boolean;
@@ -221,10 +213,6 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
bracketedPasteMode: boolean;
applicationCursor: boolean;
/**
* Emit the 'data' event and populate the given data.
* @param data The data to populate in the event.
*/
handler(data: string): void;
scrollLines(disp: number, suppressScrollEvent?: boolean): void;
cancel(ev: Event, force?: boolean): boolean | void;
@@ -232,6 +220,58 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
showCursor(): void;
}
// Portions of the public API that are required by the internal Terminal
export interface IPublicTerminal extends IDisposable, IEventEmitter {
textarea: HTMLTextAreaElement;
rows: number;
cols: number;
buffer: IBuffer;
markers: IMarker[];
onCursorMove: IEvent<void>;
onData: IEvent<string>;
onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>;
onLineFeed: IEvent<void>;
onScroll: IEvent<number>;
onSelectionChange: IEvent<void>;
onRender: IEvent<{ start: number, end: number }>;
onResize: IEvent<{ cols: number, rows: number }>;
onTitleChange: IEvent<string>;
blur(): void;
focus(): void;
resize(columns: number, rows: number): void;
writeln(data: string): void;
open(parent: HTMLElement): void;
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable;
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number;
deregisterLinkMatcher(matcherId: number): void;
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
deregisterCharacterJoiner(joinerId: number): void;
addMarker(cursorYOffset: number): IMarker;
hasSelection(): boolean;
getSelection(): string;
getSelectionPosition(): ISelectionPosition | undefined;
clearSelection(): void;
select(column: number, row: number, length: number): void;
selectAll(): void;
selectLines(start: number, end: number): void;
dispose(): void;
destroy(): void;
scrollLines(amount: number): void;
scrollPages(pageCount: number): void;
scrollToTop(): void;
scrollToBottom(): void;
scrollToLine(line: number): void;
clear(): void;
write(data: string): void;
writeUtf8(data: Uint8Array): void;
getOption(key: string): any;
setOption(key: string, value: any): void;
refresh(start: number, end: number): void;
reset(): void;
}
export interface IBufferAccessor {
buffer: IBuffer;
}
@@ -252,6 +292,9 @@ export interface IMouseHelper {
export interface ICharMeasure {
width: number;
height: number;
onCharSizeChanged: IEvent<void>;
measure(options: ITerminalOptions): void;
}
@@ -289,24 +332,28 @@ export interface IBuffer {
hasScrollback: boolean;
savedY: number;
savedX: number;
savedCurAttr: number;
savedCurAttrData: IAttributeData;
isCursorInViewport: boolean;
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string;
getWrappedRangeForLine(y: number): { first: number, last: number };
nextStop(x?: number): number;
prevStop(x?: number): number;
getBlankLine(attr: number, isWrapped?: boolean): IBufferLine;
getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine;
stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[];
iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator;
getNullCell(attr?: IAttributeData): ICellData;
getWhitespaceCell(attr?: IAttributeData): ICellData;
}
export interface IBufferSet extends IEventEmitter {
export interface IBufferSet {
alt: IBuffer;
normal: IBuffer;
active: IBuffer;
onBufferActivate: IEvent<{ activeBuffer: IBuffer, inactiveBuffer: IBuffer }>;
activateNormalBuffer(): void;
activateAltBuffer(fillAttr?: number): void;
activateAltBuffer(fillAttr?: IAttributeData): void;
}
export interface ISelectionManager {
@@ -321,7 +368,17 @@ export interface ISelectionManager {
selectWordAtCursor(event: MouseEvent): void;
}
export interface ILinkifier extends IEventEmitter {
export interface ISelectionRedrawRequestEvent {
start: [number, number];
end: [number, number];
columnSelectMode: boolean;
}
export interface ILinkifier {
onLinkHover: IEvent<ILinkifierEvent>;
onLinkLeave: IEvent<ILinkifierEvent>;
onLinkTooltip: IEvent<ILinkifierEvent>;
attachToDom(mouseZoneManager: IMouseZoneManager): void;
linkifyRows(start: number, end: number): void;
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
@@ -519,21 +576,19 @@ export interface IEscapeSequenceParser extends IDisposable {
clearErrorHandler(): void;
}
/**
* Interface for a line in the terminal buffer.
*/
export interface IBufferLine {
length: number;
isWrapped: boolean;
get(index: number): CharData;
set(index: number, value: CharData): void;
insertCells(pos: number, n: number, ch: CharData): void;
deleteCells(pos: number, n: number, fill: CharData): void;
replaceCells(start: number, end: number, fill: CharData): void;
resize(cols: number, fill: CharData): void;
fill(fillCharData: CharData): void;
copyFrom(line: IBufferLine): void;
clone(): IBufferLine;
getTrimmedLength(): number;
translateToString(trimRight?: boolean, startCol?: number, endCol?: number): string;
export interface IMouseZoneManager extends IDisposable {
add(zone: IMouseZone): void;
clearAll(start?: number, end?: number): void;
}
export interface IMouseZone {
x1: number;
x2: number;
y1: number;
y2: number;
clickCallback: (e: MouseEvent) => any;
hoverCallback: (e: MouseEvent) => any | undefined;
tooltipCallback: (e: MouseEvent) => any | undefined;
leaveCallback: () => any | undefined;
willLinkActivate: (e: MouseEvent) => boolean;
}
+14 -8
View File
@@ -3,11 +3,12 @@
* @license MIT
*/
import { IColorSet } from './renderer/Types';
import { ITerminal, IViewport } from './Types';
import { CharMeasure } from './ui/CharMeasure';
import { CharMeasure } from './CharMeasure';
import { Disposable } from './common/Lifecycle';
import { addDisposableDomListener } from './ui/Lifecycle';
import { IColorSet } from './ui/Types';
import { IRenderDimensions } from './renderer/Types';
const FALLBACK_SCROLL_BAR_WIDTH = 15;
@@ -43,7 +44,8 @@ export class Viewport extends Disposable implements IViewport {
private _terminal: ITerminal,
private _viewportElement: HTMLElement,
private _scrollArea: HTMLElement,
private _charMeasure: CharMeasure
private _charMeasure: CharMeasure,
private _dimensions: IRenderDimensions
) {
super();
@@ -57,7 +59,11 @@ export class Viewport extends Disposable implements IViewport {
setTimeout(() => this.syncScrollArea(), 0);
}
public onThemeChanged(colors: IColorSet): void {
public onDimensionsChance(dimensions: IRenderDimensions): void {
this._dimensions = dimensions;
}
public onThemeChange(colors: IColorSet): void {
this._viewportElement.style.backgroundColor = colors.background.css;
}
@@ -73,9 +79,9 @@ export class Viewport extends Disposable implements IViewport {
private _innerRefresh(): void {
if (this._charMeasure.height > 0) {
this._currentRowHeight = this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio;
this._currentRowHeight = this._dimensions.scaledCellHeight / window.devicePixelRatio;
this._lastRecordedViewportHeight = this._viewportElement.offsetHeight;
const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._terminal.renderer.dimensions.canvasHeight);
const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._dimensions.canvasHeight);
if (this._lastRecordedBufferHeight !== newBufferHeight) {
this._lastRecordedBufferHeight = newBufferHeight;
this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px';
@@ -106,7 +112,7 @@ export class Viewport extends Disposable implements IViewport {
}
// If viewport height changed
if (this._lastRecordedViewportHeight !== (<any>this._terminal).renderer.dimensions.canvasHeight) {
if (this._lastRecordedViewportHeight !== this._dimensions.canvasHeight) {
this._refresh();
return;
}
@@ -125,7 +131,7 @@ export class Viewport extends Disposable implements IViewport {
}
// If row height changed
if (this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
if (this._dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
this._refresh();
return;
}
+28
View File
@@ -0,0 +1,28 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable } from 'xterm';
import { ITerminal } from './Types';
import { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from './core/buffer/BufferLine';
export function applyWindowsMode(terminal: ITerminal): IDisposable {
// Winpty does not support wraparound mode which means that lines will never
// be marked as wrapped. This causes issues for things like copying a line
// retaining the wrapped new line characters or if consumers are listening
// in on the data stream.
//
// The workaround for this is to listen to every incoming line feed and mark
// the line as wrapped if the last character in the previous line is not a
// space. This is certainly not without its problems, but generally on
// Windows when text reaches the end of the terminal it's likely going to be
// wrapped.
return terminal.onLineFeed(() => {
const line = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y - 1);
const lastChar = line.get(terminal.cols - 1);
const nextLine = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y);
nextLine.isWrapped = (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE);
});
}
+1
View File
@@ -14,6 +14,7 @@ export interface IAttachAddonTerminal extends Terminal {
__socket?: WebSocket;
__attachSocketBuffer?: string;
__dataListener?: IDisposable;
__getMessage?(ev: MessageEvent): void;
__flushBuffer?(): void;
+4 -2
View File
@@ -90,7 +90,8 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean
addonTerminal._core.register(addSocketListener(socket, 'message', addonTerminal.__getMessage));
if (bidirectional) {
addonTerminal._core.register(addonTerminal.addDisposableListener('data', addonTerminal.__sendData));
addonTerminal.__dataListener = addonTerminal.onData(addonTerminal.__sendData);
addonTerminal._core.register(addonTerminal.__dataListener);
}
addonTerminal._core.register(addSocketListener(socket, 'close', () => detach(addonTerminal, socket)));
@@ -119,7 +120,8 @@ function addSocketListener(socket: WebSocket, type: string, handler: (this: WebS
*/
export function detach(term: Terminal, socket: WebSocket): void {
const addonTerminal = <IAttachAddonTerminal>term;
addonTerminal.off('data', addonTerminal.__sendData);
addonTerminal.__dataListener.dispose();
addonTerminal.__dataListener = undefined;
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;
+1 -2
View File
@@ -10,8 +10,7 @@
"outDir": "../../../lib/addons/attach/",
"sourceMap": true,
"removeComments": true,
"declaration": true,
"preserveWatchOutput": true
"declaration": true
},
"include": [
"**/*.ts",
+3 -3
View File
@@ -39,8 +39,8 @@ export function proposeGeometry(term: Terminal): IGeometry {
const availableHeight = parentElementHeight - elementPaddingVer;
const availableWidth = parentElementWidth - elementPaddingHor - (<any>term)._core.viewport.scrollBarWidth;
const geometry = {
cols: Math.floor(availableWidth / (<any>term)._core.renderer.dimensions.actualCellWidth),
rows: Math.floor(availableHeight / (<any>term)._core.renderer.dimensions.actualCellHeight)
cols: Math.floor(availableWidth / (<any>term)._core._renderCoordinator.dimensions.actualCellWidth),
rows: Math.floor(availableHeight / (<any>term)._core._renderCoordinator.dimensions.actualCellHeight)
};
return geometry;
}
@@ -50,7 +50,7 @@ export function fit(term: Terminal): void {
if (geometry) {
// Force a full render
if (term.rows !== geometry.rows || term.cols !== geometry.cols) {
(<any>term)._core.renderer.clear();
(<any>term)._core._renderCoordinator.clear();
term.resize(geometry.cols, geometry.rows);
}
}
-1
View File
@@ -11,7 +11,6 @@
"sourceMap": true,
"removeComments": true,
"declaration": true,
"preserveWatchOutput": true,
"types": [
"../../node_modules/@types/mocha"
]
-1
View File
@@ -11,7 +11,6 @@
"sourceMap": true,
"removeComments": true,
"declaration": true,
"preserveWatchOutput": true,
"types": [
"../../node_modules/@types/mocha"
]
-7
View File
@@ -5,15 +5,8 @@
import { Terminal } from 'xterm';
// TODO: Don't rely on this private API
export interface ITerminalCore {
buffer: any;
selectionManager: any;
}
export interface ISearchAddonTerminal extends Terminal {
__searchHelper?: ISearchHelper;
_core: ITerminalCore;
}
export interface ISearchHelper {
+78 -36
View File
@@ -4,6 +4,7 @@
*/
import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces';
import { IDisposable } from 'xterm';
const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?';
const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
@@ -19,6 +20,8 @@ export class SearchHelper implements ISearchHelper {
*/
private _linesCache: string[] = null;
private _linesCacheTimeoutId = 0;
private _cursorMoveListener: IDisposable | undefined;
private _resizeListener: IDisposable | undefined;
constructor(private _terminal: ISearchAddonTerminal) {
this._destroyLinesCache = this._destroyLinesCache.bind(this);
@@ -32,35 +35,48 @@ export class SearchHelper implements ISearchHelper {
* @return Whether a result was found.
*/
public findNext(term: string, searchOptions?: ISearchOptions): boolean {
const selectionManager = this._terminal._core.selectionManager;
const {incremental} = searchOptions;
let result: ISearchResult;
if (!term || term.length === 0) {
selectionManager.clearSelection();
this._terminal.clearSelection();
return false;
}
let startCol: number = 0;
let startRow = this._terminal._core.buffer.ydisp;
let startRow = this._terminal.buffer.viewportY;
if (selectionManager.selectionEnd) {
if (this._terminal.hasSelection()) {
// Start from the selection end if there is a selection
// For incremental search, use existing row
if (this._terminal.getSelection().length !== 0) {
startRow = incremental ? selectionManager.selectionStart[1] : selectionManager.selectionEnd[1];
startCol = incremental ? selectionManager.selectionStart[0] : selectionManager.selectionEnd[0];
}
const currentSelection = this._terminal.getSelectionPosition();
startRow = incremental ? currentSelection.startRow : currentSelection.endRow;
startCol = incremental ? currentSelection.startColumn : currentSelection.endColumn;
}
this._initLinesCache();
// A row that has isWrapped = false
let findingRow = startRow;
// index of beginning column that _findInLine need to scan.
let cumulativeCols = startCol;
// If startRow is wrapped row, scan for unwrapped row above.
// So we can start matching on wrapped line from long unwrapped line.
while (this._terminal.buffer.getLine(findingRow).isWrapped) {
findingRow--;
cumulativeCols += this._terminal.cols;
}
// Search startRow
result = this._findInLine(term, startRow, startCol, searchOptions);
result = this._findInLine(term, findingRow, cumulativeCols, searchOptions);
// Search from startRow + 1 to end
if (!result) {
for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
for (let y = startRow + 1; y < this._terminal.buffer.baseY + this._terminal.rows; y++) {
// If the current line is wrapped line, increase index of column to ignore the previous scan
// Otherwise, reset beginning column index to zero with set new unwrapped line index
result = this._findInLine(term, y, 0, searchOptions);
if (result) {
break;
@@ -71,7 +87,7 @@ export class SearchHelper implements ISearchHelper {
// Search from the top to the startRow (search the whole startRow again in
// case startCol > 0)
if (!result) {
for (let y = 0; y <= startRow; y++) {
for (let y = 0; y < findingRow; y++) {
result = this._findInLine(term, y, 0, searchOptions);
if (result) {
break;
@@ -91,24 +107,22 @@ export class SearchHelper implements ISearchHelper {
* @return Whether a result was found.
*/
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean {
const selectionManager = this._terminal._core.selectionManager;
let result: ISearchResult;
if (!term || term.length === 0) {
selectionManager.clearSelection();
this._terminal.clearSelection();
return false;
}
const isReverseSearch = true;
let startRow = this._terminal._core.buffer.ydisp;
let startCol: number = this._terminal._core.buffer.lines.get(startRow).length;
let startRow = this._terminal.buffer.viewportY + this._terminal.rows - 1;
let startCol = this._terminal.cols;
if (selectionManager.selectionStart) {
if (this._terminal.hasSelection()) {
// Start from the selection start if there is a selection
if (this._terminal.getSelection().length !== 0) {
startRow = selectionManager.selectionStart[1];
startCol = selectionManager.selectionStart[0];
}
const currentSelection = this._terminal.getSelectionPosition();
startRow = currentSelection.startRow;
startCol = currentSelection.startColumn;
}
this._initLinesCache();
@@ -118,23 +132,42 @@ export class SearchHelper implements ISearchHelper {
// Search from startRow - 1 to top
if (!result) {
// If the line is wrapped line, increase number of columns that is needed to be scanned
// Se we can scan on wrapped line from unwrapped line
let cumulativeCols = this._terminal.cols;
if (this._terminal.buffer.getLine(startRow).isWrapped) {
cumulativeCols += startCol;
}
for (let y = startRow - 1; y >= 0; y--) {
result = this._findInLine(term, y, this._terminal._core.buffer.lines.get(y).length, searchOptions, isReverseSearch);
result = this._findInLine(term, y, cumulativeCols, searchOptions, isReverseSearch);
if (result) {
break;
}
// If the current line is wrapped line, increase scanning range,
// preparing for scanning on unwrapped line
if (this._terminal.buffer.getLine(y).isWrapped) {
cumulativeCols += this._terminal.cols;
} else {
cumulativeCols = this._terminal.cols;
}
}
}
// Search from the bottom to startRow (search the whole startRow again in
// case startCol > 0)
if (!result) {
const searchFrom = this._terminal._core.buffer.ybase + this._terminal.rows - 1;
const searchFrom = this._terminal.buffer.baseY + this._terminal.rows - 1;
let cumulativeCols = this._terminal.cols;
for (let y = searchFrom; y >= startRow; y--) {
result = this._findInLine(term, y, this._terminal._core.buffer.lines.get(y).length, searchOptions, isReverseSearch);
result = this._findInLine(term, y, cumulativeCols, searchOptions, isReverseSearch);
if (result) {
break;
}
if (this._terminal.buffer.getLine(y).isWrapped) {
cumulativeCols += this._terminal.cols;
} else {
cumulativeCols = this._terminal.cols;
}
}
}
@@ -147,8 +180,9 @@ export class SearchHelper implements ISearchHelper {
*/
private _initLinesCache(): void {
if (!this._linesCache) {
this._linesCache = new Array(this._terminal._core.buffer.length);
this._terminal.on('cursormove', this._destroyLinesCache);
this._linesCache = new Array(this._terminal.buffer.length);
this._cursorMoveListener = this._terminal.onCursorMove(() => this._destroyLinesCache());
this._resizeListener = this._terminal.onResize(() => this._destroyLinesCache());
}
window.clearTimeout(this._linesCacheTimeoutId);
@@ -157,7 +191,14 @@ export class SearchHelper implements ISearchHelper {
private _destroyLinesCache(): void {
this._linesCache = null;
this._terminal.off('cursormove', this._destroyLinesCache);
if (this._cursorMoveListener) {
this._cursorMoveListener.dispose();
this._cursorMoveListener = undefined;
}
if (this._resizeListener) {
this._resizeListener.dispose();
this._resizeListener = undefined;
}
if (this._linesCacheTimeoutId) {
window.clearTimeout(this._linesCacheTimeoutId);
this._linesCacheTimeoutId = 0;
@@ -187,10 +228,11 @@ export class SearchHelper implements ISearchHelper {
* @return The search result if it was found.
*/
protected _findInLine(term: string, row: number, col: number, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult {
if (this._terminal._core.buffer.lines.get(row).isWrapped) {
// Ignore wrapped lines, only consider on unwrapped line (first row of command string).
if (this._terminal.buffer.getLine(row).isWrapped) {
return;
}
let stringLine = this._linesCache ? this._linesCache[row] : void 0;
if (stringLine === void 0) {
stringLine = this.translateBufferLineToStringWithWrap(row, true);
@@ -240,18 +282,18 @@ export class SearchHelper implements ISearchHelper {
return;
}
const line = this._terminal._core.buffer.lines.get(row);
const line = this._terminal.buffer.getLine(row);
for (let i = 0; i < resultIndex; i++) {
const charData = line.get(i);
const cell = line.getCell(i);
// Adjust the searchIndex to normalize emoji into single chars
const char = charData[1/*CHAR_DATA_CHAR_INDEX*/];
const char = cell.char;
if (char.length > 1) {
resultIndex -= char.length - 1;
}
// Adjust the searchIndex for empty characters following wide unicode
// chars (eg. CJK)
const charWidth = charData[2/*CHAR_DATA_WIDTH_INDEX*/];
const charWidth = cell.width;
if (charWidth === 0) {
resultIndex++;
}
@@ -276,9 +318,9 @@ export class SearchHelper implements ISearchHelper {
let lineWrapsToNext: boolean;
do {
const nextLine = this._terminal._core.buffer.lines.get(lineIndex + 1);
const nextLine = this._terminal.buffer.getLine(lineIndex + 1);
lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
lineString += this._terminal._core.buffer.translateBufferLineToString(lineIndex, !lineWrapsToNext && trimRight).substring(0, this._terminal.cols);
lineString += this._terminal.buffer.getLine(lineIndex).translateToString(!lineWrapsToNext && trimRight).substring(0, this._terminal.cols);
lineIndex++;
} while (lineWrapsToNext);
@@ -295,8 +337,8 @@ export class SearchHelper implements ISearchHelper {
this._terminal.clearSelection();
return false;
}
this._terminal._core.selectionManager.setSelection(result.col, result.row, result.term.length);
this._terminal.scrollLines(result.row - this._terminal._core.buffer.ydisp);
this._terminal.select(result.col, result.row, result.term.length);
this._terminal.scrollLines(result.row - this._terminal.buffer.viewportY);
return true;
}
}
+45
View File
@@ -23,11 +23,56 @@ class MockTerminal {
get core(): any {
return this._core;
}
get buffer(): IBuffer {
// TODO: This is a hacky workaround until we use puppeteer for addon tests
const buffer = this._core.buffer;
return {
cursorY: buffer.y,
cursorX: buffer.x,
viewportY: buffer.ydisp,
baseY: buffer.ybase,
length: buffer.length,
getLine(y: number): IBufferLine {
return {
isWrapped: buffer.lines.get(y) ? buffer.lines.get(y).isWrapped : false,
getCell(x: number): IBufferCell {
return {
char: buffer.lines.get(y).get(x)[1/*CHAR_DATA_CHAR_INDEX*/],
width: buffer.lines.get(y).get(x)[2/*CHAR_DATA_WIDTH_INDEX*/]
};
},
translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string {
return buffer.translateBufferLineToString(y, trimRight);
}
};
}
};
}
pushWriteData(): void {
this._core._innerWrite();
}
}
interface IBuffer {
readonly cursorY: number;
readonly cursorX: number;
readonly viewportY: number;
readonly baseY: number;
readonly length: number;
getLine(y: number): IBufferLine | undefined;
}
interface IBufferLine {
readonly isWrapped: boolean;
getCell(x: number): IBufferCell;
translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string;
}
interface IBufferCell {
readonly char: string;
readonly width: number;
}
class TestSearchHelper extends SearchHelper {
public findInLine(term: string, rowNumber: number, searchOptions?: ISearchOptions): ISearchResult {
return this._findInLine(term, rowNumber, 0, searchOptions);
-1
View File
@@ -11,7 +11,6 @@
"sourceMap": true,
"removeComments": true,
"declaration": true,
"preserveWatchOutput": true,
"types": [
"../../node_modules/@types/mocha"
]
+6 -1
View File
@@ -5,11 +5,16 @@
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
import { Terminal } from 'xterm';
import { Terminal, IDisposable } from 'xterm';
export interface ITerminadoAddonTerminal extends Terminal {
_core: {
register<T extends IDisposable>(d: T): void;
};
__socket?: WebSocket;
__attachSocketBuffer?: string;
__dataListener?: IDisposable;
__getMessage?(ev: MessageEvent): void;
__flushBuffer?(): void;
+4 -3
View File
@@ -59,9 +59,9 @@ export function terminadoAttach(term: Terminal, socket: WebSocket, bidirectional
socket.addEventListener('message', addonTerminal.__getMessage);
if (bidirectional) {
addonTerminal.on('data', addonTerminal.__sendData);
addonTerminal._core.register(addonTerminal.onData(addonTerminal.__sendData));
}
addonTerminal.on('resize', addonTerminal.__setSize);
addonTerminal._core.register(addonTerminal.onResize(addonTerminal.__setSize));
socket.addEventListener('close', () => terminadoDetach(addonTerminal, socket));
socket.addEventListener('error', () => terminadoDetach(addonTerminal, socket));
@@ -75,7 +75,8 @@ export function terminadoAttach(term: Terminal, socket: WebSocket, bidirectional
*/
export function terminadoDetach(term: Terminal, socket: WebSocket): void {
const addonTerminal = <ITerminadoAddonTerminal>term;
addonTerminal.off('data', addonTerminal.__sendData);
addonTerminal.__dataListener.dispose();
addonTerminal.__dataListener = undefined;
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;
-1
View File
@@ -10,7 +10,6 @@
"sourceMap": true,
"removeComments": true,
"declaration": true,
"preserveWatchOutput": true,
"types": [
"../../node_modules/@types/mocha"
]
-1
View File
@@ -11,7 +11,6 @@
"sourceMap": true,
"removeComments": true,
"declaration": true,
"preserveWatchOutput": true,
"types": [
"../../node_modules/@types/mocha"
]
+157 -35
View File
@@ -28,63 +28,185 @@ describe('webLinks addon', () => {
});
});
it('should allow ~ character in URI path', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
describe('should allow simple URI path', () => {
it('foo.com', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = ' http://foo.com/a~b#c~d?e~f ';
const row = ' http://foo.com ';
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://foo.com/a~b#c~d?e~f');
assert.equal(uri, 'http://foo.com');
});
it('bar.io', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = ' http://bar.io ';
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://bar.io');
});
});
it('should allow : character in URI path', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
describe('should allow ~ character in URI path', () => {
it('foo.com', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = ' http://foo.com/colon:test ';
const row = ' http://foo.com/a~b#c~d?e~f ';
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://foo.com/colon:test');
assert.equal(uri, 'http://foo.com/a~b#c~d?e~f');
});
it('bar.io', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = ' http://bar.io/a~b#c~d?e~f ';
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://bar.io/a~b#c~d?e~f');
});
});
it('should not allow : character at the end of a URI path', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
describe('should allow : character in URI path', () => {
it('foo.com', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = ' http://foo.com/colon:test: ';
const row = ' http://foo.com/colon:test ';
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://foo.com/colon:test');
assert.equal(uri, 'http://foo.com/colon:test');
});
it('bar.io', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = ' http://bar.io/colon:test ';
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://bar.io/colon:test');
});
});
it('should not allow " character at the end of a URI enclosed with ""', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
describe('should not allow : character at the end of a URI path', () => {
it('foo.com', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = '"http://foo.com/"';
const row = ' http://foo.com/colon:test: ';
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://foo.com/');
assert.equal(uri, 'http://foo.com/colon:test');
});
it('bar.io', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = ' http://bar.io/colon:test: ';
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://bar.io/colon:test');
});
});
it('should not allow \' character at the end of a URI enclosed with \'\'', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
describe('should not allow " character at the end of a URI enclosed with ""', () => {
it('foo.com', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = '\'http://foo.com/\'';
const row = '"http://foo.com/"';
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://foo.com/');
assert.equal(uri, 'http://foo.com/');
});
it('bar.io', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = '"http://bar.io/"';
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://bar.io/');
});
});
describe('should not allow \' character at the end of a URI enclosed with \'\'', () => {
it('foo.com', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = '\'http://foo.com/\'';
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://foo.com/');
});
it('bar.io', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = '\'http://bar.io/\'';
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://bar.io/');
});
});
describe('should allow + character in URI path', () => {
it('foo.com', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = 'http://foo.com/subpath/+/id';
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://foo.com/subpath/+/id');
});
it('bar.io', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = 'http://bar.io/subpath/+/id';
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://bar.io/subpath/+/id');
});
});
});
+2 -1
View File
@@ -14,7 +14,8 @@ const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
const localHostClause = '(localhost)';
const portClause = '(:\\d{1,5})';
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
const pathClause = '(\\/[\\/\\w\\.\\-%~:]*)*([^:"\'\\s])';
const pathCharacterSet = '(\\/[\\/\\w\\.\\-%~:+]*)*([^:"\'\\s])';
const pathClause = '(' + pathCharacterSet + ')?';
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
-14
View File
@@ -1,14 +0,0 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Terminal } from 'xterm';
export interface ITerminalCore {
buffer: any;
}
export interface IWinptyCompatAddonTerminal extends Terminal {
_core: ITerminalCore;
}
-5
View File
@@ -1,5 +0,0 @@
{
"name": "xterm.winptycompat",
"main": "winptyCompat.js",
"private": true
}
-22
View File
@@ -1,22 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"es5"
],
"rootDir": ".",
"outDir": "../../../lib/addons/winptyCompat/",
"sourceMap": true,
"removeComments": true,
"declaration": true,
"preserveWatchOutput": true,
"types": [
"../../node_modules/@types/mocha"
]
},
"include": [
"**/*.ts",
"../../../typings/xterm.d.ts"
]
}
@@ -1,19 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import * as winptyCompat from './winptyCompat';
class MockTerminal {}
describe('winptyCompat addon', () => {
describe('apply', () => {
it('should do register the `winptyCompatInit` method', () => {
winptyCompat.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.winptyCompatInit, 'function');
});
});
});
-47
View File
@@ -1,47 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Terminal } from 'xterm';
import { IWinptyCompatAddonTerminal } from './Interfaces';
const CHAR_DATA_CODE_INDEX = 3;
const NULL_CELL_CODE = 0;
const WHITESPACE_CELL_CODE = 32;
export function winptyCompatInit(terminal: Terminal): void {
const addonTerminal = <IWinptyCompatAddonTerminal>terminal;
// Don't do anything when the platform is not Windows
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
if (!isWindows) {
return;
}
// Winpty does not support wraparound mode which means that lines will never
// be marked as wrapped. This causes issues for things like copying a line
// retaining the wrapped new line characters or if consumers are listening
// in on the data stream.
//
// The workaround for this is to listen to every incoming line feed and mark
// the line as wrapped if the last character in the previous line is not a
// space. This is certainly not without its problems, but generally on
// Windows when text reaches the end of the terminal it's likely going to be
// wrapped.
addonTerminal.on('linefeed', () => {
const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1);
const lastChar = line.get(addonTerminal.cols - 1);
if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE) {
const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y);
nextLine.isWrapped = true;
}
});
}
export function apply(terminalConstructor: typeof Terminal): void {
(<any>terminalConstructor.prototype).winptyCompatInit = function (): void {
winptyCompatInit(this);
};
}
-1
View File
@@ -10,7 +10,6 @@
"sourceMap": true,
"removeComments": true,
"declaration": true,
"preserveWatchOutput": true,
"types": [
"../../node_modules/@types/mocha"
]
+14 -8
View File
@@ -3,8 +3,8 @@
* @license MIT
*/
import { EventEmitter } from './EventEmitter';
import { ICircularList } from './Types';
import { EventEmitter2, IEvent } from './EventEmitter2';
export interface IInsertEvent {
index: number;
@@ -20,15 +20,21 @@ export interface IDeleteEvent {
* Represents a circular list; a list with a maximum size that wraps around when push is called,
* overriding values at the start of the list.
*/
export class CircularList<T> extends EventEmitter implements ICircularList<T> {
export class CircularList<T> implements ICircularList<T> {
protected _array: (T | undefined)[];
private _startIndex: number;
private _length: number;
public onDeleteEmitter = new EventEmitter2<IDeleteEvent>();
public get onDelete(): IEvent<IDeleteEvent> { return this.onDeleteEmitter.event; }
public onInsertEmitter = new EventEmitter2<IInsertEvent>();
public get onInsert(): IEvent<IInsertEvent> { return this.onInsertEmitter.event; }
public onTrimEmitter = new EventEmitter2<number>();
public get onTrim(): IEvent<number> { return this.onTrimEmitter.event; }
constructor(
private _maxLength: number
) {
super();
this._array = new Array<T>(this._maxLength);
this._startIndex = 0;
this._length = 0;
@@ -101,7 +107,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
this._array[this._getCyclicIndex(this._length)] = value;
if (this._length === this._maxLength) {
this._startIndex = ++this._startIndex % this._maxLength;
this.emitMayRemoveListeners('trim', 1);
this.onTrimEmitter.fire(1);
} else {
this._length++;
}
@@ -117,7 +123,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
throw new Error('Can only recycle when the buffer is full');
}
this._startIndex = ++this._startIndex % this._maxLength;
this.emitMayRemoveListeners('trim', 1);
this.onTrimEmitter.fire(1);
return this._array[this._getCyclicIndex(this._length - 1)]!;
}
@@ -167,7 +173,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
const countToTrim = (this._length + items.length) - this._maxLength;
this._startIndex += countToTrim;
this._length = this._maxLength;
this.emitMayRemoveListeners('trim', countToTrim);
this.onTrimEmitter.fire(countToTrim);
} else {
this._length += items.length;
}
@@ -183,7 +189,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
}
this._startIndex += count;
this._length -= count;
this.emitMayRemoveListeners('trim', count);
this.onTrimEmitter.fire(count);
}
public shiftElements(start: number, count: number, offset: number): void {
@@ -207,7 +213,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
while (this._length > this._maxLength) {
this._length--;
this._startIndex++;
this.emitMayRemoveListeners('trim', 1);
this.onTrimEmitter.fire(1);
}
}
} else {
+1 -2
View File
@@ -3,8 +3,7 @@
* @license MIT
*/
import { XtermListener } from './Types';
import { IEventEmitter, IDisposable } from 'xterm';
import { IDisposable, IEventEmitter, XtermListener } from './Types';
import { Disposable } from './Lifecycle';
export class EventEmitter extends Disposable implements IEventEmitter, IDisposable {
+31
View File
@@ -0,0 +1,31 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { EventEmitter2 } from './EventEmitter2';
describe('EventEmitter2', () => {
it('should fire listeners multiple times', () => {
const order: string[] = [];
const emitter = new EventEmitter2<number>();
emitter.event(data => order.push(data + 'a'));
emitter.event(data => order.push(data + 'b'));
emitter.fire(1);
emitter.fire(2);
assert.deepEqual(order, [ '1a', '1b', '2a', '2b' ]);
});
it('should not fire listeners once disposed', () => {
const order: string[] = [];
const emitter = new EventEmitter2<number>();
emitter.event(data => order.push(data + 'a'));
const disposeB = emitter.event(data => order.push(data + 'b'));
emitter.event(data => order.push(data + 'c'));
emitter.fire(1);
disposeB.dispose();
emitter.fire(2);
assert.deepEqual(order, [ '1a', '1b', '1c', '2a', '2c' ]);
});
});
+49
View File
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable } from './Types';
interface IListener<T> {
(e: T): void;
}
export interface IEvent<T> {
(listener: (e: T) => any): IDisposable;
}
export class EventEmitter2<T> {
private _listeners: IListener<T>[] = [];
private _event?: IEvent<T>;
public get event(): IEvent<T> {
if (!this._event) {
this._event = (listener: (e: T) => any) => {
this._listeners.push(listener);
const disposable = {
dispose: () => {
for (let i = 0; i < this._listeners.length; i++) {
if (this._listeners[i] === listener) {
this._listeners.splice(i, 1);
return;
}
}
}
};
return disposable;
};
}
return this._event;
}
public fire(data: T): void {
const queue: IListener<T>[] = [];
for (let i = 0; i < this._listeners.length; i++) {
queue.push(this._listeners[i]);
}
for (let i = 0; i < queue.length; i++) {
queue[i].call(undefined, data);
}
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IDisposable } from 'xterm';
import { IDisposable } from './Types';
/**
* A base class that can be extended to provide convenience methods for managing the lifecycle of an
@@ -3,6 +3,16 @@
* @license MIT
*/
interface INavigator {
userAgent: string;
language: string;
platform: string;
}
// We're declaring a navigator global here as we expect it in all runtimes (node and browser), but
// we want this module to live in common.
declare const navigator: INavigator;
const isNode = (typeof navigator === 'undefined') ? true : false;
const userAgent = (isNode) ? 'node' : navigator.userAgent;
const platform = (isNode) ? 'node' : navigator.platform;
+23 -2
View File
@@ -3,7 +3,21 @@
* @license MIT
*/
import { IEventEmitter } from 'xterm';
import { IEvent, EventEmitter2 } from './EventEmitter2';
import { IDeleteEvent, IInsertEvent } from './CircularList';
export const DEFAULT_COLOR = 256;
export interface IDisposable {
dispose(): void;
}
export interface IEventEmitter {
on(type: string, listener: (...args: any[]) => void): void;
off(type: string, listener: (...args: any[]) => void): void;
emit(type: string, data?: any): void;
addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable;
}
export type XtermListener = (...args: any[]) => void;
@@ -21,11 +35,18 @@ export interface IKeyboardEvent {
type: string;
}
export interface ICircularList<T> extends IEventEmitter {
export interface ICircularList<T> {
length: number;
maxLength: number;
isFull: boolean;
onDeleteEmitter: EventEmitter2<IDeleteEvent>;
onDelete: IEvent<IDeleteEvent>;
onInsertEmitter: EventEmitter2<IInsertEvent>;
onInsert: IEvent<IInsertEvent>;
onTrimEmitter: EventEmitter2<number>;
onTrim: IEvent<number>;
get(index: number): T | undefined;
set(index: number, value: T): void;
push(value: T): void;
+4 -13
View File
@@ -1,19 +1,10 @@
{
"extends": "../tsconfig-library-base",
"compilerOptions": {
"target": "es5",
"lib": [
"es5"
],
"rootDir": ".",
"noEmit": true,
"strict": true,
"pretty": true,
"outDir": "../../lib",
"types": [
"../../node_modules/@types/mocha",
"../../"
"../../node_modules/@types/mocha"
]
},
"include": [
"./**/*"
]
"include": [ "./**/*" ]
}
+87
View File
@@ -3,6 +3,8 @@
* @license MIT
*/
import { IDisposable } from '../common/Types';
export const enum KeyboardResultType {
SEND_KEY,
SELECT_ALL,
@@ -19,3 +21,88 @@ export interface IKeyboardResult {
export interface ICharset {
[key: string]: string;
}
export type CharData = [number, string, number, number];
export type IColorRGB = [number, number, number];
/** Attribute data */
export interface IAttributeData {
fg: number;
bg: number;
clone(): IAttributeData;
// flags
isInverse(): number;
isBold(): number;
isUnderline(): number;
isBlink(): number;
isInvisible(): number;
isItalic(): number;
isDim(): number;
// color modes
getFgColorMode(): number;
getBgColorMode(): number;
isFgRGB(): boolean;
isBgRGB(): boolean;
isFgPalette(): boolean;
isBgPalette(): boolean;
isFgDefault(): boolean;
isBgDefault(): boolean;
// colors
getFgColor(): number;
getBgColor(): number;
}
/** Cell data */
export interface ICellData extends IAttributeData {
content: number;
combinedData: string;
isCombined(): number;
getWidth(): number;
getChars(): string;
getCode(): number;
setFromCharData(value: CharData): void;
getAsCharData(): CharData;
}
/**
* Interface for a line in the terminal buffer.
*/
export interface IBufferLine {
length: number;
isWrapped: boolean;
get(index: number): CharData;
set(index: number, value: CharData): void;
loadCell(index: number, cell: ICellData): ICellData;
setCell(index: number, cell: ICellData): void;
setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void;
addCodepointToCell(index: number, codePoint: number): void;
insertCells(pos: number, n: number, ch: ICellData): void;
deleteCells(pos: number, n: number, fill: ICellData): void;
replaceCells(start: number, end: number, fill: ICellData): void;
resize(cols: number, fill: ICellData): void;
fill(fillCellData: ICellData): void;
copyFrom(line: IBufferLine): void;
clone(): IBufferLine;
getTrimmedLength(): number;
translateToString(trimRight?: boolean, startCol?: number, endCol?: number): string;
/* direct access to cell attrs */
getWidth(index: number): number;
hasWidth(index: number): number;
getFg(index: number): number;
getBg(index: number): number;
hasContent(index: number): number;
getCodePoint(index: number): number;
isCombined(index: number): number;
getString(index: number): string;
}
export interface IMarker extends IDisposable {
readonly id: number;
readonly isDisposed: boolean;
readonly line: number;
}
+367
View File
@@ -0,0 +1,367 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import * as chai from 'chai';
import { BufferLine, CellData, Content, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from './BufferLine';
import { CharData, IBufferLine } from '../Types';
class TestBufferLine extends BufferLine {
public get combined(): {[index: number]: string} {
return this._combined;
}
public toArray(): CharData[] {
const result = [];
for (let i = 0; i < this.length; ++i) {
result.push(this.loadCell(i, new CellData()).getAsCharData());
}
return result;
}
}
describe('CellData', () => {
it('CharData <--> CellData equality', () => {
const cell = new CellData();
// ASCII
cell.setFromCharData([123, 'a', 1, 'a'.charCodeAt(0)]);
chai.assert.deepEqual(cell.getAsCharData(), [123, 'a', 1, 'a'.charCodeAt(0)]);
chai.assert.equal(cell.isCombined(), 0);
// combining
cell.setFromCharData([123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK);
// surrogate
cell.setFromCharData([123, '𝄞', 1, 0x1D11E]);
chai.assert.deepEqual(cell.getAsCharData(), [123, '𝄞', 1, 0x1D11E]);
chai.assert.equal(cell.isCombined(), 0);
// surrogate + combining
cell.setFromCharData([123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]);
chai.assert.deepEqual(cell.getAsCharData(), [123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]);
chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK);
// wide char
cell.setFromCharData([123, '1', 2, '1'.charCodeAt(0)]);
chai.assert.deepEqual(cell.getAsCharData(), [123, '1', 2, '1'.charCodeAt(0)]);
chai.assert.equal(cell.isCombined(), 0);
});
});
describe('BufferLine', function(): void {
it('ctor', function(): void {
let line: IBufferLine = new TestBufferLine(0);
chai.expect(line.length).equals(0);
chai.expect(line.isWrapped).equals(false);
line = new TestBufferLine(10);
chai.expect(line.length).equals(10);
chai.expect(line.loadCell(0, new CellData()).getAsCharData()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
chai.expect(line.isWrapped).equals(false);
line = new TestBufferLine(10, undefined, true);
chai.expect(line.length).equals(10);
chai.expect(line.loadCell(0, new CellData()).getAsCharData()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
chai.expect(line.isWrapped).equals(true);
line = new TestBufferLine(10, CellData.fromCharData([123, 'a', 456, 'a'.charCodeAt(0)]), true);
chai.expect(line.length).equals(10);
chai.expect(line.loadCell(0, new CellData()).getAsCharData()).eql([123, 'a', 456, 'a'.charCodeAt(0)]);
chai.expect(line.isWrapped).equals(true);
});
it('insertCells', function(): void {
const line = new TestBufferLine(3);
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
line.insertCells(1, 3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
chai.expect(line.toArray()).eql([
[1, 'a', 0, 'a'.charCodeAt(0)],
[4, 'd', 0, 'd'.charCodeAt(0)],
[4, 'd', 0, 'd'.charCodeAt(0)]
]);
});
it('deleteCells', function(): void {
const line = new TestBufferLine(5);
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
line.deleteCells(1, 2, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)]));
chai.expect(line.toArray()).eql([
[1, 'a', 0, 'a'.charCodeAt(0)],
[4, 'd', 0, 'd'.charCodeAt(0)],
[5, 'e', 0, 'e'.charCodeAt(0)],
[6, 'f', 0, 'f'.charCodeAt(0)],
[6, 'f', 0, 'f'.charCodeAt(0)]
]);
});
it('replaceCells', function(): void {
const line = new TestBufferLine(5);
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
line.replaceCells(2, 4, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)]));
chai.expect(line.toArray()).eql([
[1, 'a', 0, 'a'.charCodeAt(0)],
[2, 'b', 0, 'b'.charCodeAt(0)],
[6, 'f', 0, 'f'.charCodeAt(0)],
[6, 'f', 0, 'f'.charCodeAt(0)],
[5, 'e', 0, 'e'.charCodeAt(0)]
]);
});
it('fill', function(): void {
const line = new TestBufferLine(5);
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
line.fill(CellData.fromCharData([123, 'z', 0, 'z'.charCodeAt(0)]));
chai.expect(line.toArray()).eql([
[123, 'z', 0, 'z'.charCodeAt(0)],
[123, 'z', 0, 'z'.charCodeAt(0)],
[123, 'z', 0, 'z'.charCodeAt(0)],
[123, 'z', 0, 'z'.charCodeAt(0)],
[123, 'z', 0, 'z'.charCodeAt(0)]
]);
});
it('clone', function(): void {
const line = new TestBufferLine(5, undefined, true);
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
const line2 = line.clone();
chai.expect(TestBufferLine.prototype.toArray.apply(line2)).eql(line.toArray());
chai.expect(line2.length).equals(line.length);
chai.expect(line2.isWrapped).equals(line.isWrapped);
});
it('copyFrom', function(): void {
const line = new TestBufferLine(5);
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
const line2 = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), true);
line2.copyFrom(line);
chai.expect(line2.toArray()).eql(line.toArray());
chai.expect(line2.length).equals(line.length);
chai.expect(line2.isWrapped).equals(line.isWrapped);
});
it('should support combining chars', function(): void {
// CHAR_DATA_CODE_INDEX resembles current behavior in InputHandler.print
// --> set code to the last charCodeAt value of the string
// Note: needs to be fixed once the string pointer is in place
const line = new TestBufferLine(2, CellData.fromCharData([1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]));
chai.expect(line.toArray()).eql([[1, 'e\u0301', 0, '\u0301'.charCodeAt(0)], [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]]);
const line2 = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, '\u0301'.charCodeAt(0)]), true);
line2.copyFrom(line);
chai.expect(line2.toArray()).eql(line.toArray());
const line3 = line.clone();
chai.expect(TestBufferLine.prototype.toArray.apply(line3)).eql(line.toArray());
});
describe('resize', function(): void {
it('enlarge(false)', function(): void {
const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
chai.expect(line.toArray()).eql((Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('enlarge(true)', function(): void {
const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
chai.expect(line.toArray()).eql((Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(true) - should apply new size', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
chai.expect(line.toArray()).eql((Array(5) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink to 0 length', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
line.resize(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
chai.expect(line.toArray()).eql((Array(0) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('should remove combining data on replaced cells after shrinking then enlarging', () => {
const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
line.set(2, [ 0, '😁', 1, '😁'.charCodeAt(0) ]);
line.set(9, [ 0, '😁', 1, '😁'.charCodeAt(0) ]);
chai.expect(line.translateToString()).eql('aa😁aaaaaa😁');
chai.expect(Object.keys(line.combined).length).eql(2);
line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
chai.expect(line.translateToString()).eql('aa😁aa');
line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
chai.expect(line.translateToString()).eql('aa😁aaaaaaa');
chai.expect(Object.keys(line.combined).length).eql(1);
});
});
describe('getTrimLength', function(): void {
it('empty line', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
chai.expect(line.getTrimmedLength()).equal(0);
});
it('ASCII', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.expect(line.getTrimmedLength()).equal(3);
});
it('surrogate', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)]));
chai.expect(line.getTrimmedLength()).equal(3);
});
it('combining', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
chai.expect(line.getTrimmedLength()).equal(3);
});
it('fullwidth', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)]));
line.setCell(3, CellData.fromCharData([0, '', 0, 0]));
chai.expect(line.getTrimmedLength()).equal(4); // also counts null cell after fullwidth
});
});
describe('translateToString with and w\'o trimming', function(): void {
it('empty line', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
chai.expect(line.translateToString(false)).equal(' ');
chai.expect(line.translateToString(true)).equal('');
});
it('ASCII', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(5, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.expect(line.translateToString(false)).equal('a a aa ');
chai.expect(line.translateToString(true)).equal('a a aa');
chai.expect(line.translateToString(false, 0, 5)).equal('a a a');
chai.expect(line.translateToString(false, 0, 4)).equal('a a ');
chai.expect(line.translateToString(false, 0, 3)).equal('a a');
chai.expect(line.translateToString(true, 0, 5)).equal('a a a');
chai.expect(line.translateToString(true, 0, 4)).equal('a a ');
chai.expect(line.translateToString(true, 0, 3)).equal('a a');
});
it('surrogate', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)]));
line.setCell(5, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)]));
chai.expect(line.translateToString(false)).equal('a 𝄞 𝄞𝄞 ');
chai.expect(line.translateToString(true)).equal('a 𝄞 𝄞𝄞');
chai.expect(line.translateToString(false, 0, 5)).equal('a 𝄞 𝄞');
chai.expect(line.translateToString(false, 0, 4)).equal('a 𝄞 ');
chai.expect(line.translateToString(false, 0, 3)).equal('a 𝄞');
chai.expect(line.translateToString(true, 0, 5)).equal('a 𝄞 𝄞');
chai.expect(line.translateToString(true, 0, 4)).equal('a 𝄞 ');
chai.expect(line.translateToString(true, 0, 3)).equal('a 𝄞');
});
it('combining', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
line.setCell(5, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
chai.expect(line.translateToString(false)).equal('a e\u0301 e\u0301e\u0301 ');
chai.expect(line.translateToString(true)).equal('a e\u0301 e\u0301e\u0301');
chai.expect(line.translateToString(false, 0, 5)).equal('a e\u0301 e\u0301');
chai.expect(line.translateToString(false, 0, 4)).equal('a e\u0301 ');
chai.expect(line.translateToString(false, 0, 3)).equal('a e\u0301');
chai.expect(line.translateToString(true, 0, 5)).equal('a e\u0301 e\u0301');
chai.expect(line.translateToString(true, 0, 4)).equal('a e\u0301 ');
chai.expect(line.translateToString(true, 0, 3)).equal('a e\u0301');
});
it('fullwidth', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)]));
line.setCell(3, CellData.fromCharData([0, '', 0, 0]));
line.setCell(5, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)]));
line.setCell(6, CellData.fromCharData([0, '', 0, 0]));
line.setCell(7, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)]));
line.setCell(8, CellData.fromCharData([0, '', 0, 0]));
chai.expect(line.translateToString(false)).equal('a 1 11 ');
chai.expect(line.translateToString(true)).equal('a 1 11');
chai.expect(line.translateToString(false, 0, 7)).equal('a 1 1');
chai.expect(line.translateToString(false, 0, 6)).equal('a 1 1');
chai.expect(line.translateToString(false, 0, 5)).equal('a 1 ');
chai.expect(line.translateToString(false, 0, 4)).equal('a 1');
chai.expect(line.translateToString(false, 0, 3)).equal('a 1');
chai.expect(line.translateToString(false, 0, 2)).equal('a ');
chai.expect(line.translateToString(true, 0, 7)).equal('a 1 1');
chai.expect(line.translateToString(true, 0, 6)).equal('a 1 1');
chai.expect(line.translateToString(true, 0, 5)).equal('a 1 ');
chai.expect(line.translateToString(true, 0, 4)).equal('a 1');
chai.expect(line.translateToString(true, 0, 3)).equal('a 1');
chai.expect(line.translateToString(true, 0, 2)).equal('a ');
});
it('space at end', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(5, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(6, CellData.fromCharData([1, ' ', 1, ' '.charCodeAt(0)]));
chai.expect(line.translateToString(false)).equal('a a aa ');
chai.expect(line.translateToString(true)).equal('a a aa ');
});
it('should always return some sane value', function(): void {
// sanity check - broken line with invalid out of bound null width cells
// this can atm happen with deleting/inserting chars in inputhandler by "breaking"
// fullwidth pairs --> needs to be fixed after settling BufferLine impl
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
chai.expect(line.translateToString(false)).equal(' ');
chai.expect(line.translateToString(true)).equal('');
});
it('should work with endCol=0', () => {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.expect(line.translateToString(true, 0, 0)).equal('');
});
});
describe('addCharToCell', () => {
it('should set width to 1 for empty cell', () => {
const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
line.addCodepointToCell(0, '\u0301'.charCodeAt(0));
const cell = line.loadCell(0, new CellData());
// chars contains single combining char
// width is set to 1
chai.assert.deepEqual(cell.getAsCharData(), [DEFAULT_ATTR, '\u0301', 1, 0x0301]);
// do not account a single combining char as combined
chai.assert.equal(cell.isCombined(), 0);
});
it('should add char to combining string in cell', () => {
const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
const cell = line .loadCell(0, new CellData());
cell.setFromCharData([123, 'e\u0301', 1, 'e\u0301'.charCodeAt(1)]);
line.setCell(0, cell);
line.addCodepointToCell(0, '\u0301'.charCodeAt(0));
line.loadCell(0, cell);
// chars contains 3 chars
// width is set to 1
chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301\u0301', 1, 0x0301]);
// do not account a single combining char as combined
chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK);
});
it('should create combining string on taken cell', () => {
const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
const cell = line .loadCell(0, new CellData());
cell.setFromCharData([123, 'e', 1, 'e'.charCodeAt(1)]);
line.setCell(0, cell);
line.addCodepointToCell(0, '\u0301'.charCodeAt(0));
line.loadCell(0, cell);
// chars contains 2 chars
// width is set to 1
chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, 0x0301]);
// do not account a single combining char as combined
chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK);
});
});
});
+665
View File
@@ -0,0 +1,665 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { CharData, IBufferLine, ICellData, IColorRGB, IAttributeData } from '../Types';
import { stringFromCodePoint } from '../input/TextDecoder';
import { DEFAULT_COLOR } from '../../common/Types';
export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0);
export const CHAR_DATA_ATTR_INDEX = 0;
export const CHAR_DATA_CHAR_INDEX = 1;
export const CHAR_DATA_WIDTH_INDEX = 2;
export const CHAR_DATA_CODE_INDEX = 3;
/**
* Null cell - a real empty cell (containing nothing).
* Note that code should always be 0 for a null cell as
* several test condition of the buffer line rely on this.
*/
export const NULL_CELL_CHAR = '';
export const NULL_CELL_WIDTH = 1;
export const NULL_CELL_CODE = 0;
/**
* Whitespace cell.
* This is meant as a replacement for empty cells when needed
* during rendering lines to preserve correct aligment.
*/
export const WHITESPACE_CELL_CHAR = ' ';
export const WHITESPACE_CELL_WIDTH = 1;
export const WHITESPACE_CELL_CODE = 32;
/**
* buffer memory layout:
*
* | uint32_t | uint32_t | uint32_t |
* | `content` | `FG` | `BG` |
* | wcwidth(2) comb(1) codepoint(21) | flags(8) R(8) G(8) B(8) | flags(8) R(8) G(8) B(8) |
*/
/** typed array slots taken by one cell */
const CELL_SIZE = 3;
/**
* Cell member indices.
*
* Direct access:
* `content = data[column * CELL_SIZE + Cell.CONTENT];`
* `fg = data[column * CELL_SIZE + Cell.FG];`
* `bg = data[column * CELL_SIZE + Cell.BG];`
*/
const enum Cell {
CONTENT = 0,
FG = 1, // currently simply holds all known attrs
BG = 2 // currently unused
}
/**
* Bitmasks for accessing data in `content`.
*/
export const enum Content {
/**
* bit 1..21 codepoint, max allowed in UTF32 is 0x10FFFF (21 bits taken)
* read: `codepoint = content & Content.codepointMask;`
* write: `content |= codepoint & Content.codepointMask;`
* shortcut if precondition `codepoint <= 0x10FFFF` is met:
* `content |= codepoint;`
*/
CODEPOINT_MASK = 0x1FFFFF,
/**
* bit 22 flag indication whether a cell contains combined content
* read: `isCombined = content & Content.isCombined;`
* set: `content |= Content.isCombined;`
* clear: `content &= ~Content.isCombined;`
*/
IS_COMBINED_MASK = 0x200000, // 1 << 21
/**
* bit 1..22 mask to check whether a cell contains any string data
* we need to check for codepoint and isCombined bits to see
* whether a cell contains anything
* read: `isEmpty = !(content & Content.hasContent)`
*/
HAS_CONTENT_MASK = 0x3FFFFF,
/**
* bit 23..24 wcwidth value of cell, takes 2 bits (ranges from 0..2)
* read: `width = (content & Content.widthMask) >> Content.widthShift;`
* `hasWidth = content & Content.widthMask;`
* as long as wcwidth is highest value in `content`:
* `width = content >> Content.widthShift;`
* write: `content |= (width << Content.widthShift) & Content.widthMask;`
* shortcut if precondition `0 <= width <= 3` is met:
* `content |= width << Content.widthShift;`
*/
WIDTH_MASK = 0xC00000, // 3 << 22
WIDTH_SHIFT = 22
}
export const enum Attributes {
/**
* bit 1..8 blue in RGB, color in P256 and P16
*/
BLUE_MASK = 0xFF,
BLUE_SHIFT = 0,
PCOLOR_MASK = 0xFF,
PCOLOR_SHIFT = 0,
/**
* bit 9..16 green in RGB
*/
GREEN_MASK = 0xFF00,
GREEN_SHIFT = 8,
/**
* bit 17..24 red in RGB
*/
RED_MASK = 0xFF0000,
RED_SHIFT = 16,
/**
* bit 25..26 color mode: DEFAULT (0) | P16 (1) | P256 (2) | RGB (3)
*/
CM_MASK = 0x3000000,
CM_DEFAULT = 0,
CM_P16 = 0x1000000,
CM_P256 = 0x2000000,
CM_RGB = 0x3000000,
/**
* bit 1..24 RGB room
*/
RGB_MASK = 0xFFFFFF
}
export const enum FgFlags {
/**
* bit 27..31 (32th bit unused)
*/
INVERSE = 0x4000000,
BOLD = 0x8000000,
UNDERLINE = 0x10000000,
BLINK = 0x20000000,
INVISIBLE = 0x40000000
}
export const enum BgFlags {
/**
* bit 27..32 (upper 4 unused)
*/
ITALIC = 0x4000000,
DIM = 0x8000000
}
export class AttributeData implements IAttributeData {
static toColorRGB(value: number): IColorRGB {
return [
value >>> Attributes.RED_SHIFT & 255,
value >>> Attributes.GREEN_SHIFT & 255,
value & 255
];
}
static fromColorRGB(value: IColorRGB): number {
return (value[0] & 255) << Attributes.RED_SHIFT | (value[1] & 255) << Attributes.GREEN_SHIFT | value[2] & 255;
}
public clone(): IAttributeData {
const newObj = new AttributeData();
newObj.fg = this.fg;
newObj.bg = this.bg;
return newObj;
}
// data
public fg: number = 0;
public bg: number = 0;
// flags
public isInverse(): number { return this.fg & FgFlags.INVERSE; }
public isBold(): number { return this.fg & FgFlags.BOLD; }
public isUnderline(): number { return this.fg & FgFlags.UNDERLINE; }
public isBlink(): number { return this.fg & FgFlags.BLINK; }
public isInvisible(): number { return this.fg & FgFlags.INVISIBLE; }
public isItalic(): number { return this.bg & BgFlags.ITALIC; }
public isDim(): number { return this.bg & BgFlags.DIM; }
// color modes
public getFgColorMode(): number { return this.fg & Attributes.CM_MASK; }
public getBgColorMode(): number { return this.bg & Attributes.CM_MASK; }
public isFgRGB(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_RGB; }
public isBgRGB(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_RGB; }
public isFgPalette(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.fg & Attributes.CM_MASK) === Attributes.CM_P256; }
public isBgPalette(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.bg & Attributes.CM_MASK) === Attributes.CM_P256; }
public isFgDefault(): boolean { return (this.fg & Attributes.CM_MASK) === 0; }
public isBgDefault(): boolean { return (this.bg & Attributes.CM_MASK) === 0; }
// colors
public getFgColor(): number {
switch (this.fg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256: return this.fg & Attributes.PCOLOR_MASK;
case Attributes.CM_RGB: return this.fg & Attributes.RGB_MASK;
default: return -1; // CM_DEFAULT defaults to -1
}
}
public getBgColor(): number {
switch (this.bg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256: return this.bg & Attributes.PCOLOR_MASK;
case Attributes.CM_RGB: return this.bg & Attributes.RGB_MASK;
default: return -1; // CM_DEFAULT defaults to -1
}
}
}
export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData());
/**
* CellData - represents a single Cell in the terminal buffer.
*/
export class CellData extends AttributeData implements ICellData {
/** Helper to create CellData from CharData. */
public static fromCharData(value: CharData): CellData {
const obj = new CellData();
obj.setFromCharData(value);
return obj;
}
/** Primitives from terminal buffer. */
public content: number = 0;
public fg: number = 0;
public bg: number = 0;
public combinedData: string = '';
/** Whether cell contains a combined string. */
public isCombined(): number {
return this.content & Content.IS_COMBINED_MASK;
}
/** Width of the cell. */
public getWidth(): number {
return this.content >> Content.WIDTH_SHIFT;
}
/** JS string of the content. */
public getChars(): string {
if (this.content & Content.IS_COMBINED_MASK) {
return this.combinedData;
}
if (this.content & Content.CODEPOINT_MASK) {
return stringFromCodePoint(this.content & Content.CODEPOINT_MASK);
}
return '';
}
/**
* Codepoint of cell
* Note this returns the UTF32 codepoint of single chars,
* if content is a combined string it returns the codepoint
* of the last char in string to be in line with code in CharData.
* */
public getCode(): number {
return (this.isCombined())
? this.combinedData.charCodeAt(this.combinedData.length - 1)
: this.content & Content.CODEPOINT_MASK;
}
/** Set data from CharData */
public setFromCharData(value: CharData): void {
this.fg = value[CHAR_DATA_ATTR_INDEX];
this.bg = 0;
let combined = false;
// surrogates and combined strings need special treatment
if (value[CHAR_DATA_CHAR_INDEX].length > 2) {
combined = true;
} else if (value[CHAR_DATA_CHAR_INDEX].length === 2) {
const code = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0);
// if the 2-char string is a surrogate create single codepoint
// everything else is combined
if (0xD800 <= code && code <= 0xDBFF) {
const second = value[CHAR_DATA_CHAR_INDEX].charCodeAt(1);
if (0xDC00 <= second && second <= 0xDFFF) {
this.content = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
} else {
combined = true;
}
} else {
combined = true;
}
} else {
this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
}
if (combined) {
this.combinedData = value[CHAR_DATA_CHAR_INDEX];
this.content = Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
}
}
/** Get data as CharData. */
public getAsCharData(): CharData {
return [this.fg, this.getChars(), this.getWidth(), this.getCode()];
}
}
/**
* Typed array based bufferline implementation.
*
* There are 2 ways to insert data into the cell buffer:
* - `setCellFromCodepoint` + `addCodepointToCell`
* Use these for data that is already UTF32.
* Used during normal input in `InputHandler` for faster buffer access.
* - `setCell`
* This method takes a CellData object and stores the data in the buffer.
* Use `CellData.fromCharData` to create the CellData object (e.g. from JS string).
*
* To retrieve data from the buffer use either one of the primitive methods
* (if only one particular value is needed) or `loadCell`. For `loadCell` in a loop
* memory allocs / GC pressure can be greatly reduced by reusing the CellData object.
*/
export class BufferLine implements IBufferLine {
protected _data: Uint32Array;
protected _combined: {[index: number]: string} = {};
public length: number;
constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) {
this._data = new Uint32Array(cols * CELL_SIZE);
const cell = fillCellData || CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
for (let i = 0; i < cols; ++i) {
this.setCell(i, cell);
}
this.length = cols;
}
/**
* Get cell data CharData.
* @deprecated
*/
public get(index: number): CharData {
const content = this._data[index * CELL_SIZE + Cell.CONTENT];
const cp = content & Content.CODEPOINT_MASK;
return [
this._data[index * CELL_SIZE + Cell.FG],
(content & Content.IS_COMBINED_MASK)
? this._combined[index]
: (cp) ? stringFromCodePoint(cp) : '',
content >> Content.WIDTH_SHIFT,
(content & Content.IS_COMBINED_MASK)
? this._combined[index].charCodeAt(this._combined[index].length - 1)
: cp
];
}
/**
* Set cell data from CharData.
* @deprecated
*/
public set(index: number, value: CharData): void {
this._data[index * CELL_SIZE + Cell.FG] = value[CHAR_DATA_ATTR_INDEX];
if (value[CHAR_DATA_CHAR_INDEX].length > 1) {
this._combined[index] = value[1];
this._data[index * CELL_SIZE + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
} else {
this._data[index * CELL_SIZE + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
}
}
/**
* primitive getters
* use these when only one value is needed, otherwise use `loadCell`
*/
public getWidth(index: number): number {
return this._data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT;
}
/** Test whether content has width. */
public hasWidth(index: number): number {
return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.WIDTH_MASK;
}
/** Get FG cell component. */
public getFg(index: number): number {
return this._data[index * CELL_SIZE + Cell.FG];
}
/** Get BG cell component. */
public getBg(index: number): number {
return this._data[index * CELL_SIZE + Cell.BG];
}
/**
* Test whether contains any chars.
* Basically an empty has no content, but other cells might differ in FG/BG
* from real empty cells.
* */
public hasContent(index: number): number {
return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK;
}
/**
* Get codepoint of the cell.
* To be in line with `code` in CharData this either returns
* a single UTF32 codepoint or the last codepoint of a combined string.
*/
public getCodePoint(index: number): number {
const content = this._data[index * CELL_SIZE + Cell.CONTENT];
if (content & Content.IS_COMBINED_MASK) {
return this._combined[index].charCodeAt(this._combined[index].length - 1);
}
return content & Content.CODEPOINT_MASK;
}
/** Test whether the cell contains a combined string. */
public isCombined(index: number): number {
return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.IS_COMBINED_MASK;
}
/** Returns the string content of the cell. */
public getString(index: number): string {
const content = this._data[index * CELL_SIZE + Cell.CONTENT];
if (content & Content.IS_COMBINED_MASK) {
return this._combined[index];
}
if (content & Content.CODEPOINT_MASK) {
return stringFromCodePoint(content & Content.CODEPOINT_MASK);
}
// return empty string for empty cells
return '';
}
/**
* Load data at `index` into `cell`. This is used to access cells in a way that's more friendly
* to GC as it significantly reduced the amount of new objects/references needed.
*/
public loadCell(index: number, cell: ICellData): ICellData {
const startIndex = index * CELL_SIZE;
cell.content = this._data[startIndex + Cell.CONTENT];
cell.fg = this._data[startIndex + Cell.FG];
cell.bg = this._data[startIndex + Cell.BG];
if (cell.content & Content.IS_COMBINED_MASK) {
cell.combinedData = this._combined[index];
}
return cell;
}
/**
* Set data at `index` to `cell`.
*/
public setCell(index: number, cell: ICellData): void {
if (cell.content & Content.IS_COMBINED_MASK) {
this._combined[index] = cell.combinedData;
}
this._data[index * CELL_SIZE + Cell.CONTENT] = cell.content;
this._data[index * CELL_SIZE + Cell.FG] = cell.fg;
this._data[index * CELL_SIZE + Cell.BG] = cell.bg;
}
/**
* Set cell data from input handler.
* Since the input handler see the incoming chars as UTF32 codepoints,
* it gets an optimized access method.
*/
public setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void {
this._data[index * CELL_SIZE + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT);
this._data[index * CELL_SIZE + Cell.FG] = fg;
this._data[index * CELL_SIZE + Cell.BG] = bg;
}
/**
* Add a codepoint to a cell from input handler.
* During input stage combining chars with a width of 0 follow and stack
* onto a leading char. Since we already set the attrs
* by the previous `setDataFromCodePoint` call, we can omit it here.
*/
public addCodepointToCell(index: number, codePoint: number): void {
let content = this._data[index * CELL_SIZE + Cell.CONTENT];
if (content & Content.IS_COMBINED_MASK) {
// we already have a combined string, simply add
this._combined[index] += stringFromCodePoint(codePoint);
} else {
if (content & Content.CODEPOINT_MASK) {
// normal case for combining chars:
// - move current leading char + new one into combined string
// - set combined flag
this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint);
content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0
content |= Content.IS_COMBINED_MASK;
} else {
// should not happen - we actually have no data in the cell yet
// simply set the data in the cell buffer with a width of 1
content = codePoint | (1 << Content.WIDTH_SHIFT);
}
this._data[index * CELL_SIZE + Cell.CONTENT] = content;
}
}
public insertCells(pos: number, n: number, fillCellData: ICellData): void {
pos %= this.length;
if (n < this.length - pos) {
const cell = new CellData();
for (let i = this.length - pos - n - 1; i >= 0; --i) {
this.setCell(pos + n + i, this.loadCell(pos + i, cell));
}
for (let i = 0; i < n; ++i) {
this.setCell(pos + i, fillCellData);
}
} else {
for (let i = pos; i < this.length; ++i) {
this.setCell(i, fillCellData);
}
}
}
public deleteCells(pos: number, n: number, fillCellData: ICellData): void {
pos %= this.length;
if (n < this.length - pos) {
const cell = new CellData();
for (let i = 0; i < this.length - pos - n; ++i) {
this.setCell(pos + i, this.loadCell(pos + n + i, cell));
}
for (let i = this.length - n; i < this.length; ++i) {
this.setCell(i, fillCellData);
}
} else {
for (let i = pos; i < this.length; ++i) {
this.setCell(i, fillCellData);
}
}
}
public replaceCells(start: number, end: number, fillCellData: ICellData): void {
while (start < end && start < this.length) {
this.setCell(start++, fillCellData);
}
}
public resize(cols: number, fillCellData: ICellData): void {
if (cols === this.length) {
return;
}
if (cols > this.length) {
const data = new Uint32Array(cols * CELL_SIZE);
if (this.length) {
if (cols * CELL_SIZE < this._data.length) {
data.set(this._data.subarray(0, cols * CELL_SIZE));
} else {
data.set(this._data);
}
}
this._data = data;
for (let i = this.length; i < cols; ++i) {
this.setCell(i, fillCellData);
}
} else {
if (cols) {
const data = new Uint32Array(cols * CELL_SIZE);
data.set(this._data.subarray(0, cols * CELL_SIZE));
this._data = data;
// Remove any cut off combined data
const keys = Object.keys(this._combined);
for (let i = 0; i < keys.length; i++) {
const key = parseInt(keys[i], 10);
if (key >= cols) {
delete this._combined[key];
}
}
} else {
this._data = new Uint32Array(0);
this._combined = {};
}
}
this.length = cols;
}
/** fill a line with fillCharData */
public fill(fillCellData: ICellData): void {
this._combined = {};
for (let i = 0; i < this.length; ++i) {
this.setCell(i, fillCellData);
}
}
/** alter to a full copy of line */
public copyFrom(line: BufferLine): void {
if (this.length !== line.length) {
this._data = new Uint32Array(line._data);
} else {
// use high speed copy if lengths are equal
this._data.set(line._data);
}
this.length = line.length;
this._combined = {};
for (const el in line._combined) {
this._combined[el] = line._combined[el];
}
this.isWrapped = line.isWrapped;
}
/** create a new clone */
public clone(): IBufferLine {
const newLine = new BufferLine(0);
newLine._data = new Uint32Array(this._data);
newLine.length = this.length;
for (const el in this._combined) {
newLine._combined[el] = this._combined[el];
}
newLine.isWrapped = this.isWrapped;
return newLine;
}
public getTrimmedLength(): number {
for (let i = this.length - 1; i >= 0; --i) {
if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) {
return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT);
}
}
return 0;
}
public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void {
const srcData = src._data;
if (applyInReverse) {
for (let cell = length - 1; cell >= 0; cell--) {
for (let i = 0; i < CELL_SIZE; i++) {
this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i];
}
}
} else {
for (let cell = 0; cell < length; cell++) {
for (let i = 0; i < CELL_SIZE; i++) {
this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i];
}
}
}
// Move any combined data over as needed
const srcCombinedKeys = Object.keys(src._combined);
for (let i = 0; i < srcCombinedKeys.length; i++) {
const key = parseInt(srcCombinedKeys[i], 10);
if (key >= srcCol) {
this._combined[key - srcCol + destCol] = src._combined[key];
}
}
}
public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string {
if (trimRight) {
endCol = Math.min(endCol, this.getTrimmedLength());
}
let result = '';
while (startCol < endCol) {
const content = this._data[startCol * CELL_SIZE + Cell.CONTENT];
const cp = content & Content.CODEPOINT_MASK;
result += (content & Content.IS_COMBINED_MASK) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR;
startCol += (content >> Content.WIDTH_SHIFT) || 1; // always advance by 1
}
return result;
}
}
@@ -3,18 +3,17 @@
* @license MIT
*/
import { assert } from 'chai';
import { BufferLine } from './BufferLine';
import { BufferLine, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './BufferLine';
import { reflowSmallerGetNewLineLengths } from './BufferReflow';
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer';
describe('BufferReflow', () => {
describe('reflowSmallerGetNewLineLengths', () => {
it('should return correct line lengths for a small line with wide characters', () => {
const line = new BufferLine(4);
line.set(0, [null, '汉', 2, '汉'.charCodeAt(0)]);
line.set(1, [null, '', 0, undefined]);
line.set(2, [null, '语', 2, '语'.charCodeAt(0)]);
line.set(3, [null, '', 0, undefined]);
line.set(0, [0, '汉', 2, '汉'.charCodeAt(0)]);
line.set(1, [0, '', 0, 0]);
line.set(2, [0, '语', 2, '语'.charCodeAt(0)]);
line.set(3, [0, '', 0, 0]);
assert.equal(line.translateToString(true), '汉语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 4, 3), [2, 2], 'line: 汉, 语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 4, 2), [2, 2], 'line: 汉, 语');
@@ -22,12 +21,12 @@ describe('BufferReflow', () => {
it('should return correct line lengths for a large line with wide characters', () => {
const line = new BufferLine(12);
for (let i = 0; i < 12; i += 4) {
line.set(i, [null, '汉', 2, '汉'.charCodeAt(0)]);
line.set(i + 2, [null, '语', 2, '语'.charCodeAt(0)]);
line.set(i, [0, '汉', 2, '汉'.charCodeAt(0)]);
line.set(i + 2, [0, '语', 2, '语'.charCodeAt(0)]);
}
for (let i = 1; i < 12; i += 2) {
line.set(i, [null, '', 0, undefined]);
line.set(i, [null, '', 0, undefined]);
line.set(i, [0, '', 0, 0]);
line.set(i, [0, '', 0, 0]);
}
assert.equal(line.translateToString(), '汉语汉语汉语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 11), [10, 2], 'line: 汉语汉语汉, 语');
@@ -43,12 +42,12 @@ describe('BufferReflow', () => {
});
it('should return correct line lengths for a string with wide and single characters', () => {
const line = new BufferLine(6);
line.set(0, [null, 'a', 1, 'a'.charCodeAt(0)]);
line.set(1, [null, '汉', 2, '汉'.charCodeAt(0)]);
line.set(2, [null, '', 0, undefined]);
line.set(3, [null, '语', 2, '语'.charCodeAt(0)]);
line.set(4, [null, '', 0, undefined]);
line.set(5, [null, 'b', 1, 'b'.charCodeAt(0)]);
line.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]);
line.set(1, [0, '汉', 2, '汉'.charCodeAt(0)]);
line.set(2, [0, '', 0, 0]);
line.set(3, [0, '语', 2, '语'.charCodeAt(0)]);
line.set(4, [0, '', 0, 0]);
line.set(5, [0, 'b', 1, 'b'.charCodeAt(0)]);
assert.equal(line.translateToString(), 'a汉语b');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 6, 5), [5, 1], 'line: a汉语b');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 6, 4), [3, 3], 'line: a汉, 语b');
@@ -57,19 +56,19 @@ describe('BufferReflow', () => {
});
it('should return correct line lengths for a wrapped line with wide and single characters', () => {
const line1 = new BufferLine(6);
line1.set(0, [null, 'a', 1, 'a'.charCodeAt(0)]);
line1.set(1, [null, '汉', 2, '汉'.charCodeAt(0)]);
line1.set(2, [null, '', 0, undefined]);
line1.set(3, [null, '语', 2, '语'.charCodeAt(0)]);
line1.set(4, [null, '', 0, undefined]);
line1.set(5, [null, 'b', 1, 'b'.charCodeAt(0)]);
line1.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]);
line1.set(1, [0, '汉', 2, '汉'.charCodeAt(0)]);
line1.set(2, [0, '', 0, 0]);
line1.set(3, [0, '语', 2, '语'.charCodeAt(0)]);
line1.set(4, [0, '', 0, 0]);
line1.set(5, [0, 'b', 1, 'b'.charCodeAt(0)]);
const line2 = new BufferLine(6, undefined, true);
line2.set(0, [null, 'a', 1, 'a'.charCodeAt(0)]);
line2.set(1, [null, '汉', 2, '汉'.charCodeAt(0)]);
line2.set(2, [null, '', 0, undefined]);
line2.set(3, [null, '语', 2, '语'.charCodeAt(0)]);
line2.set(4, [null, '', 0, undefined]);
line2.set(5, [null, 'b', 1, 'b'.charCodeAt(0)]);
line2.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]);
line2.set(1, [0, '汉', 2, '汉'.charCodeAt(0)]);
line2.set(2, [0, '', 0, 0]);
line2.set(3, [0, '语', 2, '语'.charCodeAt(0)]);
line2.set(4, [0, '', 0, 0]);
line2.set(5, [0, 'b', 1, 'b'.charCodeAt(0)]);
assert.equal(line1.translateToString(), 'a汉语b');
assert.equal(line2.translateToString(), 'a汉语b');
assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 5), [5, 4, 3], 'lines: a汉语, ba汉, 语b');
@@ -79,11 +78,11 @@ describe('BufferReflow', () => {
});
it('should work on lines ending in null space', () => {
const line = new BufferLine(5);
line.set(0, [null, '汉', 2, '汉'.charCodeAt(0)]);
line.set(1, [null, '', 0, undefined]);
line.set(2, [null, '语', 2, '语'.charCodeAt(0)]);
line.set(3, [null, '', 0, undefined]);
line.set(4, [null, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
line.set(0, [0, '汉', 2, '汉'.charCodeAt(0)]);
line.set(1, [0, '', 0, 0]);
line.set(2, [0, '语', 2, '语'.charCodeAt(0)]);
line.set(3, [0, '', 0, 0]);
line.set(4, [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
assert.equal(line.translateToString(true), '汉语');
assert.equal(line.translateToString(false), '汉语 ');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 4, 3), [2, 2], 'line: 汉, 语');
@@ -3,10 +3,9 @@
* @license MIT
*/
import { FILL_CHAR_DATA } from './Buffer';
import { BufferLine } from './BufferLine';
import { CircularList, IDeleteEvent } from './common/CircularList';
import { IBufferLine } from './Types';
import { CircularList } from '../../common/CircularList';
import { IBufferLine, ICellData } from '../Types';
export interface INewLayoutResult {
layout: number[];
@@ -19,7 +18,7 @@ export interface INewLayoutResult {
* @param lines The buffer lines.
* @param newCols The columns after resize.
*/
export function reflowLargerGetLinesToRemove(lines: CircularList<IBufferLine>, newCols: number, bufferAbsoluteY: number): number[] {
export function reflowLargerGetLinesToRemove(lines: CircularList<IBufferLine>, oldCols: number, newCols: number, bufferAbsoluteY: number, nullCell: ICellData): number[] {
// Gather all BufferLines that need to be removed from the Buffer here so that they can be
// batched up and only committed once
const toRemove: number[] = [];
@@ -48,11 +47,11 @@ export function reflowLargerGetLinesToRemove(lines: CircularList<IBufferLine>, n
// Copy buffer data to new locations
let destLineIndex = 0;
let destCol = wrappedLines[destLineIndex].getTrimmedLength();
let destCol = getWrappedLineTrimmedLength(wrappedLines, destLineIndex, oldCols);
let srcLineIndex = 1;
let srcCol = 0;
while (srcLineIndex < wrappedLines.length) {
const srcTrimmedTineLength = wrappedLines[srcLineIndex].getTrimmedLength();
const srcTrimmedTineLength = getWrappedLineTrimmedLength(wrappedLines, srcLineIndex, oldCols);
const srcRemainingCells = srcTrimmedTineLength - srcCol;
const destRemainingCells = newCols - destCol;
const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells);
@@ -75,13 +74,13 @@ export function reflowLargerGetLinesToRemove(lines: CircularList<IBufferLine>, n
if (wrappedLines[destLineIndex - 1].getWidth(newCols - 1) === 2) {
wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[destLineIndex - 1], newCols - 1, destCol++, 1, false);
// Null out the end of the last row
wrappedLines[destLineIndex - 1].set(newCols - 1, FILL_CHAR_DATA);
wrappedLines[destLineIndex - 1].setCell(newCols - 1, nullCell);
}
}
}
// Clear out remaining cells or fragments could remain;
wrappedLines[destLineIndex].replaceCells(destCol, newCols, FILL_CHAR_DATA);
wrappedLines[destLineIndex].replaceCells(destCol, newCols, nullCell);
// Work backwards and remove any rows at the end that only contain null cells
let countToRemove = 0;
@@ -119,10 +118,10 @@ export function reflowLargerCreateNewLayout(lines: CircularList<IBufferLine>, to
const countToRemove = toRemove[++nextToRemoveIndex];
// Tell markers that there was a deletion
lines.emit('delete', {
lines.onDeleteEmitter.fire({
index: i - countRemovedSoFar,
amount: countToRemove
} as IDeleteEvent);
});
i += countToRemove - 1;
countRemovedSoFar += countToRemove;
@@ -173,7 +172,7 @@ export function reflowLargerApplyNewLayout(lines: CircularList<IBufferLine>, new
*/
export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCols: number, newCols: number): number[] {
const newLineLengths: number[] = [];
const cellsNeeded = wrappedLines.map(l => l.getTrimmedLength()).reduce((p, c) => p + c);
const cellsNeeded = wrappedLines.map((l, i) => getWrappedLineTrimmedLength(wrappedLines, i, oldCols)).reduce((p, c) => p + c);
// Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and
// linesNeeded
@@ -187,7 +186,7 @@ export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCo
break;
}
srcCol += newCols;
const oldTrimmedLength = wrappedLines[srcLine].getTrimmedLength();
const oldTrimmedLength = getWrappedLineTrimmedLength(wrappedLines, srcLine, oldCols);
if (srcCol > oldTrimmedLength) {
srcCol -= oldTrimmedLength;
srcLine++;
@@ -203,3 +202,19 @@ export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCo
return newLineLengths;
}
export function getWrappedLineTrimmedLength(lines: BufferLine[], i: number, cols: number): number {
// If this is the last row in the wrapped line, get the actual trimmed length
if (i === lines.length - 1) {
return lines[i].getTrimmedLength();
}
// Detect whether the following line starts with a wide character and the end of the current line
// is null, if so then we can be pretty sure the null character should be excluded from the line
// length]
const endsInNull = !(lines[i].hasContent(cols - 1)) && lines[i].getWidth(cols - 1) === 1;
const followingLineStartsWithWide = lines[i + 1].getWidth(0) === 2;
if (endsInNull && followingLineStartsWithWide) {
return cols - 1;
}
return cols;
}
+35
View File
@@ -0,0 +1,35 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { EventEmitter2, IEvent } from '../../common/EventEmitter2';
import { Disposable } from '../../common/Lifecycle';
import { IMarker } from '../Types';
export class Marker extends Disposable implements IMarker {
private static _nextId = 1;
private _id: number = Marker._nextId++;
public isDisposed: boolean = false;
public get id(): number { return this._id; }
private _onDispose = new EventEmitter2<void>();
public get onDispose(): IEvent<void> { return this._onDispose.event; }
constructor(
public line: number
) {
super();
}
public dispose(): void {
if (this.isDisposed) {
return;
}
this.isDisposed = true;
// Emit before super.dispose such that dispose listeners get a change to react
this._onDispose.fire();
}
}
+4
View File
@@ -352,6 +352,10 @@ export function evaluateKeyboardEvent(
} else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.keyCode >= 48 && ev.key.length === 1) {
// Include only keys that that result in a _single_ character; don't include num lock, volume up, etc.
result.key = ev.key;
} else if (ev.key && ev.ctrlKey) {
if (ev.key === '_') { // ^_
result.key = C0.US;
}
}
break;
}
+156 -4
View File
@@ -4,7 +4,41 @@
*/
import { assert } from 'chai';
import { StringToUtf32, stringFromCodePoint, utf32ToString } from './TextDecoder';
import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32, utf32ToString } from './TextDecoder';
import { encode } from 'utf8';
// convert UTF32 codepoints to string
function toString(data: Uint32Array, length: number): string {
if ((String as any).fromCodePoint) {
return (String as any).fromCodePoint.apply(null, data.subarray(0, length));
}
let result = '';
for (let i = 0; i < length; ++i) {
result += stringFromCodePoint(data[i]);
}
return result;
}
// convert "bytestring" (charCode 0-255) to bytes
function fromByteString(s: string): Uint8Array {
const result = new Uint8Array(s.length);
for (let i = 0; i < s.length; ++i) {
result[i] = s.charCodeAt(i);
}
return result;
}
const TEST_STRINGS = [
'Лорем ипсум долор сит амет, ех сеа аццусам диссентиет. Ан еос стет еирмод витуперата. Иус дицерет урбанитас ет. Ан при алтера долорес сплендиде, цу яуо интегре денияуе, игнота волуптариа инструцтиор цу вим.',
'ლორემ იფსუმ დოლორ სით ამეთ, ფაცერ მუციუს ცონსეთეთურ ყუო იდ, ფერ ვივენდუმ ყუაერენდუმ ეა, ესთ ამეთ მოვეთ სუავითათე ცუ. ვითაე სენსიბუს ან ვიხ. ეხერცი დეთერრუისსეთ უთ ყუი. ვოცენთ დებითის ადიფისცი ეთ ფერ. ნეც ან ფეუგაით ფორენსიბუს ინთერესსეთ. იდ დიცო რიდენს იუს. დისსენთიეთ ცონსეყუუნთურ სედ ნე, ნოვუმ მუნერე ეუმ ათ, ნე ეუმ ნიჰილ ირაცუნდია ურბანითას.',
'अधिकांश अमितकुमार प्रोत्साहित मुख्य जाने प्रसारन विश्लेषण विश्व दारी अनुवादक अधिकांश नवंबर विषय गटकउसि गोपनीयता विकास जनित परस्पर गटकउसि अन्तरराष्ट्रीयकरन होसके मानव पुर्णता कम्प्युटर यन्त्रालय प्रति साधन',
'覧六子当聞社計文護行情投身斗来。増落世的況上席備界先関権能万。本物挙歯乳全事携供板栃果以。頭月患端撤競見界記引去法条公泊候。決海備駆取品目芸方用朝示上用報。講申務紙約週堂出応理田流団幸稿。起保帯吉対阜庭支肯豪彰属本躍。量抑熊事府募動極都掲仮読岸。自続工就断庫指北速配鳴約事新住米信中験。婚浜袋著金市生交保他取情距。',
'八メル務問へふらく博辞説いわょ読全タヨムケ東校どっ知壁テケ禁去フミ人過を装5階がねぜ法逆はじ端40落ミ予竹マヘナセ任1悪た。省ぜりせ製暇ょへそけ風井イ劣手はぼまず郵富法く作断タオイ取座ゅょが出作ホシ月給26島ツチ皇面ユトクイ暮犯リワナヤ断連こうでつ蔭柔薄とレにの。演めけふぱ損田転10得観びトげぎ王物鉄夜がまけ理惜くち牡提づ車惑参ヘカユモ長臓超漫ぼドかわ。',
'모든 국민은 행위시의 법률에 의하여 범죄를 구성하지 아니하는 행위로 소추되지 아니하며. 전직대통령의 신분과 예우에 관하여는 법률로 정한다, 국회는 헌법 또는 법률에 특별한 규정이 없는 한 재적의원 과반수의 출석과 출석의원 과반수의 찬성으로 의결한다. 군인·군무원·경찰공무원 기타 법률이 정하는 자가 전투·훈련등 직무집행과 관련하여 받은 손해에 대하여는 법률이 정하는 보상외에 국가 또는 공공단체에 공무원의 직무상 불법행위로 인한 배상은 청구할 수 없다.',
'كان فشكّل الشرقي مع, واحدة للمجهود تزامناً بعض بل. وتم جنوب للصين غينيا لم, ان وبدون وكسبت الأمور ذلك, أسر الخاسر الانجليزية هو. نفس لغزو مواقعها هو. الجو علاقة الصعداء انه أي, كما مع بمباركة للإتحاد الوزراء. ترتيب الأولى أن حدى, الشتوية باستحداث مدن بل, كان قد أوسع عملية. الأوضاع بالمطالبة كل قام, دون إذ شمال الربيع،. هُزم الخاصّة ٣٠ أما, مايو الصينية مع قبل.',
'או סדר החול מיזמי קרימינולוגיה. קהילה בגרסה לויקיפדים אל היא, של צעד ציור ואלקטרוניקה. מדע מה ברית המזנון ארכיאולוגיה, אל טבלאות מבוקשים כלל. מאמרשיחהצפה העריכהגירסאות שכל אל, כתב עיצוב מושגי של. קבלו קלאסיים ב מתן. נבחרים אווירונאוטיקה אם מלא, לוח למנוע ארכיאולוגיה מה. ארץ לערוך בקרבת מונחונים או, עזרה רקטות לויקיפדים אחר גם.',
'Лорем ლორემ अधिकांश 覧六子 八メル 모든 בקרבת 💮 😂 äggg 123€ 𝄞.'
];
describe('text encodings', () => {
it('stringFromCodePoint/utf32ToString', () => {
@@ -17,7 +51,7 @@ describe('text encodings', () => {
assert.equal(utf32ToString(data), s);
});
describe('StringToUtf32 Decoder', () => {
describe('StringToUtf32 decoder', () => {
describe('full codepoint test', () => {
it('0..65535', () => {
const decoder = new StringToUtf32();
@@ -34,7 +68,8 @@ describe('text encodings', () => {
decoder.clear();
}
});
it('65536..0x10FFFF (surrogates)', function(): void {
it('65536..0x10FFFF (surrogates)', function (): void {
this.timeout(20000);
const decoder = new StringToUtf32();
const target = new Uint32Array(5);
@@ -50,6 +85,16 @@ describe('text encodings', () => {
});
});
it('test strings', () => {
const decoder = new StringToUtf32();
const target = new Uint32Array(500);
for (let i = 0; i < TEST_STRINGS.length; ++i) {
const length = decoder.decode(TEST_STRINGS[i], target);
assert.equal(toString(target, length), TEST_STRINGS[i]);
decoder.clear();
}
});
describe('stream handling', () => {
it('surrogates mixed advance by 1', () => {
const decoder = new StringToUtf32();
@@ -58,7 +103,114 @@ describe('text encodings', () => {
let decoded = '';
for (let i = 0; i < input.length; ++i) {
const written = decoder.decode(input[i], target);
decoded += utf32ToString(target, written);
decoded += toString(target, written);
}
assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€');
});
});
});
describe('Utf8ToUtf32 decoder', () => {
describe('full codepoint test', () => {
it('0..65535 (1/2/3 byte sequences)', () => {
const decoder = new Utf8ToUtf32();
const target = new Uint32Array(5);
for (let i = 0; i < 65536; ++i) {
// skip surrogate pairs
if (i >= 0xD800 && i <= 0xDFFF) {
continue;
}
const utf8Data = fromByteString(encode(String.fromCharCode(i)));
const length = decoder.decode(utf8Data, target);
assert.equal(length, 1);
assert.equal(toString(target, length), String.fromCharCode(i));
decoder.clear();
}
});
it('65536..0x10FFFF (4 byte sequences)', function (): void {
this.timeout(20000);
const decoder = new Utf8ToUtf32();
const target = new Uint32Array(5);
for (let i = 65536; i < 0x10FFFF; ++i) {
const utf8Data = fromByteString(encode(stringFromCodePoint(i)));
const length = decoder.decode(utf8Data, target);
assert.equal(length, 1);
assert.equal(target[0], i);
decoder.clear();
}
});
});
it('test strings', () => {
const decoder = new Utf8ToUtf32();
const target = new Uint32Array(500);
for (let i = 0; i < TEST_STRINGS.length; ++i) {
const utf8Data = fromByteString(encode(TEST_STRINGS[i]));
const length = decoder.decode(utf8Data, target);
assert.equal(toString(target, length), TEST_STRINGS[i]);
decoder.clear();
}
});
describe('stream handling', () => {
it('2 byte sequences - advance by 1', () => {
const decoder = new Utf8ToUtf32();
const target = new Uint32Array(5);
const utf8Data = fromByteString('\xc3\x84\xc3\x96\xc3\x9c\xc3\x9f\xc3\xb6\xc3\xa4\xc3\xbc');
let decoded = '';
for (let i = 0; i < utf8Data.length; ++i) {
const written = decoder.decode(utf8Data.slice(i, i + 1), target);
decoded += toString(target, written);
}
assert(decoded, 'ÄÖÜßöäü');
});
it('2/3 byte sequences - advance by 1', () => {
const decoder = new Utf8ToUtf32();
const target = new Uint32Array(5);
const utf8Data = fromByteString('\xc3\x84\xe2\x82\xac\xc3\x96\xe2\x82\xac\xc3\x9c\xe2\x82\xac\xc3\x9f\xe2\x82\xac\xc3\xb6\xe2\x82\xac\xc3\xa4\xe2\x82\xac\xc3\xbc');
let decoded = '';
for (let i = 0; i < utf8Data.length; ++i) {
const written = decoder.decode(utf8Data.slice(i, i + 1), target);
decoded += toString(target, written);
}
assert(decoded, 'Āր܀߀ö€ä€ü');
});
it('2/3/4 byte sequences - advance by 1', () => {
const decoder = new Utf8ToUtf32();
const target = new Uint32Array(5);
const utf8Data = fromByteString('\xc3\x84\xe2\x82\xac\xf0\x9d\x84\x9e\xc3\x96\xf0\x9d\x84\x9e\xe2\x82\xac\xc3\x9c\xf0\x9d\x84\x9e\xe2\x82\xac');
let decoded = '';
for (let i = 0; i < utf8Data.length; ++i) {
const written = decoder.decode(utf8Data.slice(i, i + 1), target);
decoded += toString(target, written);
}
assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€');
});
it('2/3/4 byte sequences - advance by 2', () => {
const decoder = new Utf8ToUtf32();
const target = new Uint32Array(5);
const utf8Data = fromByteString('\xc3\x84\xe2\x82\xac\xf0\x9d\x84\x9e\xc3\x96\xf0\x9d\x84\x9e\xe2\x82\xac\xc3\x9c\xf0\x9d\x84\x9e\xe2\x82\xac');
let decoded = '';
for (let i = 0; i < utf8Data.length; i += 2) {
const written = decoder.decode(utf8Data.slice(i, i + 2), target);
decoded += toString(target, written);
}
assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€');
});
it('2/3/4 byte sequences - advance by 3', () => {
const decoder = new Utf8ToUtf32();
const target = new Uint32Array(5);
const utf8Data = fromByteString('\xc3\x84\xe2\x82\xac\xf0\x9d\x84\x9e\xc3\x96\xf0\x9d\x84\x9e\xe2\x82\xac\xc3\x9c\xf0\x9d\x84\x9e\xe2\x82\xac');
let decoded = '';
for (let i = 0; i < utf8Data.length; i += 3) {
const written = decoder.decode(utf8Data.slice(i, i + 3), target);
decoded += toString(target, written);
}
assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€');
});
+263 -30
View File
@@ -3,6 +3,45 @@
* @license MIT
*/
/**
* Polyfill - Convert UTF32 codepoint into JS string.
* Note: The built-in String.fromCodePoint happens to be much slower
* due to additional sanity checks. We can avoid them since
* we always operate on legal UTF32 (granted by the input decoders)
* and use this faster version instead.
*/
export function stringFromCodePoint(codePoint: number): string {
if (codePoint > 0xFFFF) {
codePoint -= 0x10000;
return String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00);
}
return String.fromCharCode(codePoint);
}
/**
* Convert UTF32 char codes into JS string.
* Basically the same as `stringFromCodePoint` but for multiple codepoints
* in a loop (which is a lot faster).
*/
export function utf32ToString(data: Uint32Array, start: number = 0, end: number = data.length): string {
let result = '';
for (let i = start; i < end; ++i) {
let codepoint = data[i];
if (codepoint > 0xFFFF) {
// JS strings are encoded as UTF16, thus a non BMP codepoint gets converted into a surrogate pair
// conversion rules:
// - subtract 0x10000 from code point, leaving a 20 bit number
// - add high 10 bits to 0xD800 --> first surrogate
// - add low 10 bits to 0xDC00 --> second surrogate
codepoint -= 0x10000;
result += String.fromCharCode((codepoint >> 10) + 0xD800) + String.fromCharCode((codepoint % 0x400) + 0xDC00);
} else {
result += String.fromCharCode(codepoint);
}
}
return result;
}
/**
* StringToUtf32 - decodes UTF16 sequences into UTF32 codepoints.
* To keep the decoder in line with JS strings it handles single surrogates as UCS2.
@@ -73,37 +112,231 @@ export class StringToUtf32 {
}
/**
* Convert UTF32 codepoint into JS string.
* Utf8Decoder - decodes UTF8 byte sequences into UTF32 codepoints.
*/
export function stringFromCodePoint(codePoint: number): string {
if (codePoint > 0xFFFF) {
// UTF32 to UTF16 conversion (see comments in utf32ToString)
codePoint -= 0x10000;
return String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00);
}
return String.fromCharCode(codePoint);
}
export class Utf8ToUtf32 {
public interim: Uint8Array = new Uint8Array(3);
/**
* Convert UTF32 char codes into JS string.
* Basically the same as `stringFromCodePoint` but for multiple codepoints
* in a loop (which is a lot faster).
*/
export function utf32ToString(data: Uint32Array, start: number = 0, end: number = data.length): string {
let result = '';
for (let i = start; i < end; ++i) {
let codepoint = data[i];
if (codepoint > 0xFFFF) {
// JS string are encoded as UTF16, thus a non BMP codepoint gets converted into a surrogate pair
// conversion rules:
// - subtract 0x10000 from code point, leaving a 20 bit number
// - add high 10 bits to 0xD800 --> first surrogate
// - add low 10 bits to 0xDC00 --> second surrogate
codepoint -= 0x10000;
result += String.fromCharCode((codepoint >> 10) + 0xD800) + String.fromCharCode((codepoint % 0x400) + 0xDC00);
} else {
result += String.fromCharCode(codepoint);
}
/**
* Clears interim bytes and resets decoder to clean state.
*/
public clear(): void {
this.interim.fill(0);
}
/**
* Decodes UTF8 byte sequences in `input` to UTF32 codepoints in `target`.
* The methods assumes stream input and will store partly transmitted bytes
* and decode them with the next data chunk.
* Note: The method does no bound checks for target, therefore make sure
* the provided data chunk does not exceed the size of `target`.
* Returns the number of written codepoints in `target`.
*/
decode(input: Uint8Array, target: Uint32Array): number {
const length = input.length;
if (!length) {
return 0;
}
let size = 0;
let byte1: number;
let byte2: number;
let byte3: number;
let byte4: number;
let codepoint = 0;
let startPos = 0;
// handle leftover bytes
if (this.interim[0]) {
let discardInterim = false;
let cp = this.interim[0];
cp &= ((((cp & 0xE0) === 0xC0)) ? 0x1F : (((cp & 0xF0) === 0xE0)) ? 0x0F : 0x07);
let pos = 0;
let tmp: number;
while ((tmp = this.interim[++pos] & 0x3F) && pos < 4) {
cp <<= 6;
cp |= tmp;
}
// missing bytes - read ahead from input
const type = (((this.interim[0] & 0xE0) === 0xC0)) ? 2 : (((this.interim[0] & 0xF0) === 0xE0)) ? 3 : 4;
const missing = type - pos;
while (startPos < missing) {
if (startPos >= length) {
return 0;
}
tmp = input[startPos++];
if ((tmp & 0xC0) !== 0x80) {
// wrong continuation, discard interim bytes completely
startPos--;
discardInterim = true;
break;
} else {
// need to save so we can continue short inputs in next call
this.interim[pos++] = tmp;
cp <<= 6;
cp |= tmp & 0x3F;
}
}
if (!discardInterim) {
// final test is type dependent
if (type === 2) {
if (cp < 0x80) {
// wrong starter byte
startPos--;
} else {
target[size++] = cp;
}
} else if (type === 3) {
if (cp < 0x0800 || (cp >= 0xD800 && cp <= 0xDFFF)) {
// illegal codepoint
} else {
target[size++] = cp;
}
} else {
if (codepoint < 0x010000 || codepoint > 0x10FFFF) {
// illegal codepoint
} else {
target[size++] = cp;
}
}
}
this.interim.fill(0);
}
// loop through input
const fourStop = length - 4;
let i = startPos;
while (i < length) {
/**
* ASCII shortcut with loop unrolled to 4 consecutive ASCII chars.
* This is a compromise between speed gain for ASCII
* and penalty for non ASCII:
* For best ASCII performance the char should be stored directly into target,
* but even a single attempt to write to target and compare afterwards
* penalizes non ASCII really bad (-50%), thus we load the char into byteX first,
* which reduces ASCII performance by ~15%.
* This trial for ASCII reduces non ASCII performance by ~10% which seems acceptible
* compared to the gains.
* Note that this optimization only takes place for 4 consecutive ASCII chars,
* for any shorter it bails out. Worst case - all 4 bytes being read but
* thrown away due to the last being a non ASCII char (-10% performance).
*/
while (i < fourStop
&& !((byte1 = input[i]) & 0x80)
&& !((byte2 = input[i + 1]) & 0x80)
&& !((byte3 = input[i + 2]) & 0x80)
&& !((byte4 = input[i + 3]) & 0x80))
{
target[size++] = byte1;
target[size++] = byte2;
target[size++] = byte3;
target[size++] = byte4;
i += 4;
}
// reread byte1
byte1 = input[i++];
// 1 byte
if (byte1 < 0x80) {
target[size++] = byte1;
// 2 bytes
} else if ((byte1 & 0xE0) === 0xC0) {
if (i >= length) {
this.interim[0] = byte1;
return size;
}
byte2 = input[i++];
if ((byte2 & 0xC0) !== 0x80) {
// wrong continuation
i--;
continue;
}
codepoint = (byte1 & 0x1F) << 6 | (byte2 & 0x3F);
if (codepoint < 0x80) {
// wrong starter byte
i--;
continue;
}
target[size++] = codepoint;
// 3 bytes
} else if ((byte1 & 0xF0) === 0xE0) {
if (i >= length) {
this.interim[0] = byte1;
return size;
}
byte2 = input[i++];
if ((byte2 & 0xC0) !== 0x80) {
// wrong continuation
i--;
continue;
}
if (i >= length) {
this.interim[0] = byte1;
this.interim[1] = byte2;
return size;
}
byte3 = input[i++];
if ((byte3 & 0xC0) !== 0x80) {
// wrong continuation
i--;
continue;
}
codepoint = (byte1 & 0x0F) << 12 | (byte2 & 0x3F) << 6 | (byte3 & 0x3F);
if (codepoint < 0x0800 || (codepoint >= 0xD800 && codepoint <= 0xDFFF)) {
// illegal codepoint, no i-- here
continue;
}
target[size++] = codepoint;
// 4 bytes
} else if ((byte1 & 0xF8) === 0xF0) {
if (i >= length) {
this.interim[0] = byte1;
return size;
}
byte2 = input[i++];
if ((byte2 & 0xC0) !== 0x80) {
// wrong continuation
i--;
continue;
}
if (i >= length) {
this.interim[0] = byte1;
this.interim[1] = byte2;
return size;
}
byte3 = input[i++];
if ((byte3 & 0xC0) !== 0x80) {
// wrong continuation
i--;
continue;
}
if (i >= length) {
this.interim[0] = byte1;
this.interim[1] = byte2;
this.interim[2] = byte3;
return size;
}
byte4 = input[i++];
if ((byte4 & 0xC0) !== 0x80) {
// wrong continuation
i--;
continue;
}
codepoint = (byte1 & 0x07) << 18 | (byte2 & 0x3F) << 12 | (byte3 & 0x3F) << 6 | (byte4 & 0x3F);
if (codepoint < 0x010000 || codepoint > 0x10FFFF) {
// illegal codepoint, no i-- here
continue;
}
target[size++] = codepoint;
} else {
// illegal byte, just skip
}
}
return size;
}
return result;
}
+6 -13
View File
@@ -1,20 +1,13 @@
{
"extends": "../tsconfig-library-base",
"compilerOptions": {
"target": "es5",
"lib": [
"es5"
],
"rootDir": ".",
"noEmit": true,
"strict": true,
"pretty": true,
"outDir": "../../lib",
"types": [
"../../node_modules/@types/mocha",
"../../"
"../../node_modules/@types/mocha"
]
},
"include": [
"./**/*",
"../common/**/*"
"include": [ "./**/*" ],
"references": [
{ "path": "../common" }
]
}
+2 -1
View File
@@ -3,7 +3,8 @@
* @license MIT
*/
import { ITerminal, IBufferLine } from '../Types';
import { ITerminal } from '../Types';
import { IBufferLine } from '../core/Types';
import { ICircularList } from '../common/Types';
import { C0 } from '../common/data/EscapeSequences';
+54
View File
@@ -0,0 +1,54 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { AddonManager, ILoadedAddon } from './AddonManager';
import { ITerminalAddon } from 'xterm';
class TestAddonManager extends AddonManager {
public get addons(): ILoadedAddon[] {
return this._addons;
}
}
describe('AddonManager', () => {
let manager: TestAddonManager;
beforeEach(() => {
manager = new TestAddonManager();
});
describe('loadAddon', () => {
it('should call addon constructor', () => {
let called = false;
class Addon implements ITerminalAddon {
activate(terminal: any): void {
assert.equal(terminal, 'foo', 'The first constructor arg should be Terminal');
called = true;
}
dispose(): void { }
}
manager.loadAddon('foo' as any, new Addon());
assert.equal(called, true);
});
});
describe('dispose', () => {
it('should dispose all loaded addons', () => {
let called = 0;
class Addon implements ITerminalAddon {
activate(): void {}
dispose(): void { called++; }
}
manager.loadAddon(null, new Addon());
manager.loadAddon(null, new Addon());
manager.loadAddon(null, new Addon());
assert.equal(manager.addons.length, 3);
manager.dispose();
assert.equal(called, 3);
assert.equal(manager.addons.length, 0);
});
});
});

Some files were not shown because too many files have changed in this diff Show More