Compare commits

...
359 Commits
Author SHA1 Message Date
Daniel Imms 740ca9ce6d v3.10.0 2019-01-02 14:23:09 -08:00
Daniel Imms 29ff74e069 Merge pull request #1859 from Tyriar/baseline
Fix text being top aligned in latest Chrome and Firefox
2019-01-01 18:58:32 -08:00
Daniel Imms c66a24fab4 Merge branch 'master' into baseline 2019-01-01 15:33:57 -08:00
jerch 7a579a374a Merge pull request #1853 from PerBothner/control-seq-handler
hooks for custom CSI/OSC control sequences
2019-01-01 21:25:52 +01:00
jerch f534758e29 Merge branch 'master' into control-seq-handler 2019-01-01 21:19:34 +01:00
Daniel Imms dbd9b2af37 Merge branch 'master' into baseline 2019-01-01 11:50:09 -08:00
Daniel Imms 68b2b79880 Merge pull request #1861 from Tyriar/1860_style_npe
Add sanity checks to dom renderer underline code
2019-01-01 11:47:17 -08:00
Per Bothner 8fbeadd5f9 Add missing deleteCount argument to Array.splice calls. 2019-01-01 09:45:23 -08:00
Daniel Imms 8b3513f009 Merge branch 'master' into 1860_style_npe 2018-12-31 19:15:15 -08:00
Daniel Imms 9b0b323957 Merge branch 'master' into baseline 2018-12-31 19:14:53 -08:00
Daniel Imms 48c1d36d77 Merge branch 'master' into control-seq-handler 2018-12-31 19:07:58 -08:00
Per Bothner bbfe149397 Merge pull request #1 from Tyriar/hooks_changes
Move to array-based instead of linked list, simplify, add tests
2018-12-29 07:01:39 -08:00
Daniel Imms 4375b3cdb9 Merge pull request #1865 from ntchjb/master
Fix search addons: searchTerm should not be matched if the beginning of matching index < 0
2018-12-28 15:02:51 -08:00
ntchjb 913150fe13 Fix search addons:
searchTerm should not be matched if the beginning of matching index < 0
2018-12-29 03:03:23 +07:00
Daniel Imms c133c644a6 Merge pull request #1834 from Tyriar/debugging
Add VS Code debug targets for demo server, client and unit tests
2018-12-27 14:18:51 -08:00
Daniel Imms 951f281bb0 Merge branch 'master' into debugging 2018-12-27 14:09:48 -08:00
Daniel Imms d5cacbaf4a Merge pull request #1863 from vincentwoo/patch-2
Small indent fix
2018-12-27 14:02:53 -08:00
Vincent Woo bb198a980c Small indent fix 2018-12-27 13:54:11 -08:00
Daniel Imms 2611a9b1d1 Merge pull request #1846 from linrock/no-end-quotes-in-weblinks
Weblinks should not allow quotes at end of urls enclosed in quotes
2018-12-27 11:32:40 -08:00
Daniel Imms 689ac3e073 Merge branch 'master' into no-end-quotes-in-weblinks 2018-12-27 11:29:33 -08:00
Daniel Imms 51e1f49bf3 Make dispose more resilient 2018-12-27 10:26:40 -08:00
Daniel Imms 838d727a89 Merge pull request #1849 from epicfaace/feature/hold-key
Allow holding key on mac to send multiple keys to terminal
2018-12-27 10:08:12 -08:00
Daniel Imms 9bcfb4d722 Tidy up wrapping style 2018-12-27 10:04:48 -08:00
Ashwin Ramaswami 0d9487c10f Merge branch 'master' into feature/hold-key 2018-12-26 20:36:53 -05:00
Daniel Imms b48f9108c8 Merge pull request #1763 from noamyogev84/issue_1654
Finding multiple instances on the same line
2018-12-26 16:13:24 -08:00
Daniel Imms d698faa183 Comment how the regex reverse search while works 2018-12-26 16:12:53 -08:00
Daniel Imms 1a1f7e984f Remove ISearchIndex object 2018-12-26 16:03:31 -08:00
Daniel Imms e0d535e0da Fix previous search, remove incremental previous search 2018-12-26 15:49:08 -08:00
Daniel Imms 76302e2146 Get multiple matches working after incremental changes 2018-12-26 15:32:34 -08:00
Daniel Imms 204e42a151 Fix incremental search 2018-12-26 15:15:12 -08:00
Daniel Imms bb9732599b Merge remote-tracking branch 'origin/master' into pr/noamyogev84/1763 2018-12-26 15:08:16 -08:00
Daniel Imms adbb929f78 Wrap .d.ts comments to 80 chars 2018-12-26 14:49:49 -08:00
Daniel Imms c045c803d6 Add tests for dispose 2018-12-26 14:48:35 -08:00
Daniel Imms 38796a0f74 Add tests, fix NPE 2018-12-26 14:40:23 -08:00
Daniel Imms d01efdda27 Use array instead of linkedlist, add typings 2018-12-26 14:08:53 -08:00
Daniel Imms 9e3e724f63 Add sanity checks to dom renderer underline code
Fixes #1860
2018-12-26 10:53:40 -08:00
Daniel Imms a80baa71e8 Merge branch 'master' into control-seq-handler 2018-12-23 22:24:26 -08:00
Daniel Imms 272cb08472 Merge pull request #1847 from linrock/update-natives
Fix npm scripts crashing when using node 10+
2018-12-23 22:19:12 -08:00
Per Bothner 48ff841d6a Be more paranoid about cleaning up escape sequence handlers. 2018-12-23 12:27:26 -08:00
Daniel Imms 0e8a102e17 Merge pull request #1856 from Tyriar/readme
Remove gitter and improve README
2018-12-23 10:57:49 -08:00
Daniel Imms c6e6c51929 Use textBaseline middle to draw instead of top
Chrome and Firefox behavior differs, see:
- https://bugzilla.mozilla.org/show_bug.cgi?id=737852
- https://bugs.chromium.org/p/chromium/issues/detail?id=607053

Fixes #1858
2018-12-23 10:51:41 -08:00
Daniel Imms 81ada39989 Remove Gitter link from issue template
Fixes #1838
2018-12-21 23:07:25 -08:00
Daniel Imms 78e7e39bb4 Improve the readme 2018-12-21 23:04:38 -08:00
Ashwin Ramaswami 548d8cd764 Merge branch 'feature/hold-key' of https://github.com/epicfaace/xterm.js into feature/hold-key 2018-12-21 07:38:19 -05:00
Ashwin Ramaswami a4b4d082fb Don't include num lock and scroll lock 2018-12-21 07:38:14 -05:00
Per Bothner 29cc0bf933 Merge remote-tracking branch 'upstream/master' into control-seq-handler 2018-12-20 18:12:53 -08:00
Daniel Imms 2896f56ebb Merge branch 'master' into feature/hold-key 2018-12-20 12:13:43 -08:00
Daniel Imms fcef4d5d28 Merge pull request #1851 from robins1212/bindfullscreen
bind classList function before calling in toggleFullScreen
2018-12-20 12:11:53 -08:00
robins1212 701f50f47b Merge branch 'master' into bindfullscreen 2018-12-20 12:03:43 -08:00
Daniel Imms 61d0e99ffd Merge pull request #1850 from Tyriar/vscode65485
Fix winptyCompat wrapped line heuristic
2018-12-20 11:58:55 -08:00
Robin Sturm 3eac446a85 bind function before calling 2018-12-20 11:54:26 -08:00
Daniel Imms 71d8a4d1ed Merge branch 'master' into vscode65485 2018-12-20 11:48:51 -08:00
Daniel Imms ff73f705ae Fix winptyCompat wrapped line heuristic
Part of Microsoft/vscode#65485
2018-12-20 11:47:51 -08:00
Ashwin Ramaswami 64dd9d46fb Change keyCode cutoff 2018-12-20 14:21:35 -05:00
Ashwin Ramaswami 0782fbb828 Fix check 2018-12-20 09:57:24 -05:00
Ashwin Ramaswami b820f23da6 Add additional test 2018-12-20 09:50:57 -05:00
Ashwin Ramaswami ec1060187d Add tests 2018-12-20 09:50:18 -05:00
Ashwin Ramaswami 1104bcbccb Allow holding key on mac to send multiple keys to terminal 2018-12-20 09:41:44 -05:00
Daniel Imms 90ec90746e Merge pull request #1848 from linrock/fix-addons-typo
Fix missing quotes in import statement in readme
2018-12-19 11:09:25 -08:00
Daniel Imms b39e8cfbb9 Merge pull request #1843 from Tyriar/endcol_null
Fix translateToString endCol default arg
2018-12-19 11:04:55 -08:00
Linmiao Xu 5ebd543b8b Fix missing quotes in import statement in readme 2018-12-19 14:32:15 +09:00
Linmiao Xu 86f194a6d3 Fix npm scripts when using node 10+ 2018-12-19 13:50:41 +09:00
Linmiao Xu 9cd7bf2af1 Weblinks should not allow quotes at end of urls enclosed in quotes 2018-12-19 13:28:16 +09:00
Daniel Imms c961175c03 Merge branch 'master' into debugging 2018-12-18 16:28:26 -08:00
Daniel Imms 04ac36fc16 Fix translateToString endCol default arg 2018-12-18 16:22:41 -08:00
Daniel Imms d27b43d515 Merge pull request #1841 from jerch/fix/1840
fix invalid string and buffer index in Linkifier
2018-12-18 10:57:00 -08:00
Jörg Breitbart 0da6300102 make linter happy 2018-12-18 10:50:30 +01:00
Jörg Breitbart e5ff04e798 Merge branch 'master' into fix/1840 2018-12-18 00:04:48 +01:00
Jörg Breitbart 5460068d52 fix invalid string and buffer index 2018-12-17 23:59:48 +01:00
Daniel Imms a53c6f1728 Merge pull request #1826 from nojvek/master
Fix #1660: Search as you type
2018-12-17 11:56:22 -08:00
Daniel Imms 6a5b5c23f8 Merge branch 'master' into master 2018-12-17 11:46:33 -08:00
Per Bothner 6b65ebd4aa Various typing and API fixes, doc comments, typing test etc. 2018-12-15 11:34:15 -08:00
Per Bothner 8a5a03238f New method _linkHandler used by both addCsiHandler and addOscHandler. 2018-12-15 09:42:19 -08:00
jerch 4feed2dabc Merge pull request #1775 from jerch/trim_behavior
explicit null char representation and fix for right side trimming
2018-12-15 00:11:41 +01:00
Jörg Breitbart 415e760a8a Merge branch 'trim_behavior' of git+ssh://github.com/jerch/xterm.js into trim_behavior 2018-12-15 00:04:15 +01:00
Jörg Breitbart 3927364f0e remove nonsense comments 2018-12-15 00:03:54 +01:00
Jörg Breitbart 62be3047d8 infer types fix 2018-12-15 00:01:18 +01:00
Jörg Breitbart d18c947854 Merge branch 'master' into trim_behavior 2018-12-14 23:59:40 +01:00
Per Bothner 5af4626ec7 Change addCsiHandler/addOscHandler to not use Object.assign. 2018-12-13 17:33:33 -08:00
Daniel Imms 4446fa34de Merge branch 'master' into master 2018-12-13 10:55:51 -08:00
Daniel Imms 4a7a19c368 Merge branch 'master' into trim_behavior 2018-12-13 10:19:58 -08:00
Daniel Imms 6351a5b748 Merge branch 'master' into master 2018-12-13 10:14:15 -08:00
Daniel Imms 551feb849e Merge pull request #1832 from Tyriar/remove_jsdoc
Remove jsdoc dependency
2018-12-12 09:44:40 -08:00
Daniel Imms 98285d3a84 Merge pull request #1831 from Tyriar/1830_addon_types_fix
Remove triple slash directive error in addon types
2018-12-12 09:30:34 -08:00
Daniel Imms 98ccc23934 Merge pull request #1827 from Tyriar/1605_remove_authors
Remove authors
2018-12-12 09:11:24 -08:00
Jörg Breitbart d0d94584ad Merge branch 'master' into trim_behavior 2018-12-12 18:10:24 +01:00
Noam Yogev e00c7d535d Merge branch 'master' into issue_1654 2018-12-12 11:59:48 +02:00
Per Bothner 8ceea112f7 hooks for custom control sequences
This re-implements addCsiHandler/addOscHandler to return an IDisposable.
2018-12-11 17:18:10 -08:00
Per Bothner 53fd04a886 Revert "hooks for custom control sequences"
This reverts commit 4d660deff3.
2018-12-11 16:50:40 -08:00
Per Bothner ffb2708a81 Revert "Cleanups required by tslink."
This reverts commit b68974517e.
2018-12-11 16:50:14 -08:00
Per Bothner 8ad2d1bf43 Merge remote-tracking branch 'upstream/master' 2018-12-11 16:30:18 -08:00
Daniel Imms a758f8c82f Merge pull request #1822 from PerBothner/optimize-osc-parse
Optimize parsing of OSC_STRING to minimize string concatenation.
2018-12-11 11:09:27 -08:00
Daniel Imms bbbfc04fa5 Merge branch 'master' into optimize-osc-parse 2018-12-11 11:07:12 -08:00
Daniel Imms d7f284c676 Add debug target for demo server, merge 2 start processes into one 2018-12-11 09:56:33 -08:00
Daniel Imms 943def1391 Add debug target for client, fix source maps in demo to point to ts 2018-12-11 09:37:20 -08:00
Daniel Imms a0a8003d60 Add unit tests debug target 2018-12-11 09:06:50 -08:00
Daniel Imms 6cc0a1447d Merge branch 'master' into remove_jsdoc 2018-12-11 08:58:28 -08:00
Daniel Imms 4d3345182f Remove jsdoc dependency 2018-12-11 08:57:44 -08:00
Daniel Imms 3a5d07a8f2 Merge branch 'master' into 1830_addon_types_fix 2018-12-11 07:21:34 -08:00
Daniel Imms 1e94fce545 Remove triple slash directive error in addon types
Fixes #1830
2018-12-11 07:19:37 -08:00
Daniel Imms da8546725f Merge branch 'master' into master 2018-12-11 06:28:08 -08:00
Daniel Imms 53f3b4e368 Merge pull request #1828 from Tyriar/1507_remove_layering
Remove utils/ folder
2018-12-11 06:27:49 -08:00
Noj Vek a0d583b075 handle non-cached scenario in tests land 2018-12-10 17:23:28 -08:00
Noj Vek 9005de1c42 adding a linesCache ttl and cursor move invalidation 2018-12-10 17:13:28 -08:00
Daniel Imms b9a22eb468 Remove utils/ folder
Part of #1507
2018-12-10 16:46:57 -08:00
Daniel Imms b51c07c408 Merge branch 'master' into 1605_remove_authors 2018-12-10 16:41:04 -08:00
Daniel Imms 3397ecf977 Remove authors
Fixes #1605
2018-12-10 16:40:04 -08:00
Noj Vek 801dcc3b75 Merge branch 'master' of github.com:xtermjs/xterm.js 2018-12-10 15:54:38 -08:00
Noj Vek 91ef7f24a6 Fixes #1660: Search as you type 2018-12-10 15:54:35 -08:00
Daniel Imms 65877d491e Merge pull request #1788 from gou4shi1/fix-get-mouse-coord
getCoordsRelativeToElement: use getBoundingClientRect and clientX/Y.
2018-12-10 14:46:33 -08:00
Daniel Imms f34c3d509d Merge pull request #1821 from Tyriar/dispose_atlas
Call dispose when atlas is no longer used in cache
2018-12-10 14:45:59 -08:00
Daniel Imms e1b3c7233a Merge pull request #1820 from Tyriar/add_missing_break
Add missing break in options code
2018-12-10 14:45:49 -08:00
Daniel Imms 08dc8a3e5f Merge pull request #1819 from Tyriar/fix_demo_line_height
Allow decimal line heights in demo
2018-12-10 14:45:41 -08:00
jerch 6a2e6a5edc Merge branch 'master' into optimize-osc-parse 2018-12-10 20:17:51 +01:00
Daniel Imms 1c62980d20 v3.9.0 2018-12-10 07:51:28 -08:00
jerch 2d8f420db8 fix default transition for 0xa0 2018-12-10 16:37:48 +01:00
Per Bothner 2cb293ceb2 Further tweaks to optimize parsing of OSC_STRING. 2018-12-09 19:03:58 -08:00
Per Bothner 1c4a71f0df Optimize parsing of OSC_STRING to minimize string concatenation. 2018-12-09 13:29:09 -08:00
Per Bothner 30a667c3be Revert "Optimize parsing of OSC_STRING to minimize string concatenation."
This reverts commit 03115639a9.
2018-12-09 13:11:17 -08:00
Per Bothner 03115639a9 Optimize parsing of OSC_STRING to minimize string concatenation. 2018-12-09 10:56:34 -08:00
Daniel Imms 48629e7eb2 Merge remote-tracking branch 'origin/master' into pr/gou4shi1/1788 2018-12-09 10:02:55 -08:00
Daniel Imms 6b9fb48898 Fix tests/lint, use element instead of event.target 2018-12-09 09:59:33 -08:00
Daniel Imms fe9eb7d31e Call dispose when atlas is no longer used in cache
Only runtime change is that this clears the bitmap commit timeout
in DynamicCharAtlas
2018-12-09 09:19:56 -08:00
Daniel Imms 693e17a5cb Add missing break in options code
Atlas is reset as part of onResize, this should be safe
2018-12-09 09:13:30 -08:00
Daniel Imms b5b2bcd0c9 Allow decimal line heights in demo 2018-12-09 09:08:11 -08:00
Noam 278d696709 remove reverseSearch from ISearchOptions
add isReverseSearch argument to findInLine
modify unit tests
2018-12-07 22:56:57 +02:00
gou4shi1 7f18199985 Fix build fail 2018-12-06 14:37:27 +08:00
Per Bothner b68974517e Cleanups required by tslink. 2018-12-05 14:45:57 -08:00
Per Bothner 4d660deff3 hooks for custom control sequences
This fixes (at least partially) issue #1176
"Add a way to plugin a custom control sequence handler".
2018-12-04 19:00:44 -08:00
jerch b5f1c3384a Merge pull request #1812 from jerch/default_buffer_to_typedarray
Deprecate JS array based terminal buffer
2018-12-04 20:40:47 +01:00
Jörg Breitbart 6f4a6ef308 deprecate experimentalBufferLineImpl 2018-12-04 20:33:12 +01:00
Jörg Breitbart 18902dd789 make linter happy 2018-12-04 19:11:39 +01:00
Jörg Breitbart 66ec57b431 cleanup 2018-12-04 19:02:27 +01:00
Jörg Breitbart 6ee9178302 make typed array buffer default 2018-12-04 18:55:11 +01:00
Daniel Imms 6e45d022ac Merge pull request #1806 from dividuum/patch-1
Added info-beamer hosted
2018-12-03 06:58:09 -08:00
Florian Wesch 4d270bf72c Added info-beamer hosted
Works pretty well. See https://community.infobeamer.com/t/174 for details. Wouldn't be able to add this feature without Xterm.js.
2018-12-02 15:38:50 +01:00
Noam Yogev 38adfcd81c Merge branch 'master' into issue_1654 2018-11-30 20:49:12 +02:00
Noam 376c790e1d implement find multiple matches in line.
start search from current selection.
add unit tests.
2018-11-30 20:46:47 +02:00
Daniel Imms 5f989929c3 Merge pull request #1799 from Tyriar/remove_shared_folder
Remove shared/ folder
2018-11-29 14:03:20 -08:00
Jörg Breitbart 9d4beb024a keep whitespace chars 2018-11-29 22:11:38 +01:00
Jörg Breitbart 99e4a9649f fix failing test cases 2018-11-29 21:48:48 +01:00
Jörg Breitbart eb27641928 Merge branch 'master' into trim_behavior 2018-11-29 21:26:45 +01:00
Daniel Imms 4beb3a9ecd Merge remote-tracking branch 'origin/master' into remove_shared_folder 2018-11-29 11:56:27 -08:00
Daniel Imms 9df56aba13 Merge pull request #1795 from Tyriar/clipboard_layering
Move Clipboard to ui/
2018-11-29 11:54:15 -08:00
Daniel Imms 8cf98ec546 Merge pull request #1800 from Tyriar/typescript_3_1
Upgrade to typescript 3.1
2018-11-29 11:53:50 -08:00
Daniel Imms 68d5ce947e Merge pull request #1772 from whydoubt/fix_1767
Only show cross-hairs when terminal has focus
2018-11-26 11:20:36 -08:00
Daniel Imms 13175976d0 Upgrade to typescript 3.1
Bunch of types needed changing in this version, also the
IOffscreenCanvas "support" in the canvas renderer was removed
as we're moving away from that.
2018-11-24 07:16:43 -08:00
Daniel Imms f4a522516c Remove shared/ folder
We changed plans from moving web worker compatible code to shared/
to enforcing layers using core/, common/, ui/ and public/. This
change moves files back from the shared/ dir to clean up the
codebase.

Part of #1507
2018-11-24 07:03:59 -08:00
Daniel Imms 1db17701df Merge pull request #1739 from Tyriar/1737_inverse_dynamic_cache
Allow inverse colors to be stored in glyph keys, fix inverted colors in DOM renderer
2018-11-24 05:32:21 -08:00
Daniel Imms c6f1de7970 Introduce is256Color helper 2018-11-24 05:24:15 -08:00
Daniel Imms 5fca73aa5a Merge branch 'master' into 1737_inverse_dynamic_cache 2018-11-23 14:31:16 -08:00
Daniel Imms 929d722e0e Merge pull request #1794 from Tyriar/remove_unused_line_height
Remove unused lineHeight parameters
2018-11-23 14:30:08 -08:00
jerch 79a1194bea Merge pull request #1793 from Tyriar/improve_fill_fallback
Improve types of fill polyfill and test fallback
2018-11-23 19:25:22 +01:00
jerch 8a04431417 Merge branch 'master' into improve_fill_fallback 2018-11-23 19:18:21 +01:00
jerch 76e7057c94 Merge pull request #1764 from whydoubt/alt_buffer
Alternate buffer improvement
2018-11-23 19:15:27 +01:00
jerch 62b1aa5a67 Merge branch 'master' into alt_buffer 2018-11-23 19:09:08 +01:00
jerch 41ba20f749 remove not longer needed test cases
Removed the `loopFill` test method as it is not needed anymore with the separation of the looping variant in `fillFallback`.
2018-11-23 18:40:52 +01:00
Daniel Imms 32574a1a09 Move Clipboard to ui/
Belong here as it interacts with DOM

Part of #1507
2018-11-23 09:05:07 -08:00
Daniel Imms fc6f101e16 Remove unused lineHeight parameters 2018-11-23 08:59:30 -08:00
Daniel Imms 2e889852b0 Merge branch 'master' into 1737_inverse_dynamic_cache 2018-11-23 08:52:32 -08:00
Daniel Imms 1b5ef5d93a Move TypedArrayUtils to common
It belongs here since the renderers will use it
2018-11-23 08:08:30 -08:00
Daniel Imms 4f18717940 Improve handling of default values 2018-11-23 07:49:35 -08:00
Daniel Imms 266ea38d91 Improve types of fill polyfill and test fallback
The polyfill will now return the T instead of the TypedArray union
type. Tests now target the fallback only so engines that support fill
will run tests against the fallback code.
2018-11-23 07:36:01 -08:00
Daniel Imms c6c67587e1 Merge pull request #1792 from Tyriar/1609_dom_no_null_cells
DOM renderer: Don't output empty cells at end of a line
2018-11-23 07:06:00 -08:00
Daniel Imms f363872f1e Merge branch 'master' into 1609_dom_no_null_cells 2018-11-23 06:51:53 -08:00
Daniel Imms 478dfee6e2 Ensure wide chars don't overflow onto following row 2018-11-23 06:51:39 -08:00
Daniel Imms d9513ce50d Don't bother checking beyond cols 2018-11-23 06:48:37 -08:00
jerch a693eb8e9b Merge pull request #1789 from jerch/optimisation_wcwidth
Optimisation for wcwidth
2018-11-23 15:33:33 +01:00
Daniel Imms fe027a2e9d Merge branch 'master' into optimisation_wcwidth 2018-11-23 06:26:36 -08:00
Daniel Imms 65c5df92d9 Move import below copyright 2018-11-23 06:26:06 -08:00
Daniel Imms dc0ae4a815 Fix indentation 2018-11-23 06:24:27 -08:00
Daniel Imms 66272eb310 Get the line length by going backwards instead 2018-11-23 05:40:35 -08:00
Daniel Imms 913adf9ccc Merge branch 'master' into 1609_dom_no_null_cells 2018-11-22 12:46:00 -08:00
jerch b4faaef785 Merge pull request #1731 from jerch/reuse_bufferlines
Recycle buffer lines
2018-11-22 21:24:18 +01:00
jerch 2f453f968c Merge branch 'master' into reuse_bufferlines 2018-11-22 21:17:38 +01:00
Daniel Imms dc077a181e Fix tests and the colCount feature 2018-11-22 12:03:08 -08:00
Daniel Imms 692cdfa390 DOM renderer: Don't output empty cells at end
Fixes #1609
2018-11-22 11:38:00 -08:00
Daniel Imms 6b89ec9d8e Merge pull request #1754 from vladimirze/run-tests-and-tslint-for-addons-directory
Run tests & tslint for src/addons directory
2018-11-22 10:57:42 -08:00
Daniel Imms 53fe096455 Type fn strongly 2018-11-22 10:52:55 -08:00
Daniel Imms 549c28bfa4 Merge branch 'master' into run-tests-and-tslint-for-addons-directory 2018-11-22 10:51:55 -08:00
Daniel Imms 2687fd02b2 Merge branch 'master' into reuse_bufferlines 2018-11-22 10:48:47 -08:00
Daniel Imms 1ceecf3f14 Merge pull request #1791 from xtermjs/Tyriar-patch-1
Add help wanted/good first issue links
2018-11-22 09:44:40 -08:00
Daniel Imms 98a3892e52 Fix typo 2018-11-22 09:44:13 -08:00
Jörg Breitbart 8c33de395d fix linter errors 2018-11-21 00:15:04 +01:00
Jörg Breitbart ef6fb93a92 polyfill for TypedArray.fill 2018-11-21 00:07:28 +01:00
Jörg Breitbart 36db03e713 keep old impl in test as reference; minor changes 2018-11-20 22:35:51 +01:00
Jörg Breitbart 895d6fc479 Merge branch 'master' into reuse_bufferlines 2018-11-20 22:03:24 +01:00
Jörg Breitbart 15c4cc8a74 remove experimental flag for recycling, merge with TypedArray setting 2018-11-20 22:01:57 +01:00
Jörg Breitbart 3964bb28b5 throw error in recycle with buffer not full 2018-11-20 21:49:31 +01:00
Daniel Imms 006869704b Add help wanted/good first issue links 2018-11-20 09:44:10 -08:00
Jörg Breitbart 21f848d0ab regression test 2018-11-18 23:48:59 +01:00
Jörg Breitbart 9b762854eb faster wcwidth init and runtime; fix slightly wrong surrogate handling 2018-11-18 23:31:45 +01:00
Daniel Imms e4a84643b8 Merge pull request #1765 from jrkong/vscode-issue-60401
Updated webLinks regex accept colons, fix vscode issue#60401
2018-11-18 14:12:30 -08:00
jerch 02732d9272 Merge pull request #1787 from jerch/fix_audiocontext
hold single AudioContext instance for all terminal instances
2018-11-18 22:54:57 +01:00
Jörg Breitbart 9286ea3eaf make statix usage explicit 2018-11-18 17:50:29 +01:00
Jörg Breitbart 203ee0f644 remove ctx args from ctor 2018-11-18 17:47:49 +01:00
Jörg Breitbart 579ef28929 Merge branch 'master' into fix_audiocontext 2018-11-18 17:46:19 +01:00
Daniel Imms bcd3a4dc40 Merge pull request #1784 from xtermjs/Tyriar-patch-1
Fixing isClickInSelection to cover missing cases
2018-11-16 18:03:27 -08:00
Daniel Imms a480f483cd Add tests 2018-11-16 17:54:03 -08:00
Jeff Smith 2f90035243 Clear alt buffer with erase attributes upon entering 2018-11-16 14:33:37 -06:00
Jeff Smith a19d93851c Each buffer needs its own saved cursor attributes 2018-11-16 14:33:37 -06:00
Jeff Smith a5894ee234 Fix cursor movement when swapping buffers 2018-11-16 14:33:37 -06:00
gou4shi1 3ca0598279 fix EventTarget type 2018-11-15 11:56:41 +08:00
gou4shi1 3d2035e1a0 getCoordsRelativeToElement: use getBoundingClientRect and clientX/Y. 2018-11-15 11:43:38 +08:00
Jörg Breitbart ef6f0db0a0 Merge branch 'master' into reuse_bufferlines 2018-11-15 01:05:33 +01:00
Jörg Breitbart 0e3f9b72d1 allow to set audio context per argument 2018-11-15 00:48:31 +01:00
Jörg Breitbart e3146961be make linter happy 2018-11-15 00:25:23 +01:00
Jörg Breitbart 9faebaf5fe hold single AudioContext instance 2018-11-15 00:16:52 +01:00
jerch 19174dd836 Merge branch 'master' into fix_1767 2018-11-14 20:25:25 +01:00
jerch e4e5b966da Merge pull request #1780 from whydoubt/fix_1751
Make scrollbar draggable when using DOM renderer
2018-11-14 19:59:00 +01:00
Jeff Smith 8ea0bb55b4 Make scrollbar draggable when using DOM renderer
Fixes #1751
2018-11-12 21:08:51 -06:00
jerch cb97ab35cb Merge pull request #1783 from jerch/fix_1778
fix minor parser issues:
- dont expose ESC+\ (7bit ST)
- add missing executables FS, GS, RS and US
2018-11-09 13:39:50 +01:00
Daniel Imms 774793c7ca Fixing isClickInSelection to cover missing cases
The following cases weren't working:

- The first character on single line selections
- The first row on multi-line selection
2018-11-08 19:20:12 -08:00
Jörg Breitbart 4aa822e7a9 Merge branch 'master' into fix_1778 2018-11-09 01:07:11 +01:00
Jörg Breitbart de7d5e1eec swallow 7bit ST, add missing executables 2018-11-09 01:03:44 +01:00
jerch 3e233be54b Merge branch 'master' into fix_1767 2018-11-08 19:45:34 +01:00
jerch 70981c7a29 Merge pull request #1782 from astefanutti/pr-wheel
Use WheelEvent.deltaY instead of WheelEvent.wheelDeltaY for wheel event
2018-11-08 19:43:24 +01:00
Antonin Stefanutti 8acf137d14 Use WheelEvent.deltaY instead of WheelEvent.wheelDeltaY for wheel event
Fixes #1781
2018-11-08 14:44:18 +01:00
Jörg Breitbart a15b866013 test cases for getTrimLength and translateToString 2018-11-08 06:13:05 +01:00
Jörg Breitbart 2f9c04c3af cleanup test cases 2018-11-08 05:00:08 +01:00
Jörg Breitbart 67df9a5cdc cleanup 2018-11-08 04:03:48 +01:00
Jörg Breitbart 22e4a2dc97 Merge branch 'master' into reuse_bufferlines 2018-11-07 00:58:35 +01:00
Jörg Breitbart e9906f9b9e compromise between code safety and speed 2018-11-07 00:56:45 +01:00
Daniel Imms eca1170a45 Merge branch 'master' into fix_1767 2018-11-01 11:12:53 -07:00
Jörg Breitbart 5395503031 apply code and char replacement to renderer 2018-10-31 15:47:35 +01:00
Jörg Breitbart 200b59b47c fix canvas renderer 2018-10-31 15:35:30 +01:00
Jörg Breitbart cabd4a721c fix dom renderer 2018-10-31 15:30:29 +01:00
Jörg Breitbart 745e228e98 fix input handler and terminal tests 2018-10-31 15:26:40 +01:00
Jörg Breitbart 8ffcd65cff fix buffer tests 2018-10-31 15:11:18 +01:00
Jörg Breitbart 1c111d3e60 skip empty cells after fullwidth 2018-10-31 14:53:55 +01:00
Jörg Breitbart 62f1d64af4 fix trimmedLength for TypedArray 2018-10-31 14:26:54 +01:00
Jörg Breitbart ee6fa6ca00 change NULL_CHAR and trim 2018-10-31 12:42:56 +01:00
jrkong 5173bd6419 Merge branch 'master' into vscode-issue-60401 2018-10-30 19:33:05 -04:00
Daniel Imms 160b4a7d66 Revert "Remove request term info handler"
This reverts commit c2994b0ae1.
2018-10-30 11:04:32 -07:00
Jörg Breitbart fb64c527a5 revert to precheck pushWouldTrim 2018-10-30 17:40:11 +01:00
Jörg Breitbart aaffc2b2be Merge branch 'master' into reuse_bufferlines 2018-10-30 14:18:28 +01:00
Jeff Smith c7eb727220 Only show cross-hairs when terminal has focus
Fixes #1767
2018-10-29 16:18:35 -05:00
Daniel Imms f3cd652e74 Merge pull request #1733 from leomoty/line-height
Support for lineHeight in DOM Renderer
2018-10-28 10:54:52 -07:00
Daniel Imms 108438dbeb Use setRenderer instead of ctor
Just in case something hangs onto MouseHelper
2018-10-28 10:51:57 -07:00
Daniel Imms 41b1c929e0 Fix screen element remaining large if originally using canvas 2018-10-28 10:50:16 -07:00
leomoty 793516060f Recreate mouseHelper after renderer is disposed 2018-10-28 14:24:08 -03:00
leomoty 60468356db Merge branch 'master' into line-height 2018-10-27 13:26:50 -03:00
Daniel Imms e90f8611d5 Merge pull request #1752 from Tyriar/improve_dec
Improve types for listener args in .d.ts
2018-10-26 13:24:01 -07:00
Jörg Breitbart 93d7a3021d remove pushRecycling, test case for trimAndRecycle, docs 2018-10-26 16:11:47 +02:00
Jörg Breitbart 982006836b Merge branch 'master' into reuse_bufferlines 2018-10-26 15:23:43 +02:00
vladimirze d137880876 Merge branch 'master' into run-tests-and-tslint-for-addons-directory 2018-10-26 11:15:59 +03:00
Daniel Imms fc8640af1e Merge pull request #1768 from huashengdun/master
Added webssh to real-world uses
2018-10-25 09:05:15 -07:00
Shengdun Hua 213dadf1bd Update README.md 2018-10-25 23:01:03 +08:00
jrkong 2efd28ad4b updated webLinks regex to ignore colon at the end of a line and added the corresponding tests 2018-10-25 00:14:20 -04:00
Daniel Imms ba8d341416 Merge pull request #1748 from Tyriar/1747_npe
Fix NPE in DOM renderer underline
2018-10-24 16:28:57 -07:00
Daniel Imms 16614b7179 Merge pull request #1766 from whydoubt/has_focus 2018-10-24 16:10:01 -07:00
jerch c77af88010 Merge pull request #1706 from whydoubt/blank_line_attributes
Fill lines inserted from scrolling with erase attributes
2018-10-24 17:39:05 +02:00
jerch 50bc11b28b Merge branch 'master' into blank_line_attributes 2018-10-24 16:32:28 +02:00
Daniel Imms 565f46075c Merge pull request #1720 from alexr00/alexr00/ClearShouldClear
Cleared terminal lines should have isWrapped reset
2018-10-23 15:48:52 -07:00
Daniel Imms c2994b0ae1 Remove request term info handler
We're not supporting it anyway, no point having it.
2018-10-23 15:45:52 -07:00
Jeff Smith 4d556fcaf0 Check for focus on document as well as on textarea
When clicking outside of the browser document area, the cursor does not
appear to get 'blurred'. This can be confusing as it implies that the
cursor is still active. This is caused by Terminal's isFocused getter
only checking if the textarea is the active element of the document.

Expand isFocused to also check that the document itself has focus.
2018-10-23 11:57:20 -05:00
Alex Ross 083bc0f3d1 Merge branch 'master' into alexr00/ClearShouldClear 2018-10-23 10:17:26 +02:00
jrkong 1208fc9e10 updated webLinks regex to fix vscode issue#60401 2018-10-22 16:32:04 -04:00
Jörg Breitbart 1585aba5b5 slightly faster trimAndRecycle 2018-10-22 21:12:51 +02:00
Jörg Breitbart 82228ad891 trimAndRecycle with external check 2018-10-22 18:15:47 +02:00
Jörg Breitbart 6f6c7c20c0 Merge branch 'master' into reuse_bufferlines 2018-10-22 17:20:28 +02:00
Daniel Imms b9ab35dfe9 Merge pull request #1760 from skprabhanjan/fix-1749
Search from top of viewport when there is no selection
2018-10-20 13:56:53 -07:00
Daniel Imms 35db216f64 Fix indent 2018-10-20 13:55:13 -07:00
Daniel Imms 00c1211435 Merge pull request #1740 from jerch/bufferline_fixes
fix handling of length in BufferLine.resize
2018-10-20 12:57:47 -07:00
Jeff Smith 8bd49cba35 Fill inserted lines with background color
When scrolling occurs due to a LF or scroll code, fill the new line with
the currently-set background color.
2018-10-19 10:27:52 -05:00
Alex Ross 4a2189d140 Merge branch 'master' into alexr00/ClearShouldClear 2018-10-17 10:45:12 +02:00
Prabhanjan S Koushik f2fb46cc82 Merge branch 'master' into fix-1749 2018-10-17 10:51:27 +05:30
pkoushik 74664a9161 Fix-1749 2018-10-17 10:48:46 +05:30
Daniel Imms 5bc7fc4256 Merge pull request #1758 from Tyriar/1757_cjk_glyph_key
Move dynamic atlas canCache check higher
2018-10-16 11:23:22 -07:00
Daniel Imms af0ca6c2cb Merge pull request #1759 from stkvrs/fix-comment-typos
Fix typos
2018-10-16 11:22:49 -07:00
Vladimir Zeifman 3f3263cbb0 Fix tslint errors for all addons and un-exclude tests for search addon 2018-10-16 19:36:14 +03:00
Stathis Kavros 161dfa8d2d Fix typos 2018-10-16 14:23:58 +03:00
Alex Ross 8a6ed52e4e Merge branch 'master' into alexr00/ClearShouldClear 2018-10-16 12:21:00 +02:00
Daniel Imms 3edd2f2816 Merge branch 'master' into 1757_cjk_glyph_key 2018-10-15 11:44:51 -07:00
Daniel Imms 33ee08da47 Merge pull request #1756 from skprabhanjan/fix-1750
Fixes-1750
2018-10-15 11:43:58 -07:00
Daniel Imms a53aaa5f25 Move dynamic atlas canCache check higher
Fixes #1757
2018-10-15 11:35:21 -07:00
pkoushik f61ba9558b Fixes-1750 2018-10-15 23:44:30 +05:30
Alex Ross c6eb56670d Merge branch 'master' into alexr00/ClearShouldClear 2018-10-15 11:34:06 +02:00
Daniel Imms 19f6c0a940 Fix tests 2018-10-12 09:02:03 -07:00
Daniel Imms 37972aa5dd Merge pull request #1745 from xtermjs/Tyriar-patch-1
Update Azure data studio info
2018-10-12 08:58:06 -07:00
Daniel Imms a0250c5cea Improve types for listener args in .d.ts
Without this consumers need an undefined check with strictNullChecks
turned on.
2018-10-12 08:33:59 -07:00
Daniel Imms 9e1551e2f1 Fix NPE in DOM renderer underline
Fixes #1747
2018-10-11 09:37:19 -07:00
Jörg Breitbart d25e55ff45 Merge branch 'master' into bufferline_fixes 2018-10-11 18:33:21 +02:00
Daniel Imms b650db5b1a Merge pull request #1742 from AndrienkoAleksandr/fixDockerImageBuild
Fix docker image build.
2018-10-11 09:26:39 -07:00
Jörg Breitbart 4450dc9206 Merge branch 'master' into bufferline_fixes 2018-10-11 18:25:38 +02:00
jerch 69e6d7d9cc Merge pull request #1744 from jerch/demo_buffer
add buffer to server part in demo
2018-10-11 18:20:53 +02:00
Daniel Imms e66a10889f Merge branch 'master' into alexr00/ClearShouldClear 2018-10-11 09:02:54 -07:00
Daniel Imms 375b5c5b4f Fix indent 2018-10-11 09:02:36 -07:00
leomoty 2eec8f951f Merge branch 'master' into line-height 2018-10-11 11:55:12 -04:00
Alex Ross 64d6354983 Added testing and better erase left and above handling 2018-10-11 11:18:56 +02:00
Daniel Imms 8843b0cbc6 Update Azure data strudio info 2018-10-10 18:20:12 -07:00
Jörg Breitbart 588ed5e3af Merge branch 'demo_buffer' into reuse_bufferlines 2018-10-11 02:03:01 +02:00
Jörg Breitbart 05b68ee4ae Merge branch 'master' into demo_buffer 2018-10-11 01:59:03 +02:00
Daniel Imms 25e5795374 Merge branch 'master' into fixDockerImageBuild 2018-10-10 16:36:36 -07:00
Jörg Breitbart eb123ca787 add tiny buffer to server part in demo 2018-10-11 00:32:14 +02:00
Daniel Imms 58003d4e29 Merge pull request #1743 from alexr00/alexr00/nonWordChars
Remove underscore from non word characters list
2018-10-10 14:08:39 -07:00
Alex Ross c12aec3484 Merge branch 'alexr00/ClearShouldClear' of https://github.com/alexr00/xterm.js into alexr00/ClearShouldClear 2018-10-10 15:32:39 +02:00
Alex Ross f2ecd06e64 Additional isWrapped clearing 2018-10-10 15:31:49 +02:00
Jörg Breitbart 751e2efa75 better integration and experimentalPushRecycling option 2018-10-10 15:00:38 +02:00
Alex Ross c739d2fe87 Remove underscore from non word characters list
This is the upstream fix for Microsoft/vscode#59379
2018-10-10 14:05:23 +02:00
Oleksandr Andriienko 45e64a9167 Remove from package.json unused script command
Signed-off-by: Oleksandr Andriienko <oandriie@redhat.com>
2018-10-10 11:39:34 +03:00
Oleksandr Andriienko a63779ae7b Fix docker image build.
Signed-off-by: Oleksandr Andriienko <oandriie@redhat.com>
2018-10-10 10:26:32 +03:00
leomoty 6e350219aa Support for lineHeight in DOM Renderer 2018-10-09 18:58:25 -03:00
Jörg Breitbart 33a358033c new approach 2018-10-09 23:48:58 +02:00
Jörg Breitbart 0065682bd7 resize should not alter length if shrink=false 2018-10-09 21:17:33 +02:00
Daniel Imms 18035e239e Fix tests 2018-10-09 11:56:08 -07:00
Daniel Imms 91d04f2552 Merge branch 'master' into 1737_inverse_dynamic_cache 2018-10-09 11:43:26 -07:00
Daniel Imms bc16a8b7e7 Handle inverted default colors properly in DOM renderer
This uses the actual fg for bg and bg to fg, rather than the
black/white theme colors

Fixes #1738
2018-10-09 11:38:53 -07:00
Daniel Imms 1e9564788e Use constants for default colors 2018-10-09 11:12:35 -07:00
Daniel Imms 791c020a56 Allow inverse colors to be stored in glyph keys
Fixes #1737
2018-10-09 11:03:31 -07:00
jerch febc3ba2b8 Merge pull request #1736 from jerch/fix_bufferline_resize
fix BufferLine resize
2018-10-09 19:09:23 +02:00
Daniel Imms 1c5dfb5f34 Merge branch 'master' into fix_bufferline_resize 2018-10-09 09:58:00 -07:00
Jörg Breitbart 3877e96d7f fix BufferLine resize 2018-10-09 18:56:48 +02:00
Daniel Imms f2c7de8d53 Merge pull request #1735 from Tyriar/60291_infinite_loop
Fix infinite loop
2018-10-09 09:37:21 -07:00
Daniel Imms ed5467689a Fix infinite loop
See Microsoft/vscode#60291
2018-10-09 08:38:08 -07:00
jerch 8d16bb6ea2 Merge pull request #1641 from jerch/typedarray_BufferLine
buffer redesign - 2. Typedarray based BufferLine
2018-10-07 23:29:29 +02:00
Jörg Breitbart 2febd9b248 Merge branch 'typedarray_BufferLine' of git+ssh://github.com/jerch/xterm.js into typedarray_BufferLine 2018-10-07 22:10:10 +02:00
Jörg Breitbart 5b7aa68299 Merge branch 'master' into typedarray_BufferLine 2018-10-07 22:08:46 +02:00
Jörg Breitbart 3dbc6e604f move SIZE out of Cell enum 2018-10-07 22:07:55 +02:00
Daniel Imms 211cca8fda Merge branch 'master' into alexr00/ClearShouldClear 2018-10-06 10:36:18 -07:00
Daniel Imms e7ae4be394 Merge pull request #1728 from Tyriar/yarn_integrity
Commit yarn integrity checks
2018-10-05 10:14:28 -07:00
Daniel Imms b85b9bb3c3 Merge pull request #1699 from Tyriar/common_layering_strict
Enable strict null checks and no DOM access in common and core
2018-10-05 09:36:49 -07:00
Daniel Imms 77da89f5da Merge branch 'master' into alexr00/ClearShouldClear 2018-10-05 09:25:30 -07:00
Daniel Imms f5038bea3d Merge branch 'master' into common_layering_strict 2018-10-05 09:17:15 -07:00
Daniel Imms 92b5c902de Commit yarn integrity checks 2018-10-05 08:59:54 -07:00
Alex Ross 18871f0d3c Cleared terminal lines should have isWrapped reset
Fixes #1710
2018-10-01 13:34:13 -07:00
Daniel Imms efe007b89b Merge branch 'master' into typedarray_BufferLine 2018-10-01 07:34:40 -07:00
Daniel Imms da027b2e11 Merge branch 'master' into common_layering_strict 2018-10-01 07:32:09 -07:00
Jörg Breitbart 00bf16f610 Merge branch 'master' into typedarray_BufferLine 2018-09-25 01:02:14 +02:00
Daniel Imms 4cd591cb21 Merge branch 'master' into common_layering_strict 2018-09-21 11:50:22 -07:00
Daniel Imms 36c53a5f8c Merge branch 'master' into common_layering_strict 2018-09-21 07:31:18 -07:00
Daniel Imms 995e384980 Fix search addon build 2018-09-21 07:31:06 -07:00
Daniel Imms 57bbc817bb Reduce addon lib usage 2018-09-18 10:25:49 -07:00
Daniel Imms fb0b73ab83 Enforce core layering and enable strict
Part of #1507
Part of #1319
2018-09-18 10:21:59 -07:00
Daniel Imms d1deca2330 Merge branch 'master' into common_layering_strict 2018-09-18 09:49:38 -07:00
Daniel Imms 33915f3ae0 Improve structure of addon tsconfigs 2018-09-18 09:49:07 -07:00
Daniel Imms a7a58aa055 Enable strict null checks and restrict DOM access in common
Part of #1507
Part of #1319
2018-09-17 13:44:31 -07:00
Jörg Breitbart 84e9ad2adc Merge branch 'master' into typedarray_BufferLine 2018-09-14 19:19:41 +02:00
Jörg Breitbart 69ab27004e Merge branch 'typedarray_BufferLine' of git+ssh://github.com/jerch/xterm.js into typedarray_BufferLine 2018-09-13 19:30:50 +02:00
Jörg Breitbart 65a528c18b Merge branch 'master' into typedarray_BufferLine 2018-09-13 19:18:37 +02:00
Daniel Imms 6c2acd6e34 Fix reference to new setting 2018-09-09 08:00:05 -07:00
Daniel Imms 7447a8d08d Rename setting to experimentalBufferLineImpl, expose in d.ts 2018-09-09 07:20:13 -07:00
Daniel Imms d4d1426bff Merge branch 'master' into typedarray_BufferLine 2018-09-08 13:47:06 -07:00
Jörg Breitbart a754f37209 fix conditions in Buffer.setBufferLineFactory 2018-09-08 18:02:59 +02:00
Jörg Breitbart d4f7c6bcf5 rename to copyFrom; use _push for JSArray based bufferline 2018-09-08 17:51:22 +02:00
Jörg Breitbart bc943b7b57 merge with 3.7 2018-09-08 16:54:10 +02:00
Jörg Breitbart c8b8e39dde move blankLine to Buffer 2018-09-04 22:19:10 +02:00
Jörg Breitbart 7873598a41 rename BufferLineJsArray as BufferLine 2018-09-04 21:41:00 +02:00
Jörg Breitbart 2877c0da80 switch for BufferLineJsArray vs BufferLineTypedArray 2018-09-04 18:20:01 +02:00
Jörg Breitbart 165c8d4522 convenient methods fill, makeCopyOf and clone for BufferLine 2018-09-04 16:30:17 +02:00
Jörg Breitbart 02672dc22e Merge branch 'master' into typedarray_BufferLine 2018-09-04 15:05:13 +02:00
Jörg Breitbart 76edd8a3bb fix error in resize 2018-09-01 05:31:32 +02:00
Jörg Breitbart 284b083274 note on copy on data access 2018-09-01 02:10:11 +02:00
Jörg Breitbart f9453ef4fa rename fill to fillCharData 2018-09-01 00:50:40 +02:00
Jörg Breitbart 024404f548 make cols mandatory in BufferLine ctor 2018-09-01 00:18:52 +02:00
Jörg Breitbart f3561fad30 cleanup 2018-08-31 17:51:47 +02:00
Jörg Breitbart 3f2605f35c typed array based BufferLine 2018-08-31 17:45:32 +02:00
Jörg Breitbart d1f6798e75 Merge branch 'buffer_preparations' into buffer_preparations_part2 2018-08-31 04:41:57 +02:00
Jörg Breitbart 841a33be28 remove push/pop/splice calls to BufferLine 2018-08-31 04:40:08 +02:00
103 changed files with 3836 additions and 1620 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
---
name: Question
about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/xtermjs or https://gitter.im/sourcelair/xterm.js.
about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/xtermjs
---
🛑 The issue tracker is not for questions 🛑
If you have a question, please ask it on https://stackoverflow.com/questions/tagged/xtermjs or https://gitter.im/sourcelair/xterm.js.
If you have a question, please ask it on https://stackoverflow.com/questions/tagged/xtermjs.
-1
View File
@@ -12,7 +12,6 @@ npm-debug.log
/.idea/
.env
build/
.vscode/
.DS_Store
fixtures/typings-test/*.js
package-lock.json
+44
View File
@@ -0,0 +1,44 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Unit Tests",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha.cmd"
},
"runtimeArgs": [
"--colors",
"--recursive",
"${workspaceRoot}/lib"
],
"sourceMaps": true,
"outFiles": [ "${workspaceRoot}/lib/**/*.js" ],
"internalConsoleOptions": "openOnSessionStart"
},
{
"type": "chrome",
"request": "launch",
"name": "Demo Client",
"url": "http://0.0.0.0:3000",
"windows": {
"url": "http://127.0.0.1:3000"
},
"webRoot": "${workspaceFolder}/"
},
{
"type": "node",
"request": "launch",
"name": "Demo Server",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"start-debug"
],
"port": 9229
}
]
}
-134
View File
@@ -1,134 +0,0 @@
List of xterm.js contributors. Updated before every release.
7PH <b.raymond@cern.ch>
7PH <b.raymond@protonmail.com>
Adrian Labbé <adrilabbelol@gmail.com>
Aleksandr Andrienko <aandrienko@codenvy.com>
Aleksandr Andriienko <oandriie@redhat.com>
Alessandro Nadalin <alessandro.nadalin@gmail.com>
Alexander Olsson <noseglid@gmail.com>
Alexandre Petit-Pas <petitpasalexandre@gmail.com>
Alexey Kontsevoy <alexey@gravitational.com>
Andres Mejia <amejia004@gmail.com>
Anish Athalye <me@anishathalye.com>
Anthony Lapenna <lapenna.anthony@gmail.com>
Antonin Stefanutti <antonin@stefanutti.fr>
Antonis Kalipetis <akalipetis@sourcelair.com>
Anton Skshidlevsky <meefik@gmail.com>
Anton Yurovskykh <anton.yurovskykh@gmail.com>
Artem Arbatskiy <artyom.arbatskiy@gmail.com>
Austin Robertson <austinrobertson@gmail.com>
ayapi <colors.aya@gmail.com>
Ben Hall <ben@benhall.me.uk>
Benjamin Fischer <benjamin.fischer@rwth-aachen.de>
Benjamin Raymond <b.raymond@cern.ch>
Benjamin Woodruff <github@benjam.info>
Bill Church <billchurch@users.noreply.github.com>
Bob Reid <bobreid@Bobs-MacBook-Pro.local>
bottleofwater <nison.mael+bottleofwater@gmail.com>
Brandon Bayer <b@bayer.ws>
Brian Mock <brian@mockbrian.com>
Bruno Ribeiro <b.m.fernandes.ribeiro@gmail.com>
Bruno Ribeito <b.m.fernandes.ribeiro@gmail.com>
Carson Anderson <carson@betterservers.com>
CHaBou <chabup@delean.fr>
Christian Budde Christensen <budde377@gmail.com>
Christof Marti <chrmarti@microsoft.com>
Christopher Jeffrey <chjjeffrey@gmail.com>
coderaiser <mnemonic.enemy@gmail.com>
Damien Tournoud <damien@platform.sh>
Dan Brown <jdanbrown@gmail.com>
Daniel Griffen <dagriffe@microsoft.com>
Daniel Griffen <daniel@griffen.io>
Daniel Imms <daimms@microsoft.com>
Daniel Risacher <drisacher@gmail.com>
Dan Kaplun <dbkaplun@twitch.tv>
Darin Morrison <freebroccolo@users.noreply.github.com>
dcylabs <dcylabs@gmail.com>
Dominik Csapak <d.csapak@proxmox.com>
Edgar Andrés Margffoy Tuay <andfoy@gmail.com>
Elliot Saba <staticfloat@gmail.com>
Exile <exile@chamber.ee>
Felipe Gasper <felipe@felipegasper.com>
Felix <30559812+felixse@users.noreply.github.com>
ficristo <ficristo.work@gmail.com>
Gary Ritchie <gary@rstudio.com>
hiro-su <h.sugipon@gmail.com>
Ian Lewis <ianlewis@google.com>
imoses <ido@twiggle.com>
InDieTasten <indietasten@gmail.com>
irokas <akasidiari@inf.uth.gr>
Jakob Gillich <jakob@gillich.me>
Jan Kuri <jkuri88@gmail.com>
Jean Bruenn <himself@jeanbruenn.info>
Jeff Principe <princjef@gmail.com>
Jeremy Danyow <jedanyow@microsoft.com>
Jianhui Zhao <jianhuizhao329@gmail.com>
Joao Moreno <jomo@microsoft.com>
Joao Moreno <mail@joaomoreno.com>
Johannes Zellner <johannes@nebulon.de>
Jon Austin <jon.i.austin@gmail.com>
Jon Kohler <jon@nutanix.com>
Jon Masters <jon.masters@sky.com>
Jörg Breitbart <jerch@rockborn.de>
Jose Anton <roke22@gmail.com>
jpoth <poth.john@gmail.com>
Justin Luk <jluk@users.noreply.github.com>
Justin Mecham <justin@mecham.me>
Kirill Merkushev <lanwen@yandex.ru>
Krasimir Tsonev <krasimir@outset.ws>
Ledion Bitincka <lbitincka@gmail.com>
Linus Unnebäck <linus@folkdatorn.se>
Luca <LucaT1@users.noreply.github.com>
Lucian Buzzo <lucian.buzzo@gmail.com>
Lukas Drgon <lukas.drgon@gmail.com>
Lukas Geiger <lukas.geiger94@gmail.com>
Maël Nison <nison.mael@gmail.com>
Marc Dumais <marc.dumais@ericsson.com>
Marek Libra <mlibra@redhat.com>
Markus F.X.J. Oberhumer <markus@oberhumer.com>
Martin Chloride <i@martincl2.me>
Martin Koppehel <martin.koppehel@st.ovgu.de>
Martin Wang <jiahaow@ca.ibm.com>
Matt Bierner <matb@microsoft.com>
Matthew James <matthew.d.james87@gmail.com>
Michael Irwin <mikesir87@gmail.com>
Mikko Karvonen <mikko.karvonen@arm.com>
mofux <t.zilz@mofux.org>
muji <muji@tmpfs.org>
Nicolas Ramz <nicolas.ramz@gmail.com>
npezza93 <npezza93@gmail.com>
Oleksandr Andriienko <oandriie@redhat.com>
Paris Kasidiaris <pariskasidiaris@gmail.com>
Paris Kasidiaris <paris@sourcelair.com>
Peng Xiao <pengxiao@outlook.com>
Peter Baumgarten <me@peterbaumgarten.com>
Philip Olson <philip.olson@protonmail.ch>
PowerHat <31401273+7PH@users.noreply.github.com>
PowerHat <b.raymond@protonmail.com>
pro-src <34285059+pro-src@users.noreply.github.com>
pro-src <rodneyd.teal@gmail.com>
Rick Baker <rick@ricktbaker.com>
runarberg <runar@greenqloud.com>
Saad Malik <simfox3@gmail.com>
Samuel Williams <samuel.williams@oriontransfer.co.nz>
Saswat Das <saswatds@users.noreply.github.com>
Saul Costa <saul@codevolve.com>
Segev Finer <segev208@gmail.com>
Shuanglei Tao <tsl0922@gmail.com>
sitzmar <sitzmar@gmail.com>
Steven Silvester <steven.silvester@ieee.org>
stuicey <stuicey@users.noreply.github.com>
Sven-Hendrik Haase <svenstaro@gmail.com>
t-amqi <umberos@gmail.com>
Thanasis Daglis <thanasis@sourcelair.com>
thomas <thomas@kreativkonzentrat.de>
Thomas Zilz <t.zilz@mofux.org>
Tine Jozelj <tine.jozelj@outlook.com>
Tyler Jewell <tjewell@codenvy.com>
Vincent Woo <me@vincentwoo.com>
Viraj Sinha <virajosinha@gmail.com>
yutaka <shigure@refy.net>
YuviPanda <yuvipanda@gmail.com>
ZHAO Xudong <zxdong@gmail.com>
赵雪珂 <zhaoxueke@onething.net>
+2 -1
View File
@@ -30,7 +30,8 @@ opening an issue, read these pointers.
## Contributing code
- Make sure you have a [GitHub account](https://github.com/join)
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
+1 -1
View File
@@ -16,4 +16,4 @@ RUN npm install
COPY . /usr/src/app
# Run the tests and build, to make sure everything is working nicely
RUN npm run build && npm run webpack && npm run test
RUN npm run build && npm run test
+23 -40
View File
@@ -2,23 +2,20 @@
[![Build Status](https://dev.azure.com/xtermjs/xterm.js/_apis/build/status/xtermjs.xterm.js)](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3)
[![Coverage Status](https://coveralls.io/repos/github/xtermjs/xterm.js/badge.svg?branch=master)](https://coveralls.io/github/xtermjs/xterm.js?branch=master)
[![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/xterm/badge?style=rounded)](https://www.jsdelivr.com/package/npm/xterm)
Xterm.js is a terminal front-end component written in JavaScript that works in the browser.
It enables applications to provide fully featured terminals to their users and create great development experiences.
Xterm.js is a front-end component written in TypeScript that lets applications bring fully-featured terminals to their users in the browser. It's used by popular projects such as VS Code, Hyper and Theia.
## Features
- **Text-based application support**: Use xterm.js to work with applications like `bash`, `git` etc.
- **Curses-based application support**: Use xterm.js to work with applications like `vim`, `tmux` etc.
- **Mouse events support**: Xterm.js captures mouse events like click and scroll and passes them to the terminal's back-end controlling process
- **CJK (Chinese, Japanese, Korean) character support**: Xterm.js renders CJK characters seamlessly
- **IME support**: Insert international (including CJK) characters using IME input with your keyboard
- **Self-contained library**: Xterm.js works on its own. It does not require any external libraries like jQuery or React to work
- **Modular, event-based API**: Lets you build addons and themes with ease
- **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)
@@ -30,7 +27,7 @@ First you need to install the module, we ship exclusively through [npm](https://
npm install xterm
```
To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your html page. Then create a `<div id="terminal"></div>` onto which xterm can attach itself.
To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your html page. Then create a `<div id="terminal"></div>` onto which xterm can attach itself. Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
```html
<!doctype html>
@@ -50,33 +47,24 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
</html>
```
Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
### Importing
The proposed way to load xterm.js is via the ES6 module syntax.
The recommended way to load xterm.js is via the ES6 module syntax:
```javascript
import { Terminal } from 'xterm';
```
### API
The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API.
Note that some APIs are marked *experimental*, these are added so we can experiment with new ideas without committing to support it like a normal semver API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs.
### Addons
Addons are JavaScript modules that extend the `Terminal` prototype with new methods and attributes to provide additional functionality. There are a handful available in the main repository in the `src/addons` directory and you can even write your own, by using xterm.js' public API.
Addons are JavaScript modules that extend the `Terminal` prototype with new methods and attributes to provide additional functionality. There are a handful available in the main repository in the `src/addons` directory and you can even write your own by using the [public API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts).
To use an addon, just import the JavaScript module and pass it to `Terminal`'s `applyAddon` method:
```javascript
import { Terminal } from xterm;
import { Terminal } from 'xterm';
import * as fit from 'xterm/lib/addons/fit/fit';
Terminal.applyAddon(fit);
var xterm = new Terminal(); // Instantiate the terminal
@@ -87,25 +75,16 @@ You will also need to include the addon's CSS file if it has one in the folder.
#### Importing Addons in TypeScript
There are currently no typings for addons if they are accessed via extending Terminal prototype, so you will need to upcast if using TypeScript, eg. `(<any>xterm).fit()`.
Alternatively, you can import addon function and enhance the terminal on demand. This would have better typing support and is friendly to treeshaking. E.g.:
There are currently no typings for addons if they are accessed via extending Terminal prototype, so you will need to upcast if using TypeScript, eg. `(xterm as any).fit()`. Alternatively, you can import the addon function and enhance the terminal on demand. This has better typing support and is friendly to treeshaking.
```typescript
import { Terminal } from 'xterm';
import { fit } from 'xterm/lib/addons/fit/fit';
const xterm = new Terminal();
// Fit the terminal when necessary:
fit(xterm);
fit(xterm); // Fit the terminal when necessary
```
#### Third party addons
There are also the following third party addons available:
- [xterm-webfont](https://www.npmjs.com/package/xterm-webfont)
## Browser Support
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support:
@@ -116,11 +95,13 @@ Since xterm.js is typically implemented as a developer tool, only modern browser
- Safari latest
- IE11
Xterm.js works seamlessly in Electron apps and may even work on earlier versions of the browsers but these are the browsers we strive to keep working.
Xterm.js works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers, these are the versions we strive to keep working.
## API
The current full API documentation is available in the [TypeScript declaration file on the repository](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), switch the tag (press `w` when viewing the file) to point at the specific version tag you're using.
The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API.
Note that some APIs are marked *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs.
## Real-world uses
Xterm.js is used in several world-class applications to provide great terminal experiences.
@@ -159,7 +140,7 @@ computational environment for Jupyter, supporting interactive data science and s
- [**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
- [**abstruse**](https://github.com/bleenco/abstruse): Abstruse CI is a continuous integration platform based on Node.JS and Docker.
- [**Microsoft SQL Operations Studio**](https://github.com/Microsoft/sqlopsstudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux
- [**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
- [**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
@@ -171,6 +152,8 @@ computational environment for Jupyter, supporting interactive data science and s
- [**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.
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
@@ -180,7 +163,7 @@ Do you use xterm.js in your application as well? Please [open a Pull Request](ht
Xterm.js follows a monthly release cycle roughly.
The existing releases are available at this GitHub repo's [Releases](https://github.com/sourcelair/xterm.js/releases), while the roadmap is available as [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), while a rough roadmap is available by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones).
## Contributing
-7
View File
@@ -1,7 +0,0 @@
#! /usr/bin/env sh
tail --lines=+3 AUTHORS > AUTHORS.tmp
git log --format='%aN <%aE>' >> AUTHORS.tmp
echo "List of xterm.js contributors. Updated before every release.\n" > AUTHORS
sort -u AUTHORS.tmp >> AUTHORS
rm -f AUTHORS.tmp
-3
View File
@@ -17,9 +17,6 @@ CURRENT_PACKAGE_JSON_VERSION=$(cat package.json \
export BUILD_DIR=dist
npm run build
# Update AUTHORS file
sh bin/generate-authors
# Update version in package.json
sed -i "s/\"version\": \"$CURRENT_PACKAGE_JSON_VERSION\"/\"version\": \"$NEW_VERSION\"/g" package.json
+21 -22
View File
@@ -14,6 +14,7 @@ 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
@@ -50,6 +51,14 @@ function setPadding(): void {
term.fit();
}
function getSearchOptions(): ISearchOptions {
return {
regex: (document.getElementById('regex') as HTMLInputElement).checked,
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
};
}
createTerminal();
const disposeRecreateButtonHandler = () => {
@@ -97,26 +106,15 @@ function createTerminal(): void {
addDomListener(paddingElement, 'change', setPadding);
addDomListener(actionElements.findNext, 'keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const searchOptions = {
regex: (document.getElementById('regex') as HTMLInputElement).checked,
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
};
term.findNext(actionElements.findNext.value, searchOptions);
}
addDomListener(actionElements.findNext, 'keyup', (e) => {
const searchOptions = getSearchOptions();
searchOptions.incremental = e.key !== `Enter`;
term.findNext(actionElements.findNext.value, searchOptions);
});
addDomListener(actionElements.findPrevious, 'keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const searchOptions = {
regex: (document.getElementById('regex') as HTMLInputElement).checked,
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
};
term.findPrevious(actionElements.findPrevious.value, searchOptions);
addDomListener(actionElements.findPrevious, 'keyup', (e) => {
if (e.key === `Enter`) {
term.findPrevious(actionElements.findPrevious.value, getSearchOptions());
}
});
@@ -207,7 +205,8 @@ function initOptions(term: TerminalType): void {
fontFamily: null,
fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
rendererType: ['dom', 'canvas']
rendererType: ['dom', 'canvas'],
experimentalBufferLineImpl: ['JsArray', 'TypedArray']
};
const options = Object.keys((<any>term)._core.options);
const booleanOptions = [];
@@ -234,7 +233,7 @@ function initOptions(term: TerminalType): void {
});
html += '</div><div class="option-group">';
numberOptions.forEach(o => {
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${term.getOption(o)}"/></label></div>`;
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${term.getOption(o)}" step="${o === 'lineHeight' ? '0.1' : '1'}"/></label></div>`;
});
html += '</div><div class="option-group">';
Object.keys(stringOptions).forEach(o => {
@@ -264,7 +263,7 @@ function initOptions(term: TerminalType): void {
if (o === 'cols' || o === 'rows') {
updateTerminalSize();
} else {
term.setOption(o, parseInt(input.value, 10));
term.setOption(o, o === 'lineHeight' ? parseFloat(input.value) : parseInt(input.value, 10));
}
});
});
+91 -69
View File
@@ -1,84 +1,106 @@
var express = require('express');
var app = express();
var expressWs = require('express-ws')(app);
var expressWs = require('express-ws');
var os = require('os');
var pty = require('node-pty');
var terminals = {},
logs = {};
function startServer() {
var app = express();
expressWs(app);
app.use('/build', express.static(__dirname + '/../build'));
var terminals = {},
logs = {};
app.get('/', function(req, res){
res.sendFile(__dirname + '/index.html');
});
app.use('/build', express.static(__dirname + '/../build'));
app.get('/style.css', function(req, res){
res.sendFile(__dirname + '/style.css');
});
app.get('/dist/client-bundle.js', function(req, res){
res.sendFile(__dirname + '/dist/client-bundle.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', [], {
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;
app.get('/', function(req, res){
res.sendFile(__dirname + '/index.html');
});
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];
app.get('/style.css', function(req, res){
res.sendFile(__dirname + '/style.css');
});
term.resize(cols, rows);
console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.');
res.end();
});
app.get('/dist/client-bundle.js', function(req, res){
res.sendFile(__dirname + '/dist/client-bundle.js');
});
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]);
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', [], {
name: 'xterm-color',
cols: cols || 80,
rows: rows || 24,
cwd: process.env.PWD,
env: process.env
});
term.on('data', function(data) {
try {
ws.send(data);
} catch (ex) {
// The WebSocket is not open, ignore
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]);
function buffer(socket, timeout) {
let s = '';
let sender = null;
return (data) => {
s += data;
if (!sender) {
sender = setTimeout(() => {
socket.send(s);
s = '';
sender = null;
}, timeout);
}
};
}
});
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];
});
});
const send = buffer(ws, 5);
var port = process.env.PORT || 3000,
host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0';
term.on('data', function(data) {
try {
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];
});
});
console.log('App listening to http://' + host + ':' + port);
app.listen(port, host);
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);
}
module.exports = startServer;
+7 -2
View File
@@ -8,9 +8,9 @@
const cp = require('child_process');
const path = require('path');
const webpack = require('webpack');
const startServer = require('./server.js');
// Launch server
cp.spawn('node', [path.resolve(__dirname, 'server.js')], { stdio: 'inherit' });
startServer();
// Build/watch client source
const clientConfig = {
@@ -22,6 +22,11 @@ const clientConfig = {
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
},
{
test: /\.js$/,
use: ["source-map-loader"],
enforce: "pre"
}
]
},
+7 -1
View File
@@ -4,7 +4,7 @@
/// <reference path="../../typings/xterm.d.ts" />
import { Terminal } from 'xterm';
import { Terminal, IDisposable } from 'xterm';
namespace constructor {
{
@@ -119,6 +119,12 @@ 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);
d1.dispose();
const d2: IDisposable = t.addOscHandler(199,
(data: string): boolean => true);
d2.dispose();
}
namespace options {
{
+7 -7
View File
@@ -1,7 +1,7 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
"version": "3.8.0",
"version": "3.10.0",
"main": "lib/public/Terminal.js",
"types": "typings/xterm.d.ts",
"repository": "https://github.com/xtermjs/xterm.js",
@@ -28,18 +28,17 @@
"gulp-sourcemaps": "1.9.1",
"gulp-typescript": "^3.1.3",
"gulp-util": "3.0.8",
"jsdoc": "3.4.3",
"jsdom": "^11.11.0",
"merge-stream": "^1.0.1",
"node-pty": "0.7.6",
"nodemon": "1.10.2",
"nyc": "^11.8.0",
"sorcery": "^0.10.0",
"source-map-loader": "^0.2.3",
"source-map-loader": "^0.2.4",
"ts-loader": "^4.5.0",
"tslint": "^5.9.1",
"tslint-consistent-codestyle": "^1.13.0",
"typescript": "3.0",
"typescript": "3.1",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"webpack": "^4.17.1",
@@ -48,22 +47,23 @@
},
"scripts": {
"start": "node demo/start",
"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",
"mocha": "gulp test",
"build:docs": "jsdoc -c jsdoc.json",
"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\"",
"build": "gulp build",
"prepublish": "npm run build",
"coveralls": "nyc report --reporter=text-lcov | coveralls",
"webpack": "gulp webpack",
"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\""
"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\""
}
}
+1 -1
View File
@@ -5,7 +5,7 @@
import * as Strings from './Strings';
import { ITerminal, IBuffer } from './Types';
import { isMac } from './shared/utils/Browser';
import { isMac } from './core/Platform';
import { RenderDebouncer } from './ui/RenderDebouncer';
import { addDisposableDomListener } from './ui/Lifecycle';
import { Disposable } from './common/Lifecycle';
+67 -35
View File
@@ -7,7 +7,7 @@ import { assert, expect } from 'chai';
import { ITerminal } from './Types';
import { Buffer, DEFAULT_ATTR, CHAR_DATA_CHAR_INDEX } from './Buffer';
import { CircularList } from './common/CircularList';
import { MockTerminal, TestTerminal } from './utils/TestUtils.test';
import { MockTerminal, TestTerminal } from './ui/TestUtils.test';
import { BufferLine } from './BufferLine';
const INIT_COLS = 80;
@@ -37,7 +37,7 @@ describe('Buffer', () => {
describe('fillViewportRows', () => {
it('should fill the buffer with blank lines based on the size of the viewport', () => {
const blankLineChar = BufferLine.blankLine(terminal.cols, DEFAULT_ATTR).get(0);
const blankLineChar = buffer.getBlankLine(DEFAULT_ATTR).get(0);
buffer.fillViewportRows();
assert.equal(buffer.lines.length, INIT_ROWS);
for (let y = 0; y < INIT_ROWS; y++) {
@@ -155,8 +155,12 @@ describe('Buffer', () => {
assert.equal(buffer.lines.maxLength, INIT_ROWS);
buffer.y = INIT_ROWS - 1;
buffer.fillViewportRows();
buffer.lines.get(5).get(0)[1] = 'a';
buffer.lines.get(INIT_ROWS - 1).get(0)[1] = 'b';
let chData = buffer.lines.get(5).get(0);
chData[1] = 'a';
buffer.lines.get(5).set(0, chData);
chData = buffer.lines.get(INIT_ROWS - 1).get(0);
chData[1] = 'b';
buffer.lines.get(INIT_ROWS - 1).set(0, 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');
@@ -180,7 +184,7 @@ describe('Buffer', () => {
buffer.fillViewportRows();
// Create 10 extra blank lines
for (let i = 0; i < 10; i++) {
buffer.lines.push(BufferLine.blankLine(terminal.cols, DEFAULT_ATTR));
buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR));
}
// Set cursor to the bottom of the buffer
buffer.y = INIT_ROWS - 1;
@@ -200,7 +204,7 @@ describe('Buffer', () => {
buffer.fillViewportRows();
// Create 10 extra blank lines
for (let i = 0; i < 10; i++) {
buffer.lines.push(BufferLine.blankLine(terminal.cols, DEFAULT_ATTR));
buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR));
}
// Set cursor to the bottom of the buffer
buffer.y = INIT_ROWS - 1;
@@ -273,11 +277,11 @@ describe('Buffer', () => {
describe ('translateBufferLineToString', () => {
it('should handle selecting a section of ascii text', () => {
const line = new BufferLine();
line.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
line.push([ null, 'b', 1, 'b'.charCodeAt(0)]);
line.push([ null, 'c', 1, 'c'.charCodeAt(0)]);
line.push([ null, 'd', 1, 'd'.charCodeAt(0)]);
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)]);
buffer.lines.set(0, line);
const str = buffer.translateBufferLineToString(0, true, 0, 2);
@@ -285,10 +289,10 @@ describe('Buffer', () => {
});
it('should handle a cut-off double width character by including it', () => {
const line = new BufferLine();
line.push([ null, '語', 2, 35486 ]);
line.push([ null, '', 0, null]);
line.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
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)]);
buffer.lines.set(0, line);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
@@ -296,10 +300,10 @@ describe('Buffer', () => {
});
it('should handle a zero width character in the middle of the string by not including it', () => {
const line = new BufferLine();
line.push([ null, '語', 2, '語'.charCodeAt(0) ]);
line.push([ null, '', 0, null]);
line.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
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)]);
buffer.lines.set(0, line);
const str0 = buffer.translateBufferLineToString(0, true, 0, 1);
@@ -313,9 +317,9 @@ describe('Buffer', () => {
});
it('should handle single width emojis', () => {
const line = new BufferLine();
line.push([ null, '😁', 1, '😁'.charCodeAt(0) ]);
line.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
const line = new BufferLine(2);
line.set(0, [ null, '😁', 1, '😁'.charCodeAt(0) ]);
line.set(1, [ null, 'a', 1, 'a'.charCodeAt(0)]);
buffer.lines.set(0, line);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
@@ -326,9 +330,9 @@ describe('Buffer', () => {
});
it('should handle double width emojis', () => {
const line = new BufferLine();
line.push([ null, '😁', 2, '😁'.charCodeAt(0) ]);
line.push([ null, '', 0, null]);
const line = new BufferLine(2);
line.set(0, [ null, '😁', 2, '😁'.charCodeAt(0) ]);
line.set(1, [ null, '', 0, null]);
buffer.lines.set(0, line);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
@@ -337,10 +341,10 @@ describe('Buffer', () => {
const str2 = buffer.translateBufferLineToString(0, true, 0, 2);
assert.equal(str2, '😁');
const line2 = new BufferLine();
line2.push([ null, '😁', 2, '😁'.charCodeAt(0) ]);
line2.push([ null, '', 0, null]);
line2.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
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)]);
buffer.lines.set(0, line2);
const str3 = buffer.translateBufferLineToString(0, true, 0, 3);
@@ -351,7 +355,7 @@ describe('Buffer', () => {
let terminal: TestTerminal;
beforeEach(() => {
terminal = new TestTerminal({rows: 5, cols: 10});
terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5});
});
it('multiline ascii', () => {
@@ -504,18 +508,46 @@ describe('Buffer', () => {
// the dangling last cell is wrongly added in the string
// --> fixable after resolving #1685
terminal.writeSync(input);
// TODO: reenable after fix
// const s = terminal.buffer.contents(true).toArray()[0];
// assert.equal(input, s);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 10; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i + 1); // TODO: remove +1 after fix
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
const j = (i - 0) << 1;
assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex);
}
});
it('test fully wrapped buffer up to last char', () => {
const input = Array(6).join('1234567890');
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]);
}
});
it('test fully wrapped buffer up to last char with full width odd', () => {
const input = 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301'
+ 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.equal(
(!(i % 3))
? input[i]
: (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]);
}
});
});
describe('BufferStringIterator', function(): void {
it('iterator does not ovrflow buffer limits', function(): void {
it('iterator does not overflow buffer limits', function(): void {
const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5});
const data = [
'aaaaaaaaaa',
+56 -69
View File
@@ -4,21 +4,26 @@
*/
import { CircularList } from './common/CircularList';
import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from './Types';
import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult, IBufferLineConstructor } from './Types';
import { EventEmitter } from './common/EventEmitter';
import { IMarker } from 'xterm';
import { BufferLine } from './BufferLine';
import { BufferLine, BufferLineJSArray } from './BufferLine';
import { DEFAULT_COLOR } from './renderer/atlas/Types';
export const DEFAULT_ATTR = (0 << 18) | (257 << 9) | (256 << 0);
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_CHAR = '';
export const NULL_CELL_WIDTH = 1;
export const NULL_CELL_CODE = 32;
export const NULL_CELL_CODE = 0;
export const WHITESPACE_CELL_CHAR = ' ';
export const WHITESPACE_CELL_WIDTH = 1;
export const WHITESPACE_CELL_CODE = 32;
/**
* This class represents a terminal buffer (an internal state of the terminal), where the
@@ -38,7 +43,9 @@ export class Buffer implements IBuffer {
public tabs: any;
public savedY: number;
public savedX: number;
public savedCurAttr: number;
public markers: Marker[] = [];
private _bufferLineConstructor: IBufferLineConstructor;
/**
* Create a new Buffer.
@@ -53,6 +60,37 @@ export class Buffer implements IBuffer {
this.clear();
}
public setBufferLineFactory(type: string): void {
if (type === 'JsArray') {
if (this._bufferLineConstructor !== BufferLineJSArray) {
this._bufferLineConstructor = BufferLineJSArray;
this._recreateLines();
}
} else {
if (this._bufferLineConstructor !== BufferLine) {
this._bufferLineConstructor = BufferLine;
this._recreateLines();
}
}
}
private _recreateLines(): void {
if (!this.lines) return;
for (let i = 0; i < this.lines.length; ++i) {
const oldLine = this.lines.get(i);
const newLine = new this._bufferLineConstructor(oldLine.length);
for (let j = 0; j < oldLine.length; ++j) {
newLine.set(j, oldLine.get(j));
}
this.lines.set(i, newLine);
}
}
public getBlankLine(attr: number, isWrapped?: boolean): IBufferLine {
const fillCharData: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
return new this._bufferLineConstructor(this._terminal.cols, fillCharData, isWrapped);
}
public get hasScrollback(): boolean {
return this._hasScrollback && this.lines.maxLength > this._terminal.rows;
}
@@ -81,11 +119,14 @@ export class Buffer implements IBuffer {
/**
* Fills the buffer's viewport with blank lines.
*/
public fillViewportRows(): void {
public fillViewportRows(fillAttr?: number): void {
if (this.lines.length === 0) {
if (fillAttr === undefined) {
fillAttr = DEFAULT_ATTR;
}
let i = this._terminal.rows;
while (i--) {
this.lines.push(BufferLine.blankLine(this._terminal.cols, DEFAULT_ATTR));
this.lines.push(this.getBlankLine(fillAttr));
}
}
}
@@ -94,6 +135,7 @@ export class Buffer implements IBuffer {
* Clears the buffer to it's initial state, discarding all previous data.
*/
public clear(): void {
this.setBufferLineFactory(this._terminal.options.experimentalBufferLineImpl);
this.ydisp = 0;
this.ybase = 0;
this.y = 0;
@@ -124,9 +166,7 @@ export class Buffer implements IBuffer {
if (this._terminal.cols < newCols) {
const ch: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // does xterm use the default attr?
for (let i = 0; i < this.lines.length; i++) {
while (this.lines.get(i).length < newCols) {
this.lines.get(i).push(ch);
}
this.lines.get(i).resize(newCols, ch);
}
}
@@ -147,7 +187,8 @@ 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(BufferLine.blankLine(newCols, DEFAULT_ATTR));
const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
this.lines.push(new this._bufferLineConstructor(newCols, fillCharData));
}
}
}
@@ -211,7 +252,7 @@ export class Buffer implements IBuffer {
while (stringIndex) {
const line = this.lines.get(lineIndex);
if (!line) {
[-1, -1];
return [-1, -1];
}
for (let i = 0; i < line.length; ++i) {
stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length;
@@ -234,65 +275,12 @@ export class Buffer implements IBuffer {
* @param startCol The column to start at.
* @param endCol The column to end at.
*/
public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol: number = null): string {
// Get full line
let lineString = '';
public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol?: number): string {
const line = this.lines.get(lineIndex);
if (!line) {
return '';
}
// Initialize column and index values. Column values represent the actual
// cell column, indexes represent the index in the string. Indexes are
// needed here because some chars are 0 characters long (eg. after wide
// chars) and some chars are longer than 1 characters long (eg. emojis).
let startIndex = startCol;
// Only set endCol to the line length when it is null. 0 is a valid column.
if (endCol === null) {
endCol = line.length;
}
let endIndex = endCol;
for (let i = 0; i < line.length; i++) {
const char = line.get(i);
lineString += char[CHAR_DATA_CHAR_INDEX];
// Adjust start and end cols for wide characters if they affect their
// column indexes
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
if (startCol >= i) {
startIndex--;
}
if (endCol > i) {
endIndex--;
}
} else {
// Adjust the columns to take glyphs that are represented by multiple
// code points into account.
if (char[CHAR_DATA_CHAR_INDEX].length > 1) {
if (startCol > i) {
startIndex += char[CHAR_DATA_CHAR_INDEX].length - 1;
}
if (endCol > i) {
endIndex += char[CHAR_DATA_CHAR_INDEX].length - 1;
}
}
}
}
// Calculate the final end col by trimming whitespace on the right of the
// line if needed.
if (trimRight) {
const rightWhitespaceIndex = lineString.search(/\s+$/);
if (rightWhitespaceIndex !== -1) {
endIndex = Math.min(endIndex, rightWhitespaceIndex);
}
// Return the empty string if only trimmed whitespace is selected
if (endIndex <= startIndex) {
return '';
}
}
return lineString.substring(startIndex, endIndex);
return line.translateToString(trimRight, startCol, endCol);
}
public getWrappedRangeForLine(y: number): { first: number, last: number } {
@@ -451,8 +439,7 @@ export class BufferStringIterator implements IBufferStringIterator {
range.last = Math.min(range.last, this._buffer.lines.length);
let result = '';
for (let i = range.first; i <= range.last; ++i) {
// TODO: always apply trimRight after fixing #1685
result += this._buffer.translateBufferLineToString(i, (this._trimRight) ? i === range.last : false);
result += this._buffer.translateBufferLineToString(i, this._trimRight);
}
this._current = range.last + 1;
return {range: range, content: result};
+300 -63
View File
@@ -5,93 +5,330 @@
import * as chai from 'chai';
import { BufferLine } from './BufferLine';
import { CharData, IBufferLine } from './Types';
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, CHAR_DATA_ATTR_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer';
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from './Buffer';
class TestBufferLine extends BufferLine {
public toArray(): CharData[] {
return this._data;
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();
let line: IBufferLine = new TestBufferLine(0);
chai.expect(line.length).equals(0);
chai.expect(line.pop()).equals(undefined);
chai.expect(line.isWrapped).equals(false);
line = new TestBufferLine(10);
chai.expect(line.length).equals(10);
chai.expect(line.pop()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
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.pop()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
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, 789], true);
line = new TestBufferLine(10, [123, 'a', 456, 'a'.charCodeAt(0)], true);
chai.expect(line.length).equals(10);
chai.expect(line.pop()).eql([123, 'a', 456, 789]);
chai.expect(line.get(0)).eql([123, 'a', 456, 'a'.charCodeAt(0)]);
chai.expect(line.isWrapped).equals(true);
});
it('splice', function(): void {
const line = new TestBufferLine();
const data: CharData[] = [
[1, 'a', 0, 0],
[2, 'b', 0, 0],
[3, 'c', 0, 0]
];
for (let i = 0; i < data.length; ++i) line.push(data[i]);
chai.expect(line.length).equals(data.length);
const removed1 = line.splice(1, 1, [4, 'd', 0, 0]);
const removed2 = data.splice(1, 1, [4, 'd', 0, 0]);
chai.expect(removed1).eql(removed2);
chai.expect(line.toArray()).eql(data);
});
it('TerminalLine.blankLine', function(): void {
const line = TestBufferLine.blankLine(5, 123);
chai.expect(line.length).equals(5);
chai.expect(line.isWrapped).equals(false);
const ch = line.get(0);
chai.expect(ch[CHAR_DATA_ATTR_INDEX]).equals(123);
chai.expect(ch[CHAR_DATA_CHAR_INDEX]).equals(NULL_CELL_CHAR);
chai.expect(ch[CHAR_DATA_WIDTH_INDEX]).equals(NULL_CELL_WIDTH);
chai.expect(ch[CHAR_DATA_CODE_INDEX]).equals(NULL_CELL_CODE);
});
it('insertCells', function(): void {
const line = new TestBufferLine();
const data: CharData[] = [
[1, 'a', 0, 0],
[2, 'b', 0, 0],
[3, 'c', 0, 0]
];
for (let i = 0; i < data.length; ++i) line.push(data[i]);
line.insertCells(1, 3, [4, 'd', 0, 0]);
chai.expect(line.toArray()).eql([[1, 'a', 0, 0], [4, 'd', 0, 0], [4, 'd', 0, 0]]);
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();
const data: CharData[] = [
[1, 'a', 0, 0],
[2, 'b', 0, 0],
[3, 'c', 0, 0],
[4, 'd', 0, 0],
[5, 'e', 0, 0]
];
for (let i = 0; i < data.length; ++i) line.push(data[i]);
line.deleteCells(1, 2, [6, 'f', 0, 0]);
chai.expect(line.toArray()).eql([[1, 'a', 0, 0], [4, 'd', 0, 0], [5, 'e', 0, 0], [6, 'f', 0, 0], [6, 'f', 0, 0]]);
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();
const data: CharData[] = [
[1, 'a', 0, 0],
[2, 'b', 0, 0],
[3, 'c', 0, 0],
[4, 'd', 0, 0],
[5, 'e', 0, 0]
];
for (let i = 0; i < data.length; ++i) line.push(data[i]);
line.replaceCells(2, 4, [6, 'f', 0, 0]);
chai.expect(line.toArray()).eql([[1, 'a', 0, 0], [2, 'b', 0, 0], [6, 'f', 0, 0], [6, 'f', 0, 0], [5, 'e', 0, 0]]);
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)], true);
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)], true);
chai.expect(line.toArray()).eql(Array(5).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(false) - should not 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)], false);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(false) + shrink(false) - should not apply new size', function(): void {
const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
chai.expect(line.toArray()).eql(Array(20).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(false) + enlarge(false) to smaller than before', function(): void {
const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(15, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.toArray()).eql(Array(20).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(false) + enlarge(false) to bigger than before', function(): void {
const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(25, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.toArray()).eql(Array(25).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(false) + resize shrink=true should enforce shrinking', function(): void {
const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], true);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('enlarge from 0 length', function(): void {
const line = new TestBufferLine(0, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
chai.expect(line.toArray()).eql(Array(10).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)], true);
chai.expect(line.toArray()).eql(Array(0).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(false) to 0 and enlarge to different sizes', function(): void {
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(0, [1, 'a', 0, 'a'.charCodeAt(0)], false);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
line.resize(7, [1, 'a', 0, 'a'.charCodeAt(0)], false);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
line.resize(7, [1, 'a', 0, 'a'.charCodeAt(0)], true);
chai.expect(line.toArray()).eql(Array(7).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
});
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('');
});
});
});
+275 -39
View File
@@ -3,34 +3,47 @@
* @license MIT
*/
import { CharData, IBufferLine } from './Types';
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer';
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, WHITESPACE_CELL_CHAR } from './Buffer';
/**
* Class representing a terminal line.
*
* @deprecated to be removed with one of the next releases
*/
export class BufferLine implements IBufferLine {
static blankLine(cols: number, attr: number, isWrapped?: boolean): IBufferLine {
const ch: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
return new BufferLine(cols, ch, isWrapped);
}
export class BufferLineJSArray implements IBufferLine {
protected _data: CharData[];
public isWrapped = false;
public length: number;
constructor(cols?: number, ch?: CharData, isWrapped?: boolean) {
constructor(cols: number, fillCharData?: CharData, isWrapped?: boolean) {
this._data = [];
this.length = this._data.length;
if (cols) {
if (!ch) {
ch = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
}
for (let i = 0; i < cols; i++) {
this.push(ch); // Note: the ctor ch is not cloned (resembles old behavior)
}
if (!fillCharData) {
fillCharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
}
for (let i = 0; i < cols; i++) {
this._push(fillCharData); // Note: the ctor ch is not cloned (resembles old behavior)
}
if (isWrapped) {
this.isWrapped = true;
}
this.length = this._data.length;
}
private _pop(): CharData | undefined {
const data = this._data.pop();
this.length = this._data.length;
return data;
}
private _push(data: CharData): void {
this._data.push(data);
this.length = this._data.length;
}
private _splice(start: number, deleteCount: number, ...items: CharData[]): CharData[] {
const removed = this._data.splice(start, deleteCount, ...items);
this.length = this._data.length;
return removed;
}
public get(index: number): CharData {
@@ -41,43 +54,266 @@ export class BufferLine implements IBufferLine {
this._data[index] = data;
}
public pop(): CharData | undefined {
const data = this._data.pop();
this.length = this._data.length;
return data;
}
public push(data: CharData): void {
this._data.push(data);
this.length = this._data.length;
}
public splice(start: number, deleteCount: number, ...items: CharData[]): CharData[] {
const removed = this._data.splice(start, deleteCount, ...items);
this.length = this._data.length;
return removed;
}
/** insert n cells ch at pos, right cells are lost (stable length) */
public insertCells(pos: number, n: number, ch: CharData): void {
while (n--) {
this.splice(pos, 0, ch);
this.pop();
this._splice(pos, 0, ch);
this._pop();
}
}
/** delete n cells at pos, right side is filled with fill (stable length) */
public deleteCells(pos: number, n: number, fill: CharData): void {
public deleteCells(pos: number, n: number, fillCharData: CharData): void {
while (n--) {
this.splice(pos, 1);
this.push(fill);
this._splice(pos, 1);
this._push(fillCharData);
}
}
/** replace cells from pos to pos + n - 1 with fill */
public replaceCells(start: number, end: number, fill: CharData): void {
public replaceCells(start: number, end: number, fillCharData: CharData): void {
while (start < end && start < this.length) {
this.set(start++, fill); // Note: fill is not cloned (resembles old behavior)
this.set(start++, fillCharData); // Note: fill is not cloned (resembles old behavior)
}
}
/** resize line to cols filling new cells with fill */
public resize(cols: number, fillCharData: CharData, shrink: boolean = false): void {
while (this._data.length < cols) {
this._data.push(fillCharData);
}
if (shrink) {
while (this._data.length > cols) {
this._data.pop();
}
}
this.length = this._data.length;
}
public fill(fillCharData: CharData): void {
for (let i = 0; i < this.length; ++i) {
this.set(i, fillCharData);
}
}
public copyFrom(line: BufferLineJSArray): void {
this._data = line._data.slice(0);
this.length = line.length;
this.isWrapped = line.isWrapped;
}
public clone(): IBufferLine {
const newLine = new BufferLineJSArray(0);
newLine.copyFrom(this);
return newLine;
}
public getTrimmedLength(): number {
for (let i = this.length - 1; i >= 0; --i) {
const ch = this.get(i);
if (ch[CHAR_DATA_CHAR_INDEX] !== '') {
return i + ch[CHAR_DATA_WIDTH_INDEX];
}
}
return 0;
}
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) {
result += this.get(startCol)[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR;
startCol += this.get(startCol)[CHAR_DATA_WIDTH_INDEX] || 1;
}
return result;
}
}
/** 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 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, shrink: boolean = false): void {
if (cols === this.length || (!shrink && 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 (shrink) {
if (cols) {
const data = new Uint32Array(cols * CELL_SIZE);
data.set(this._data.subarray(0, cols * CELL_SIZE));
this._data = data;
} else {
this._data = null;
}
}
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 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;
}
}
+27 -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 './utils/TestUtils.test';
import { MockTerminal } from './ui/TestUtils.test';
describe('BufferSet', () => {
let terminal: ITerminal;
@@ -48,4 +48,30 @@ describe('BufferSet', () => {
assert.equal(bufferSet.active, bufferSet.alt);
});
});
describe('cursor handling when swapping buffers', () => {
beforeEach(() => {
bufferSet.normal.x = 0;
bufferSet.normal.y = 0;
bufferSet.alt.x = 0;
bufferSet.alt.y = 0;
});
it('should keep the cursor stationary when activating alt buffer', () => {
bufferSet.activateNormalBuffer();
bufferSet.active.x = 30;
bufferSet.active.y = 10;
bufferSet.activateAltBuffer();
assert.equal(bufferSet.active.x, 30);
assert.equal(bufferSet.active.y, 10);
});
it('should keep the cursor stationary when activating normal buffer', () => {
bufferSet.activateAltBuffer();
bufferSet.active.x = 30;
bufferSet.active.y = 10;
bufferSet.activateNormalBuffer();
assert.equal(bufferSet.active.x, 30);
assert.equal(bufferSet.active.y, 10);
});
});
});
+6 -2
View File
@@ -61,6 +61,8 @@ export class BufferSet extends EventEmitter implements IBufferSet {
if (this._activeBuffer === this._normal) {
return;
}
this._normal.x = this._alt.x;
this._normal.y = this._alt.y;
// The alt buffer should always be cleared when we switch to the normal
// buffer. This frees up memory since the alt buffer should always be new
// when activated.
@@ -75,13 +77,15 @@ export class BufferSet extends EventEmitter implements IBufferSet {
/**
* Sets the alt Buffer of the BufferSet as its currently active Buffer
*/
public activateAltBuffer(): void {
public activateAltBuffer(fillAttr?: number): void {
if (this._activeBuffer === this._alt) {
return;
}
// Since the alt buffer is always cleared when the normal buffer is
// activated, we want to fill it when switching to it.
this._alt.fillViewportRows();
this._alt.fillViewportRows(fillAttr);
this._alt.x = this._normal.x;
this._alt.y = this._normal.y;
this._activeBuffer = this._alt;
this.emit('activate', {
activeBuffer: this._alt,
+177 -2
View File
@@ -3,9 +3,9 @@
* @license MIT
*/
import { TestTerminal } from './utils/TestUtils.test';
import { TestTerminal } from './ui/TestUtils.test';
import { assert } from 'chai';
import { getStringCellWidth } from './CharWidth';
import { getStringCellWidth, wcwidth } from './CharWidth';
import { IBuffer } from './Types';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
@@ -77,3 +77,178 @@ describe('getStringCellWidth', function(): void {
});
// TODO: multiline tests once #1685 is resolved
});
it('wcwidth should match all values from the old implementation', function(): void {
// old implementation
const wcwidthOld = (function(opts: {nul: number, control: number}): (ucs: number) => number {
// extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c
// combining characters
const COMBINING_BMP = [
[0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489],
[0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2],
[0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603],
[0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670],
[0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED],
[0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A],
[0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902],
[0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D],
[0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981],
[0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD],
[0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C],
[0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D],
[0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC],
[0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD],
[0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C],
[0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D],
[0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0],
[0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48],
[0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC],
[0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD],
[0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D],
[0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6],
[0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E],
[0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC],
[0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35],
[0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E],
[0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97],
[0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030],
[0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039],
[0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F],
[0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753],
[0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD],
[0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD],
[0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922],
[0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B],
[0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34],
[0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42],
[0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF],
[0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063],
[0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],
[0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],
[0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB]
];
const COMBINING_HIGH = [
[0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],
[0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169],
[0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD],
[0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F],
[0xE0100, 0xE01EF]
];
// binary search
function bisearch(ucs: number, data: number[][]): boolean {
let min = 0;
let max = data.length - 1;
let mid;
if (ucs < data[0][0] || ucs > data[max][1]) {
return false;
}
while (max >= min) {
mid = (min + max) >> 1;
if (ucs > data[mid][1]) {
min = mid + 1;
} else if (ucs < data[mid][0]) {
max = mid - 1;
} else {
return true;
}
}
return false;
}
function wcwidthBMP(ucs: number): number {
// test for 8-bit control characters
if (ucs === 0) {
return opts.nul;
}
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) {
return opts.control;
}
// binary search in table of non-spacing characters
if (bisearch(ucs, COMBINING_BMP)) {
return 0;
}
// if we arrive here, ucs is not a combining or C0/C1 control character
if (isWideBMP(ucs)) {
return 2;
}
return 1;
}
function isWideBMP(ucs: number): boolean {
return (
ucs >= 0x1100 && (
ucs <= 0x115f || // Hangul Jamo init. consonants
ucs === 0x2329 ||
ucs === 0x232a ||
(ucs >= 0x2e80 && ucs <= 0xa4cf && ucs !== 0x303f) || // CJK..Yi
(ucs >= 0xac00 && ucs <= 0xd7a3) || // Hangul Syllables
(ucs >= 0xf900 && ucs <= 0xfaff) || // CJK Compat Ideographs
(ucs >= 0xfe10 && ucs <= 0xfe19) || // Vertical forms
(ucs >= 0xfe30 && ucs <= 0xfe6f) || // CJK Compat Forms
(ucs >= 0xff00 && ucs <= 0xff60) || // Fullwidth Forms
(ucs >= 0xffe0 && ucs <= 0xffe6)));
}
function wcwidthHigh(ucs: number): 0 | 1 | 2 {
if (bisearch(ucs, COMBINING_HIGH)) {
return 0;
}
if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) {
return 2;
}
return 1;
}
const control = opts.control | 0;
let table: number[] | Uint32Array = null;
function initTable(): number[] | Uint32Array {
// lookup table for BMP
const CODEPOINTS = 65536; // BMP holds 65536 codepoints
const BITWIDTH = 2; // a codepoint can have a width of 0, 1 or 2
const ITEMSIZE = 32; // using uint32_t
const CONTAINERSIZE = CODEPOINTS * BITWIDTH / ITEMSIZE;
const CODEPOINTS_PER_ITEM = ITEMSIZE / BITWIDTH;
table = (typeof Uint32Array === 'undefined')
? new Array(CONTAINERSIZE)
: new Uint32Array(CONTAINERSIZE);
for (let i = 0; i < CONTAINERSIZE; ++i) {
let num = 0;
let pos = CODEPOINTS_PER_ITEM;
while (pos--) {
num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos);
}
table[i] = num;
}
return table;
}
// get width from lookup table
// position in container : num / CODEPOINTS_PER_ITEM
// ==> n = table[Math.floor(num / 16)]
// ==> n = table[num >> 4]
// 16 codepoints per number: FFEEDDCCBBAA99887766554433221100
// position in number : (num % CODEPOINTS_PER_ITEM) * BITWIDTH
// ==> m = (n % 16) * 2
// ==> m = (num & 15) << 1
// right shift to position m
// ==> n = n >> m e.g. m=12 000000000000FFEEDDCCBBAA99887766
// we are only interested in 2 LSBs, cut off higher bits
// ==> n = n & 3 e.g. 000000000000000000000000000000XX
return function (num: number): number {
num = num | 0; // get asm.js like optimization under V8
if (num < 32) {
return control | 0;
}
if (num < 127) {
return 1;
}
const t = table || initTable();
if (num < 65536) {
return t[num >> 4] >> ((num & 15) << 1) & 3;
}
// do a full search for high codepoints
return wcwidthHigh(num);
};
})({nul: 0, control: 0}); // configurable options
// test full BMP range old vs new implmenetation
for (let i = 0; i < 65536; ++i) {
assert.equal(wcwidth(i), wcwidthOld(i), `mismatch for i: ${i}`);
}
});
+144 -166
View File
@@ -3,171 +3,139 @@
* @license MIT
*/
import { fill } from './common/TypedArrayUtils';
export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: number) => number {
// extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c
// combining characters
const COMBINING_BMP = [
[0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489],
[0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2],
[0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603],
[0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670],
[0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED],
[0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A],
[0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902],
[0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D],
[0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981],
[0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD],
[0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C],
[0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D],
[0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC],
[0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD],
[0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C],
[0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D],
[0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0],
[0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48],
[0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC],
[0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD],
[0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D],
[0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6],
[0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E],
[0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC],
[0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35],
[0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E],
[0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97],
[0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030],
[0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039],
[0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F],
[0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753],
[0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD],
[0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD],
[0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922],
[0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B],
[0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34],
[0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42],
[0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF],
[0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063],
[0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],
[0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],
[0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB]
];
const COMBINING_HIGH = [
[0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],
[0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169],
[0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD],
[0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F],
[0xE0100, 0xE01EF]
];
// binary search
function bisearch(ucs: number, data: number[][]): boolean {
let min = 0;
let max = data.length - 1;
let mid;
if (ucs < data[0][0] || ucs > data[max][1]) {
return false;
}
while (max >= min) {
mid = (min + max) >> 1;
if (ucs > data[mid][1]) {
min = mid + 1;
} else if (ucs < data[mid][0]) {
max = mid - 1;
} else {
return true;
}
}
// extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c
// combining characters
const COMBINING_BMP = [
[0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489],
[0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2],
[0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603],
[0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670],
[0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED],
[0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A],
[0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902],
[0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D],
[0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981],
[0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD],
[0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C],
[0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D],
[0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC],
[0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD],
[0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C],
[0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D],
[0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0],
[0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48],
[0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC],
[0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD],
[0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D],
[0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6],
[0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E],
[0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC],
[0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35],
[0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E],
[0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97],
[0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030],
[0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039],
[0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F],
[0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753],
[0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD],
[0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD],
[0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922],
[0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B],
[0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34],
[0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42],
[0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF],
[0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063],
[0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],
[0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],
[0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB]
];
const COMBINING_HIGH = [
[0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],
[0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169],
[0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD],
[0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F],
[0xE0100, 0xE01EF]
];
// binary search
function bisearch(ucs: number, data: number[][]): boolean {
let min = 0;
let max = data.length - 1;
let mid;
if (ucs < data[0][0] || ucs > data[max][1]) {
return false;
}
function wcwidthBMP(ucs: number): number {
// test for 8-bit control characters
if (ucs === 0) {
return opts.nul;
}
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) {
return opts.control;
}
// binary search in table of non-spacing characters
if (bisearch(ucs, COMBINING_BMP)) {
return 0;
}
// if we arrive here, ucs is not a combining or C0/C1 control character
if (isWideBMP(ucs)) {
return 2;
while (max >= min) {
mid = (min + max) >> 1;
if (ucs > data[mid][1]) {
min = mid + 1;
} else if (ucs < data[mid][0]) {
max = mid - 1;
} else {
return true;
}
}
return false;
}
function wcwidthHigh(ucs: number): 0 | 1 | 2 {
if (bisearch(ucs, COMBINING_HIGH)) {
return 0;
}
if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) {
return 2;
}
return 1;
}
const control = opts.control | 0;
// create lookup table for BMP plane
const table = new Uint8Array(65536);
fill(table, 1);
table[0] = opts.nul;
// control chars
fill(table, opts.control, 1, 32);
fill(table, opts.control, 0x7f, 0xa0);
// apply wide char rules first
// wide chars
fill(table, 2, 0x1100, 0x1160);
table[0x2329] = 2;
table[0x232a] = 2;
fill(table, 2, 0x2e80, 0xa4d0);
table[0x303f] = 1; // wrongly in last line
fill(table, 2, 0xac00, 0xd7a4);
fill(table, 2, 0xf900, 0xfb00);
fill(table, 2, 0xfe10, 0xfe1a);
fill(table, 2, 0xfe30, 0xfe70);
fill(table, 2, 0xff00, 0xff61);
fill(table, 2, 0xffe0, 0xffe7);
// apply combining last to ensure we overwrite
// wrongly wide set chars:
// the original algo evals combining first and falls
// through to wide check so we simply do here the opposite
// combining 0
for (let r = 0; r < COMBINING_BMP.length; ++r) {
fill(table, 0, COMBINING_BMP[r][0], COMBINING_BMP[r][1] + 1);
}
return function (num: number): number {
if (num < 32) {
return control | 0;
}
if (num < 127) {
return 1;
}
function isWideBMP(ucs: number): boolean {
return (
ucs >= 0x1100 && (
ucs <= 0x115f || // Hangul Jamo init. consonants
ucs === 0x2329 ||
ucs === 0x232a ||
(ucs >= 0x2e80 && ucs <= 0xa4cf && ucs !== 0x303f) || // CJK..Yi
(ucs >= 0xac00 && ucs <= 0xd7a3) || // Hangul Syllables
(ucs >= 0xf900 && ucs <= 0xfaff) || // CJK Compat Ideographs
(ucs >= 0xfe10 && ucs <= 0xfe19) || // Vertical forms
(ucs >= 0xfe30 && ucs <= 0xfe6f) || // CJK Compat Forms
(ucs >= 0xff00 && ucs <= 0xff60) || // Fullwidth Forms
(ucs >= 0xffe0 && ucs <= 0xffe6)));
if (num < 65536) {
return table[num];
}
function wcwidthHigh(ucs: number): 0 | 1 | 2 {
if (bisearch(ucs, COMBINING_HIGH)) {
return 0;
}
if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) {
return 2;
}
return 1;
}
const control = opts.control | 0;
let table: number[] | Uint32Array = null;
function initTable(): number[] | Uint32Array {
// lookup table for BMP
const CODEPOINTS = 65536; // BMP holds 65536 codepoints
const BITWIDTH = 2; // a codepoint can have a width of 0, 1 or 2
const ITEMSIZE = 32; // using uint32_t
const CONTAINERSIZE = CODEPOINTS * BITWIDTH / ITEMSIZE;
const CODEPOINTS_PER_ITEM = ITEMSIZE / BITWIDTH;
table = (typeof Uint32Array === 'undefined')
? new Array(CONTAINERSIZE)
: new Uint32Array(CONTAINERSIZE);
for (let i = 0; i < CONTAINERSIZE; ++i) {
let num = 0;
let pos = CODEPOINTS_PER_ITEM;
while (pos--) {
num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos);
}
table[i] = num;
}
return table;
}
// get width from lookup table
// position in container : num / CODEPOINTS_PER_ITEM
// ==> n = table[Math.floor(num / 16)]
// ==> n = table[num >> 4]
// 16 codepoints per number: FFEEDDCCBBAA99887766554433221100
// position in number : (num % CODEPOINTS_PER_ITEM) * BITWIDTH
// ==> m = (n % 16) * 2
// ==> m = (num & 15) << 1
// right shift to position m
// ==> n = n >> m e.g. m=12 000000000000FFEEDDCCBBAA99887766
// we are only interested in 2 LSBs, cut off higher bits
// ==> n = n & 3 e.g. 000000000000000000000000000000XX
return function (num: number): number {
num = num | 0; // get asm.js like optimization under V8
if (num < 32) {
return control | 0;
}
if (num < 127) {
return 1;
}
const t = table || initTable();
if (num < 65536) {
return t[num >> 4] >> ((num & 15) << 1) & 3;
}
// do a full search for high codepoints
return wcwidthHigh(num);
};
// do a full search for high codepoints
return wcwidthHigh(num);
};
})({nul: 0, control: 0}); // configurable options
/**
@@ -175,17 +143,27 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
*/
export function getStringCellWidth(s: string): number {
let result = 0;
for (let i = 0; i < s.length; ++i) {
const length = s.length;
for (let i = 0; i < length; ++i) {
let code = s.charCodeAt(i);
// surrogate pair first
if (0xD800 <= code && code <= 0xDBFF) {
const low = s.charCodeAt(i + 1);
if (isNaN(low)) {
return result;
if (++i >= length) {
// this should not happen with strings retrieved from
// Buffer.translateToString as it converts from UTF-32
// and therefore always should contain the second part
// for any other string we still have to handle it somehow:
// simply treat the lonely surrogate first as a single char (UCS-2 behavior)
return result + wcwidth(code);
}
const second = s.charCodeAt(i);
// convert surrogate pair to high codepoint only for valid second part (UTF-16)
// otherwise treat them independently (UCS-2 behavior)
if (0xDC00 <= second && second <= 0xDFFF) {
code = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
} else {
result += wcwidth(second);
}
code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
}
if (0xDC00 <= code && code <= 0xDFFF) {
continue;
}
result += wcwidth(code);
}
+144 -2
View File
@@ -369,7 +369,8 @@ describe('EscapeSequenceParser', function (): void {
parser.currentState = ParserState.ESCAPE_INTERMEDIATE;
parser.parse(collect[i]);
chai.expect(parser.currentState).equal(ParserState.GROUND);
testTerminal.compare([['esc', '', collect[i]]]);
// '\x5c' --> ESC + \ (7bit ST) parser does not expose this as it already got handled
testTerminal.compare((collect[i] === '\x5c') ? [] : [['esc', '', collect[i]]]);
parser.reset();
testTerminal.clear();
}
@@ -1051,6 +1052,13 @@ describe('EscapeSequenceParser', function (): void {
['csi', '<', [0, 0], 'c']
], null);
});
it('7bit ST should be swallowed', function(): void {
test('abc\x9d123tzf\x1b\\defg', [
['print', 'abc'],
['osc', '123tzf'],
['print', 'defg']
], null);
});
});
});
@@ -1089,7 +1097,7 @@ describe('EscapeSequenceParser', function (): void {
parser.reset();
testTerminal.clear();
parser.currentState = ParserState.GROUND;
parser.parse('\x1e');
parser.parse('\x9c');
chai.expect(parser.currentState).equal(ParserState.GROUND);
testTerminal.compare([]);
parser.reset();
@@ -1161,6 +1169,73 @@ describe('EscapeSequenceParser', function (): void {
parser2.parse(INPUT);
chai.expect(csi).eql([]);
});
describe('CSI custom handlers', () => {
it('Prevent fallback', () => {
const csiCustom: [string, number[], string][] = [];
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; });
parser2.parse(INPUT);
chai.expect(csi).eql([], 'Should not fallback to original handler');
chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]);
});
it('Allow fallback', () => {
const csiCustom: [string, number[], string][] = [];
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return false; });
parser2.parse(INPUT);
chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']], 'Should fallback to original handler');
chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]);
});
it('Multiple custom handlers fallback once', () => {
const csiCustom: [string, number[], string][] = [];
const csiCustom2: [string, number[], string][] = [];
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; });
parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return false; });
parser2.parse(INPUT);
chai.expect(csi).eql([], 'Should not fallback to original handler');
chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]);
chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]);
});
it('Multiple custom handlers no fallback', () => {
const csiCustom: [string, number[], string][] = [];
const csiCustom2: [string, number[], string][] = [];
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; });
parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return true; });
parser2.parse(INPUT);
chai.expect(csi).eql([], 'Should not fallback to original handler');
chai.expect(csiCustom).eql([], 'Should not fallback once');
chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]);
});
it('Execution order should go from latest handler down to the original', () => {
const order: number[] = [];
parser2.setCsiHandler('m', () => order.push(1));
parser2.addCsiHandler('m', () => { order.push(2); return false; });
parser2.addCsiHandler('m', () => { order.push(3); return false; });
parser2.parse('\x1b[0m');
chai.expect(order).eql([3, 2, 1]);
});
it('Dispose should work', () => {
const csiCustom: [string, number[], string][] = [];
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; });
customHandler.dispose();
parser2.parse(INPUT);
chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]);
chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed');
});
it('Should not corrupt the parser when dispose is called twice', () => {
const csiCustom: [string, number[], string][] = [];
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; });
customHandler.dispose();
customHandler.dispose();
parser2.parse(INPUT);
chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]);
chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed');
});
});
it('EXECUTE handler', function (): void {
parser2.setExecuteHandler('\n', function (): void {
exe.push('\n');
@@ -1188,6 +1263,73 @@ describe('EscapeSequenceParser', function (): void {
parser2.parse(INPUT);
chai.expect(osc).eql([]);
});
describe('OSC custom handlers', () => {
it('Prevent fallback', () => {
const oscCustom: [number, string][] = [];
parser2.setOscHandler(1, data => osc.push([1, data]));
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
parser2.parse(INPUT);
chai.expect(osc).eql([], 'Should not fallback to original handler');
chai.expect(oscCustom).eql([[1, 'foo=bar']]);
});
it('Allow fallback', () => {
const oscCustom: [number, string][] = [];
parser2.setOscHandler(1, data => osc.push([1, data]));
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return false; });
parser2.parse(INPUT);
chai.expect(osc).eql([[1, 'foo=bar']], 'Should fallback to original handler');
chai.expect(oscCustom).eql([[1, 'foo=bar']]);
});
it('Multiple custom handlers fallback once', () => {
const oscCustom: [number, string][] = [];
const oscCustom2: [number, string][] = [];
parser2.setOscHandler(1, data => osc.push([1, data]));
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return false; });
parser2.parse(INPUT);
chai.expect(osc).eql([], 'Should not fallback to original handler');
chai.expect(oscCustom).eql([[1, 'foo=bar']]);
chai.expect(oscCustom2).eql([[1, 'foo=bar']]);
});
it('Multiple custom handlers no fallback', () => {
const oscCustom: [number, string][] = [];
const oscCustom2: [number, string][] = [];
parser2.setOscHandler(1, data => osc.push([1, data]));
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return true; });
parser2.parse(INPUT);
chai.expect(osc).eql([], 'Should not fallback to original handler');
chai.expect(oscCustom).eql([], 'Should not fallback once');
chai.expect(oscCustom2).eql([[1, 'foo=bar']]);
});
it('Execution order should go from latest handler down to the original', () => {
const order: number[] = [];
parser2.setOscHandler(1, () => order.push(1));
parser2.addOscHandler(1, () => { order.push(2); return false; });
parser2.addOscHandler(1, () => { order.push(3); return false; });
parser2.parse('\x1b]1;foo=bar\x1b\\');
chai.expect(order).eql([3, 2, 1]);
});
it('Dispose should work', () => {
const oscCustom: [number, string][] = [];
parser2.setOscHandler(1, data => osc.push([1, data]));
const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
customHandler.dispose();
parser2.parse(INPUT);
chai.expect(osc).eql([[1, 'foo=bar']]);
chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed');
});
it('Should not corrupt the parser when dispose is called twice', () => {
const oscCustom: [number, string][] = [];
parser2.setOscHandler(1, data => osc.push([1, data]));
const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
customHandler.dispose();
customHandler.dispose();
parser2.parse(INPUT);
chai.expect(osc).eql([[1, 'foo=bar']]);
chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed');
});
});
it('DCS handler', function (): void {
parser2.setDcsHandler('+p', {
hook: function (collect: string, params: number[], flag: number): void {
+85 -21
View File
@@ -4,8 +4,16 @@
*/
import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types';
import { IDisposable } from 'xterm';
import { Disposable } from './common/Lifecycle';
interface IHandlerCollection<T> {
[key: string]: T[];
}
type CsiHandler = (params: number[], collect: string) => boolean | void;
type OscHandler = (data: string) => boolean | void;
/**
* Returns an array filled with numbers between the low and high parameters (right exclusive).
* @param low The low number.
@@ -66,8 +74,9 @@ export class TransitionTable {
const PRINTABLES = r(0x20, 0x7f);
const EXECUTABLES = r(0x00, 0x18);
EXECUTABLES.push(0x19);
EXECUTABLES.concat(r(0x1c, 0x20));
const DEFAULT_TRANSITION = ParserAction.ERROR << 4 | ParserState.GROUND;
EXECUTABLES.push.apply(EXECUTABLES, r(0x1c, 0x20));
// Pseudo-character placeholder for printable non-ascii characters.
const NON_ASCII_PRINTABLE = 0xA0;
/**
* VT500 compatible transition table.
@@ -79,10 +88,10 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
const states: number[] = r(ParserState.GROUND, ParserState.DCS_PASSTHROUGH + 1);
let state: any;
// table with default transition [any] --> DEFAULT_TRANSITION
// table with default transition
for (state in states) {
// NOTE: table lookup is capped at 0xa0 in parse to keep the table small
for (let code = 0; code < 160; ++code) {
for (let code = 0; code <= NON_ASCII_PRINTABLE; ++code) {
table.add(code, state, ParserAction.ERROR, ParserState.GROUND);
}
}
@@ -184,6 +193,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
table.addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);
table.add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH);
table.addMany([0x1b, 0x9c], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND);
table.add(NON_ASCII_PRINTABLE, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);
return table;
})();
@@ -220,9 +230,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
// handler lookup containers
protected _printHandler: (data: string, start: number, end: number) => void;
protected _executeHandlers: any;
protected _csiHandlers: any;
protected _csiHandlers: IHandlerCollection<CsiHandler>;
protected _escHandlers: any;
protected _oscHandlers: any;
protected _oscHandlers: IHandlerCollection<OscHandler>;
protected _dcsHandlers: any;
protected _activeDcsHandler: IDcsHandler | null;
protected _errorHandler: (state: IParsingState) => IParsingState;
@@ -261,6 +271,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._dcsHandlers = Object.create(null);
this._activeDcsHandler = null;
this._errorHandler = this._errorHandlerFb;
// swallow 7bit ST (ESC+\)
this.setEscHandler('\\', () => {});
}
public dispose(): void {
@@ -273,8 +286,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._errorHandlerFb = null;
this._printHandler = null;
this._executeHandlers = null;
this._csiHandlers = null;
this._escHandlers = null;
this._csiHandlers = null;
this._oscHandlers = null;
this._dcsHandlers = null;
this._activeDcsHandler = null;
@@ -298,8 +311,24 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._executeHandlerFb = callback;
}
addCsiHandler(flag: string, callback: CsiHandler): IDisposable {
const index = flag.charCodeAt(0);
if (this._csiHandlers[index] === undefined) {
this._csiHandlers[index] = [];
}
const handlerList = this._csiHandlers[index];
handlerList.push(callback);
return {
dispose: () => {
const handlerIndex = handlerList.indexOf(callback);
if (handlerIndex !== -1) {
handlerList.splice(handlerIndex, 1);
}
}
};
}
setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void {
this._csiHandlers[flag.charCodeAt(0)] = callback;
this._csiHandlers[flag.charCodeAt(0)] = [callback];
}
clearCsiHandler(flag: string): void {
if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)];
@@ -318,8 +347,23 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._escHandlerFb = callback;
}
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
if (this._oscHandlers[ident] === undefined) {
this._oscHandlers[ident] = [];
}
const handlerList = this._oscHandlers[ident];
handlerList.push(callback);
return {
dispose: () => {
const handlerIndex = handlerList.indexOf(callback);
if (handlerIndex !== -1) {
handlerList.splice(handlerIndex, 1);
}
}
};
}
setOscHandler(ident: number, callback: (data: string) => void): void {
this._oscHandlers[ident] = callback;
this._oscHandlers[ident] = [callback];
}
clearOscHandler(ident: number): void {
if (this._oscHandlers[ident]) delete this._oscHandlers[ident];
@@ -388,7 +432,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
}
// normal transition & action lookup
transition = (code < 0xa0) ? (table[currentState << 8 | code]) : DEFAULT_TRANSITION;
transition = table[currentState << 8 | (code < 0xa0 ? code : NON_ASCII_PRINTABLE)];
switch (transition >> 4) {
case ParserAction.PRINT:
print = (~print) ? print : i;
@@ -420,10 +464,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
case ParserState.GROUND:
print = (~print) ? print : i;
break;
case ParserState.OSC_STRING:
osc += String.fromCharCode(code);
transition |= ParserState.OSC_STRING;
break;
case ParserState.CSI_IGNORE:
transition |= ParserState.CSI_IGNORE;
break;
@@ -460,9 +500,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
}
break;
case ParserAction.CSI_DISPATCH:
callback = this._csiHandlers[code];
if (callback) callback(params, collect);
else this._csiHandlerFb(collect, params, code);
// Trigger CSI Handler
const handlers = this._csiHandlers[code];
let j = handlers ? handlers.length - 1 : -1;
for (; j >= 0; j--) {
if (handlers[j](params, collect)) {
break;
}
}
if (j < 0) {
this._csiHandlerFb(collect, params, code);
}
break;
case ParserAction.PARAM:
if (code === 0x3b) params.push(0);
@@ -514,7 +562,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
osc = '';
break;
case ParserAction.OSC_PUT:
osc += data.charAt(i);
for (let j = i + 1; ; j++) {
if (j >= l
|| (code = data.charCodeAt(j)) < 0x20
|| (code > 0x7f && code <= 0x9f)) {
osc += data.substring(i, j);
i = j - 1;
break;
}
}
break;
case ParserAction.OSC_END:
if (osc && code !== 0x18 && code !== 0x1a) {
@@ -529,9 +585,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
// or with an explicit NaN OSC handler
const identifier = parseInt(osc.substring(0, idx));
const content = osc.substring(idx + 1);
callback = this._oscHandlers[identifier];
if (callback) callback(content);
else this._oscHandlerFb(identifier, content);
// Trigger OSC Handler
const handlers = this._oscHandlers[identifier];
let j = handlers ? handlers.length - 1 : -1;
for (; j >= 0; j--) {
if (handlers[j](content)) {
break;
}
}
if (j < 0) {
this._oscHandlerFb(identifier, content);
}
}
}
if (code === 0x1b) transition |= ParserState.ESCAPE;
+200 -280
View File
@@ -5,101 +5,11 @@
import { assert, expect } from 'chai';
import { InputHandler } from './InputHandler';
import { MockInputHandlingTerminal } from './utils/TestUtils.test';
import { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, CHAR_DATA_CHAR_INDEX } from './Buffer';
import { MockInputHandlingTerminal } from './ui/TestUtils.test';
import { CHAR_DATA_ATTR_INDEX, DEFAULT_ATTR } from './Buffer';
import { Terminal } from './Terminal';
import { IBufferLine } from './Types';
// TODO: This and the sections related to this object in associated tests can be
// removed safely after InputHandler refactors are finished
class OldInputHandler extends InputHandler {
public eraseInLine(params: number[]): void {
switch (params[0]) {
case 0:
this.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
break;
case 1:
this.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y);
break;
case 2:
this.eraseLine(this._terminal.buffer.y);
break;
}
}
public eraseInDisplay(params: number[]): void {
let j;
switch (params[0]) {
case 0:
this.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
j = this._terminal.buffer.y + 1;
for (; j < this._terminal.rows; j++) {
this.eraseLine(j);
}
break;
case 1:
this.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y);
j = this._terminal.buffer.y;
while (j--) {
this.eraseLine(j);
}
break;
case 2:
j = this._terminal.rows;
while (j--) this.eraseLine(j);
break;
case 3:
// Clear scrollback (everything not in viewport)
const scrollBackSize = this._terminal.buffer.lines.length - this._terminal.rows;
if (scrollBackSize > 0) {
this._terminal.buffer.lines.trimStart(scrollBackSize);
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);
}
break;
}
}
/**
* Erase in the identified line everything from "x" to the end of the line (right).
* @param x The column from which to start erasing to the end of the line.
* @param y The line in which to operate.
*/
public eraseRight(x: number, y: number): void {
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y);
if (!line) {
return;
}
line.replaceCells(x, this._terminal.cols, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
this._terminal.updateRange(y);
}
/**
* Erase in the identified line everything from "x" to the start of the line (left).
* @param x The column from which to start erasing to the start of the line.
* @param y The line in which to operate.
*/
public eraseLeft(x: number, y: number): void {
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y);
if (!line) {
return;
}
line.replaceCells(0, x + 1, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
this._terminal.updateRange(y);
}
/**
* Erase all content in the given line
* @param y The line to erase all of its contents.
*/
public eraseLine(y: number): void {
this.eraseRight(0, y);
}
}
describe('InputHandler', () => {
describe('save and restore cursor', () => {
const terminal = new MockInputHandlingTerminal();
@@ -178,17 +88,9 @@ describe('InputHandler', () => {
});
});
describe('regression tests', function(): void {
type CharData = [number, string, number, number];
function lineContent(line: IBufferLine): string {
let content = '';
for (let i = 0; i < line.length; ++i) content += line.get(i)[CHAR_DATA_CHAR_INDEX];
return content;
}
function termContent(term: Terminal): string[] {
function termContent(term: Terminal, trim: boolean): string[] {
const result = [];
for (let i = 0; i < term.rows; ++i) result.push(lineContent(term.buffer.lines.get(i)));
for (let i = 0; i < term.rows; ++i) result.push(term.buffer.lines.get(i).translateToString(trim));
return result;
}
@@ -196,283 +98,301 @@ describe('InputHandler', () => {
const term = new Terminal();
const inputHandler = new InputHandler(term);
// old variant of the method
function insertChars(params: number[]): void {
let param = params[0];
if (param < 1) param = 1;
// make buffer local for faster access
const buffer = term.buffer;
const row = buffer.y + buffer.ybase;
let j = buffer.x;
const ch: CharData = [term.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm
while (param-- && j < term.cols) {
buffer.lines.get(row).splice(j++, 0, ch);
buffer.lines.get(row).pop();
}
}
// insert some data in first and second line
inputHandler.parse(Array(term.cols - 9).join('a'));
inputHandler.parse('1234567890');
inputHandler.parse(Array(term.cols - 9).join('a'));
inputHandler.parse('1234567890');
const line1: IBufferLine = term.buffer.lines.get(0); // line for old variant
const line2: IBufferLine = term.buffer.lines.get(1); // line for new variant
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '1234567890');
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '1234567890');
const line1: IBufferLine = term.buffer.lines.get(0);
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '1234567890');
// insert one char from params = [0]
term.buffer.y = 0;
term.buffer.x = 70;
insertChars([0]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 123456789');
term.buffer.y = 1;
term.buffer.x = 70;
inputHandler.insertChars([0]);
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 123456789');
expect(lineContent(line2)).equals(lineContent(line1));
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' 123456789');
// insert one char from params = [1]
term.buffer.y = 0;
term.buffer.x = 70;
insertChars([1]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 12345678');
term.buffer.y = 1;
term.buffer.x = 70;
inputHandler.insertChars([1]);
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 12345678');
expect(lineContent(line2)).equals(lineContent(line1));
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' 12345678');
// insert two chars from params = [2]
term.buffer.y = 0;
term.buffer.x = 70;
insertChars([2]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 123456');
term.buffer.y = 1;
term.buffer.x = 70;
inputHandler.insertChars([2]);
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 123456');
expect(lineContent(line2)).equals(lineContent(line1));
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' 123456');
// insert 10 chars from params = [10]
term.buffer.y = 0;
term.buffer.x = 70;
insertChars([10]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' ');
term.buffer.y = 1;
term.buffer.x = 70;
inputHandler.insertChars([10]);
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' ');
expect(lineContent(line2)).equals(lineContent(line1));
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' ');
expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a'));
});
it('deleteChars', function(): void {
const term = new Terminal();
const inputHandler = new InputHandler(term);
// old variant of the method
function deleteChars(params: number[]): void {
let param: number = params[0];
if (param < 1) {
param = 1;
}
// make buffer local for faster access
const buffer = term.buffer;
const row = buffer.y + buffer.ybase;
const ch: CharData = [term.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm
while (param--) {
buffer.lines.get(row).splice(buffer.x, 1);
buffer.lines.get(row).push(ch);
}
term.updateRange(buffer.y);
}
// insert some data in first and second line
inputHandler.parse(Array(term.cols - 9).join('a'));
inputHandler.parse('1234567890');
inputHandler.parse(Array(term.cols - 9).join('a'));
inputHandler.parse('1234567890');
const line1: IBufferLine = term.buffer.lines.get(0); // line for old variant
const line2: IBufferLine = term.buffer.lines.get(1); // line for new variant
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '1234567890');
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '1234567890');
const line1: IBufferLine = term.buffer.lines.get(0);
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '1234567890');
// delete one char from params = [0]
term.buffer.y = 0;
term.buffer.x = 70;
deleteChars([0]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '234567890 ');
term.buffer.y = 1;
term.buffer.x = 70;
inputHandler.deleteChars([0]);
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '234567890 ');
expect(lineContent(line2)).equals(lineContent(line1));
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '234567890 ');
expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a') + '234567890');
// insert one char from params = [1]
term.buffer.y = 0;
term.buffer.x = 70;
deleteChars([1]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '34567890 ');
term.buffer.y = 1;
term.buffer.x = 70;
inputHandler.deleteChars([1]);
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '34567890 ');
expect(lineContent(line2)).equals(lineContent(line1));
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '34567890 ');
expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a') + '34567890');
// insert two chars from params = [2]
term.buffer.y = 0;
term.buffer.x = 70;
deleteChars([2]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '567890 ');
term.buffer.y = 1;
term.buffer.x = 70;
inputHandler.deleteChars([2]);
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '567890 ');
expect(lineContent(line2)).equals(lineContent(line1));
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '567890 ');
expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a') + '567890');
// insert 10 chars from params = [10]
term.buffer.y = 0;
term.buffer.x = 70;
deleteChars([10]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' ');
term.buffer.y = 1;
term.buffer.x = 70;
inputHandler.deleteChars([10]);
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' ');
expect(lineContent(line2)).equals(lineContent(line1));
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' ');
expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a'));
});
it('eraseInLine', function(): void {
const term = new Terminal();
const inputHandler = new InputHandler(term);
const oldInputHandler = new OldInputHandler(term);
// fill 6 lines to test 3 different states
inputHandler.parse(Array(term.cols + 1).join('a'));
inputHandler.parse(Array(term.cols + 1).join('a'));
inputHandler.parse(Array(term.cols + 1).join('a'));
inputHandler.parse(Array(term.cols + 1).join('a'));
inputHandler.parse(Array(term.cols + 1).join('a'));
inputHandler.parse(Array(term.cols + 1).join('a'));
// params[0] - right erase
term.buffer.y = 0;
term.buffer.x = 70;
oldInputHandler.eraseInLine([0]);
expect(lineContent(term.buffer.lines.get(0))).equals(Array(71).join('a') + ' ');
inputHandler.eraseInLine([0]);
expect(term.buffer.lines.get(0).translateToString(false)).equals(Array(71).join('a') + ' ');
// params[1] - left erase
term.buffer.y = 1;
term.buffer.x = 70;
inputHandler.eraseInLine([0]);
expect(lineContent(term.buffer.lines.get(1))).equals(Array(71).join('a') + ' ');
inputHandler.eraseInLine([1]);
expect(term.buffer.lines.get(1).translateToString(false)).equals(Array(71).join(' ') + ' aaaaaaaaa');
// params[1] - left erase
term.buffer.y = 2;
term.buffer.x = 70;
oldInputHandler.eraseInLine([1]);
expect(lineContent(term.buffer.lines.get(2))).equals(Array(71).join(' ') + ' aaaaaaaaa');
term.buffer.y = 3;
term.buffer.x = 70;
inputHandler.eraseInLine([1]);
expect(lineContent(term.buffer.lines.get(3))).equals(Array(71).join(' ') + ' aaaaaaaaa');
// params[1] - left erase
term.buffer.y = 4;
term.buffer.x = 70;
oldInputHandler.eraseInLine([2]);
expect(lineContent(term.buffer.lines.get(4))).equals(Array(term.cols + 1).join(' '));
term.buffer.y = 5;
term.buffer.x = 70;
inputHandler.eraseInLine([2]);
expect(lineContent(term.buffer.lines.get(5))).equals(Array(term.cols + 1).join(' '));
expect(term.buffer.lines.get(2).translateToString(false)).equals(Array(term.cols + 1).join(' '));
});
it('eraseInDisplay', function(): void {
const termOld = new Terminal();
const inputHandlerOld = new OldInputHandler(termOld);
const termNew = new Terminal();
const inputHandlerNew = new InputHandler(termNew);
const term = new Terminal({cols: 80, rows: 7});
const inputHandler = new InputHandler(term);
// fill display with a's
for (let i = 0; i < termOld.rows; ++i) inputHandlerOld.parse(Array(termOld.cols + 1).join('a'));
for (let i = 0; i < termNew.rows; ++i) inputHandlerNew.parse(Array(termOld.cols + 1).join('a'));
const data = [];
for (let i = 0; i < termOld.rows; ++i) data.push(Array(termOld.cols + 1).join('a'));
expect(termContent(termOld)).eql(data);
expect(termContent(termOld)).eql(termContent(termNew));
for (let i = 0; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a'));
// params [0] - right and below erase
termOld.buffer.y = 5;
termOld.buffer.x = 40;
inputHandlerOld.eraseInDisplay([0]);
termNew.buffer.y = 5;
termNew.buffer.x = 40;
inputHandlerNew.eraseInDisplay([0]);
expect(termContent(termNew)).eql(termContent(termOld));
term.buffer.y = 5;
term.buffer.x = 40;
inputHandler.eraseInDisplay([0]);
expect(termContent(term, false)).eql([
Array(term.cols + 1).join('a'),
Array(term.cols + 1).join('a'),
Array(term.cols + 1).join('a'),
Array(term.cols + 1).join('a'),
Array(term.cols + 1).join('a'),
Array(40 + 1).join('a') + Array(term.cols - 40 + 1).join(' '),
Array(term.cols + 1).join(' ')
]);
expect(termContent(term, true)).eql([
Array(term.cols + 1).join('a'),
Array(term.cols + 1).join('a'),
Array(term.cols + 1).join('a'),
Array(term.cols + 1).join('a'),
Array(term.cols + 1).join('a'),
Array(40 + 1).join('a'),
''
]);
// reset
termOld.buffer.y = 0;
termOld.buffer.x = 0;
termNew.buffer.y = 0;
termNew.buffer.x = 0;
for (let i = 0; i < termOld.rows; ++i) inputHandlerOld.parse(Array(termOld.cols + 1).join('a'));
for (let i = 0; i < termNew.rows; ++i) inputHandlerNew.parse(Array(termOld.cols + 1).join('a'));
term.buffer.y = 0;
term.buffer.x = 0;
for (let i = 0; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a'));
// params [1] - left and above
termOld.buffer.y = 5;
termOld.buffer.x = 40;
inputHandlerOld.eraseInDisplay([1]);
termNew.buffer.y = 5;
termNew.buffer.x = 40;
inputHandlerNew.eraseInDisplay([1]);
expect(termContent(termNew)).eql(termContent(termOld));
term.buffer.y = 5;
term.buffer.x = 40;
inputHandler.eraseInDisplay([1]);
expect(termContent(term, false)).eql([
Array(term.cols + 1).join(' '),
Array(term.cols + 1).join(' '),
Array(term.cols + 1).join(' '),
Array(term.cols + 1).join(' '),
Array(term.cols + 1).join(' '),
Array(41 + 1).join(' ') + Array(term.cols - 41 + 1).join('a'),
Array(term.cols + 1).join('a')
]);
expect(termContent(term, true)).eql([
'',
'',
'',
'',
'',
Array(41 + 1).join(' ') + Array(term.cols - 41 + 1).join('a'),
Array(term.cols + 1).join('a')
]);
// reset
termOld.buffer.y = 0;
termOld.buffer.x = 0;
termNew.buffer.y = 0;
termNew.buffer.x = 0;
for (let i = 0; i < termOld.rows; ++i) inputHandlerOld.parse(Array(termOld.cols + 1).join('a'));
for (let i = 0; i < termNew.rows; ++i) inputHandlerNew.parse(Array(termOld.cols + 1).join('a'));
term.buffer.y = 0;
term.buffer.x = 0;
for (let i = 0; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a'));
// params [2] - whole screen
termOld.buffer.y = 5;
termOld.buffer.x = 40;
inputHandlerOld.eraseInDisplay([2]);
termNew.buffer.y = 5;
termNew.buffer.x = 40;
inputHandlerNew.eraseInDisplay([2]);
expect(termContent(termNew)).eql(termContent(termOld));
term.buffer.y = 5;
term.buffer.x = 40;
inputHandler.eraseInDisplay([2]);
expect(termContent(term, false)).eql([
Array(term.cols + 1).join(' '),
Array(term.cols + 1).join(' '),
Array(term.cols + 1).join(' '),
Array(term.cols + 1).join(' '),
Array(term.cols + 1).join(' '),
Array(term.cols + 1).join(' '),
Array(term.cols + 1).join(' ')
]);
expect(termContent(term, true)).eql([
'',
'',
'',
'',
'',
'',
''
]);
// reset and add a wrapped line
term.buffer.y = 0;
term.buffer.x = 0;
inputHandler.parse(Array(term.cols + 1).join('a')); // line 0
inputHandler.parse(Array(term.cols + 10).join('a')); // line 1 and 2
for (let i = 3; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a'));
// params[1] left and above with wrap
// confirm precondition that line 2 is wrapped
expect(term.buffer.lines.get(2).isWrapped).true;
term.buffer.y = 2;
term.buffer.x = 40;
inputHandler.eraseInDisplay([1]);
expect(term.buffer.lines.get(2).isWrapped).false;
// reset and add a wrapped line
term.buffer.y = 0;
term.buffer.x = 0;
inputHandler.parse(Array(term.cols + 1).join('a')); // line 0
inputHandler.parse(Array(term.cols + 10).join('a')); // line 1 and 2
for (let i = 3; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a'));
// params[1] left and above with wrap
// confirm precondition that line 2 is wrapped
expect(term.buffer.lines.get(2).isWrapped).true;
term.buffer.y = 1;
term.buffer.x = 90; // Cursor is beyond last column
inputHandler.eraseInDisplay([1]);
expect(term.buffer.lines.get(2).isWrapped).false;
});
});
it('convertEol setting', function(): void {
// not converting
let s = '';
const termNotConverting = new Terminal({cols: 15, rows: 10});
(termNotConverting as any)._inputHandler.parse('Hello\nWorld');
for (let i = 0; i < termNotConverting.cols; ++i) {
s += termNotConverting.buffer.lines.get(0).get(i)[CHAR_DATA_CHAR_INDEX];
}
expect(s).equals('Hello ');
s = '';
for (let i = 0; i < termNotConverting.cols; ++i) {
s += termNotConverting.buffer.lines.get(1).get(i)[CHAR_DATA_CHAR_INDEX];
}
expect(s).equals(' World ');
expect(termNotConverting.buffer.lines.get(0).translateToString(false)).equals('Hello ');
expect(termNotConverting.buffer.lines.get(1).translateToString(false)).equals(' World ');
expect(termNotConverting.buffer.lines.get(0).translateToString(true)).equals('Hello');
expect(termNotConverting.buffer.lines.get(1).translateToString(true)).equals(' World');
// converting
s = '';
const termConverting = new Terminal({cols: 15, rows: 10, convertEol: true});
(termConverting as any)._inputHandler.parse('Hello\nWorld');
for (let i = 0; i < termConverting.cols; ++i) {
s += termConverting.buffer.lines.get(0).get(i)[CHAR_DATA_CHAR_INDEX];
}
expect(s).equals('Hello ');
s = '';
for (let i = 0; i < termConverting.cols; ++i) {
s += termConverting.buffer.lines.get(1).get(i)[CHAR_DATA_CHAR_INDEX];
}
expect(s).equals('World ');
expect(termConverting.buffer.lines.get(0).translateToString(false)).equals('Hello ');
expect(termConverting.buffer.lines.get(1).translateToString(false)).equals('World ');
expect(termConverting.buffer.lines.get(0).translateToString(true)).equals('Hello');
expect(termConverting.buffer.lines.get(1).translateToString(true)).equals('World');
});
describe('print', () => {
it('should not cause an infinite loop (regression test)', () => {
const term = new Terminal();
const inputHandler = new InputHandler(term);
inputHandler.print(String.fromCharCode(0x200B), 0, 1);
});
});
describe('alt screen', () => {
let term: Terminal;
let handler: InputHandler;
beforeEach(() => {
term = new Terminal();
handler = new InputHandler(term);
});
it('should handle DECSET/DECRST 47 (alt screen buffer)', () => {
handler.parse('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST');
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);
});
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);
});
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);
// Text color of 'JUNK' should be red
expect((term.buffer.lines.get(1).get(0)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).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);
});
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);
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);
});
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);
});
});
});
+107 -62
View File
@@ -12,8 +12,8 @@ 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 { BufferLine } from './BufferLine';
/**
* Map collect to glevel. Used in `selectCharset`.
@@ -114,7 +114,7 @@ class DECRQSS implements IDcsHandler {
* each function's header comment.
*/
export class InputHandler extends Disposable implements IInputHandler {
private _surrogateHigh: string;
private _surrogateFirst: string;
constructor(
protected _terminal: IInputHandlingTerminal,
@@ -124,7 +124,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this.register(this._parser);
this._surrogateHigh = '';
this._surrogateFirst = '';
/**
* custom fallback handlers
@@ -312,9 +312,9 @@ export class InputHandler extends Disposable implements IInputHandler {
}
// apply leftover surrogate high from last write
if (this._surrogateHigh) {
data = this._surrogateHigh + data;
this._surrogateHigh = '';
if (this._surrogateFirst) {
data = this._surrogateFirst + data;
this._surrogateFirst = '';
}
this._parser.parse(data);
@@ -328,7 +328,6 @@ export class InputHandler extends Disposable implements IInputHandler {
public print(data: string, start: number, end: number): void {
let char: string;
let code: number;
let low: number;
let chWidth: number;
const buffer: IBuffer = this._terminal.buffer;
const charset: ICharset = this._terminal.charset;
@@ -346,20 +345,25 @@ export class InputHandler extends Disposable implements IInputHandler {
// surrogate pair handling
if (0xD800 <= code && code <= 0xDBFF) {
// we got a surrogate high
// get surrogate low (next 2 bytes)
low = data.charCodeAt(stringPosition + 1);
if (isNaN(low)) {
// end of data stream, save surrogate high
this._surrogateHigh = char;
if (++stringPosition >= end) {
// end of input:
// handle pairs as true UTF-16 and wait for the second part
// since we expect the input comming from a stream there is
// a small chance that the surrogate pair got split
// therefore we dont process the first char here, instead
// it gets added as first char to the next processed chunk
this._surrogateFirst = char;
continue;
}
code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
char += data.charAt(stringPosition + 1);
}
// surrogate low - already handled above
if (0xDC00 <= code && code <= 0xDFFF) {
continue;
const second = data.charCodeAt(stringPosition);
// if the second part is in surrogate pair range create the high codepoint
// otherwise fall back to UCS-2 behavior (handle codepoints independently)
if (0xDC00 <= second && second <= 0xDFFF) {
code = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
char += data.charAt(stringPosition);
} else {
stringPosition--;
}
}
// calculate print space
@@ -392,10 +396,12 @@ export class InputHandler extends Disposable implements IInputHandler {
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
}
}
continue;
@@ -403,6 +409,9 @@ export class InputHandler extends Disposable implements IInputHandler {
// goto next line if ch would overflow
// TODO: needs a global min terminal width of 2
// FIXME: additionally ensure chWidth fits into a line
// --> maybe forbid cols<xy at higher level as it would
// introduce a bad runtime penalty here
if (buffer.x + chWidth - 1 >= cols) {
// autowrap - DECAWM
// automatically wraps to the beginning of the next line
@@ -430,23 +439,15 @@ export class InputHandler extends Disposable implements IInputHandler {
}
// insert mode: move characters to right
// To achieve insert, we remove cells from the right
// and insert empty ones at cursor position
if (insertMode) {
// do this twice for a fullwidth char
for (let moves = 0; moves < chWidth; ++moves) {
// remove last cell
// if it's width is 0, we have to adjust the second last cell as well
const removed = bufferRow.pop();
const chMinusTwo = bufferRow.get(buffer.x - 2);
if (removed[CHAR_DATA_WIDTH_INDEX] === 0
&& chMinusTwo
&& chMinusTwo[CHAR_DATA_WIDTH_INDEX] === 2) {
bufferRow.set(this._terminal.cols - 2, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
}
// insert empty cell at cursor
bufferRow.splice(buffer.x, 0, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
// right shift cells according to the width
bufferRow.insertCells(buffer.x, chWidth, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
// 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]);
}
}
@@ -454,13 +455,24 @@ export class InputHandler extends Disposable implements IInputHandler {
bufferRow.set(buffer.x++, [curAttr, char, chWidth, code]);
// fullwidth char - also set next cell to placeholder stub and advance cursor
if (chWidth === 2) {
bufferRow.set(buffer.x++, [curAttr, '', 0, undefined]);
// 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]);
}
}
}
this._terminal.updateRange(buffer.y);
}
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
return this._parser.addCsiHandler(flag, callback);
}
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
return this._parser.addOscHandler(ident, callback);
}
/**
* BEL
* Bell (Ctrl-G).
@@ -722,12 +734,25 @@ export class InputHandler extends Disposable implements IInputHandler {
* @param start first cell index to be erased
* @param end end - 1 is last erased cell
*/
private _eraseInBufferLine(y: number, start: number, end: number): void {
this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y).replaceCells(
private _eraseInBufferLine(y: number, start: number, end: number, clearWrap: boolean = false): void {
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y);
line.replaceCells(
start,
end,
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
);
if (clearWrap) {
line.isWrapped = false;
}
}
/**
* Helper method to reset cells in a terminal row.
* The cell gets replaced with the eraseChar of the terminal and the isWrapped property is set to false.
* @param y row index
*/
private _resetBufferLine(y: number): void {
this._eraseInBufferLine(y, 0, this._terminal.cols, true);
}
/**
@@ -748,18 +773,23 @@ export class InputHandler extends Disposable implements IInputHandler {
case 0:
j = this._terminal.buffer.y;
this._terminal.updateRange(j);
this._eraseInBufferLine(j++, this._terminal.buffer.x, this._terminal.cols);
this._eraseInBufferLine(j++, this._terminal.buffer.x, this._terminal.cols, this._terminal.buffer.x === 0);
for (; j < this._terminal.rows; j++) {
this._eraseInBufferLine(j, 0, this._terminal.cols);
this._resetBufferLine(j);
}
this._terminal.updateRange(j);
break;
case 1:
j = this._terminal.buffer.y;
this._terminal.updateRange(j);
this._eraseInBufferLine(j, 0, this._terminal.buffer.x + 1);
// Deleted front part of line and everything before. This line will no longer be wrapped.
this._eraseInBufferLine(j, 0, this._terminal.buffer.x + 1, true);
if (this._terminal.buffer.x + 1 >= this._terminal.cols) {
// Deleted entire previous line. This next line can no longer be wrapped.
this._terminal.buffer.lines.get(j + 1).isWrapped = false;
}
while (j--) {
this._eraseInBufferLine(j, 0, this._terminal.cols);
this._resetBufferLine(j);
}
this._terminal.updateRange(0);
break;
@@ -767,7 +797,7 @@ export class InputHandler extends Disposable implements IInputHandler {
j = this._terminal.rows;
this._terminal.updateRange(j - 1);
while (j--) {
this._eraseInBufferLine(j, 0, this._terminal.cols);
this._resetBufferLine(j);
}
this._terminal.updateRange(0);
break;
@@ -832,7 +862,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, BufferLine.blankLine(this._terminal.cols, this._terminal.eraseAttr()));
buffer.lines.splice(row, 0, buffer.getBlankLine(this._terminal.eraseAttr()));
}
// this.maxRange();
@@ -862,7 +892,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, BufferLine.blankLine(this._terminal.cols, this._terminal.eraseAttr()));
buffer.lines.splice(j, 0, buffer.getBlankLine(this._terminal.eraseAttr()));
}
// this.maxRange();
@@ -894,7 +924,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, BufferLine.blankLine(this._terminal.cols, DEFAULT_ATTR));
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR));
}
// this.maxRange();
this._terminal.updateRange(buffer.scrollTop);
@@ -913,7 +943,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, BufferLine.blankLine(this._terminal.cols, DEFAULT_ATTR));
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR));
}
// this.maxRange();
this._terminal.updateRange(buffer.scrollTop);
@@ -1262,7 +1292,9 @@ export class InputHandler extends Disposable implements IInputHandler {
case 66:
this._terminal.log('Serial port requested application keypad.');
this._terminal.applicationKeypad = true;
this._terminal.viewport.syncScrollArea();
if (this._terminal.viewport) {
this._terminal.viewport.syncScrollArea();
}
break;
case 9: // X10 Mouse
// no release, no motion, no wheel, no modifiers.
@@ -1311,14 +1343,19 @@ export class InputHandler extends Disposable implements IInputHandler {
case 25: // show cursor
this._terminal.cursorHidden = false;
break;
case 1048: // alt screen cursor
this.saveCursor(params);
break;
case 1049: // alt screen buffer cursor
// TODO: Not sure if we need to save/restore after switching the buffer
// this.saveCursor(params);
this.saveCursor(params);
// FALL-THROUGH
case 47: // alt screen buffer
case 1047: // alt screen buffer
this._terminal.buffers.activateAltBuffer();
this._terminal.viewport.syncScrollArea();
this._terminal.buffers.activateAltBuffer(this._terminal.eraseAttr());
this._terminal.refresh(0, this._terminal.rows - 1);
if (this._terminal.viewport) {
this._terminal.viewport.syncScrollArea();
}
this._terminal.showCursor();
break;
case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste)
@@ -1451,7 +1488,9 @@ export class InputHandler extends Disposable implements IInputHandler {
case 66:
this._terminal.log('Switching back to normal keypad.');
this._terminal.applicationKeypad = false;
this._terminal.viewport.syncScrollArea();
if (this._terminal.viewport) {
this._terminal.viewport.syncScrollArea();
}
break;
case 9: // X10 Mouse
case 1000: // vt200 mouse
@@ -1479,18 +1518,22 @@ export class InputHandler extends Disposable implements IInputHandler {
case 25: // hide cursor
this._terminal.cursorHidden = true;
break;
case 1048: // alt screen cursor
this.restoreCursor(params);
break;
case 1049: // alt screen buffer cursor
// FALL-THROUGH
case 47: // normal screen buffer
case 1047: // normal screen buffer - clearing it first
// Ensure the selection manager has the correct buffer
this._terminal.buffers.activateNormalBuffer();
// TODO: Not sure if we need to save/restore after switching the buffer
// if (params[0] === 1049) {
// this.restoreCursor(params);
// }
if (params[0] === 1049) {
this.restoreCursor(params);
}
this._terminal.refresh(0, this._terminal.rows - 1);
this._terminal.viewport.syncScrollArea();
if (this._terminal.viewport) {
this._terminal.viewport.syncScrollArea();
}
this._terminal.showCursor();
break;
case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste)
@@ -1769,7 +1812,9 @@ export class InputHandler extends Disposable implements IInputHandler {
this._terminal.originMode = false;
this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false
this._terminal.applicationKeypad = false; // ?
this._terminal.viewport.syncScrollArea();
if (this._terminal.viewport) {
this._terminal.viewport.syncScrollArea();
}
this._terminal.applicationCursor = false;
this._terminal.buffer.scrollTop = 0;
this._terminal.buffer.scrollBottom = this._terminal.rows - 1;
@@ -1836,7 +1881,7 @@ 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.savedCurAttr = this._terminal.curAttr;
this._terminal.buffer.savedCurAttr = this._terminal.curAttr;
}
@@ -1848,7 +1893,7 @@ 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.savedCurAttr || DEFAULT_ATTR;
this._terminal.curAttr = this._terminal.buffer.savedCurAttr || DEFAULT_ATTR;
}
+3 -3
View File
@@ -7,7 +7,7 @@ import { assert } from 'chai';
import { IMouseZoneManager, IMouseZone } from './ui/Types';
import { ILinkMatcher, ITerminal, IBufferLine } from './Types';
import { Linkifier } from './Linkifier';
import { MockBuffer, MockTerminal, TestTerminal } from './utils/TestUtils.test';
import { MockBuffer, MockTerminal, TestTerminal } from './ui/TestUtils.test';
import { CircularList } from './common/CircularList';
import { BufferLine } from './BufferLine';
@@ -51,9 +51,9 @@ describe('Linkifier', () => {
});
function stringToRow(text: string): IBufferLine {
const result = new BufferLine();
const result = new BufferLine(text.length);
for (let i = 0; i < text.length; i++) {
result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
result.set(i, [0, text.charAt(i), 1, text.charCodeAt(i)]);
}
return result;
}
+8
View File
@@ -219,9 +219,17 @@ export class Linkifier extends EventEmitter implements ILinkifier {
// also correct regex and string search offsets for the next loop run
stringIndex = text.indexOf(uri, stringIndex + 1);
rex.lastIndex = stringIndex + uri.length;
if (stringIndex < 0) {
// invalid stringIndex (should not have happened)
break;
}
// get the buffer index as [absolute row, col] for the match
const bufferIndex = this._terminal.buffer.stringIndexToBufferIndex(rowIndex, stringIndex);
if (bufferIndex[0] < 0) {
// invalid bufferIndex (should not have happened)
break;
}
const line = this._terminal.buffer.lines.get(bufferIndex[0]);
const char = line.get(bufferIndex[1]);
+20 -7
View File
@@ -9,7 +9,7 @@ import { SelectionManager, SelectionMode } from './SelectionManager';
import { SelectionModel } from './SelectionModel';
import { BufferSet } from './BufferSet';
import { ITerminal, IBuffer, IBufferLine } from './Types';
import { MockTerminal } from './utils/TestUtils.test';
import { MockTerminal } from './ui/TestUtils.test';
import { BufferLine } from './BufferLine';
class TestMockTerminal extends MockTerminal {
@@ -30,6 +30,7 @@ class TestSelectionManager extends SelectionManager {
public selectLineAt(line: number): void { this._selectLineAt(line); }
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); }
public areCoordsInSelection(coords: [number, number], start: [number, number], end: [number, number]): boolean { return this._areCoordsInSelection(coords, start, end); }
// Disable DOM interaction
public enable(): void {}
@@ -54,16 +55,16 @@ describe('SelectionManager', () => {
});
function stringToRow(text: string): IBufferLine {
const result = new BufferLine();
const result = new BufferLine(text.length);
for (let i = 0; i < text.length; i++) {
result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
result.set(i, [0, text.charAt(i), 1, text.charCodeAt(i)]);
}
return result;
}
function stringArrayToRow(chars: string[]): IBufferLine {
const line = new BufferLine();
chars.map(c => line.push([0, c, 1, c.charCodeAt(0)]));
const line = new BufferLine(chars.length);
chars.map((c, idx) => line.set(idx, [0, c, 1, c.charCodeAt(0)]));
return line;
}
@@ -100,7 +101,6 @@ describe('SelectionManager', () => {
});
it('should expand selection for wide characters', () => {
// Wide characters use a special format
const line = new BufferLine();
const data: [number, string, number, number][] = [
[null, '中', 2, '中'.charCodeAt(0)],
[null, '', 0, null],
@@ -118,7 +118,8 @@ describe('SelectionManager', () => {
[null, 'o', 1, 'o'.charCodeAt(0)],
[null, 'o', 1, 'o'.charCodeAt(0)]
];
for (let i = 0; i < data.length; ++i) line.push(data[i]);
const line = new BufferLine(data.length);
for (let i = 0; i < data.length; ++i) line.set(i, data[i]);
buffer.lines.set(0, line);
// Ensure wide characters take up 2 columns
selectionManager.selectWordAt([0, 0]);
@@ -478,5 +479,17 @@ describe('SelectionManager', () => {
assert.equal(selectionManager.selectionText, 'a\n😁\nc');
});
});
describe('_areCoordsInSelection', () => {
it('should return whether coords are in the selection', () => {
assert.isFalse(selectionManager.areCoordsInSelection([0, 0], [2, 0], [2, 1]));
assert.isFalse(selectionManager.areCoordsInSelection([1, 0], [2, 0], [2, 1]));
assert.isTrue(selectionManager.areCoordsInSelection([2, 0], [2, 0], [2, 1]));
assert.isTrue(selectionManager.areCoordsInSelection([10, 0], [2, 0], [2, 1]));
assert.isTrue(selectionManager.areCoordsInSelection([0, 1], [2, 0], [2, 1]));
assert.isTrue(selectionManager.areCoordsInSelection([1, 1], [2, 0], [2, 1]));
assert.isFalse(selectionManager.areCoordsInSelection([2, 1], [2, 0], [2, 1]));
});
});
});
+11 -6
View File
@@ -5,8 +5,8 @@
import { ITerminal, ISelectionManager, IBuffer, CharData, IBufferLine } from './Types';
import { XtermListener } from './common/Types';
import { MouseHelper } from './utils/MouseHelper';
import * as Browser from './shared/utils/Browser';
import { MouseHelper } from './ui/MouseHelper';
import * as Browser from './core/Platform';
import { CharMeasure } from './ui/CharMeasure';
import { EventEmitter } from './common/EventEmitter';
import { SelectionModel } from './SelectionModel';
@@ -198,7 +198,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
}
} else {
// Get first row
const startRowEndCol = start[1] === end[1] ? end[0] : null;
const startRowEndCol = start[1] === end[1] ? end[0] : undefined;
result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol));
// Get middle rows
@@ -289,9 +289,14 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
return false;
}
return this._areCoordsInSelection(coords, start, end);
}
protected _areCoordsInSelection(coords: [number, number], start: [number, number], end: [number, number]): boolean {
return (coords[1] > start[1] && coords[1] < end[1]) ||
(start[1] === end[1] && coords[1] === start[1] && coords[0] > start[0] && coords[0] < end[0]) ||
(start[1] < end[1] && coords[1] === end[1] && coords[0] < end[0]);
(start[1] === end[1] && coords[1] === start[1] && coords[0] >= start[0] && coords[0] < end[0]) ||
(start[1] < end[1] && coords[1] === end[1] && coords[0] < end[0]) ||
(start[1] < end[1] && coords[1] === start[1] && coords[0] >= start[0]);
}
/**
@@ -342,7 +347,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mouse event.
*/
private _getMouseBufferCoords(event: MouseEvent): [number, number] {
const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.screenElement, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true);
const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.screenElement, this._charMeasure, this._terminal.cols, this._terminal.rows, true);
if (!coords) {
return null;
}
+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 './utils/TestUtils.test';
import { MockTerminal } from './ui/TestUtils.test';
class TestSelectionModel extends SelectionModel {
constructor(
+22 -16
View File
@@ -12,7 +12,19 @@ import { ITerminal, ISoundManager } from './Types';
export const DEFAULT_BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg==';
export class SoundManager implements ISoundManager {
private _audioContext: AudioContext;
private static _audioContext: AudioContext;
static get audioContext(): AudioContext | null {
if (!SoundManager._audioContext) {
const audioContextCtor: typeof AudioContext = (<any>window).AudioContext || (<any>window).webkitAudioContext;
if (!audioContextCtor) {
console.warn('Web Audio API is not supported by this browser. Consider upgrading to the latest version');
return null;
}
SoundManager._audioContext = new audioContextCtor();
}
return SoundManager._audioContext;
}
constructor(
private _terminal: ITerminal
@@ -20,22 +32,16 @@ export class SoundManager implements ISoundManager {
}
public playBellSound(): void {
const audioContextCtor: typeof AudioContext = (<any>window).AudioContext || (<any>window).webkitAudioContext;
if (!this._audioContext && audioContextCtor) {
this._audioContext = new audioContextCtor();
}
if (this._audioContext) {
const bellAudioSource = this._audioContext.createBufferSource();
const context = this._audioContext;
this._audioContext.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._terminal.options.bellSound)), (buffer) => {
bellAudioSource.buffer = buffer;
bellAudioSource.connect(context.destination);
bellAudioSource.start(0);
});
} else {
console.warn('Sorry, but the Web Audio API is not supported by your browser. Please, consider upgrading to the latest version');
const ctx = SoundManager.audioContext;
if (!ctx) {
return;
}
const bellAudioSource = ctx.createBufferSource();
ctx.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._terminal.options.bellSound)), (buffer) => {
bellAudioSource.buffer = buffer;
bellAudioSource.connect(ctx.destination);
bellAudioSource.start(0);
});
}
private _base64ToArrayBuffer(base64: string): ArrayBuffer {
+2 -2
View File
@@ -13,7 +13,7 @@ import * as path from 'path';
import * as pty from 'node-pty';
import { assert } from 'chai';
import { Terminal } from './Terminal';
import { CHAR_DATA_CHAR_INDEX } from './Buffer';
import { CHAR_DATA_CHAR_INDEX, WHITESPACE_CELL_CHAR } from './Buffer';
import { IViewport } from './Types';
class TestTerminal extends Terminal {
@@ -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];
lineText += term.buffer.lines.get(line).get(cell)[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR;
}
// rtrim empty cells as xterm does
lineText = lineText.replace(/\s+$/, '');
+56 -57
View File
@@ -5,9 +5,8 @@
import { assert, expect } from 'chai';
import { Terminal } from './Terminal';
import { MockViewport, MockCompositionHelper, MockRenderer } from './utils/TestUtils.test';
import { MockViewport, MockCompositionHelper, MockRenderer } from './ui/TestUtils.test';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, DEFAULT_ATTR } from './Buffer';
import { BufferLine } from './BufferLine';
const INIT_COLS = 80;
const INIT_ROWS = 24;
@@ -260,7 +259,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), BufferLine.blankLine(term.cols, DEFAULT_ATTR));
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR));
}
});
it('should clear a buffer larger than rows', () => {
@@ -277,7 +276,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), BufferLine.blankLine(term.cols, DEFAULT_ATTR));
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR));
}
});
it('should not break the prompt when cleared twice', () => {
@@ -290,7 +289,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), BufferLine.blankLine(term.cols, DEFAULT_ATTR));
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR));
}
});
});
@@ -456,20 +455,20 @@ describe('term.js addons', () => {
describe('scroll() function', () => {
describe('when scrollback > 0', () => {
it('should create a new line and scroll', () => {
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
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.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(INIT_ROWS).get(0)[CHAR_DATA_CHAR_INDEX], '');
});
it('should properly scroll inside a scroll region (scrollTop set)', () => {
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
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.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.scroll();
@@ -479,11 +478,11 @@ describe('term.js addons', () => {
});
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX] = 'd';
term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX] = 'e';
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.y = 3;
term.buffer.scrollBottom = 3;
term.scroll();
@@ -492,16 +491,16 @@ describe('term.js addons', () => {
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(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');
});
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX] = 'd';
term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX] = 'e';
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.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.buffer.scrollBottom = 3;
@@ -510,7 +509,7 @@ describe('term.js addons', () => {
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(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');
});
});
@@ -522,24 +521,24 @@ describe('term.js addons', () => {
});
it('should create a new line and shift everything up', () => {
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
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.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(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(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], '');
});
it('should properly scroll inside a scroll region (scrollTop set)', () => {
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
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.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.scroll();
@@ -549,11 +548,11 @@ describe('term.js addons', () => {
});
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX] = 'd';
term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX] = 'e';
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.y = 3;
term.buffer.scrollBottom = 3;
term.scroll();
@@ -561,16 +560,16 @@ describe('term.js addons', () => {
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(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');
});
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX] = 'd';
term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX] = 'e';
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.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.buffer.scrollBottom = 3;
@@ -579,7 +578,7 @@ describe('term.js addons', () => {
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(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');
});
});
@@ -777,7 +776,7 @@ describe('term.js addons', () => {
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(' ');
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
term.reset();
}
});
@@ -788,7 +787,7 @@ describe('term.js addons', () => {
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(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql('');
term.reset();
}
});
@@ -801,7 +800,7 @@ describe('term.js addons', () => {
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(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
term.reset();
}
});
@@ -814,7 +813,7 @@ describe('term.js addons', () => {
// 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(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
term.reset();
}
});
@@ -827,7 +826,7 @@ describe('term.js addons', () => {
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(' ');
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
term.reset();
}
});
@@ -846,8 +845,8 @@ describe('term.js addons', () => {
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(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);
});
it('multiple combined é', () => {
@@ -929,8 +928,8 @@ describe('term.js addons', () => {
}
}
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(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('¥');
@@ -954,8 +953,8 @@ describe('term.js addons', () => {
}
}
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(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');
@@ -999,8 +998,8 @@ describe('term.js addons', () => {
}
}
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(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');
@@ -1064,7 +1063,7 @@ describe('term.js addons', () => {
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).get(79)[CHAR_DATA_CHAR_INDEX]).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');
+61 -23
View File
@@ -21,26 +21,25 @@
* http://linux.die.net/man/7/urxvt
*/
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler } from './Types';
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types';
import { IMouseZoneManager } from './ui/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 } from './Buffer';
import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_ATTR_INDEX } from './Buffer';
import { CompositionHelper } from './CompositionHelper';
import { EventEmitter } from './common/EventEmitter';
import { Viewport } from './Viewport';
import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './handlers/Clipboard';
import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './ui/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 './shared/utils/Browser';
import * as Browser from './core/Platform';
import { addDisposableDomListener } from './ui/Lifecycle';
import * as Strings from './Strings';
import { MouseHelper } from './utils/MouseHelper';
import { clone } from './utils/Clone';
import { MouseHelper } from './ui/MouseHelper';
import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager';
import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager';
import { MouseZoneManager } from './ui/MouseZoneManager';
@@ -52,7 +51,7 @@ import { DomRenderer } from './renderer/dom/DomRenderer';
import { IKeyboardEvent } from './common/Types';
import { evaluateKeyboardEvent } from './core/input/Keyboard';
import { KeyboardResultType, ICharset } from './core/Types';
import { BufferLine } from './BufferLine';
import { clone } from './common/Clone';
// Let it work inside Node.js for automated testing purposes.
const document = (typeof window !== 'undefined') ? window.document : null;
@@ -106,7 +105,8 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
tabStopWidth: 8,
theme: null,
rightClickSelectsWord: Browser.isMac,
rendererType: 'canvas'
rendererType: 'canvas',
experimentalBufferLineImpl: 'TypedArray'
};
export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal {
@@ -170,7 +170,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
public savedCols: number;
public curAttr: number;
public savedCurAttr: number;
public params: (string | number)[];
public currentParam: string | number;
@@ -208,6 +207,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
private _screenDprMonitor: ScreenDprMonitor;
private _theme: ITheme;
// bufferline to clone/copy from for new blank lines
private _blankLine: IBufferLine = null;
public cols: number;
public rows: number;
@@ -342,7 +344,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
public get isFocused(): boolean {
return document.activeElement === this.textarea;
return document.activeElement === this.textarea && document.hasFocus();
}
/**
@@ -462,6 +464,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.renderer.onResize(this.cols, this.rows);
this.refresh(0, this.rows - 1);
}
break;
case 'rendererType':
if (this.renderer) {
this.unregister(this.renderer);
@@ -473,6 +476,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
if (this._theme) {
this.renderer.setTheme(this._theme);
}
this.mouseHelper.setRenderer(this.renderer);
break;
case 'scrollback':
this.buffers.resize(this.cols, this.rows);
@@ -493,6 +497,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
break;
case 'tabStopWidth': this.buffers.setupTabStops(); break;
case 'experimentalBufferLineImpl':
this.buffers.normal.setBufferLineFactory(value);
this.buffers.alt.setBufferLineFactory(value);
this._blankLine = null;
break;
}
// Inform renderer of changes
if (this.renderer) {
@@ -799,7 +808,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
button = getButton(ev);
// get mouse coordinates
pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.cols, self.rows);
if (!pos) return;
sendEvent(button, pos);
@@ -825,7 +834,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7<
function sendMove(ev: MouseEvent): void {
let button = pressed;
const pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
const pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.cols, self.rows);
if (!pos) return;
// buttons marked as motions
@@ -980,7 +989,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
: 65;
break;
case 'wheel':
button = (<WheelEvent>ev).wheelDeltaY > 0
button = (<WheelEvent>ev).deltaY < 0
? 64
: 65;
break;
@@ -1149,9 +1158,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
*/
public updateCursorStyle(ev: KeyboardEvent): void {
if (this.selectionManager && this.selectionManager.shouldColumnSelect(ev)) {
this.element.classList.add('xterm-cursor-crosshair');
this.element.classList.add('column-select');
} else {
this.element.classList.remove('xterm-cursor-crosshair');
this.element.classList.remove('column-select');
}
}
@@ -1169,20 +1178,40 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
* Scroll the terminal down 1 row, creating a blank line.
* @param isWrapped Whether the new line is wrapped from the previous line.
*/
public scroll(isWrapped?: boolean): void {
const newLine = BufferLine.blankLine(this.cols, DEFAULT_ATTR, isWrapped);
public scroll(isWrapped: boolean = false): void {
let newLine: IBufferLine;
const useRecycling = this.options.experimentalBufferLineImpl !== 'JsArray';
if (useRecycling) {
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);
this._blankLine = newLine;
}
newLine.isWrapped = isWrapped;
} else {
newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped);
}
const topRow = this.buffer.ybase + this.buffer.scrollTop;
const bottomRow = this.buffer.ybase + this.buffer.scrollBottom;
if (this.buffer.scrollTop === 0) {
// Determine whether the buffer is going to be trimmed after insertion.
const willBufferBeTrimmed = this.buffer.lines.length === this.buffer.lines.maxLength;
const willBufferBeTrimmed = this.buffer.lines.isFull;
// Insert the line using the fastest method
if (bottomRow === this.buffer.lines.length - 1) {
this.buffer.lines.push(newLine);
if (useRecycling) {
if (willBufferBeTrimmed) {
this.buffer.lines.recycle().copyFrom(newLine);
} else {
this.buffer.lines.push(newLine.clone());
}
} else {
this.buffer.lines.push(newLine);
}
} else {
this.buffer.lines.splice(bottomRow + 1, 0, newLine);
this.buffer.lines.splice(bottomRow + 1, 0, (useRecycling) ? newLine.clone() : newLine);
}
// Only adjust ybase and ydisp when the buffer is not trimmed
@@ -1204,7 +1233,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// scrollback, instead we can just shift them in-place.
const scrollRegionHeight = bottomRow - topRow + 1/*as it's zero-based*/;
this.buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1);
this.buffer.lines.set(bottomRow, newLine);
this.buffer.lines.set(bottomRow, (useRecycling) ? newLine.clone() : newLine);
}
// Move the viewport to the bottom of the buffer unless the user is
@@ -1384,6 +1413,15 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this._customKeyEventHandler = customKeyEventHandler;
}
/** Add handler for CSI escape sequence. See xterm.d.ts for details. */
public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
return this._inputHandler.addCsiHandler(flag, callback);
}
/** Add handler for OSC escape sequence. See xterm.d.ts for details. */
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
return this._inputHandler.addOscHandler(ident, callback);
}
/**
* Registers a link matcher, allowing custom link patterns to be matched and
* handled.
@@ -1722,7 +1760,7 @@ 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(BufferLine.blankLine(this.cols, DEFAULT_ATTR));
this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR));
}
this.refresh(0, this.rows - 1);
this.emit('scroll', this.buffer.ydisp);
@@ -1814,7 +1852,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, BufferLine.blankLine(this.cols, this.eraseAttr()));
this.buffer.lines.set(this.buffer.y + this.buffer.ybase, this.buffer.getBlankLine(this.eraseAttr()));
this.updateRange(this.buffer.scrollTop);
this.updateRange(this.buffer.scrollBottom);
} else {
+17 -7
View File
@@ -49,7 +49,6 @@ export interface IInputHandlingTerminal extends IEventEmitter {
wraparoundMode: boolean;
bracketedPasteMode: boolean;
curAttr: number;
savedCurAttr: number;
savedCols: number;
x10Mouse: boolean;
vt200Mouse: boolean;
@@ -246,8 +245,8 @@ export interface ILinkifierAccessor {
}
export interface IMouseHelper {
getCoords(event: { pageX: number, pageY: number }, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number];
getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number };
getCoords(event: { clientX: number, clientY: number }, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number];
getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number): { x: number, y: number };
}
export interface ICharMeasure {
@@ -290,11 +289,13 @@ export interface IBuffer {
hasScrollback: boolean;
savedY: number;
savedX: number;
savedCurAttr: number;
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;
stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[];
iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator;
}
@@ -305,7 +306,7 @@ export interface IBufferSet extends IEventEmitter {
active: IBuffer;
activateNormalBuffer(): void;
activateAltBuffer(): void;
activateAltBuffer(fillAttr?: number): void;
}
export interface ISelectionManager {
@@ -491,6 +492,8 @@ export interface IEscapeSequenceParser extends IDisposable {
setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void;
clearCsiHandler(flag: string): void;
setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void;
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable;
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
setEscHandler(collectAndFlag: string, callback: () => void): void;
clearEscHandler(collectAndFlag: string): void;
@@ -516,10 +519,17 @@ export interface IBufferLine {
isWrapped: boolean;
get(index: number): CharData;
set(index: number, value: CharData): void;
pop(): CharData | undefined;
push(data: CharData): void;
splice(start: number, deleteCount: number, ...items: CharData[]): CharData[];
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, shrink?: boolean): void;
fill(fillCharData: CharData): void;
copyFrom(line: IBufferLine): void;
clone(): IBufferLine;
getTrimmedLength(): number;
translateToString(trimRight?: boolean, startCol?: number, endCol?: number): string;
}
export interface IBufferLineConstructor {
new(cols: number, fillCharData?: CharData, isWrapped?: boolean): IBufferLine;
}
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { assert, expect } from 'chai';
import { assert } from 'chai';
import * as attach from './attach';
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { assert, expect } from 'chai';
import { assert } from 'chai';
import * as fit from './fit';
+5 -2
View File
@@ -4,14 +4,17 @@
"target": "es5",
"lib": [
"dom",
"es6",
"es5"
],
"rootDir": ".",
"outDir": "../../../lib/addons/fit/",
"sourceMap": true,
"removeComments": true,
"declaration": true,
"preserveWatchOutput": true
"preserveWatchOutput": true,
"types": [
"../../node_modules/@types/mocha"
]
},
"include": [
"**/*.ts",
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { assert, expect } from 'chai';
import { assert } from 'chai';
import * as fullscreen from './fullscreen';
+7 -5
View File
@@ -11,17 +11,19 @@ import { Terminal } from 'xterm';
* @param fullscreen Toggle fullscreen on (true) or off (false)
*/
export function toggleFullScreen(term: Terminal, fullscreen: boolean): void {
let fn: string;
let fn: (...tokens: string[]) => void;
if (typeof fullscreen === 'undefined') {
fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add';
fn = (term.element.classList.contains('fullscreen')) ?
term.element.classList.remove : term.element.classList.add;
} else if (!fullscreen) {
fn = 'remove';
fn = term.element.classList.remove;
} else {
fn = 'add';
fn = term.element.classList.add;
}
term.element.classList[fn]('fullscreen');
fn = fn.bind(term.element.classList);
fn('fullscreen');
}
export function apply(terminalConstructor: typeof Terminal): void {
+5 -2
View File
@@ -4,14 +4,17 @@
"target": "es5",
"lib": [
"dom",
"es6",
"es5"
],
"rootDir": ".",
"outDir": "../../../lib/addons/fullscreen/",
"sourceMap": true,
"removeComments": true,
"declaration": true,
"preserveWatchOutput": true
"preserveWatchOutput": true,
"types": [
"../../node_modules/@types/mocha"
]
},
"include": [
"**/*.ts",
+5
View File
@@ -25,6 +25,11 @@ export interface ISearchOptions {
regex?: boolean;
wholeWord?: boolean;
caseSensitive?: boolean;
/**
* Use this when you want the selection to expand if it still matches as the
* user types. Note that this only affects findNext.
*/
incremental?: boolean;
}
export interface ISearchResult {
+155 -67
View File
@@ -4,50 +4,75 @@
*/
import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces';
const nonWordCharacters = ' ~!@#$%^&*()_+`-=[]{}|\;:"\',./<>?';
const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?';
const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
/**
* A class that knows how to search the terminal and how to display the results.
*/
export class SearchHelper implements ISearchHelper {
/**
* translateBufferLineToStringWithWrap is a fairly expensive call.
* We memoize the calls into an array that has a time based ttl.
* _linesCache is also invalidated when the terminal cursor moves.
*/
private _linesCache: string[] = null;
private _linesCacheTimeoutId = 0;
constructor(private _terminal: ISearchAddonTerminal) {
// TODO: Search for multiple instances on 1 line
// TODO: Don't use the actual selection, instead use a "find selection" so multiple instances can be highlighted
// TODO: Highlight other instances in the viewport
this._destroyLinesCache = this._destroyLinesCache.bind(this);
}
/**
* Find the next instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
* @param term Tne search term.
* @param term The search term.
* @param searchOptions Search options.
* @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();
return false;
}
let result: ISearchResult;
let startCol: number = 0;
let startRow = this._terminal._core.buffer.ydisp;
if (this._terminal._core.selectionManager.selectionEnd) {
// Start from the selection end if there is a selection
startRow = this._terminal._core.selectionManager.selectionEnd[1];
}
// Search from ydisp + 1 to end
for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
result = this._findInLine(term, y, searchOptions);
if (result) {
break;
if (selectionManager.selectionEnd) {
// 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];
}
}
// Search from the top to the current ydisp
this._initLinesCache();
// Search startRow
result = this._findInLine(term, startRow, startCol, searchOptions);
// Search from startRow + 1 to end
if (!result) {
for (let y = 0; y < startRow; y++) {
result = this._findInLine(term, y, searchOptions);
for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
result = this._findInLine(term, y, 0, searchOptions);
if (result) {
break;
}
}
}
// 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++) {
result = this._findInLine(term, y, 0, searchOptions);
if (result) {
break;
}
@@ -61,35 +86,52 @@ export class SearchHelper implements ISearchHelper {
/**
* Find the previous instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
* @param term Tne search term.
* @param term The search term.
* @param searchOptions Search options.
* @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();
return false;
}
let result: ISearchResult;
const isReverseSearch = true;
let startRow = this._terminal._core.buffer.ydisp;
if (this._terminal._core.selectionManager.selectionStart) {
// Start from the selection end if there is a selection
startRow = this._terminal._core.selectionManager.selectionStart[1];
}
let startCol: number = this._terminal._core.buffer.lines.get(startRow).length;
// Search from ydisp + 1 to end
for (let y = startRow - 1; y >= 0; y--) {
result = this._findInLine(term, y, searchOptions);
if (result) {
break;
if (selectionManager.selectionStart) {
// Start from the selection start if there is a selection
if (this._terminal.getSelection().length !== 0) {
startRow = selectionManager.selectionStart[1];
startCol = selectionManager.selectionStart[0];
}
}
// Search from the top to the current ydisp
this._initLinesCache();
// Search startRow
result = this._findInLine(term, startRow, startCol, searchOptions, isReverseSearch);
// Search from startRow - 1 to top
if (!result) {
for (let y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
result = this._findInLine(term, y, searchOptions);
for (let y = startRow - 1; y >= 0; y--) {
result = this._findInLine(term, y, this._terminal._core.buffer.lines.get(y).length, searchOptions, isReverseSearch);
if (result) {
break;
}
}
}
// 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;
for (let y = searchFrom; y >= startRow; y--) {
result = this._findInLine(term, y, this._terminal._core.buffer.lines.get(y).length, searchOptions, isReverseSearch);
if (result) {
break;
}
@@ -100,6 +142,28 @@ export class SearchHelper implements ISearchHelper {
return this._selectResult(result);
}
/**
* Sets up a line cache with a ttl
*/
private _initLinesCache(): void {
if (!this._linesCache) {
this._linesCache = new Array(this._terminal._core.buffer.length);
this._terminal.on('cursormove', this._destroyLinesCache);
}
window.clearTimeout(this._linesCacheTimeoutId);
this._linesCacheTimeoutId = window.setTimeout(() => this._destroyLinesCache(), LINES_CACHE_TIME_TO_LIVE);
}
private _destroyLinesCache(): void {
this._linesCache = null;
this._terminal.off('cursormove', this._destroyLinesCache);
if (this._linesCacheTimeoutId) {
window.clearTimeout(this._linesCacheTimeoutId);
this._linesCacheTimeoutId = 0;
}
}
/**
* A found substring is a whole word if it doesn't have an alphanumeric character directly adjacent to it.
* @param searchIndex starting indext of the potential whole word substring
@@ -107,8 +171,8 @@ export class SearchHelper implements ISearchHelper {
* @param term the substring that starts at searchIndex
*/
private _isWholeWord(searchIndex: number, line: string, term: string): boolean {
return (((searchIndex === 0) || (nonWordCharacters.indexOf(line[searchIndex - 1]) !== -1)) &&
(((searchIndex + term.length) === line.length) || (nonWordCharacters.indexOf(line[searchIndex + term.length]) !== -1)));
return (((searchIndex === 0) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex - 1]) !== -1)) &&
(((searchIndex + term.length) === line.length) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex + term.length]) !== -1)));
}
/**
@@ -116,66 +180,89 @@ export class SearchHelper implements ISearchHelper {
* subsequent terminal lines if the text is wrapped. If the provided line number is part of a wrapped text line that
* started on an earlier line then it is skipped since it will be properly searched when the terminal line that the
* text starts on is searched.
* @param term Tne search term.
* @param y The line to search.
* @param term The search term.
* @param row The line to start the search from.
* @param col The column to start the search from.
* @param searchOptions Search options.
* @return The search result if it was found.
*/
protected _findInLine(term: string, y: number, searchOptions: ISearchOptions = {}): ISearchResult {
if (this._terminal._core.buffer.lines.get(y).isWrapped) {
protected _findInLine(term: string, row: number, col: number, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult {
if (this._terminal._core.buffer.lines.get(row).isWrapped) {
return;
}
const stringLine = this.translateBufferLineToStringWithWrap(y, true);
const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
let searchIndex = -1;
if (searchOptions.regex) {
const searchRegex = RegExp(searchTerm, 'g');
const foundTerm = searchRegex.exec(searchStringLine);
if (foundTerm && foundTerm[0].length > 0) {
searchIndex = searchRegex.lastIndex - foundTerm[0].length;
term = foundTerm[0];
let stringLine = this._linesCache ? this._linesCache[row] : void 0;
if (stringLine === void 0) {
stringLine = this.translateBufferLineToStringWithWrap(row, true);
if (this._linesCache) {
this._linesCache[row] = stringLine;
}
} else {
searchIndex = searchStringLine.indexOf(searchTerm);
}
if (searchIndex >= 0) {
// Adjust the row number and search index if needed since a "line" of text can span multiple rows
if (searchIndex >= this._terminal.cols) {
y += Math.floor(searchIndex / this._terminal.cols);
searchIndex = searchIndex % this._terminal.cols;
const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
let resultIndex = -1;
if (searchOptions.regex) {
const searchRegex = RegExp(searchTerm, 'g');
let foundTerm: RegExpExecArray;
if (isReverseSearch) {
// This loop will get the resultIndex of the _last_ regex match in the range 0..col
while (foundTerm = searchRegex.exec(searchStringLine.slice(0, col))) {
resultIndex = searchRegex.lastIndex - foundTerm[0].length;
term = foundTerm[0];
searchRegex.lastIndex -= (term.length - 1);
}
} else {
foundTerm = searchRegex.exec(searchStringLine.slice(col));
if (foundTerm && foundTerm[0].length > 0) {
resultIndex = col + (searchRegex.lastIndex - foundTerm[0].length);
term = foundTerm[0];
}
}
if (searchOptions.wholeWord && !this._isWholeWord(searchIndex, searchStringLine, term)) {
} else {
if (isReverseSearch) {
if (col - searchTerm.length >= 0) {
resultIndex = searchStringLine.lastIndexOf(searchTerm, col - searchTerm.length);
}
} else {
resultIndex = searchStringLine.indexOf(searchTerm, col);
}
}
if (resultIndex >= 0) {
// Adjust the row number and search index if needed since a "line" of text can span multiple rows
if (resultIndex >= this._terminal.cols) {
row += Math.floor(resultIndex / this._terminal.cols);
resultIndex = resultIndex % this._terminal.cols;
}
if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) {
return;
}
const line = this._terminal._core.buffer.lines.get(y);
const line = this._terminal._core.buffer.lines.get(row);
for (let i = 0; i < searchIndex; i++) {
for (let i = 0; i < resultIndex; i++) {
const charData = line.get(i);
// Adjust the searchIndex to normalize emoji into single chars
const char = charData[1/*CHAR_DATA_CHAR_INDEX*/];
if (char.length > 1) {
searchIndex -= 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*/];
if (charWidth === 0) {
searchIndex++;
resultIndex++;
}
}
return {
term,
col: searchIndex,
row: y
col: resultIndex,
row
};
}
}
/**
* Translates a buffer line to a string, including subsequent lines if they are wraps.
* Wide characters will count as two columns in the resulting string. This
@@ -191,7 +278,7 @@ export class SearchHelper implements ISearchHelper {
do {
const nextLine = this._terminal._core.buffer.lines.get(lineIndex + 1);
lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
lineString += this._terminal._core.buffer.translateBufferLineToString(lineIndex, !lineWrapsToNext && trimRight);
lineString += this._terminal._core.buffer.translateBufferLineToString(lineIndex, !lineWrapsToNext && trimRight).substring(0, this._terminal.cols);
lineIndex++;
} while (lineWrapsToNext);
@@ -205,6 +292,7 @@ export class SearchHelper implements ISearchHelper {
*/
private _selectResult(result: ISearchResult): boolean {
if (!result) {
this._terminal.clearSelection();
return false;
}
this._terminal._core.selectionManager.setSelection(result.col, result.row, result.term.length);
+71 -6
View File
@@ -2,13 +2,13 @@
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
declare var require: any;
import { assert, expect } from 'chai';
import * as search from './search';
import { SearchHelper } from './SearchHelper';
import { ISearchOptions, ISearchResult } from './Interfaces';
class MockTerminalPlain {}
class MockTerminal {
@@ -16,7 +16,7 @@ class MockTerminal {
public searchHelper: TestSearchHelper;
public cols: number;
constructor(options: any) {
this._core = new (require('../../../lib/Terminal').Terminal)(options);
this._core = new (require('../../../lib/Terminal')).Terminal(options);
this.searchHelper = new TestSearchHelper(this as any);
this.cols = options.cols;
}
@@ -29,8 +29,11 @@ class MockTerminal {
}
class TestSearchHelper extends SearchHelper {
public findInLine(term: string, y: number, searchOptions?: ISearchOptions): ISearchResult {
return this._findInLine(term, y, searchOptions);
public findInLine(term: string, rowNumber: number, searchOptions?: ISearchOptions): ISearchResult {
return this._findInLine(term, rowNumber, 0, searchOptions);
}
public findFromIndex(term: string, row: number, col: number, searchOptions?: ISearchOptions, isReverseSearch?: boolean): ISearchResult {
return this._findInLine(term, row, col, searchOptions, isReverseSearch);
}
}
@@ -82,6 +85,9 @@ describe('search addon', () => {
expect(hello3).eql(undefined);
expect(llo).eql(undefined);
expect(goodbye).eql({col: 0, row: 5, term: 'goodbye'});
term.core.resize(9, 5);
const hello0Resize = term.searchHelper.findInLine('Hello', 0);
expect(hello0Resize).eql({col: 8, row: 0, term: 'Hello'});
});
it('should respect search regex', () => {
search.apply(<any>MockTerminal);
@@ -115,8 +121,6 @@ describe('search addon', () => {
it('should not select empty lines', () => {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 3});
term.core.write(' ');
term.pushWriteData();
const line = term.searchHelper.findInLine('^.*$', 0, { regex: true });
expect(line).eql(undefined);
});
@@ -244,5 +248,66 @@ describe('search addon', () => {
expect(hello4).eql(undefined);
expect(hello5).eql(undefined);
});
it('should find multiple matches in line', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 5});
term.core.write('helloooo helloooo\r\naaaAAaaAAA');
term.pushWriteData();
const searchOptions = {
regex: false,
wholeWord: false,
caseSensitive: false
};
const find0 = term.searchHelper.findFromIndex('hello', 0, 0, searchOptions);
const find1 = term.searchHelper.findFromIndex('hello', 0, find0.col + find0.term.length, searchOptions);
const find2 = term.searchHelper.findFromIndex('aaaa', 1, 0, searchOptions);
const find3 = term.searchHelper.findFromIndex('aaaa', 1, find2.col + find2.term.length, searchOptions);
const find4 = term.searchHelper.findFromIndex('aaaa', 1, find3.col + find3.term.length, searchOptions);
expect(find0).eql({col: 0, row: 0, term: 'hello'});
expect(find1).eql({col: 9, row: 0, term: 'hello'});
expect(find2).eql({col: 0, row: 1, term: 'aaaa'});
expect(find3).eql({col: 4, row: 1, term: 'aaaa'});
expect(find4).eql(undefined);
});
it('should find multiple matches in line - reverse search', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 5});
term.core.write('it is what it is');
term.pushWriteData();
const searchOptions = {
regex: false,
wholeWord: false,
caseSensitive: false
};
const isReverseSearch = true;
const find0 = term.searchHelper.findFromIndex('is', 0, 16, searchOptions, isReverseSearch);
const find1 = term.searchHelper.findFromIndex('is', 0, find0.col, searchOptions, isReverseSearch);
const find2 = term.searchHelper.findFromIndex('it', 0, 16, searchOptions, isReverseSearch);
const find3 = term.searchHelper.findFromIndex('it', 0, find2.col, searchOptions, isReverseSearch);
expect(find0).eql({col: 14, row: 0, term: 'is'});
expect(find1).eql({col: 3, row: 0, term: 'is'});
expect(find2).eql({col: 11, row: 0, term: 'it'});
expect(find3).eql({col: 0, row: 0, term: 'it'});
});
it('should find multiple matches in line - reverse search with regex', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 5});
term.core.write('zzzABCzzzzABCABC');
term.pushWriteData();
const searchOptions = {
regex: true,
wholeWord: false,
caseSensitive: true
};
const isReverseSearch = true;
const find0 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, 16, searchOptions, isReverseSearch);
const find1 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, find0.col, searchOptions, isReverseSearch);
const find2 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, find1.col, searchOptions, isReverseSearch);
const find3 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, find2.col, searchOptions, isReverseSearch);
expect(find0).eql({col: 13, row: 0, term: 'ABC'});
expect(find1).eql({col: 10, row: 0, term: 'ABC'});
expect(find2).eql({col: 3, row: 0, term: 'ABC'});
expect(find3).eql(undefined);
});
});
});
+2 -2
View File
@@ -10,7 +10,7 @@ import { ISearchAddonTerminal, ISearchOptions } from './Interfaces';
/**
* Find the next instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
* @param term Tne search term.
* @param term The search term.
* @param searchOptions Search options
* @return Whether a result was found.
*/
@@ -25,7 +25,7 @@ export function findNext(terminal: Terminal, term: string, searchOptions: ISearc
/**
* Find the previous instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
* @param term Tne search term.
* @param term The search term.
* @param searchOptions Search options
* @return Whether a result was found.
*/
+5 -2
View File
@@ -4,14 +4,17 @@
"target": "es5",
"lib": [
"dom",
"es6",
"es5"
],
"rootDir": ".",
"outDir": "../../../lib/addons/search/",
"sourceMap": true,
"removeComments": true,
"declaration": true,
"preserveWatchOutput": true
"preserveWatchOutput": true,
"types": [
"../../node_modules/@types/mocha"
]
},
"include": [
"**/*.ts",
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { assert, expect } from 'chai';
import { assert } from 'chai';
import * as terminado from './terminado';
+5 -3
View File
@@ -3,15 +3,17 @@
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es6",
"es5"
],
"rootDir": ".",
"outDir": "../../../lib/addons/terminado/",
"sourceMap": true,
"removeComments": true,
"declaration": true,
"preserveWatchOutput": true
"preserveWatchOutput": true,
"types": [
"../../node_modules/@types/mocha"
]
},
"include": [
"**/*.ts",
+5 -2
View File
@@ -4,14 +4,17 @@
"target": "es5",
"lib": [
"dom",
"es6",
"es5",
],
"rootDir": ".",
"outDir": "../../../lib/addons/webLinks/",
"sourceMap": true,
"removeComments": true,
"declaration": true,
"preserveWatchOutput": true
"preserveWatchOutput": true,
"types": [
"../../node_modules/@types/mocha"
]
},
"include": [
"**/*.ts",
+49 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { assert, expect } from 'chai';
import { assert } from 'chai';
import * as webLinks from './webLinks';
@@ -39,4 +39,52 @@ describe('webLinks addon', () => {
assert.equal(uri, 'http://foo.com/a~b#c~d?e~f');
});
it('should allow : character in URI path', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = ' http://foo.com/colon:test ';
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://foo.com/colon:test');
});
it('should not allow : character at the end of a URI path', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = ' http://foo.com/colon:test: ';
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://foo.com/colon:test');
});
it('should not allow " character at the end of a URI enclosed with ""', () => {
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('should not allow \' character at the end of a URI enclosed with \'\'', () => {
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/');
});
});
+1 -1
View File
@@ -14,7 +14,7 @@ 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\\.\\-%~]*)*';
const pathClause = '(\\/[\\/\\w\\.\\-%~:]*)*([^:"\'\\s])';
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
+5 -3
View File
@@ -3,15 +3,17 @@
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es6",
"es5"
],
"rootDir": ".",
"outDir": "../../../lib/addons/winptyCompat/",
"sourceMap": true,
"removeComments": true,
"declaration": true,
"preserveWatchOutput": true
"preserveWatchOutput": true,
"types": [
"../../node_modules/@types/mocha"
]
},
"include": [
"**/*.ts",
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { assert, expect } from 'chai';
import { assert } from 'chai';
import * as winptyCompat from './winptyCompat';
+3 -2
View File
@@ -7,7 +7,8 @@ import { Terminal } from 'xterm';
import { IWinptyCompatAddonTerminal } from './Interfaces';
const CHAR_DATA_CODE_INDEX = 3;
const NULL_CELL_CODE = 32;
const NULL_CELL_CODE = 0;
const WHITESPACE_CELL_CODE = 32;
export function winptyCompatInit(terminal: Terminal): void {
const addonTerminal = <IWinptyCompatAddonTerminal>terminal;
@@ -32,7 +33,7 @@ export function winptyCompatInit(terminal: Terminal): void {
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) {
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;
}
+5 -3
View File
@@ -3,15 +3,17 @@
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es6",
"es5"
],
"rootDir": ".",
"outDir": "../../../lib/addons/zmodem/",
"sourceMap": true,
"removeComments": true,
"declaration": true,
"preserveWatchOutput": true
"preserveWatchOutput": true,
"types": [
"../../node_modules/@types/mocha"
]
},
"include": [
"**/*.ts",
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { assert, expect } from 'chai';
import { assert } from 'chai';
import * as zmodem from './zmodem';
+2 -2
View File
@@ -34,7 +34,7 @@ import { Terminal } from 'xterm';
* via `detach()` and a re-`attach()`.)
*/
let zmodem;
let zmodem: any;
export interface IZmodemOptions {
noTerminalWriteOutsideSession?: boolean;
@@ -44,7 +44,7 @@ function zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void {
const term = this;
const senderFunc = (octets: ArrayLike<number>) => ws.send(new Uint8Array(octets));
let zsentry;
let zsentry: any;
function shouldWrite(): boolean {
return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession;
+27 -9
View File
@@ -90,16 +90,34 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
public push(value: T): void {
this._array[this._getCyclicIndex(this._length)] = value;
if (this._length === this._maxLength) {
this._startIndex++;
if (this._startIndex === this._maxLength) {
this._startIndex = 0;
}
this._startIndex = ++this._startIndex % this._maxLength;
this.emit('trim', 1);
} else {
this._length++;
}
}
/**
* Advance ringbuffer index and return current element for recycling.
* Note: The buffer must be full for this method to work.
* @throws When the buffer is not full.
*/
public recycle(): T {
if (this._length !== this._maxLength) {
throw new Error('Can only recycle when the buffer is full');
}
this._startIndex = ++this._startIndex % this._maxLength;
this.emit('trim', 1);
return this._array[this._getCyclicIndex(this._length - 1)]!;
}
/**
* Ringbuffer is at max length.
*/
public get isFull(): boolean {
return this._length === this._maxLength;
}
/**
* Removes and returns the last value on the list.
* @return The popped value.
@@ -136,10 +154,10 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
}
// Adjust length as needed
if (this._length + items.length > this.maxLength) {
const countToTrim = (this._length + items.length) - this.maxLength;
if (this._length + items.length > this._maxLength) {
const countToTrim = (this._length + items.length) - this._maxLength;
this._startIndex += countToTrim;
this._length = this.maxLength;
this._length = this._maxLength;
this.emit('trim', countToTrim);
} else {
this._length += items.length;
@@ -178,7 +196,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
const expandListBy = (start + count + offset) - this._length;
if (expandListBy > 0) {
this._length += expandListBy;
while (this._length > this.maxLength) {
while (this._length > this._maxLength) {
this._length--;
this._startIndex++;
this.emit('trim', 1);
@@ -198,6 +216,6 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
* @returns The cyclic index.
*/
private _getCyclicIndex(index: number): number {
return (this._startIndex + index) % this.maxLength;
return (this._startIndex + index) % this._maxLength;
}
}
@@ -101,7 +101,7 @@ describe('clone', () => {
test.a.b.c.d.e.f = 'bar';
// The values at a greater depth then 5 should not be cloned
assert.equal(cloned.a.b.c.d.e.f, 'bar');
assert.equal((cloned as any).a.b.c.d.e.f, 'bar');
});
it('should allow an optional maximum depth to be set', () => {
@@ -118,7 +118,7 @@ describe('clone', () => {
test.a.b.c = 'bar';
// The values at a greater depth then 2 should not be cloned
assert.equal(cloned.a.b.c, 'bar');
assert.equal((cloned as any).a.b.c, 'bar');
});
it('should not throw when cloning a recursive reference', () => {
+2 -2
View File
@@ -6,7 +6,7 @@
/*
* A simple utility for cloning values
*/
export const clone = <T>(val: T, depth: number = 5): T => {
export function clone<T>(val: T, depth: number = 5): T | null {
if (typeof val !== 'object') {
return val;
}
@@ -25,4 +25,4 @@ export const clone = <T>(val: T, depth: number = 5): T => {
}
return clonedObject as T;
};
}
+1 -1
View File
@@ -14,7 +14,7 @@ export class EventEmitter extends Disposable implements IEventEmitter, IDisposab
super();
// Restore the previous events if available, this will happen if the
// constructor is called multiple times on the same object (terminal reset).
this._events = this._events || {};
this._events = (<any>this)._events || {};
}
public on(type: string, listener: XtermListener): void {
+89
View File
@@ -0,0 +1,89 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { fillFallback } from './TypedArrayUtils';
type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray
| Int8Array | Int16Array | Int32Array
| Float32Array | Float64Array;
describe('polyfill conformance tests', function(): void {
function deepEquals(a: TypedArray, b: TypedArray): void {
assert.equal(a.length, b.length);
for (let i = 0; i < a.length; ++i) {
assert.equal(a[i], b[i]);
}
}
describe('TypedArray.fill', function(): void {
it('should work with all typed array types', function(): void {
const u81 = new Uint8Array(5);
const u82 = new Uint8Array(5);
deepEquals(fillFallback(u81, 2), u82.fill(2));
deepEquals(fillFallback(u81, -1), u82.fill(-1));
const u161 = new Uint16Array(5);
const u162 = new Uint16Array(5);
deepEquals(fillFallback(u161, 2), u162.fill(2));
deepEquals(fillFallback(u161, 65535), u162.fill(65535));
deepEquals(fillFallback(u161, -1), u162.fill(-1));
const u321 = new Uint32Array(5);
const u322 = new Uint32Array(5);
deepEquals(fillFallback(u321, 2), u322.fill(2));
deepEquals(fillFallback(u321, 65537), u322.fill(65537));
deepEquals(fillFallback(u321, -1), u322.fill(-1));
const i81 = new Int8Array(5);
const i82 = new Int8Array(5);
deepEquals(fillFallback(i81, 2), i82.fill(2));
deepEquals(fillFallback(i81, -1), i82.fill(-1));
const i161 = new Int16Array(5);
const i162 = new Int16Array(5);
deepEquals(fillFallback(i161, 2), i162.fill(2));
deepEquals(fillFallback(i161, 65535), i162.fill(65535));
deepEquals(fillFallback(i161, -1), i162.fill(-1));
const i321 = new Int32Array(5);
const i322 = new Int32Array(5);
deepEquals(fillFallback(i321, 2), i322.fill(2));
deepEquals(fillFallback(i321, 65537), i322.fill(65537));
deepEquals(fillFallback(i321, -1), i322.fill(-1));
const f321 = new Float32Array(5);
const f322 = new Float32Array(5);
deepEquals(fillFallback(f321, 1.2345), f322.fill(1.2345));
const f641 = new Float64Array(5);
const f642 = new Float64Array(5);
deepEquals(fillFallback(f641, 1.2345), f642.fill(1.2345));
const u8Clamped1 = new Uint8ClampedArray(5);
const u8Clamped2 = new Uint8ClampedArray(5);
deepEquals(fillFallback(u8Clamped1, 2), u8Clamped2.fill(2));
deepEquals(fillFallback(u8Clamped1, 257), u8Clamped2.fill(257));
});
it('start offset', function(): void {
for (let i = -2; i < 10; ++i) {
const u81 = new Uint8Array(5);
const u83 = new Uint8Array(5);
deepEquals(fillFallback(u81, 2, i), u83.fill(2, i));
deepEquals(fillFallback(u81, -1, i), u83.fill(-1, i));
}
});
it('end offset', function(): void {
for (let i = -2; i < 10; ++i) {
const u81 = new Uint8Array(5);
const u83 = new Uint8Array(5);
deepEquals(fillFallback(u81, 2, 0, i), u83.fill(2, 0, i));
deepEquals(fillFallback(u81, -1, 0, i), u83.fill(-1, 0, i));
}
});
it('start/end offset', function(): void {
for (let i = -2; i < 10; ++i) {
for (let j = -2; j < 10; ++j) {
const u81 = new Uint8Array(5);
const u83 = new Uint8Array(5);
deepEquals(fillFallback(u81, 2, i, j), u83.fill(2, i, j));
deepEquals(fillFallback(u81, -1, i, j), u83.fill(-1, i, j));
}
}
});
});
});
+41
View File
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
/**
* polyfill for TypedArray.fill
* This is needed to support .fill in all safari versions and IE 11.
*/
type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray
| Int8Array | Int16Array | Int32Array
| Float32Array | Float64Array;
export function fill<T extends TypedArray>(array: T, value: number, start?: number, end?: number): T {
// all modern engines that support .fill
if (array.fill) {
return array.fill(value, start, end) as T;
}
return fillFallback(array, value, start, end);
}
export function fillFallback<T extends TypedArray>(array: T, value: number, start: number = 0, end: number = array.length): T {
// safari and IE 11
// since IE 11 does not support Array.prototype.fill either
// we cannot use the suggested polyfill from MDN
// instead we simply fall back to looping
if (start >= array.length) {
return array;
}
start = (array.length + start) % array.length;
if (end >= array.length) {
end = array.length;
} else {
end = (array.length + end) % array.length;
}
for (let i = start; i < end; ++i) {
array[i] = value;
}
return array;
}
+2
View File
@@ -24,10 +24,12 @@ export interface IKeyboardEvent {
export interface ICircularList<T> extends IEventEmitter {
length: number;
maxLength: number;
isFull: boolean;
get(index: number): T | undefined;
set(index: number, value: T): void;
push(value: T): void;
recycle(): T | undefined;
pop(): T | undefined;
splice(start: number, deleteCount: number, ...items: T[]): void;
trimStart(count: number): void;
+19
View File
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"es5"
],
"rootDir": ".",
"noEmit": true,
"strict": true,
"pretty": true,
"types": [
"../../node_modules/@types/mocha",
"../../"
]
},
"include": [
"./**/*"
]
}
+2 -2
View File
@@ -10,12 +10,12 @@ import { ICharset } from '../Types';
* to be represented within the terminal with only 8-bit encoding. See ISO 2022
* for a discussion on character sets. Only VT100 character sets are supported.
*/
export const CHARSETS: { [key: string]: ICharset } = {};
export const CHARSETS: { [key: string]: ICharset | null } = {};
/**
* The default character set, US.
*/
export const DEFAULT_CHARSET: ICharset = CHARSETS['B'];
export const DEFAULT_CHARSET: ICharset | null = CHARSETS['B'];
/**
* DEC Special Character and Line Drawing Set.
+14 -2
View File
@@ -2,6 +2,7 @@
import { assert } from 'chai';
import { evaluateKeyboardEvent } from './Keyboard';
import { IKeyboardResult } from '../Types';
import { IKeyboardEvent } from '../../common/Types';
/**
* A helper function for testing which allows passing in a partial event and defaults will be filled
@@ -20,12 +21,12 @@ function testEvaluateKeyboardEvent(partialEvent: {
isMac?: boolean;
macOptionIsMeta?: boolean;
} = {}): IKeyboardResult {
const event = {
const event: IKeyboardEvent = {
altKey: partialEvent.altKey || false,
ctrlKey: partialEvent.ctrlKey || false,
shiftKey: partialEvent.shiftKey || false,
metaKey: partialEvent.metaKey || false,
keyCode: partialEvent.keyCode !== undefined ? partialEvent.keyCode : undefined,
keyCode: partialEvent.keyCode !== undefined ? partialEvent.keyCode : 0,
key: partialEvent.key || '',
type: partialEvent.type || ''
};
@@ -280,5 +281,16 @@ describe('Keyboard', () => {
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }).key, '\x1b[B');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }, { applicationCursorMode: true }).key, '\x1bOB');
});
it('should handle lowercase letters', () => {
assert.equal(testEvaluateKeyboardEvent({ keyCode: 65, key: 'a' }).key, 'a');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 189, key: '-' }).key, '-');
});
it('should handle uppercase letters', () => {
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 65, key: 'A' }).key, 'A');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 49, key: '!' }).key, '!');
});
});
});
+4
View File
@@ -349,6 +349,10 @@ export function evaluateKeyboardEvent(
if (ev.keyCode === 65) { // cmd + a
result.type = KeyboardResultType.SELECT_ALL;
}
} else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey &&
ev.keyCode >= 48 && ev.keyCode !== 144 && ev.keyCode !== 145) {
// Include only keys that that result in a character; don't include num lock and scroll lock
result.key = ev.key;
}
break;
}
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"es5"
],
"rootDir": ".",
"noEmit": true,
"strict": true,
"pretty": true,
"types": [
"../../node_modules/@types/mocha",
"../../"
]
},
"include": [
"./**/*",
"../common/**/*"
]
}
-1
View File
@@ -33,7 +33,6 @@ export class AltClickHandler {
this._mouseEvent,
this._terminal.element,
this._terminal.charMeasure,
this._terminal.options.lineHeight,
this._terminal.cols,
this._terminal.rows,
false
+6
View File
@@ -59,6 +59,12 @@ export class Terminal implements ITerminalApi {
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
this._core.attachCustomKeyEventHandler(customKeyEventHandler);
}
public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
return this._core.addCsiHandler(flag, callback);
}
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
return this._core.addOscHandler(ident, callback);
}
public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number {
return this._core.registerLinkMatcher(regex, handler, options);
}
+6 -5
View File
@@ -9,6 +9,7 @@ import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, IGlyphIdentifier } from './atlas/T
import BaseCharAtlas from './atlas/BaseCharAtlas';
import { acquireCharAtlas } from './atlas/CharAtlasCache';
import { CHAR_DATA_CHAR_INDEX } from '../Buffer';
import { is256Color } from './atlas/CharAtlasUtils';
export abstract class BaseRenderLayer implements IRenderLayer {
private _canvas: HTMLCanvasElement;
@@ -235,12 +236,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
*/
protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void {
this._ctx.font = this._getFont(terminal, false, false);
this._ctx.textBaseline = 'top';
this._ctx.textBaseline = 'middle';
this._clipRow(terminal, y);
this._ctx.fillText(
charData[CHAR_DATA_CHAR_INDEX],
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop);
(y + 0.5) * this._scaledCellHeight + this._scaledCharTop);
}
/**
@@ -294,11 +295,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
private _drawUncachedChars(terminal: ITerminal, chars: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void {
this._ctx.save();
this._ctx.font = this._getFont(terminal, bold, italic);
this._ctx.textBaseline = 'top';
this._ctx.textBaseline = 'middle';
if (fg === INVERTED_DEFAULT_COLOR) {
this._ctx.fillStyle = this._colors.background.css;
} else if (fg < 256) {
} else if (is256Color(fg)) {
// 256 color support
this._ctx.fillStyle = this._colors.ansi[fg].css;
} else {
@@ -315,7 +316,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._ctx.fillText(
chars,
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop);
(y + 0.5) * this._scaledCellHeight + this._scaledCharTop);
this._ctx.restore();
}
+16 -10
View File
@@ -1,6 +1,6 @@
import { assert } from 'chai';
import { MockTerminal, MockBuffer } from '../utils/TestUtils.test';
import { MockTerminal, MockBuffer } from '../ui/TestUtils.test';
import { CircularList } from '../common/CircularList';
import { ICharacterJoinerRegistry } from './Types';
@@ -21,17 +21,21 @@ describe('CharacterJoinerRegistry', () => {
lines.set(2, lineData([['a -> b -', 0xFFFFFFFF], ['> c -> d', 0]]));
lines.set(3, lineData([['no joined ranges']]));
lines.set(4, new BufferLine());
lines.set(4, new BufferLine(0));
lines.set(5, lineData([['a', 0x11111111], [' -> b -> c -> '], ['d', 0x22222222]]));
const line6 = lineData([['wi']]);
line6.push([0, '¥', 2, '¥'.charCodeAt(0)]);
line6.push([0, '', 0, null]);
line6.resize(line6.length + 1, [0, '¥', 2, '¥'.charCodeAt(0)]);
line6.resize(line6.length + 1, [0, '', 0, null]);
let sub = lineData([['deemo']]);
for (let i = 0; i < sub.length; ++i) line6.push(sub.get(i));
line6.push([0, '\xf0\x9f\x98\x81', 1, 128513]);
line6.push([0, ' ', 1, ' '.charCodeAt(0)]);
let oldSize = line6.length;
line6.resize(oldSize + sub.length, [0, '', 0, 0]);
for (let i = 0; i < sub.length; ++i) line6.set(i + oldSize, sub.get(i));
line6.resize(line6.length + 1, [0, '\xf0\x9f\x98\x81', 1, 128513]);
line6.resize(line6.length + 1, [0, ' ', 1, ' '.charCodeAt(0)]);
sub = lineData([['jiabc']]);
for (let i = 0; i < sub.length; ++i) line6.push(sub.get(i));
oldSize = line6.length;
line6.resize(oldSize + sub.length, [0, '', 0, 0]);
for (let i = 0; i < sub.length; ++i) line6.set(i + oldSize, sub.get(i));
lines.set(6, line6);
(<MockBuffer>terminal.buffer).setLines(lines);
@@ -264,11 +268,13 @@ describe('CharacterJoinerRegistry', () => {
type IPartialLineData = ([string] | [string, number]);
function lineData(data: IPartialLineData[]): IBufferLine {
const tline = new BufferLine();
const tline = new BufferLine(0);
for (let i = 0; i < data.length; ++i) {
const line = data[i][0];
const attr = <number>(data[i][1] || 0);
line.split('').map(char => tline.push([attr, char, 1, char.charCodeAt(0)]));
const offset = tline.length;
tline.resize(tline.length + line.split('').length, [0, '', 0, 0]);
line.split('').map((char, idx) => tline.set(idx + offset, [attr, char, 1, char.charCodeAt(0)]));
}
return tline;
}
+1 -2
View File
@@ -3,8 +3,7 @@
* @license MIT
*/
import { IColorManager } from './Types';
import { IColor, IColorSet } from '../shared/Types';
import { IColorManager, IColor, IColorSet } from './Types';
import { ITheme } from 'xterm';
const DEFAULT_FOREGROUND = fromHex('#ffffff');
+2 -1
View File
@@ -7,6 +7,7 @@ import { ILinkHoverEvent, ITerminal, ILinkifierAccessor, LinkHoverEventTypes } f
import { IColorSet, IRenderDimensions } from './Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { INVERTED_DEFAULT_COLOR } from './atlas/Types';
import { is256Color } from './atlas/CharAtlasUtils';
export class LinkRenderLayer extends BaseRenderLayer {
private _state: ILinkHoverEvent = null;
@@ -42,7 +43,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
private _onLinkHover(e: ILinkHoverEvent): void {
if (e.fg === INVERTED_DEFAULT_COLOR) {
this._ctx.fillStyle = this._colors.background.css;
} else if (e.fg < 256) {
} else if (is256Color(e.fg)) {
// 256 color support
this._ctx.fillStyle = this._colors.ansi[e.fg].css;
} else {
+9 -8
View File
@@ -3,12 +3,13 @@
* @license MIT
*/
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, NULL_CELL_CODE } from '../Buffer';
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from '../Buffer';
import { FLAGS, IColorSet, IRenderDimensions, ICharacterJoinerRegistry } from './Types';
import { CharData, ITerminal } from '../Types';
import { INVERTED_DEFAULT_COLOR } from './atlas/Types';
import { INVERTED_DEFAULT_COLOR, DEFAULT_COLOR } from './atlas/Types';
import { GridCache } from './GridCache';
import { BaseRenderLayer } from './BaseRenderLayer';
import { is256Color } from './atlas/CharAtlasUtils';
/**
* This CharData looks like a null character, which will forc a clear and render
@@ -72,11 +73,11 @@ export class TextRenderLayer extends BaseRenderLayer {
const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : [];
for (let x = 0; x < terminal.cols; x++) {
const charData = line.get(x);
let code: number = <number>charData[CHAR_DATA_CODE_INDEX];
let code: number = <number>charData[CHAR_DATA_CODE_INDEX] || WHITESPACE_CELL_CODE;
// Can either represent character(s) for a single cell or multiple cells
// if indicated by a character joiner.
let chars: string = charData[CHAR_DATA_CHAR_INDEX];
let chars: string = charData[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR;
const attr: number = charData[CHAR_DATA_ATTR_INDEX];
let width: number = charData[CHAR_DATA_WIDTH_INDEX];
@@ -143,10 +144,10 @@ export class TextRenderLayer extends BaseRenderLayer {
const temp = bg;
bg = fg;
fg = temp;
if (fg === 256) {
if (fg === DEFAULT_COLOR) {
fg = INVERTED_DEFAULT_COLOR;
}
if (bg === 257) {
if (bg === DEFAULT_COLOR) {
bg = INVERTED_DEFAULT_COLOR;
}
}
@@ -186,7 +187,7 @@ export class TextRenderLayer extends BaseRenderLayer {
let nextFillStyle = null; // null represents default background color
if (bg === INVERTED_DEFAULT_COLOR) {
nextFillStyle = this._colors.foreground.css;
} else if (bg < 256) {
} else if (is256Color(bg)) {
nextFillStyle = this._colors.ansi[bg].css;
}
@@ -230,7 +231,7 @@ export class TextRenderLayer extends BaseRenderLayer {
this._ctx.save();
if (fg === INVERTED_DEFAULT_COLOR) {
this._ctx.fillStyle = this._colors.background.css;
} else if (fg < 256) {
} else if (is256Color(fg)) {
// 256 color support
this._ctx.fillStyle = this._colors.ansi[fg].css;
} else {
+14 -4
View File
@@ -5,7 +5,6 @@
import { ITerminal, CharacterJoinerHandler } from '../Types';
import { IEventEmitter, ITheme, IDisposable } from 'xterm';
import { IColorSet } from '../shared/Types';
/**
* Flags used to render terminal text properly.
@@ -48,9 +47,6 @@ export interface IColorManager {
colors: IColorSet;
}
// TODO: We should probably rewrite the imports for IColorSet, but there's a lot of them
export { IColorSet };
export interface IRenderDimensions {
scaledCharWidth: number;
scaledCharHeight: number;
@@ -134,3 +130,17 @@ export interface ICharacterJoinerRegistry {
deregisterCharacterJoiner(joinerId: number): boolean;
getJoinedCharacters(row: number): [number, number][];
}
export interface IColor {
css: string;
rgba: number; // 32-bit int with rgba in each byte
}
export interface IColorSet {
foreground: IColor;
background: IColor;
cursor: IColor;
cursorAccent: IColor;
selection: IColor;
ansi: IColor[];
}
+3 -3
View File
@@ -5,12 +5,12 @@
import { ITerminal } from '../../Types';
import { IColorSet } from '../Types';
import { ICharAtlasConfig } from '../../shared/atlas/Types';
import { generateConfig, configEquals } from './CharAtlasUtils';
import BaseCharAtlas from './BaseCharAtlas';
import DynamicCharAtlas from './DynamicCharAtlas';
import NoneCharAtlas from './NoneCharAtlas';
import StaticCharAtlas from './StaticCharAtlas';
import { ICharAtlasConfig } from './Types';
const charAtlasImplementations = {
'none': NoneCharAtlas,
@@ -42,8 +42,6 @@ export function acquireCharAtlas(
): BaseCharAtlas {
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors);
// TODO: Currently if a terminal changes configs it will not free the entry reference (until it's disposed)
// Check to see if the terminal already owns this config
for (let i = 0; i < charAtlasCache.length; i++) {
const entry = charAtlasCache[i];
@@ -54,6 +52,7 @@ export function acquireCharAtlas(
}
// The configs differ, release the terminal from the entry
if (entry.ownedBy.length === 1) {
entry.atlas.dispose();
charAtlasCache.splice(i, 1);
} else {
entry.ownedBy.splice(ownedByIndex, 1);
@@ -94,6 +93,7 @@ export function removeTerminalFromCache(terminal: ITerminal): void {
if (index !== -1) {
if (charAtlasCache[i].ownedBy.length === 1) {
// Remove the cache entry if it's the only terminal
charAtlasCache[i].atlas.dispose();
charAtlasCache.splice(i, 1);
} else {
// Remove the reference from the cache entry
@@ -4,18 +4,9 @@
*/
import { FontWeight } from 'xterm';
import { CHAR_ATLAS_CELL_SPACING, ICharAtlasConfig } from './Types';
import { isFirefox, isSafari } from '../../core/Platform';
import { IColor } from '../Types';
import { isFirefox, isSafari } from '../utils/Browser';
declare const Promise: any;
export interface IOffscreenCanvas {
width: number;
height: number;
getContext(type: '2d', config?: Canvas2DContextAttributes): CanvasRenderingContext2D;
transferToImageBitmap(): ImageBitmap;
}
import { ICharAtlasConfig, CHAR_ATLAS_CELL_SPACING } from './Types';
/**
* Generates a char atlas.
@@ -23,7 +14,7 @@ export interface IOffscreenCanvas {
* @param canvasFactory A function to generate a canvas with a width or height.
* @param config The config for the new char atlas.
*/
export function generateStaticCharAtlasTexture(context: Window, canvasFactory: (width: number, height: number) => HTMLCanvasElement | IOffscreenCanvas, config: ICharAtlasConfig): HTMLCanvasElement | Promise<ImageBitmap> {
export function generateStaticCharAtlasTexture(context: Window, canvasFactory: (width: number, height: number) => HTMLCanvasElement, config: ICharAtlasConfig): HTMLCanvasElement | Promise<ImageBitmap> {
const cellWidth = config.scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
const cellHeight = config.scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
const canvas = canvasFactory(
@@ -38,7 +29,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: (
ctx.save();
ctx.fillStyle = config.colors.foreground.css;
ctx.font = getFont(config.fontWeight, config);
ctx.textBaseline = 'top';
ctx.textBaseline = 'middle';
// Default color
for (let i = 0; i < 256; i++) {
@@ -46,7 +37,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: (
ctx.beginPath();
ctx.rect(i * cellWidth, 0, cellWidth, cellHeight);
ctx.clip();
ctx.fillText(String.fromCharCode(i), i * cellWidth, 0);
ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight / 2);
ctx.restore();
}
// Default color bold
@@ -57,7 +48,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: (
ctx.beginPath();
ctx.rect(i * cellWidth, cellHeight, cellWidth, cellHeight);
ctx.clip();
ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight);
ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight * 1.5);
ctx.restore();
}
ctx.restore();
@@ -73,7 +64,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: (
ctx.rect(i * cellWidth, y, cellWidth, cellHeight);
ctx.clip();
ctx.fillStyle = config.colors.ansi[colorIndex].css;
ctx.fillText(String.fromCharCode(i), i * cellWidth, y);
ctx.fillText(String.fromCharCode(i), i * cellWidth, y + cellHeight / 2);
ctx.restore();
}
}
@@ -89,7 +80,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: (
ctx.rect(i * cellWidth, y, cellWidth, cellHeight);
ctx.clip();
ctx.fillStyle = config.colors.ansi[colorIndex].css;
ctx.fillText(String.fromCharCode(i), i * cellWidth, y);
ctx.fillText(String.fromCharCode(i), i * cellWidth, y + cellHeight / 2);
ctx.restore();
}
}
@@ -101,12 +92,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: (
// performance (tested on v55).
if (!('createImageBitmap' in context) || isFirefox || isSafari) {
// Don't attempt to clear background colors if createImageBitmap is not supported
if (canvas instanceof HTMLCanvasElement) {
// Just return the HTMLCanvas if it's a HTMLCanvasElement
return canvas;
}
// Transfer to an ImageBitmap is this is an OffscreenCanvas
return new Promise((r: (bitmap: ImageBitmap) => void) => r(canvas.transferToImageBitmap()));
return canvas;
}
const charAtlasImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+5 -1
View File
@@ -5,7 +5,7 @@
import { ITerminal } from '../../Types';
import { IColorSet } from '../Types';
import { ICharAtlasConfig } from '../../shared/atlas/Types';
import { DEFAULT_COLOR, ICharAtlasConfig } from './Types';
export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig {
// null out some fields that don't matter
@@ -51,3 +51,7 @@ export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean
a.colors.foreground === b.colors.foreground &&
a.colors.background === b.colors.background;
}
export function is256Color(colorCode: number): boolean {
return colorCode < DEFAULT_COLOR;
}
+13 -9
View File
@@ -3,14 +3,13 @@
* @license MIT
*/
import { DIM_OPACITY, IGlyphIdentifier, INVERTED_DEFAULT_COLOR } from './Types';
import { ICharAtlasConfig } from '../../shared/atlas/Types';
import { IColor } from '../../shared/Types';
import { DIM_OPACITY, IGlyphIdentifier, INVERTED_DEFAULT_COLOR, ICharAtlasConfig } from './Types';
import BaseCharAtlas from './BaseCharAtlas';
import { DEFAULT_ANSI_COLORS } from '../ColorManager';
import { clearColor } from '../../shared/atlas/CharAtlasGenerator';
import { clearColor } from './CharAtlasGenerator';
import LRUMap from './LRUMap';
import { isFirefox, isSafari } from '../../shared/utils/Browser';
import { isFirefox, isSafari } from '../../core/Platform';
import { IColor } from '../Types';
// In practice we're probably never going to exhaust a texture this large. For debugging purposes,
// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
@@ -42,7 +41,7 @@ interface IGlyphCacheValue {
inBitmap: boolean;
}
function getGlyphCacheKey(glyph: IGlyphIdentifier): number {
export function getGlyphCacheKey(glyph: IGlyphIdentifier): number {
// Note that this only returns a valid key when code < 256
// Layout:
// 0b00000000000000000000000000000001: italic (1)
@@ -129,12 +128,17 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
return true;
}
// Exit early for uncachable glyphs
if (!this._canCache(glyph)) {
return false;
}
const glyphKey = getGlyphCacheKey(glyph);
const cacheValue = this._cacheMap.get(glyphKey);
if (cacheValue !== null && cacheValue !== undefined) {
this._drawFromCache(ctx, cacheValue, x, y);
return true;
} else if (this._canCache(glyph) && this._drawToCacheCount < FRAME_CACHE_DRAW_LIMIT) {
} else if (this._drawToCacheCount < FRAME_CACHE_DRAW_LIMIT) {
let index;
if (this._cacheMap.size < this._cacheMap.capacity) {
index = this._cacheMap.size;
@@ -246,7 +250,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
const fontStyle = glyph.italic ? 'italic' : '';
this._tmpCtx.font =
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
this._tmpCtx.textBaseline = 'top';
this._tmpCtx.textBaseline = 'middle';
this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css;
@@ -255,7 +259,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
this._tmpCtx.globalAlpha = DIM_OPACITY;
}
// Draw the character
this._tmpCtx.fillText(glyph.chars, 0, 0);
this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight / 2);
this._tmpCtx.restore();
// clear the background from the character to avoid issues with drawing over the previous
+1 -2
View File
@@ -5,8 +5,7 @@
* A dummy CharAtlas implementation that always fails to draw characters.
*/
import { IGlyphIdentifier } from './Types';
import { ICharAtlasConfig } from '../../shared/atlas/Types';
import { IGlyphIdentifier, ICharAtlasConfig } from './Types';
import BaseCharAtlas from './BaseCharAtlas';
export default class NoneCharAtlas extends BaseCharAtlas {
+7 -7
View File
@@ -3,10 +3,10 @@
* @license MIT
*/
import { DIM_OPACITY, IGlyphIdentifier } from './Types';
import { CHAR_ATLAS_CELL_SPACING, ICharAtlasConfig } from '../../shared/atlas/Types';
import { generateStaticCharAtlasTexture } from '../../shared/atlas/CharAtlasGenerator';
import { DIM_OPACITY, IGlyphIdentifier, DEFAULT_COLOR, ICharAtlasConfig, CHAR_ATLAS_CELL_SPACING } from './Types';
import { generateStaticCharAtlasTexture } from './CharAtlasGenerator';
import BaseCharAtlas from './BaseCharAtlas';
import { is256Color } from './CharAtlasUtils';
export default class StaticCharAtlas extends BaseCharAtlas {
private _texture: HTMLCanvasElement | ImageBitmap;
@@ -41,8 +41,8 @@ export default class StaticCharAtlas extends BaseCharAtlas {
const isAscii = glyph.code < 256;
// A color is basic if it is one of the 4 bit ANSI colors.
const isBasicColor = glyph.fg < 16;
const isDefaultColor = glyph.fg >= 256;
const isDefaultBackground = glyph.bg >= 256;
const isDefaultColor = glyph.fg === DEFAULT_COLOR;
const isDefaultBackground = glyph.bg === DEFAULT_COLOR;
return isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground && !glyph.italic;
}
@@ -58,9 +58,9 @@ export default class StaticCharAtlas extends BaseCharAtlas {
}
let colorIndex = 0;
if (glyph.fg < 256) {
if (is256Color(glyph.fg)) {
colorIndex = 2 + glyph.fg + (glyph.bold ? 16 : 0);
} else {
} else if (glyph.fg === DEFAULT_COLOR) {
// If default color and bold
if (glyph.bold) {
colorIndex = 1;
+20 -1
View File
@@ -3,9 +3,15 @@
* @license MIT
*/
export const INVERTED_DEFAULT_COLOR = -1;
import { FontWeight } from 'xterm';
import { IColorSet } from '../Types';
export const DEFAULT_COLOR = 256;
export const INVERTED_DEFAULT_COLOR = 257;
export const DIM_OPACITY = 0.5;
export const CHAR_ATLAS_CELL_SPACING = 1;
export interface IGlyphIdentifier {
chars: string;
code: number;
@@ -15,3 +21,16 @@ export interface IGlyphIdentifier {
dim: boolean;
italic: boolean;
}
export interface ICharAtlasConfig {
type: 'none' | 'static' | 'dynamic';
devicePixelRatio: number;
fontSize: number;
fontFamily: string;
fontWeight: FontWeight;
fontWeightBold: FontWeight;
scaledCharWidth: number;
scaledCharHeight: number;
allowTransparency: boolean;
colors: IColorSet;
}
+34 -21
View File
@@ -10,6 +10,7 @@ import { EventEmitter } from '../../common/EventEmitter';
import { ColorManager } from '../ColorManager';
import { RenderDebouncer } from '../../ui/RenderDebouncer';
import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from './DomRendererRowFactory';
import { INVERTED_DEFAULT_COLOR } from '../atlas/Types';
const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
const ROW_CONTAINER_CLASS = 'xterm-rows';
@@ -94,22 +95,25 @@ export class DomRenderer extends EventEmitter implements IRenderer {
}
private _updateDimensions(): void {
this.dimensions.scaledCharWidth = this._terminal.charMeasure.width * window.devicePixelRatio;
this.dimensions.scaledCharHeight = this._terminal.charMeasure.height * window.devicePixelRatio;
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth;
this.dimensions.scaledCellHeight = this.dimensions.scaledCharHeight;
this.dimensions.scaledCharWidth = Math.floor(this._terminal.charMeasure.width * window.devicePixelRatio);
this.dimensions.scaledCharHeight = Math.ceil(this._terminal.charMeasure.height * window.devicePixelRatio);
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing);
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight);
this.dimensions.scaledCharLeft = 0;
this.dimensions.scaledCharTop = 0;
this.dimensions.scaledCanvasWidth = this.dimensions.scaledCellWidth * this._terminal.cols;
this.dimensions.scaledCanvasHeight = this.dimensions.scaledCellHeight * this._terminal.rows;
this.dimensions.canvasWidth = this._terminal.charMeasure.width * this._terminal.cols;
this.dimensions.canvasHeight = this._terminal.charMeasure.height * this._terminal.rows;
this.dimensions.actualCellWidth = this._terminal.charMeasure.width;
this.dimensions.actualCellHeight = this._terminal.charMeasure.height;
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio);
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio);
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
this._rowElements.forEach(element => {
element.style.width = `${this.dimensions.canvasWidth}px`;
element.style.height = `${this._terminal.charMeasure.height}px`;
element.style.height = `${this.dimensions.actualCellHeight}px`;
element.style.lineHeight = `${this.dimensions.actualCellHeight}px`;
// Make sure rows don't overflow onto following row
element.style.overflow = 'hidden';
});
if (!this._dimensionsStyleElement) {
@@ -122,14 +126,14 @@ export class DomRenderer extends EventEmitter implements IRenderer {
` display: inline-block;` +
` height: 100%;` +
` vertical-align: top;` +
` width: ${this._terminal.charMeasure.width}px` +
` width: ${this.dimensions.actualCellWidth}px` +
`}`;
this._dimensionsStyleElement.innerHTML = styles;
this._selectionContainer.style.height = (<any>this._terminal)._viewportElement.style.height;
this._rowContainer.style.width = `${this.dimensions.canvasWidth}px`;
this._rowContainer.style.height = `${this.dimensions.canvasHeight}px`;
this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth}px`;
this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`;
}
public setTheme(theme: ITheme | undefined): IColorSet {
@@ -196,6 +200,9 @@ export class DomRenderer extends EventEmitter implements IRenderer {
`${this._terminalSelector} .${FG_CLASS_PREFIX}${i} { color: ${c.css}; }` +
`${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`;
});
styles +=
`${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${this.colorManager.colors.background.css}; }` +
`${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${this.colorManager.colors.foreground.css}; }`;
this._themeStyleElement.innerHTML = styles;
return this.colorManager.colors;
@@ -290,10 +297,10 @@ export class DomRenderer extends EventEmitter implements IRenderer {
*/
private _createSelectionElement(row: number, colStart: number, colEnd: number, rowCount: number = 1): HTMLElement {
const element = document.createElement('div');
element.style.height = `${rowCount * this._terminal.charMeasure.height}px`;
element.style.top = `${row * this._terminal.charMeasure.height}px`;
element.style.left = `${colStart * this._terminal.charMeasure.width}px`;
element.style.width = `${this._terminal.charMeasure.width * (colEnd - colStart)}px`;
element.style.height = `${rowCount * this.dimensions.actualCellHeight}px`;
element.style.top = `${row * this.dimensions.actualCellHeight}px`;
element.style.left = `${colStart * this.dimensions.actualCellWidth}px`;
element.style.width = `${this.dimensions.actualCellWidth * (colEnd - colStart)}px`;
return element;
}
@@ -329,7 +336,7 @@ export class DomRenderer extends EventEmitter implements IRenderer {
const row = y + terminal.buffer.ydisp;
const lineData = terminal.buffer.lines.get(row);
const cursorStyle = terminal.options.cursorStyle;
rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, terminal.charMeasure.width, terminal.cols));
rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, this.dimensions.actualCellWidth, terminal.cols));
}
this._terminal.emit('refresh', {start, end});
@@ -352,10 +359,16 @@ export class DomRenderer extends EventEmitter implements IRenderer {
private _setCellUnderline(x: number, x2: number, y: number, y2: number, cols: number, enabled: boolean): void {
while (x !== x2 || y !== y2) {
const span = <HTMLElement>this._rowElements[y].children[x];
span.style.textDecoration = enabled ? 'underline' : 'none';
x = (x + 1) % cols;
if (x === 0) {
const row = this._rowElements[y];
if (!row) {
return;
}
const span = <HTMLElement>row.children[x];
if (span) {
span.style.textDecoration = enabled ? 'underline' : 'none';
}
if (++x >= cols) {
x = 0;
y++;
}
}
+19 -28
View File
@@ -10,6 +10,7 @@ import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '.
import { FLAGS } from '../Types';
import { BufferLine } from '../../BufferLine';
import { IBufferLine } from '../../Types';
import { DEFAULT_COLOR } from '../atlas/Types';
describe('DomRendererRowFactory', () => {
let dom: jsdom.JSDOM;
@@ -23,11 +24,10 @@ describe('DomRendererRowFactory', () => {
});
describe('createRow', () => {
it('should create an element for every character in the row', () => {
it('should not create anything for an empty row', () => {
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span> </span>' +
'<span> </span>'
''
);
});
@@ -45,8 +45,7 @@ describe('DomRendererRowFactory', () => {
for (const style of ['block', 'bar', 'underline']) {
const fragment = rowFactory.createRow(lineData, true, style, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-cursor xterm-cursor-${style}"> </span>` +
'<span> </span>'
`<span class="xterm-cursor xterm-cursor-${style}"> </span>`
);
}
});
@@ -65,8 +64,7 @@ describe('DomRendererRowFactory', () => {
lineData.set(0, [DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-bold">a</span>' +
'<span> </span>'
'<span class="xterm-bold">a</span>'
);
});
@@ -74,31 +72,28 @@ describe('DomRendererRowFactory', () => {
lineData.set(0, [DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-italic">a</span>' +
'<span> </span>'
'<span class="xterm-italic">a</span>'
);
});
it('should add classes for 256 foreground colors', () => {
const defaultAttrNoFgColor = (0 << 9) | (256 << 0);
const defaultAttrNoFgColor = (0 << 9) | (DEFAULT_COLOR << 0);
for (let i = 0; i < 256; i++) {
lineData.set(0, [defaultAttrNoFgColor | (i << 9), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-fg-${i}">a</span>` +
'<span> </span>'
`<span class="xterm-fg-${i}">a</span>`
);
}
});
it('should add classes for 256 background colors', () => {
const defaultAttrNoBgColor = (257 << 9) | (0 << 0);
const defaultAttrNoBgColor = (DEFAULT_ATTR << 9) | (0 << 0);
for (let i = 0; i < 256; i++) {
lineData.set(0, [defaultAttrNoBgColor | (i << 0), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-bg-${i}">a</span>` +
'<span> </span>'
`<span class="xterm-bg-${i}">a</span>`
);
}
});
@@ -107,36 +102,32 @@ describe('DomRendererRowFactory', () => {
lineData.set(0, [(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-1 xterm-bg-2">a</span>' +
'<span> </span>'
'<span class="xterm-fg-1 xterm-bg-2">a</span>'
);
});
it('should correctly invert default fg color', () => {
lineData.set(0, [(FLAGS.INVERSE << 18) | (257 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]);
lineData.set(0, [(FLAGS.INVERSE << 18) | (DEFAULT_ATTR << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-1 xterm-bg-15">a</span>' +
'<span> </span>'
'<span class="xterm-fg-1 xterm-bg-257">a</span>'
);
});
it('should correctly invert default bg color', () => {
lineData.set(0, [(FLAGS.INVERSE << 18) | (1 << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)]);
lineData.set(0, [(FLAGS.INVERSE << 18) | (1 << 9) | (DEFAULT_COLOR << 0), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-0 xterm-bg-1">a</span>' +
'<span> </span>'
'<span class="xterm-fg-257 xterm-bg-1">a</span>'
);
});
it('should turn bold fg text bright', () => {
for (let i = 0; i < 8; i++) {
lineData.set(0, [(FLAGS.BOLD << 18) | (i << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)]);
lineData.set(0, [(FLAGS.BOLD << 18) | (i << 9) | (DEFAULT_COLOR << 0), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-bold xterm-fg-${i + 8}">a</span>` +
'<span> </span>'
`<span class="xterm-bold xterm-fg-${i + 8}">a</span>`
);
}
});
@@ -150,9 +141,9 @@ describe('DomRendererRowFactory', () => {
}
function createEmptyLineData(cols: number): IBufferLine {
const lineData = new BufferLine();
const lineData = new BufferLine(cols);
for (let i = 0; i < cols; i++) {
lineData.push([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
lineData.set(i, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
}
return lineData;
}
+29 -19
View File
@@ -3,9 +3,10 @@
* @license MIT
*/
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../../Buffer';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from '../../Buffer';
import { FLAGS } from '../Types';
import { IBufferLine } from '../../Types';
import { DEFAULT_COLOR, INVERTED_DEFAULT_COLOR } from '../atlas/Types';
export const BOLD_CLASS = 'xterm-bold';
export const ITALIC_CLASS = 'xterm-italic';
@@ -22,18 +23,27 @@ export class DomRendererRowFactory {
public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cellWidth: number, cols: number): DocumentFragment {
const fragment = this._document.createDocumentFragment();
let colCount = 0;
for (let x = 0; x < lineData.length; x++) {
// Don't allow any buffer to the right to be displayed
if (colCount >= cols) {
continue;
}
// Find the line length first, this prevents the need to output a bunch of
// empty cells at the end. This cannot easily be integrated into the main
// loop below because of the colCount feature (which can be removed after we
// properly support reflow and disallow data to go beyond the right-side of
// the viewport).
let lineLength = 0;
for (let x = Math.min(lineData.length, cols) - 1; x >= 0; x--) {
const charData = lineData.get(x);
const char: string = charData[CHAR_DATA_CHAR_INDEX];
const attr: number = charData[CHAR_DATA_ATTR_INDEX];
const width: number = charData[CHAR_DATA_WIDTH_INDEX];
const code = charData[CHAR_DATA_CODE_INDEX];
if (code !== NULL_CELL_CODE || (isCursorRow && x === cursorX)) {
lineLength = x + 1;
break;
}
}
for (let x = 0; x < lineLength; x++) {
const charData = lineData.get(x);
const char = charData[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR;
const attr = charData[CHAR_DATA_ATTR_INDEX];
const width = charData[CHAR_DATA_WIDTH_INDEX];
// The character to the left is a wide character, drawing is owned by the char at x-1
if (width === 0) {
@@ -70,16 +80,17 @@ export class DomRendererRowFactory {
const temp = bg;
bg = fg;
fg = temp;
if (fg === 256) {
fg = 0;
if (fg === DEFAULT_COLOR) {
fg = INVERTED_DEFAULT_COLOR;
}
if (bg === 257) {
bg = 15;
if (bg === DEFAULT_COLOR) {
bg = INVERTED_DEFAULT_COLOR;
}
}
if (flags & FLAGS.BOLD) {
// Convert the FG color to the bold variant
// Convert the FG color to the bold variant. This should not happen when
// the fg is the inverse default color as there is no bold variant.
if (fg < 8) {
fg += 8;
}
@@ -91,14 +102,13 @@ export class DomRendererRowFactory {
}
charElement.textContent = char;
if (fg !== 257) {
if (fg !== DEFAULT_COLOR) {
charElement.classList.add(`xterm-fg-${fg}`);
}
if (bg !== 256) {
if (bg !== DEFAULT_COLOR) {
charElement.classList.add(`xterm-bg-${bg}`);
}
fragment.appendChild(charElement);
colCount += width;
}
return fragment;
}
-18
View File
@@ -1,18 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
export interface IColor {
css: string;
rgba: number; // 32-bit int with rgba in each byte
}
export interface IColorSet {
foreground: IColor;
background: IColor;
cursor: IColor;
cursorAccent: IColor;
selection: IColor;
ansi: IColor[];
}
-22
View File
@@ -1,22 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { FontWeight } from 'xterm';
import { IColorSet } from '../Types';
export const CHAR_ATLAS_CELL_SPACING = 1;
export interface ICharAtlasConfig {
type: 'none' | 'static' | 'dynamic';
devicePixelRatio: number;
fontSize: number;
fontFamily: string;
fontWeight: FontWeight;
fontWeightBold: FontWeight;
scaledCharWidth: number;
scaledCharHeight: number;
allowTransparency: boolean;
colors: IColorSet;
}
+1 -1
View File
@@ -38,7 +38,7 @@ export class CharMeasure extends EventEmitter implements ICharMeasure {
}
public measure(options: ITerminalOptions): void {
this._measureElement.style.fontFamily = options.fontFamily;
this._measureElement.style.fontFamily = options.fontFamily;
this._measureElement.style.fontSize = `${options.fontSize}px`;
const geometry = this._measureElement.getBoundingClientRect();
// The element is likely currently display:none, we should retain the
@@ -37,34 +37,28 @@ describe('MouseHelper.getCoords', () => {
describe('when charMeasure is not initialized', () => {
it('should return null', () => {
charMeasure = new MockCharMeasure();
assert.equal(mouseHelper.getCoords({ pageX: 0, pageY: 0 }, document.createElement('div'), charMeasure, 1, 10, 10), null);
});
});
describe('when pageX/pageY are not supported', () => {
it('should return null', () => {
assert.equal(mouseHelper.getCoords({ pageX: undefined, pageY: undefined }, document.createElement('div'), charMeasure, 1, 10, 10), null);
assert.equal(mouseHelper.getCoords({ clientX: 0, clientY: 0 }, document.createElement('div'), charMeasure, 10, 10), null);
});
});
it('should return the cell that was clicked', () => {
let coords: [number, number];
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH / 2, pageY: CHAR_HEIGHT / 2 }, document.createElement('div'), charMeasure, 1, 10, 10);
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH / 2, clientY: CHAR_HEIGHT / 2 }, document.createElement('div'), charMeasure, 10, 10);
assert.deepEqual(coords, [1, 1]);
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH, pageY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 1, 10, 10);
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 10, 10);
assert.deepEqual(coords, [1, 1]);
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH, pageY: CHAR_HEIGHT + 1 }, document.createElement('div'), charMeasure, 1, 10, 10);
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT + 1 }, document.createElement('div'), charMeasure, 10, 10);
assert.deepEqual(coords, [1, 2]);
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH + 1, pageY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 1, 10, 10);
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH + 1, clientY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 10, 10);
assert.deepEqual(coords, [2, 1]);
});
it('should ensure the coordinates are returned within the terminal bounds', () => {
let coords: [number, number];
coords = mouseHelper.getCoords({ pageX: -1, pageY: -1 }, document.createElement('div'), charMeasure, 1, 10, 10);
coords = mouseHelper.getCoords({ clientX: -1, clientY: -1 }, document.createElement('div'), charMeasure, 10, 10);
assert.deepEqual(coords, [1, 1]);
// Event are double the cols/rows
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH * 20, pageY: CHAR_HEIGHT * 20 }, document.createElement('div'), charMeasure, 1, 10, 10);
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH * 20, clientY: CHAR_HEIGHT * 20 }, document.createElement('div'), charMeasure, 10, 10);
assert.deepEqual(coords, [10, 10], 'coordinates should never come back as larger than the terminal');
});
});
@@ -3,36 +3,19 @@
* @license MIT
*/
import { ICharMeasure } from '../Types';
import { ICharMeasure, IMouseHelper } from '../Types';
import { IRenderer } from '../renderer/Types';
export class MouseHelper {
export class MouseHelper implements IMouseHelper {
constructor(private _renderer: IRenderer) {}
public static getCoordsRelativeToElement(event: {pageX: number, pageY: number}, element: HTMLElement): [number, number] {
// Ignore browsers that don't support MouseEvent.pageX
if (event.pageX === null || event.pageX === undefined) {
return null;
}
public setRenderer(renderer: IRenderer): void {
this._renderer = renderer;
}
const originalElement = element;
let x = event.pageX;
let y = event.pageY;
// Converts the coordinates from being relative to the document to being
// relative to the terminal.
while (element) {
x -= element.offsetLeft;
y -= element.offsetTop;
element = <HTMLElement>element.offsetParent;
}
element = originalElement;
while (element && element !== element.ownerDocument.body) {
x += element.scrollLeft;
y += element.scrollTop;
element = <HTMLElement>element.parentElement;
}
return [x, y];
public static getCoordsRelativeToElement(event: {clientX: number, clientY: number}, element: HTMLElement): [number, number] {
const rect = element.getBoundingClientRect();
return [event.clientX - rect.left, event.clientY - rect.top];
}
/**
@@ -48,7 +31,7 @@ export class MouseHelper {
* apply an offset to the x value such that the left half of the cell will
* select that cell and the right half will select the next cell.
*/
public getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number] {
public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number] {
// Coordinates cannot be measured if charMeasure has not been initialized
if (!charMeasure.width || !charMeasure.height) {
return null;
@@ -81,8 +64,8 @@ export class MouseHelper {
* @param colCount The number of columns in the terminal.
* @param rowCount The number of rows in the terminal.
*/
public getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number } {
const coords = this.getCoords(event, element, charMeasure, lineHeight, colCount, rowCount);
public getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number): { x: number, y: number } {
const coords = this.getCoords(event, element, charMeasure, colCount, rowCount);
let x = coords[0];
let y = coords[1];
+1 -1
View File
@@ -180,7 +180,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
}
private _findZoneEventAt(e: MouseEvent): IMouseZone {
const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.screenElement, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows);
const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.screenElement, this._terminal.charMeasure, this._terminal.cols, this._terminal.rows);
if (!coords) {
return null;
}
+1 -1
View File
@@ -19,7 +19,7 @@ export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRat
*/
export class ScreenDprMonitor extends Disposable {
private _currentDevicePixelRatio: number;
private _outerListener: MediaQueryListListener;
private _outerListener: (this: MediaQueryList, ev: MediaQueryListEvent) => any;
private _listener: ScreenDprListener;
private _resolutionMediaMatchList: MediaQueryList;
@@ -7,7 +7,7 @@ import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../rende
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 '../shared/utils/Browser';
import * as Browser from '../core/Platform';
import { ITheme, IDisposable, IMarker } from 'xterm';
import { Terminal } from '../Terminal';
@@ -54,6 +54,12 @@ export class MockTerminal implements ITerminal {
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
throw new Error('Method not implemented.');
}
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
throw new Error('Method not implemented.');
}
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
throw new Error('Method not implemented.');
}
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number {
throw new Error('Method not implemented.');
}
@@ -121,7 +127,7 @@ export class MockTerminal implements ITerminal {
handler(data: string): void {
throw new Error('Method not implemented.');
}
on(event: string, callback: () => void): void {
on(event: string, callback: (...args: any[]) => void): void {
throw new Error('Method not implemented.');
}
off(type: string, listener: XtermListener): void {
@@ -182,7 +188,6 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
wraparoundMode: boolean;
bracketedPasteMode: boolean;
curAttr: number;
savedCurAttr: number;
savedCols: number;
x10Mouse: boolean;
vt200Mouse: boolean;
@@ -304,6 +309,7 @@ export class MockBuffer implements IBuffer {
scrollTop: number;
savedY: number;
savedX: number;
savedCurAttr: number;
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string {
return Buffer.prototype.translateBufferLineToString.apply(this, arguments);
}
@@ -319,6 +325,9 @@ export class MockBuffer implements IBuffer {
setLines(lines: ICircularList<IBufferLine>): void {
this.lines = lines;
}
getBlankLine(attr: number, isWrapped?: boolean): IBufferLine {
return Buffer.prototype.getBlankLine.apply(this, arguments);
}
stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[] {
return Buffer.prototype.stringIndexToBufferIndex.apply(this, arguments);
}

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