Compare commits

...
389 Commits
Author SHA1 Message Date
Daniel Imms d306a96918 v3.9.1 2018-12-12 09:35:17 -08:00
Daniel Imms 24eb0130f5 Remove triple slash directive error in addon types
Fixes #1830
2018-12-12 09:34:20 -08:00
Daniel Imms 1c62980d20 v3.9.0 2018-12-10 07:51:28 -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
Daniel Imms 5f989929c3 Merge pull request #1799 from Tyriar/remove_shared_folder
Remove shared/ folder
2018-11-29 14:03:20 -08: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
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 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
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 ae61292b10 Merge pull request #1729 from xtermjs/Tyriar-patch-1
Bump version to 3.8.0
2018-10-05 09:10:36 -07:00
Daniel Imms 8fd4bdd678 Bump version to 3.8.0 2018-10-05 09:04:20 -07:00
Daniel Imms 92b5c902de Commit yarn integrity checks 2018-10-05 08:59:54 -07:00
Daniel Imms a7b051c52f Merge pull request #1717 from Tyriar/charatlas_npe
Prevent NPE when disposing terminal shortly after creation
2018-10-04 12:02:09 -07:00
Daniel Imms b92bbf578b Merge branch 'master' into charatlas_npe 2018-10-04 10:30:04 -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
Daniel Imms d4ec50e7de Merge pull request #1718 from zwhitchcox/patch-2
Don't delete fake terminal
2018-10-01 07:31:45 -07:00
Zane Hitchcox 5a1c71c1d5 Don't delete fake terminal 2018-10-01 10:16:59 -04:00
Daniel Imms bbf36fe091 Prevent NPE when disposing terminal shortly after creation 2018-10-01 06:38:24 -07:00
Daniel Imms 990bf5c973 Merge pull request #1711 from pfitzseb/patch-1
Add new use case: Juno
2018-09-27 09:23:02 -07:00
Sebastian Pfitzner b28bba56df Add new use case: Juno 2018-09-27 15:46:33 +02:00
jerch 21f3534668 Merge pull request #1693 from jerch/fix_1691
fix overscan issue at viewborders and enable linkifier on partial shown matches
2018-09-26 22:14:57 +02:00
Jörg Breitbart fefc6c6a34 fix typo 2018-09-26 22:07:02 +02:00
Jörg Breitbart 00bf16f610 Merge branch 'master' into typedarray_BufferLine 2018-09-25 01:02:14 +02:00
Jörg Breitbart 57a6fe5b87 Merge branch 'master' into fix_1691 2018-09-25 00:47:48 +02:00
Daniel Imms 9e446a9a0e Merge pull request #1692 from Tyriar/1677_perf_improve
Dynamic atlas: Reduce unnecessary objects generated and draw from ImageBitmap
2018-09-23 10:41:40 -07:00
Daniel Imms 16c0ad858a Make a new empty array instead of shift 2018-09-22 13:23:22 -07:00
Daniel Imms 750ec3870c Resolve feedback 2018-09-22 13:20:29 -07:00
Daniel Imms e9d54bf011 Merge branch 'master' into 1677_perf_improve 2018-09-22 13:04:05 -07:00
Jörg Breitbart 408403e2a0 Merge branch 'master' into fix_1691 2018-09-22 15:21:32 +02:00
Jörg Breitbart a30bb22c7a fix comments to <80 2018-09-22 15:20:31 +02:00
Daniel Imms a531b4dd9b Merge pull request #1695 from Tyriar/coveralls_fix
Fix coveralls integration
2018-09-21 12:40:03 -07:00
Daniel Imms 4cd591cb21 Merge branch 'master' into common_layering_strict 2018-09-21 11:50:22 -07:00
Jörg Breitbart 10687b138d range checks for buffer iterator 2018-09-21 20:23:41 +02:00
Jörg Breitbart 1108d0746e Merge branch 'master' into fix_1691 2018-09-21 19:23:49 +02:00
Daniel Imms 005785b84d Merge pull request #1705 from agurodriguez/xterm-1704
Support link underlines in the DOM renderer
2018-09-21 10:02:30 -07:00
Daniel Imms 2e8e64eb4d Merge pull request #1703 from Tyriar/1702_isWrapped_fix
Speculative fix for NPE in linkifier
2018-09-21 09:58:38 -07:00
Daniel Imms f44d57b2e8 Merge branch 'master' into coveralls_fix 2018-09-21 07:33:10 -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 4941d5a19e Simplify loop, pass lint 2018-09-21 07:08:17 -07:00
Agustín Rodríguez 1f2d223e64 remove line in xterm.d.ts that says link underlines are not supported 2018-09-21 01:42:20 -03:00
Agustín Rodríguez d1991396e7 generalized loops to handle both single and multi line links 2018-09-21 01:38:37 -03:00
Agustín Rodríguez 478d824fa7 fix underline don't appearing when using the fallback DOM renderer 2018-09-21 01:38:37 -03:00
Daniel Imms 39a7b18065 Keep track of glyphs using cache values, not keys 2018-09-20 10:09:39 -07:00
Daniel Imms 2c62c3a8c3 Fix tests 2018-09-19 20:06:09 -07:00
Daniel Imms 4463f8d810 Speculative fix for NPE
Fixes #1702
2018-09-19 19:47:16 -07:00
Daniel Imms c05b6bd885 Merge branch 'master' into 1677_perf_improve 2018-09-19 08:03:09 -07:00
Daniel Imms 290c95ad71 Merge pull request #1698 from Tyriar/1697_scrolltop
Fix scroll APIs not affecting scroll bar
2018-09-19 07:58:53 -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 e736f1223f Merge pull request #1696 from Tyriar/posttest
Remove npm-run-all
2018-09-18 09:29:45 -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
Daniel Imms cbe7b21090 Don't unlink node when updating inBitmap and verify undefined 2018-09-17 07:52:37 -07:00
Daniel Imms d9ec47ab7a Clear bitmap timeout on dispose 2018-09-17 07:48:05 -07:00
Daniel Imms 94b20d40bf Reuse object for glyph identifier 2018-09-17 07:43:31 -07:00
Daniel Imms 202734f10c Fix typo which broke dynamic atlas 2018-09-17 07:43:10 -07:00
Daniel Imms 4656bd433c Revert "Remove IGlyphIdentifier in-between object"
This reverts commit 04f2d52f26.
2018-09-17 07:37:04 -07:00
Daniel Imms f6d5abf7b3 Fix scroll APIs not affecting scroll bar
Fixes #1697
2018-09-17 06:52:22 -07:00
Daniel Imms 6ea56cd85e Remove npm-run-all
We can just use posttest
2018-09-16 11:42:09 -07:00
Daniel Imms 4d7ce4da46 Set coveralls branch name 2018-09-15 11:02:53 -07:00
Daniel Imms 39047767e4 Move coverage to mac agent as it's much faster 2018-09-15 10:40:54 -07:00
Daniel Imms 8d4b5cdc55 Run yarn coveralls after generating report 2018-09-15 10:38:06 -07:00
Daniel Imms 00af63b271 Expand glyph when needed 2018-09-15 10:10:50 -07:00
Daniel Imms 608d78b6ac Add fallback for window.createImageBitmap 2018-09-15 09:46:19 -07:00
Daniel Imms ac3595e8b4 Generate and use a bitmap instead of canvas 2018-09-15 09:44:01 -07:00
Jörg Breitbart 5284a5a742 fix spelling 2018-09-15 00:41:55 +02:00
Jörg Breitbart 842ee6c164 change overscan limit to 2k chars 2018-09-15 00:22:00 +02:00
Jörg Breitbart d0dc387921 fix overscan issue and enable linkify of partial shown matches 2018-09-14 23:16:24 +02:00
Daniel Imms 7762ab96a5 Exit draw early for the space character 2018-09-14 12:06:14 -07:00
Jörg Breitbart 84e9ad2adc Merge branch 'master' into typedarray_BufferLine 2018-09-14 19:19:41 +02:00
Daniel Imms 6528ba84ff Avoid creation of array converting index to coordinates 2018-09-14 10:12:43 -07:00
Daniel Imms 0bcf30a21b Merge remote-tracking branch 'origin/master' into 1677_perf_improve 2018-09-14 10:10:03 -07:00
Daniel Imms 86c78e726d Merge pull request #1675 from Tyriar/1674_watch_addons
Split addon watch into watch-addons
2018-09-14 10:09:34 -07:00
Daniel Imms cf9cb80cb4 Use a number as the dynamic cache key
This prevents string creation on every draw
2018-09-14 10:08:26 -07:00
Daniel Imms 04f2d52f26 Remove IGlyphIdentifier in-between object
This was being constructed for every character draw
2018-09-14 09:58:49 -07:00
Daniel Imms 192f751862 Merge branch 'master' into 1674_watch_addons 2018-09-14 09:35:20 -07:00
Daniel Imms de143b552d Merge pull request #1676 from Tyriar/1671_dom_renderer_stable
Make the DOM renderer stable
2018-09-14 09:35:02 -07:00
jerch d37a8ce539 Merge pull request #1678 from jerch/fix_linkifier
Improve unicode string handling in linkifier
2018-09-14 18:28:46 +02:00
Daniel Imms 2f66ec452c 💄 2018-09-14 08:35:14 -07:00
Jörg Breitbart 4b68976dfb hide regex error behind debug flag 2018-09-14 15:43:29 +02:00
Jörg Breitbart 3143ad7f02 Merge branch 'master' into fix_linkifier 2018-09-14 14:52:35 +02:00
Jörg Breitbart 631e331077 fix test cases 2018-09-14 14:48:41 +02:00
Jörg Breitbart 70be55df75 test cases for getStringCellWidth 2018-09-14 14:46:57 +02:00
Jörg Breitbart 14c20c5f32 simplify stringIndexToBufferIndex, fix docs 2018-09-14 14:19:25 +02:00
Jörg Breitbart 57bbbda7e8 rename stringWidth; add some comments 2018-09-14 14:15:27 +02:00
Jörg Breitbart 8664f54a76 rename contents to iterator 2018-09-14 13:22:02 +02:00
Jörg Breitbart 9514bfd31c cleanup BufferStringIterator 2018-09-14 13:15:35 +02:00
Jörg Breitbart 9ce2894353 remove toArray; interface type for iterator result 2018-09-14 12:59:56 +02:00
Daniel Imms 5c873c1ce2 Merge pull request #1664 from alexr00/alexr00/searchWord
Added search support for 'match whole word'
2018-09-13 20:38:22 -07:00
Daniel Imms 4d4d9914a8 Merge branch 'master' into alexr00/searchWord 2018-09-13 20:32:05 -07:00
Daniel Imms a19673d835 Merge branch 'master' into 1671_dom_renderer_stable 2018-09-13 20:31:55 -07:00
Daniel Imms f7dbadc79b Merge branch 'master' into 1674_watch_addons 2018-09-13 20:31:53 -07:00
Daniel Imms 50d600877c Use common TestTerminal for writeSync across Buffer/Linkifier test 2018-09-13 19:54:53 -07:00
Daniel Imms cd2ac929c3 💄 2018-09-13 19:44:02 -07:00
Daniel Imms 7401567856 Remove unsafe cast in Buffer.test.ts 2018-09-13 19:34:44 -07: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
Jörg Breitbart a8d9c92ae1 make linter happy 2018-09-13 19:03:57 +02:00
Jörg Breitbart bd68853575 Merge branch 'master' into fix_linkifier 2018-09-13 19:01:31 +02:00
Daniel Imms 69a6c04620 Merge pull request #1687 from xtermjs/Tyriar-patch-1
Use new domain for azure pipelines
2018-09-13 08:50:49 -07:00
Daniel Imms 68766d540f Use new domain for azure pipelines 2018-09-13 08:19:59 -07:00
Daniel Imms a09e6a6238 Merge pull request #1681 from xtermjs/azure-pipelines
Set up CI with Azure Pipelines
2018-09-13 08:07:40 -07:00
Jörg Breitbart 30f74bf5f3 cover & comment right border trim behavior to be fixed 2018-09-13 16:20:37 +02:00
Jörg Breitbart 2708a76501 fix next() invocation; handle wild matches 2018-09-13 15:44:26 +02:00
Jörg Breitbart c8c22e3ddf docs 2018-09-13 14:30:11 +02:00
Alex Ross 7fc1fd947f Added search support for 'match whole word' 2018-09-12 09:30:34 -07:00
Jörg Breitbart 7e69c5d0db apply linkifier only to rows in viewport 2018-09-12 15:37:05 +02:00
Jörg Breitbart 0068c477b9 Merge branch 'master' into fix_linkifier 2018-09-12 15:05:12 +02:00
Daniel Imms 74474da7f3 Merge pull request #1663 from alexr00/alexr00/caseSearch
Added optional case sensitive searching
2018-09-11 22:35:12 -07:00
Daniel Imms e6e079bfa1 Merge branch 'master' into alexr00/caseSearch 2018-09-11 22:29:01 -07:00
Daniel Imms 7dc8118367 Merge pull request #1682 from alexr00/alexr00/spaceTrimming
Fixed translateBufferLineToStringWithWrap to correctly not remove spaces in the middle of a multiline wrapped string
2018-09-11 22:25:52 -07:00
Alex Ross 5aa6656971 Added test and fixed uncovered buy 2018-09-11 16:29:11 -07:00
Alex Ross 8979eea810 Fixed translateBufferLineToStringWithWrap to correctly not remove spaces in the middle of a multiline wrapped string 2018-09-11 16:12:53 -07:00
Alex Ross 7d2bdacf77 Added test case for when regex and case sensitive are both selected 2018-09-11 15:48:24 -07:00
Daniel Imms 1edc732c55 Use new Azure Pipelines badge 2018-09-11 11:34:47 -07:00
Daniel Imms e03c93fb81 Install yarn using installer task 2018-09-11 00:19:42 -07:00
Daniel Imms d23c65d1db Symlink yarn to /usr/bin 2018-09-11 00:09:36 -07:00
Daniel Imms 07b5bd0b64 Echo path 2018-09-11 00:04:27 -07:00
Daniel Imms 35483b3bf4 Source bashrc after yarn install 2018-09-11 00:00:33 -07:00
Daniel Imms 6ebf17bf04 Export yarn path 2018-09-10 23:53:45 -07:00
Daniel Imms 5d37a17383 Install latest yarn with install script
Avoids dpkg lock flakes
2018-09-10 23:49:52 -07:00
Daniel Imms dade9ba050 Simplify steps 2018-09-10 23:17:03 -07:00
Daniel Imms 5909885d43 Download and install yarn directly from deb 2018-09-10 23:10:42 -07:00
Daniel Imms 0446cf3aee Install yarn manually on Linux 2018-09-10 23:04:33 -07:00
Daniel Imms 3a97dbecde Add more platforms, build out script 2018-09-10 23:01:18 -07:00
azure-pipelines[bot] c88bc62ff9 Set up CI with Azure Pipelines 2018-09-11 05:53:14 +00:00
Jörg Breitbart a2bfbeaa74 Merge branch 'master' into fix_linkifier 2018-09-10 23:54:44 +02:00
Alex Ross ade3e0943c Fixed a merge mistake 2018-09-10 13:54:29 -07:00
Alex Ross 16de86f5ea Fixed an unintentionally capitalized i 2018-09-10 13:50:35 -07:00
Alex Ross fed579238a Added optional case sensitive searching 2018-09-10 13:45:29 -07:00
Daniel Imms 11426bb0d0 Merge pull request #1662 from alexr00/alexr00/searchWrapped
Added support for searching accross wrapped lines
2018-09-10 12:26:52 -07:00
Daniel Imms 5e004b21a3 Disallow the empty string from being selected by find 2018-09-10 12:17:17 -07:00
Daniel Imms b2ba281916 Fix references in demo to addons 2018-09-10 12:09:26 -07:00
Alex Ross bcf223e235 Fixed merge errors 2018-09-10 10:43:03 -07:00
Alex Ross d5c17a0cc9 Added one more test case 2018-09-10 10:25:23 -07:00
Alex Ross 54ed988fd1 Fixed issues around searching lines twice and considering a wrapped line both it's own line and part of the previous line. 2018-09-10 10:25:23 -07:00
Alex Ross 1460a34796 Added another test case and fixed the multi-line issue that uncovered 2018-09-10 10:22:12 -07:00
Alex Ross bc156294aa Udated comment 2018-09-10 10:20:32 -07:00
Alex Ross 09028d4d86 Added support for searching accross wrapped lines 2018-09-10 10:20:31 -07:00
Jörg Breitbart c35306aaa6 handle unicode within a match; test cases 2018-09-10 17:47:29 +02:00
Jörg Breitbart 3289eed2a2 merge master 2018-09-10 16:17:41 +02:00
Jörg Breitbart a76d4a6055 fix multiple row linkifier attempts; tests for unicode before a match 2018-09-10 16:10:41 +02:00
Daniel Imms 12e38404a2 Merge pull request #1672 from Tyriar/demo_lint
Fix lint in demo
2018-09-09 16:38:14 -07:00
Jörg Breitbart 08af72be1a linter errors 2018-09-10 01:13:21 +02:00
Jörg Breitbart daa6fa0a66 change Linkifier._doLinkifyRow 2018-09-10 00:40:29 +02:00
Jörg Breitbart 4aa8e402a4 fix MockBuffer 2018-09-09 22:21:00 +02:00
Jörg Breitbart a494e80a9f BufferStringIterator 2018-09-09 22:17:49 +02:00
Jörg Breitbart a8fe4107a3 Buffer.stringIndexToBufferIndex method 2018-09-09 21:18:33 +02:00
Daniel Imms 6c2acd6e34 Fix reference to new setting 2018-09-09 08:00:05 -07:00
Daniel Imms f2049694c8 Make the DOM renderer stable
Also updated the list of features which are not supported now. Note
that this may be moved to an addon eventually, this can be part of a
major release rather than included in the comment.

Fixes #1671
2018-09-09 07:38:38 -07:00
Daniel Imms e59fef87cc Split addon watch into watch-addons
Fixes #1674
2018-09-09 07:33:13 -07:00
Daniel Imms d943a5c890 Run tslint on the demo
Fixes #1673
2018-09-09 07:28:22 -07:00
Daniel Imms 715c078b6d Fix lint in demo 2018-09-09 07:22:18 -07:00
Daniel Imms 7447a8d08d Rename setting to experimentalBufferLineImpl, expose in d.ts 2018-09-09 07:20:13 -07:00
Daniel Imms f9717bc635 Merge pull request #1670 from Tyriar/1669_linkifier_exception
Fallback on default color when linkifier can't find the character
2018-09-09 07:00:45 -07:00
Daniel Imms 7ef18cca1e Merge pull request #1646 from peacefullatom/add-tests-for-events
#900 Add tests for events on Terminal
2018-09-08 14:15:56 -07:00
Daniel Imms c291ab1058 Remove failing refresh test 2018-09-08 14:04:04 -07:00
Daniel Imms d4d1426bff Merge branch 'master' into typedarray_BufferLine 2018-09-08 13:47:06 -07:00
Daniel Imms 25e0e97ef2 Merge branch 'master' into add-tests-for-events 2018-09-08 13:44:57 -07:00
jerch 0f28ccb093 Merge pull request #1668 from jerch/fix_demo
add explicit type to demo/client.ts
2018-09-08 22:10:51 +02:00
Daniel Imms f4bb4a22d4 Fallback on default color when linkifier can't find the character
This probably doesn't fix the underlying problem, but it will make it
obvious by using the white background color and more easily help identify
the problem case.

Fixes #1669
2018-09-08 13:01:59 -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 4eede85ef6 fixes #1667 2018-09-08 17:26:19 +02:00
Jörg Breitbart bc943b7b57 merge with 3.7 2018-09-08 16:54:10 +02:00
Daniel Imms bdb6647ae0 Merge pull request #1652 from Tyriar/1337_project_refs_prep
Preparation for project references
2018-09-08 02:59:26 -07:00
Daniel Imms 0d89fcdeeb Merge pull request #1648 from Tyriar/webpack_demo
Improve demo build task
2018-09-08 02:58:58 -07:00
Daniel Imms 4ffc076f84 Merge pull request #1661 from Hindol/fix/1640
Add support for block and underline style cursor in DOM rendering mode
2018-09-08 02:53:51 -07:00
Daniel Imms 2ed3997aaa Merge pull request #1651 from xtermjs/Tyriar-patch-1
Move tslint prefer-const with the other core rules
2018-09-08 02:52:05 -07:00
Daniel Imms a0f9768fe8 Merge remote-tracking branch 'ups/master' into webpack_demo 2018-09-08 02:51:07 -07:00
Daniel Imms 8b334b01a6 Merge branch 'master' into 1337_project_refs_prep 2018-09-08 02:50:45 -07:00
Daniel Imms c6029765a4 Merge branch 'master' into Tyriar-patch-1 2018-09-08 02:50:39 -07:00
Daniel Imms 520dacac27 Merge pull request #1666 from alexr00/alexr00/searchRegex
Added support for searching by regular expression
2018-09-08 02:50:18 -07:00
Daniel Imms dcc1b7a3e3 Merge branch 'master' into webpack_demo 2018-09-08 02:49:36 -07:00
Daniel Imms 599271bde4 Make search option props optional, test optional typings 2018-09-08 02:10:02 -07:00
Hindol Adhya 2716413e66 Updates as per code review comments
Getting rid of the ICursorOptions interface to improve performance
2018-09-08 13:20:31 +05:30
Alex Ross e5465841f8 Responded to feedback and fixed the returned found term 2018-09-07 15:29:51 -07:00
Alex Ross 03e54cce7d Added regex search test 2018-09-07 11:01:08 -07:00
Alex Ross 88205511e6 Added support for searching by regular expression 2018-09-07 09:47:15 -07:00
Hindol Adhya 374a8b70eb Add support for block and underline style cursor in DOM rendering mode 2018-09-06 21:27:35 +05:30
Jörg Breitbart c8b8e39dde move blankLine to Buffer 2018-09-04 22:19:10 +02:00
peacefullatom 48cf73c152 rewrote to async 2018-09-05 01:11:23 +05:00
peacefullatom 1dc88d2e29 Merge remote-tracking branch 'upstream/master' into add-tests-for-events 2018-09-05 01:08:57 +05:00
Jörg Breitbart 7873598a41 rename BufferLineJsArray as BufferLine 2018-09-04 21:41:00 +02:00
Daniel Imms 7c1b1e2eb8 Fix strict null checks in ./common
Part of #1319
2018-09-04 09:56:29 -07:00
Jörg Breitbart 2877c0da80 switch for BufferLineJsArray vs BufferLineTypedArray 2018-09-04 18:20:01 +02:00
Daniel Imms fb72f25067 Merge branch 'master' into 1337_project_refs_prep 2018-09-04 08:04:39 -07:00
Daniel Imms 3618c6495b typescript@3.0 2018-09-04 08:02:47 -07:00
Daniel Imms 443b2dbc28 Have common only reference within common
Part of #1337
2018-09-04 08:01:04 -07:00
Daniel Imms 53e995a2d8 Add dom lib to typings-test 2018-09-04 07:38:52 -07:00
Daniel Imms 53193d0a47 Use es6 in typings test 2018-09-04 07:34:13 -07:00
Jörg Breitbart 165c8d4522 convenient methods fill, makeCopyOf and clone for BufferLine 2018-09-04 16:30:17 +02:00
Daniel Imms 92341c3e7b Merge branch 'master' into webpack_demo 2018-09-04 06:56:06 -07:00
Jörg Breitbart 02672dc22e Merge branch 'master' into typedarray_BufferLine 2018-09-04 15:05:13 +02:00
Daniel Imms 57b8e064a8 Move tslint prefer-const with the other core rules
The ones at the bottom are separated as they're part of tslint-consistent-codestyle
2018-09-02 13:43:37 -07:00
peacefullatom cbbf6bd654 Merge branch 'master' into add-tests-for-events 2018-09-02 22:50:45 +05:00
Daniel Imms e2267fce92 Remove old webpack.config.js 2018-09-01 19:57:51 -07:00
Daniel Imms 784a142dac Remove old gulp tasks 2018-09-01 19:50:45 -07:00
Daniel Imms 71aa7027bd Use new script for yarn start 2018-09-01 19:48:06 -07:00
Daniel Imms ca76ebc396 Remove todo 2018-09-01 17:34:52 -07:00
Daniel Imms 54187b3597 Fix build by adding es6 lib
Targets are still es5, es6 is needed for @types/webpack to parse
2018-09-01 17:03:02 -07:00
Daniel Imms d58f5cac01 Type Terminal 2018-09-01 16:48:55 -07:00
Daniel Imms b3e8980270 Convert demo client to TS 2018-09-01 16:34:07 -07:00
Daniel Imms 4d14e00034 Add webpack dependencies, update to webpack 4 2018-09-01 16:22:56 -07:00
Daniel Imms e75578440e Improve demo webpack build 2018-09-01 16:22:27 -07:00
peacefullatom 70afb13c9b #900 Add tests for events on Terminal 2018-09-02 02:31:26 +05: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
104 changed files with 5106 additions and 1443 deletions
+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
+5 -2
View File
@@ -1,6 +1,6 @@
# [![xterm.js logo](logo-full.png)](https://xtermjs.org)
[![VSTS Build status](https://xtermjs.visualstudio.com/_apis/public/build/definitions/3e323cf7-5760-460d-af64-ee5675baf366/1/badge)](https://xtermjs.visualstudio.com/xterm.js/_build/index?definitionId=1)
[![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)
@@ -159,7 +159,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
@@ -170,6 +170,9 @@ computational environment for Jupyter, supporting interactive data science and s
- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js
- [**SSH Web Client**](https://github.com/roke22/PHP-SSH2-Web-Client): SSH Web Client with PHP.
- [**Shellvault**](https://www.shellvault.io): The cloud-based SSH terminal you can access from anywhere.
- [**Juno**](http://junolab.org/): A flexible Julia IDE, based on Atom.
- [**webssh**](https://github.com/huashengdun/webssh): Web based ssh client.
- [**info-beamer hosted**](https://info-beamer.com): Uses Xterm.js to manage digital signage devices from the web dashboard.
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
+68
View File
@@ -0,0 +1,68 @@
# Node.js
# Build a general Node.js application with npm.
# Add steps that analyze code, save build artifacts, deploy, and more:
# https://docs.microsoft.com/vsts/pipelines/languages/javascript
jobs:
- job: Linux
pool:
vmImage: 'ubuntu-16.04'
steps:
- task: NodeTool@0
inputs:
versionSpec: '8.x'
displayName: 'Install Node.js'
- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2
inputs:
versionSpec: "1.9.4"
displayName: 'Install Yarn'
- script: |
yarn
displayName: 'Install dependencies and build'
- script: |
yarn mocha
displayName: 'Test'
- script: |
yarn lint
displayName: 'Lint'
- job: macOS
pool:
vmImage: 'xcode9-macos10.13'
steps:
- task: NodeTool@0
inputs:
versionSpec: '8.x'
displayName: 'Install Node.js'
- script: |
yarn
displayName: 'Install dependencies and build'
- script: |
yarn mocha
displayName: 'Test'
- script: |
yarn lint
displayName: 'Lint'
- script: |
yarn test-coverage
export COVERALLS_GIT_BRANCH=$BUILD_SOURCEBRANCH
yarn coveralls
displayName: 'Generate and publish coverage'
- job: Windows
pool:
vmImage: 'vs2017-win2016'
steps:
- task: NodeTool@0
inputs:
versionSpec: '8.x'
displayName: 'Install Node.js'
- script: |
yarn
displayName: 'Install dependencies and build'
- script: |
yarn mocha
displayName: 'Test'
- script: |
yarn lint
displayName: 'Lint'
+93 -69
View File
@@ -1,11 +1,28 @@
import * as Terminal from '../build/xterm';
import * as attach from '../build/addons/attach/attach';
import * as fit from '../build/addons/fit/fit';
import * as fullscreen from '../build/addons/fullscreen/fullscreen';
import * as search from '../build/addons/search/search';
import * as webLinks from '../build/addons/webLinks/webLinks';
import * as winptyCompat from '../build/addons/winptyCompat/winptyCompat';
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*
* This file is the entry point for browserify.
*/
/// <reference path="../typings/xterm.d.ts"/>
import { Terminal } from '../lib/public/Terminal';
import * as attach from '../lib/addons/attach/attach';
import * as fit from '../lib/addons/fit/fit';
import * as fullscreen from '../lib/addons/fullscreen/fullscreen';
import * as search from '../lib/addons/search/search';
import * as webLinks from '../lib/addons/webLinks/webLinks';
import * as winptyCompat from '../lib/addons/winptyCompat/winptyCompat';
// Pulling in the module's types relies on the <reference> above, it's looks a
// little weird here as we're importing "this" module
import { Terminal as TerminalType } from 'xterm';
export interface IWindowWithTerminal extends Window {
term: TerminalType;
}
declare let window: IWindowWithTerminal;
Terminal.applyAddon(attach);
Terminal.applyAddon(fit);
@@ -15,20 +32,20 @@ Terminal.applyAddon(webLinks);
Terminal.applyAddon(winptyCompat);
var term,
protocol,
socketURL,
socket,
pid;
let term;
let protocol;
let socketURL;
let socket;
let pid;
var terminalContainer = document.getElementById('terminal-container'),
actionElements = {
findNext: document.querySelector('#find-next'),
findPrevious: document.querySelector('#find-previous')
},
paddingElement = document.getElementById('padding');
const terminalContainer = document.getElementById('terminal-container');
const actionElements = {
findNext: <HTMLInputElement>document.querySelector('#find-next'),
findPrevious: <HTMLInputElement>document.querySelector('#find-previous')
};
const paddingElement = <HTMLInputElement>document.getElementById('padding');
function setPadding() {
function setPadding(): void {
term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px';
term.fit();
}
@@ -52,20 +69,20 @@ const disposeRecreateButtonHandler = () => {
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
function createTerminal() {
function createTerminal(): void {
// Clean terminal
while (terminalContainer.children.length) {
terminalContainer.removeChild(terminalContainer.children[0]);
}
term = new Terminal({});
window.term = term; // Expose `term` to window for debugging purposes
term.on('resize', function (size) {
term.on('resize', (size: { cols: number, rows: number }) => {
if (!pid) {
return;
}
var cols = size.cols,
rows = size.rows,
url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows;
const cols = size.cols;
const rows = size.rows;
const url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows;
fetch(url, {method: 'POST'});
});
@@ -80,32 +97,42 @@ function createTerminal() {
addDomListener(paddingElement, 'change', setPadding);
addDomListener(actionElements.findNext, 'keypress', function (e) {
if (e.key === "Enter") {
addDomListener(actionElements.findNext, 'keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
term.findNext(actionElements.findNext.value);
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.findPrevious, 'keypress', function (e) {
if (e.key === "Enter") {
addDomListener(actionElements.findPrevious, 'keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
term.findPrevious(actionElements.findPrevious.value);
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);
}
});
// fit is called within a setTimeout, cols and rows need this.
setTimeout(function () {
setTimeout(() => {
initOptions(term);
document.getElementById(`opt-cols`).value = term.cols;
document.getElementById(`opt-rows`).value = term.rows;
paddingElement.value = 0;
// TODO: Clean this up, opt-cols/rows doesn't exist anymore
(<HTMLInputElement>document.getElementById(`opt-cols`)).value = term.cols;
(<HTMLInputElement>document.getElementById(`opt-rows`)).value = term.rows;
paddingElement.value = '0';
// Set terminal size again to set the specific dimensions on the demo
updateTerminalSize();
fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) {
res.text().then(function (processId) {
fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then((res) => {
res.text().then((processId) => {
pid = processId;
socketURL += processId;
socket = new WebSocket(socketURL);
@@ -117,22 +144,20 @@ function createTerminal() {
}, 0);
}
function runRealTerminal() {
function runRealTerminal(): void {
term.attach(socket);
term._initialized = true;
}
function runFakeTerminal() {
function runFakeTerminal(): void {
if (term._initialized) {
return;
}
term._initialized = true;
var shellprompt = '$ ';
term.prompt = function () {
term.write('\r\n' + shellprompt);
term.prompt = () => {
term.write('\r\n$ ');
};
term.writeln('Welcome to xterm.js');
@@ -141,14 +166,12 @@ function runFakeTerminal() {
term.writeln('');
term.prompt();
term._core.register(term.addDisposableListener('key', function (key, ev) {
var printable = (
!ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey
);
term._core.register(term.addDisposableListener('key', (key, ev) => {
const printable = !ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey;
if (ev.keyCode == 13) {
if (ev.keyCode === 13) {
term.prompt();
} else if (ev.keyCode == 8) {
} else if (ev.keyCode === 8) {
// Do not delete the prompt
if (term.x > 2) {
term.write('\b \b');
@@ -158,13 +181,13 @@ function runFakeTerminal() {
}
}));
term._core.register(term.addDisposableListener('paste', function (data, ev) {
term._core.register(term.addDisposableListener('paste', (data, ev) => {
term.write(data);
}));
}
function initOptions(term) {
var blacklistedOptions = [
function initOptions(term: TerminalType): void {
const blacklistedOptions = [
// Internal only options
'cancelEvents',
'convertEol',
@@ -176,7 +199,7 @@ function initOptions(term) {
// Complex option
'theme'
];
var stringOptions = {
const stringOptions = {
bellSound: null,
bellStyle: ['none', 'sound'],
cursorStyle: ['block', 'underline', 'bar'],
@@ -184,11 +207,12 @@ function initOptions(term) {
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']
};
var options = Object.keys(term._core.options);
var booleanOptions = [];
var numberOptions = [];
const options = Object.keys((<any>term)._core.options);
const booleanOptions = [];
const numberOptions = [];
options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => {
switch (typeof term.getOption(o)) {
case 'boolean':
@@ -204,7 +228,7 @@ function initOptions(term) {
}
});
var html = '';
let html = '';
html += '<div class="option-group">';
booleanOptions.forEach(o => {
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${term.getOption(o) ? 'checked' : ''}/> ${o}</label></div>`;
@@ -218,24 +242,24 @@ function initOptions(term) {
if (stringOptions[o]) {
html += `<div class="option"><label>${o} <select id="opt-${o}">${stringOptions[o].map(v => `<option ${term.getOption(o) === v ? 'selected' : ''}>${v}</option>`).join('')}</select></label></div>`;
} else {
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${term.getOption(o)}"/></label></div>`
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${term.getOption(o)}"/></label></div>`;
}
});
html += '</div>';
var container = document.getElementById('options-container');
const container = document.getElementById('options-container');
container.innerHTML = html;
// Attach listeners
booleanOptions.forEach(o => {
var input = document.getElementById(`opt-${o}`);
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
addDomListener(input, 'change', () => {
console.log('change', o, input.checked);
term.setOption(o, input.checked);
});
});
numberOptions.forEach(o => {
var input = document.getElementById(`opt-${o}`);
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
if (o === 'cols' || o === 'rows') {
@@ -246,7 +270,7 @@ function initOptions(term) {
});
});
Object.keys(stringOptions).forEach(o => {
var input = document.getElementById(`opt-${o}`);
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
term.setOption(o, input.value);
@@ -254,16 +278,16 @@ function initOptions(term) {
});
}
function addDomListener(element, type, handler) {
function addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void {
element.addEventListener(type, handler);
term._core.register({ dispose: () => element.removeEventListener(type, handler) });
}
function updateTerminalSize() {
var cols = parseInt(document.getElementById(`opt-cols`).value, 10);
var rows = parseInt(document.getElementById(`opt-rows`).value, 10);
var width = (cols * term._core.renderer.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px';
var height = (rows * term._core.renderer.dimensions.actualCellHeight).toString() + 'px';
function updateTerminalSize(): void {
const cols = parseInt((<HTMLInputElement>document.getElementById(`opt-cols`)).value, 10);
const rows = parseInt((<HTMLInputElement>document.getElementById(`opt-rows`)).value, 10);
const width = (cols * term._core.renderer.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px';
const height = (rows * term._core.renderer.dimensions.actualCellHeight).toString() + 'px';
terminalContainer.style.width = width;
terminalContainer.style.height = height;
term.fit();
+4 -1
View File
@@ -16,6 +16,9 @@
<p>
<label>Find next <input id="find-next"/></label>
<label>Find previous <input id="find-previous"/></label>
<label>Use regex<input type="checkbox" id="regex"/></label>
<label>Case sensitive<input type="checkbox" id="case-sensitive"/></label>
<label>Whole word<input type="checkbox" id="whole-word"/></label>
</p>
</div>
<div>
@@ -33,6 +36,6 @@
<hr/>
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for the development and evaluation of xterm.js only. Exposing the demo to the public as is would introduce security risks for the host.</p>
<button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button>
<script src="dist/bundle.js" defer ></script>
<script src="dist/client-bundle.js" defer ></script>
</body>
</html>
+19 -7
View File
@@ -17,12 +17,8 @@ app.get('/style.css', function(req, res){
res.sendFile(__dirname + '/style.css');
});
app.get('/dist/bundle.js', function(req, res){
res.sendFile(__dirname + '/dist/bundle.js');
});
app.get('/dist/bundle.js.map', function(req, res){
res.sendFile(__dirname + '/dist/bundle.js.map');
app.get('/dist/client-bundle.js', function(req, res){
res.sendFile(__dirname + '/dist/client-bundle.js');
});
app.post('/terminals', function (req, res) {
@@ -62,9 +58,25 @@ app.ws('/terminals/:pid', function (ws, req) {
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);
}
};
}
const send = buffer(ws, 5);
term.on('data', function(data) {
try {
ws.send(data);
send(data);
} catch (ex) {
// The WebSocket is not open, ignore
}
+49
View File
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*
* This file is the entry point for browserify.
*/
const cp = require('child_process');
const path = require('path');
const webpack = require('webpack');
// Launch server
cp.spawn('node', [path.resolve(__dirname, 'server.js')], { stdio: 'inherit' });
// Build/watch client source
const clientConfig = {
entry: path.resolve(__dirname, 'client.ts'),
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ]
},
output: {
filename: 'client-bundle.js',
path: path.resolve(__dirname, 'dist')
},
mode: 'development',
watch: true
};
const compiler = webpack(clientConfig);
compiler.watch({
// Example watchOptions
aggregateTimeout: 300,
poll: undefined
}, (err, stats) => {
// Print watch/build result here...
console.log(stats.toString({
colors: true
}));
});
+12
View File
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"rootDir": ".",
"sourceMap": true
},
"include": [
"client.ts",
"../typings/xterm.d.ts"
]
}
+4
View File
@@ -5,6 +5,10 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es6"
],
"noEmit": true
}
}
-12
View File
@@ -14,7 +14,6 @@ const source = require('vinyl-source-stream');
const sourcemaps = require('gulp-sourcemaps');
const ts = require('gulp-typescript');
const util = require('gulp-util');
const webpack = require('webpack-stream');
const buildDir = process.env.BUILD_DIR || 'build';
const tsProject = ts.createProject('tsconfig.json');
@@ -137,17 +136,6 @@ gulp.task('sorcery-addons', ['browserify-addons'], function () {
})
});
gulp.task('webpack', ['build'], function() {
return gulp.src('demo/main.js')
.pipe(webpack(require('./webpack.config.js')))
.pipe(gulp.dest('demo/dist/'));
});
gulp.task('watch-demo', ['webpack'], () => {
gulp.watch(['./demo/*', './lib/**/*'], ['webpack']);
});
gulp.task('build', ['sorcery', 'sorcery-addons']);
gulp.task('test', ['mocha']);
gulp.task('default', ['build']);
+14 -10
View File
@@ -1,7 +1,7 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
"version": "3.7.0",
"version": "3.9.1",
"main": "lib/public/Terminal.js",
"types": "typings/xterm.d.ts",
"repository": "https://github.com/xtermjs/xterm.js",
@@ -12,6 +12,7 @@
"@types/jsdom": "11.0.1",
"@types/mocha": "^2.2.33",
"@types/node": "6.0.108",
"@types/webpack": "^4.4.11",
"browserify": "^13.3.0",
"chai": "3.5.0",
"concurrently": "^3.5.1",
@@ -32,24 +33,26 @@
"merge-stream": "^1.0.1",
"node-pty": "0.7.6",
"nodemon": "1.10.2",
"npm-run-all": "^4.1.2",
"nyc": "^11.8.0",
"sorcery": "^0.10.0",
"source-map-loader": "^0.2.3",
"ts-loader": "^4.5.0",
"tslint": "^5.9.1",
"tslint-consistent-codestyle": "^1.13.0",
"typescript": "2.8.3",
"typescript": "3.1",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"webpack": "^3.10.0",
"webpack-stream": "^4.0.0",
"webpack": "^4.17.1",
"webpack-cli": "^3.1.0",
"zmodem.js": "^0.1.5"
},
"scripts": {
"start": "concurrently --kill-others-on-fail --names \"demo,server\" \"gulp watch-demo\" \"node demo/app\"",
"start": "node demo/start",
"start-zmodem": "node demo/zmodem/app",
"lint": "tslint 'src/**/*.ts'",
"test": "npm-run-all mocha lint",
"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",
@@ -60,7 +63,8 @@
"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,attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem,css\" \"tsc -w\" \"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\" \"gulp watch-css\""
"watch": "concurrently --kill-others-on-fail --names \"lib,css\" \"tsc -w\" \"gulp watch-css\"",
"watch-addons": "concurrently --kill-others-on-fail --names \"attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem\" \"tsc -w -p ./src/addons/attach\" \"tsc -w -p ./src/addons/fit\" \"tsc -w -p ./src/addons/fullscreen\" \"tsc -w -p ./src/addons/search\" \"tsc -w -p ./src/addons/terminado\" \"tsc -w -p ./src/addons/webLinks\" \"tsc -w -p ./src/addons/winptyCompat\" \"tsc -w -p ./src/addons/zmodem\"",
"layering": "concurrently --kill-others-on-fail --names \"common,core\" \"tsc -p ./src/common\" \"tsc -p ./src/core\""
}
}
+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';
+233 -31
View File
@@ -3,11 +3,11 @@
* @license MIT
*/
import { assert } from 'chai';
import { assert, expect } from 'chai';
import { ITerminal } from './Types';
import { Buffer, DEFAULT_ATTR } from './Buffer';
import { Buffer, DEFAULT_ATTR, CHAR_DATA_CHAR_INDEX } from './Buffer';
import { CircularList } from './common/CircularList';
import { MockTerminal } from './utils/TestUtils.test';
import { MockTerminal, TestTerminal } from './utils/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,14 +341,212 @@ 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);
assert.equal(str3, '😁a');
});
});
describe('stringIndexToBufferIndex', () => {
let terminal: TestTerminal;
beforeEach(() => {
terminal = new TestTerminal({rows: 5, cols: 10});
});
it('multiline ascii', () => {
const input = 'This is ASCII text spanning multiple lines.';
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.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
}
});
it('combining e\u0301 in a sentence', () => {
const input = 'Sitting in the cafe\u0301 drinking coffee.';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < 19; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
}
// string index 18 & 19 point to combining char e\u0301 ---> same buffer Index
assert.deepEqual(
terminal.buffer.stringIndexToBufferIndex(0, 18),
terminal.buffer.stringIndexToBufferIndex(0, 19));
// after the combining char every string index has an offset of -1
for (let i = 19; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex);
}
});
it('multiline combining e\u0301', () => {
const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
// every buffer cell index contains 2 string indices
for (let i = 0; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex);
}
});
it('surrogate char in a sentence', () => {
const input = 'The 𝄞 is a clef widely used in modern notation.';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < 5; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
}
// string index 4 & 5 point to surrogate char 𝄞 ---> same buffer Index
assert.deepEqual(
terminal.buffer.stringIndexToBufferIndex(0, 4),
terminal.buffer.stringIndexToBufferIndex(0, 5));
// after the combining char every string index has an offset of -1
for (let i = 5; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex);
}
});
it('multiline surrogate char', () => {
const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
// every buffer cell index contains 2 string indices
for (let i = 0; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex);
}
});
it('surrogate char with combining', () => {
// eye of Ra with acute accent - string length of 3
const input = '𓂀\u0301 - the eye hiroglyph with an acute accent.';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
// index 0..2 should map to 0
assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 1));
assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 2));
for (let i = 2; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([((i - 2) / terminal.cols) | 0, (i - 2) % terminal.cols], bufferIndex);
}
});
it('multiline surrogate with combining', () => {
const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
// every buffer cell index contains 3 string indices
for (let i = 0; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([(((i / 3) | 0) / terminal.cols) | 0, ((i / 3) | 0) % terminal.cols], bufferIndex);
}
});
it('fullwidth chars', () => {
const input = 'These 123 are some fat numbers.';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < 6; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
}
// string index 6, 7, 8 take 2 cells
assert.deepEqual([0, 8], terminal.buffer.stringIndexToBufferIndex(0, 7));
assert.deepEqual([1, 0], terminal.buffer.stringIndexToBufferIndex(0, 8));
// rest of the string has offset of +3
for (let i = 9; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([((i + 3) / terminal.cols) | 0, (i + 3) % terminal.cols], bufferIndex);
}
});
it('multiline fullwidth chars', () => {
const input = '12345678901234567890';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 9; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([((i << 1) / terminal.cols) | 0, (i << 1) % terminal.cols], bufferIndex);
}
});
it('fullwidth combining with emoji - match emoji cell', () => {
const input = 'Lots of ¥\u0301 make me 😃.';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
const stringIndex = s.match(/😃/).index;
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex);
assert(terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX], '😃');
});
it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => {
const input = 'a12345678901234567890';
// the 'a' at the beginning moves all fullwidth chars one to the right
// now the end of the line contains a dangling empty cell since
// the next fullwidth char has to wrap early
// the dangling last cell is wrongly added in the string
// --> fixable after resolving #1685
terminal.writeSync(input);
// TODO: reenable after fix
// const s = terminal.buffer.contents(true).toArray()[0];
// 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 j = (i - 0) << 1;
assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex);
}
});
});
describe('BufferStringIterator', function(): void {
it('iterator does not ovrflow buffer limits', function(): void {
const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5});
const data = [
'aaaaaaaaaa',
'aaaaaaaaa\n',
'aaaaaaaaaa',
'aaaaaaaaa\n',
'aaaaaaaaaa',
'aaaaaaaaaa',
'aaaaaaaaaa',
'aaaaaaaaa\n',
'aaaaaaaaaa',
'aaaaaaaaaa'
];
terminal.writeSync(data.join(''));
// brute force test with insane values
expect(() => {
for (let overscan = 0; overscan < 20; ++overscan) {
for (let start = -10; start < 20; ++start) {
for (let end = -10; end < 20; ++end) {
const it = terminal.buffer.iterator(false, start, end, overscan, overscan);
while (it.hasNext()) {
it.next();
}
}
}
}
}).to.not.throw();
});
});
});
+139 -10
View File
@@ -4,12 +4,13 @@
*/
import { CircularList } from './common/CircularList';
import { CharData, ITerminal, IBuffer, IBufferLine } from './Types';
import { EventEmitter } from './EventEmitter';
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;
@@ -38,7 +39,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 +56,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 +115,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 +131,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 +162,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 +183,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));
}
}
}
@@ -194,6 +231,36 @@ export class Buffer implements IBuffer {
this.scrollBottom = newRows - 1;
}
/**
* Translates a string index back to a BufferIndex.
* To get the correct buffer position the string must start at `startCol` 0
* (default in translateBufferLineToString).
* The method also works on wrapped line strings given rows were not trimmed.
* The method operates on the CharData string length, there are no
* additional content or boundary checks. Therefore the string and the buffer
* should not be altered in between.
* TODO: respect trim flag after fixing #1685
* @param lineIndex line index the string was retrieved from
* @param stringIndex index within the string
* @param startCol column offset the string was retrieved from
*/
public stringIndexToBufferIndex(lineIndex: number, stringIndex: number): BufferIndex {
while (stringIndex) {
const line = this.lines.get(lineIndex);
if (!line) {
[-1, -1];
}
for (let i = 0; i < line.length; ++i) {
stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length;
if (stringIndex < 0) {
return [lineIndex, i];
}
}
lineIndex++;
}
return [lineIndex, 0];
}
/**
* Translates a buffer line to a string, with optional start and end columns.
* Wide characters will count as two columns in the resulting string. This
@@ -340,6 +407,10 @@ export class Buffer implements IBuffer {
// TODO: This could probably be optimized by relying on sort order and trimming the array using .length
this.markers.splice(this.markers.indexOf(marker), 1);
}
public iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator {
return new BufferStringIterator(this, trimRight, startIndex, endIndex, startOverscan, endOverscan);
}
}
export class Marker extends EventEmitter implements IMarker {
@@ -366,3 +437,61 @@ export class Marker extends EventEmitter implements IMarker {
super.dispose();
}
}
/**
* Iterator to get unwrapped content strings from the buffer.
* The iterator returns at least the string data between the borders
* `startIndex` and `endIndex` (exclusive) and will expand the lines
* by `startOverscan` to the top and by `endOverscan` to the bottom,
* if no new line was found in between.
* It will never read/return string data beyond `startIndex - startOverscan`
* or `endIndex + endOverscan`. Therefore the first and last line might be truncated.
* It is possible to always get the full string for the first and last line as well
* by setting the overscan values to the actual buffer length. This not recommended
* since it might return the whole buffer within a single string in a worst case scenario.
*/
export class BufferStringIterator implements IBufferStringIterator {
private _current: number;
constructor (
private _buffer: IBuffer,
private _trimRight: boolean,
private _startIndex: number = 0,
private _endIndex: number = _buffer.lines.length,
private _startOverscan: number = 0,
private _endOverscan: number = 0
) {
if (this._startIndex < 0) {
this._startIndex = 0;
}
if (this._endIndex > this._buffer.lines.length) {
this._endIndex = this._buffer.lines.length;
}
this._current = this._startIndex;
}
public hasNext(): boolean {
return this._current < this._endIndex;
}
public next(): IBufferStringIteratorResult {
const range = this._buffer.getWrappedRangeForLine(this._current);
// limit search window to overscan value at both borders
if (range.first < this._startIndex - this._startOverscan) {
range.first = this._startIndex - this._startOverscan;
}
if (range.last > this._endIndex + this._endOverscan) {
range.last = this._endIndex + this._endOverscan;
}
// limit to current buffer length
range.first = Math.max(range.first, 0);
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);
}
this._current = range.last + 1;
return {range: range, content: result};
}
}
+169 -63
View File
@@ -5,93 +5,199 @@
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 } 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)]));
});
});
});
+227 -38
View File
@@ -7,30 +7,43 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_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,219 @@ 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;
}
}
/** typed array slots taken by one cell */
const CELL_SIZE = 3;
/** cell member indices */
const enum Cell {
FLAGS = 0,
STRING = 1,
WIDTH = 2
}
/**
* 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 & 0x80000000)
? this._combined[index]
: (stringData) ? String.fromCharCode(stringData) : '',
this._data[index * CELL_SIZE + Cell.WIDTH],
(stringData & 0x80000000)
? 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 | 0x80000000;
} 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;
}
}
+26
View File
@@ -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);
});
});
});
+7 -3
View File
@@ -5,7 +5,7 @@
import { ITerminal, IBufferSet } from './Types';
import { Buffer } from './Buffer';
import { EventEmitter } from './EventEmitter';
import { EventEmitter } from './common/EventEmitter';
/**
* The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and
@@ -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,
+254
View File
@@ -0,0 +1,254 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { TestTerminal } from './utils/TestUtils.test';
import { assert } from 'chai';
import { getStringCellWidth, wcwidth } from './CharWidth';
import { IBuffer } from './Types';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
describe('getStringCellWidth', function(): void {
let terminal: TestTerminal;
beforeEach(() => {
terminal = new TestTerminal({rows: 5, cols: 30});
});
function sumWidths(buffer: IBuffer, start: number, end: number, sentinel: string): number {
let result = 0;
for (let i = start; i < end; ++i) {
const line = buffer.lines.get(i);
for (let j = 0; j < line.length; ++j) { // TODO: change to trimBorder with multiline
const ch = line.get(j);
result += ch[CHAR_DATA_WIDTH_INDEX];
// return on sentinel
if (ch[CHAR_DATA_CHAR_INDEX] === sentinel) {
return result;
}
}
}
return result;
}
it('ASCII chars', function(): void {
const input = 'This is just ASCII text.#';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
});
it('combining chars', function(): void {
const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301#';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
});
it('surrogate chars', function(): void {
const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞#';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
});
it('surrogate combining chars', function(): void {
const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301#';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
});
it('fullwidth chars', function(): void {
const input = '1234567890#';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
});
it('fullwidth chars offset 1', function(): void {
const input = 'a1234567890#';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
});
// 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}`);
}
});
+158 -158
View File
@@ -3,169 +3,169 @@
* @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
/**
* Get the terminal cell width for a string.
*/
export function getStringCellWidth(s: string): number {
let result = 0;
const length = s.length;
for (let i = 0; i < length; ++i) {
let code = s.charCodeAt(i);
// surrogate pair first
if (0xD800 <= code && code <= 0xDBFF) {
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);
}
}
result += wcwidth(code);
}
return result;
}
+10 -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();
+4 -1
View File
@@ -66,7 +66,7 @@ export class TransitionTable {
const PRINTABLES = r(0x20, 0x7f);
const EXECUTABLES = r(0x00, 0x18);
EXECUTABLES.push(0x19);
EXECUTABLES.concat(r(0x1c, 0x20));
EXECUTABLES.push.apply(EXECUTABLES, r(0x1c, 0x20));
const DEFAULT_TRANSITION = ParserAction.ERROR << 4 | ParserState.GROUND;
/**
@@ -261,6 +261,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 {
+139 -47
View File
@@ -6,7 +6,7 @@
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 { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, DEFAULT_ATTR } from './Buffer';
import { Terminal } from './Terminal';
import { IBufferLine } from './Types';
@@ -98,6 +98,36 @@ class OldInputHandler extends InputHandler {
public eraseLine(y: number): void {
this.eraseRight(0, y);
}
public insertChars(params: number[]): void {
let param = params[0];
if (param < 1) param = 1;
// make buffer local for faster access
const buffer = this._terminal.buffer;
const row = buffer.y + buffer.ybase;
let j = buffer.x;
while (param-- && j < this._terminal.cols) {
buffer.lines.get(row).insertCells(j++, 1, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
}
}
public deleteChars(params: number[]): void {
let param: number = params[0];
if (param < 1) {
param = 1;
}
// make buffer local for faster access
const buffer = this._terminal.buffer;
const row = buffer.y + buffer.ybase;
while (param--) {
buffer.lines.get(row).deleteCells(buffer.x, 1, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
}
this._terminal.updateRange(buffer.y);
}
}
describe('InputHandler', () => {
@@ -178,8 +208,6 @@ 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];
@@ -195,23 +223,7 @@ describe('InputHandler', () => {
it('insertChars', function(): void {
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();
}
}
const oldInputHandler = new OldInputHandler(term);
// insert some data in first and second line
inputHandler.parse(Array(term.cols - 9).join('a'));
@@ -226,7 +238,7 @@ describe('InputHandler', () => {
// insert one char from params = [0]
term.buffer.y = 0;
term.buffer.x = 70;
insertChars([0]);
oldInputHandler.insertChars([0]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 123456789');
term.buffer.y = 1;
term.buffer.x = 70;
@@ -237,7 +249,7 @@ describe('InputHandler', () => {
// insert one char from params = [1]
term.buffer.y = 0;
term.buffer.x = 70;
insertChars([1]);
oldInputHandler.insertChars([1]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 12345678');
term.buffer.y = 1;
term.buffer.x = 70;
@@ -248,7 +260,7 @@ describe('InputHandler', () => {
// insert two chars from params = [2]
term.buffer.y = 0;
term.buffer.x = 70;
insertChars([2]);
oldInputHandler.insertChars([2]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 123456');
term.buffer.y = 1;
term.buffer.x = 70;
@@ -259,7 +271,7 @@ describe('InputHandler', () => {
// insert 10 chars from params = [10]
term.buffer.y = 0;
term.buffer.x = 70;
insertChars([10]);
oldInputHandler.insertChars([10]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' ');
term.buffer.y = 1;
term.buffer.x = 70;
@@ -270,25 +282,7 @@ describe('InputHandler', () => {
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);
}
const oldInputHandler = new OldInputHandler(term);
// insert some data in first and second line
inputHandler.parse(Array(term.cols - 9).join('a'));
@@ -303,7 +297,7 @@ describe('InputHandler', () => {
// delete one char from params = [0]
term.buffer.y = 0;
term.buffer.x = 70;
deleteChars([0]);
oldInputHandler.deleteChars([0]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '234567890 ');
term.buffer.y = 1;
term.buffer.x = 70;
@@ -314,7 +308,7 @@ describe('InputHandler', () => {
// insert one char from params = [1]
term.buffer.y = 0;
term.buffer.x = 70;
deleteChars([1]);
oldInputHandler.deleteChars([1]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '34567890 ');
term.buffer.y = 1;
term.buffer.x = 70;
@@ -325,7 +319,7 @@ describe('InputHandler', () => {
// insert two chars from params = [2]
term.buffer.y = 0;
term.buffer.x = 70;
deleteChars([2]);
oldInputHandler.deleteChars([2]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '567890 ');
term.buffer.y = 1;
term.buffer.x = 70;
@@ -336,7 +330,7 @@ describe('InputHandler', () => {
// insert 10 chars from params = [10]
term.buffer.y = 0;
term.buffer.x = 70;
deleteChars([10]);
oldInputHandler.deleteChars([10]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' ');
term.buffer.y = 1;
term.buffer.x = 70;
@@ -444,6 +438,36 @@ describe('InputHandler', () => {
termNew.buffer.x = 40;
inputHandlerNew.eraseInDisplay([2]);
expect(termContent(termNew)).eql(termContent(termOld));
// reset and add a wrapped line
termNew.buffer.y = 0;
termNew.buffer.x = 0;
inputHandlerNew.parse(Array(termNew.cols + 1).join('a')); // line 0
inputHandlerNew.parse(Array(termNew.cols + 10).join('a')); // line 1 and 2
for (let i = 3; i < termOld.rows; ++i) inputHandlerNew.parse(Array(termNew.cols + 1).join('a'));
// params[1] left and above with wrap
// confirm precondition that line 2 is wrapped
expect(termNew.buffer.lines.get(2).isWrapped).true;
termNew.buffer.y = 2;
termNew.buffer.x = 40;
inputHandlerNew.eraseInDisplay([1]);
expect(termNew.buffer.lines.get(2).isWrapped).false;
// reset and add a wrapped line
termNew.buffer.y = 0;
termNew.buffer.x = 0;
inputHandlerNew.parse(Array(termNew.cols + 1).join('a')); // line 0
inputHandlerNew.parse(Array(termNew.cols + 10).join('a')); // line 1 and 2
for (let i = 3; i < termOld.rows; ++i) inputHandlerNew.parse(Array(termNew.cols + 1).join('a'));
// params[1] left and above with wrap
// confirm precondition that line 2 is wrapped
expect(termNew.buffer.lines.get(2).isWrapped).true;
termNew.buffer.y = 1;
termNew.buffer.x = 90; // Cursor is beyond last column
inputHandlerNew.eraseInDisplay([1]);
expect(termNew.buffer.lines.get(2).isWrapped).false;
});
});
it('convertEol setting', function(): void {
@@ -475,4 +499,72 @@ describe('InputHandler', () => {
}
expect(s).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;
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;
}
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(lineContent(term.buffer.lines.get(0))).to.equal(Array(term.cols + 1).join(' '));
expect(lineContent(term.buffer.lines.get(1))).to.equal(' TEST' + Array(term.cols - 7).join(' '));
// 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(lineContent(term.buffer.lines.get(0))).to.equal(Array(term.cols + 1).join(' '));
expect(lineContent(term.buffer.lines.get(1))).to.equal(' TEST' + Array(term.cols - 7).join(' '));
// 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(lineContent(term.buffer.lines.get(0))).to.equal('TEST' + Array(term.cols - 3).join(' '));
expect(lineContent(term.buffer.lines.get(1))).to.equal('JUNK' + Array(term.cols - 3).join(' '));
// 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(lineContent(term.buffer.lines.get(0))).to.equal('TEST' + Array(term.cols - 3).join(' '));
expect(lineContent(term.buffer.lines.get(1))).to.equal(Array(term.cols + 1).join(' '));
// 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(lineContent(term.buffer.lines.get(0))).to.equal('TEST' + Array(term.cols - 3).join(' '));
// 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(lineContent(term.buffer.lines.get(1))).to.equal('TEST' + Array(term.cols - 3).join(' '));
// 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);
});
});
});
+99 -62
View File
@@ -13,7 +13,6 @@ import { wcwidth } from './CharWidth';
import { EscapeSequenceParser } from './EscapeSequenceParser';
import { ICharset } from './core/Types';
import { Disposable } from './common/Lifecycle';
import { BufferLine } from './BufferLine';
/**
* Map collect to glevel. Used in `selectCharset`.
@@ -114,7 +113,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 +123,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this.register(this._parser);
this._surrogateHigh = '';
this._surrogateFirst = '';
/**
* custom fallback handlers
@@ -312,9 +311,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 +327,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 +344,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 +395,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 +408,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 +438,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,8 +454,12 @@ 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);
@@ -722,12 +726,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 +765,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 +789,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 +854,7 @@ export class InputHandler extends Disposable implements IInputHandler {
// test: echo -e '\e[44m\e[1L\e[0m'
// blankLine(true) - xterm/linux behavior
buffer.lines.splice(scrollBottomAbsolute - 1, 1);
buffer.lines.splice(row, 0, BufferLine.blankLine(this._terminal.cols, this._terminal.eraseAttr()));
buffer.lines.splice(row, 0, buffer.getBlankLine(this._terminal.eraseAttr()));
}
// this.maxRange();
@@ -862,7 +884,7 @@ export class InputHandler extends Disposable implements IInputHandler {
// test: echo -e '\e[44m\e[1M\e[0m'
// blankLine(true) - xterm/linux behavior
buffer.lines.splice(row, 1);
buffer.lines.splice(j, 0, BufferLine.blankLine(this._terminal.cols, this._terminal.eraseAttr()));
buffer.lines.splice(j, 0, buffer.getBlankLine(this._terminal.eraseAttr()));
}
// this.maxRange();
@@ -894,7 +916,7 @@ export class InputHandler extends Disposable implements IInputHandler {
while (param--) {
buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1);
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, 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 +935,7 @@ export class InputHandler extends Disposable implements IInputHandler {
while (param--) {
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1);
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, 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 +1284,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 +1335,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 +1480,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 +1510,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 +1804,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 +1873,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 +1885,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;
}
+99 -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 } from './utils/TestUtils.test';
import { MockBuffer, MockTerminal, TestTerminal } from './utils/TestUtils.test';
import { CircularList } from './common/CircularList';
import { BufferLine } from './BufferLine';
@@ -42,6 +42,7 @@ describe('Linkifier', () => {
beforeEach(() => {
terminal = new MockTerminal();
terminal.cols = 100;
terminal.rows = 10;
terminal.buffer = new MockBuffer();
(<MockBuffer>terminal.buffer).setLines(new CircularList<IBufferLine>(20));
terminal.buffer.ydisp = 0;
@@ -50,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;
}
@@ -64,6 +65,7 @@ describe('Linkifier', () => {
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void {
addRow(rowText);
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
terminal.rows = terminal.buffer.lines.length - 1;
linkifier.linkifyRows();
// Allow linkify to happen
setTimeout(() => {
@@ -238,4 +240,98 @@ describe('Linkifier', () => {
});
});
});
describe('unicode handling', () => {
let terminal: TestTerminal;
// other than the tests above unicode testing needs the full terminal instance
// to get the special handling of fullwidth, surrogate and combining chars in the input handler
beforeEach(() => {
terminal = new TestTerminal({cols: 10, rows: 5});
linkifier = new TestLinkifier(terminal);
mouseZoneManager = new TestMouseZoneManager();
linkifier.attachToDom(mouseZoneManager);
});
function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[], done: MochaDone): void {
terminal.writeSync(rowText);
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
linkifier.linkifyRows();
// Allow linkify to happen
setTimeout(() => {
assert.equal(mouseZoneManager.zones.length, links.length);
links.forEach((l, i) => {
assert.equal(mouseZoneManager.zones[i].x1, l.x1 + 1);
assert.equal(mouseZoneManager.zones[i].x2, l.x2 + 1);
assert.equal(mouseZoneManager.zones[i].y1, l.y1 + 1);
assert.equal(mouseZoneManager.zones[i].y2, l.y2 + 1);
});
done();
}, 0);
}
describe('unicode before the match', () => {
it('combining - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
});
it('combining - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
});
it('surrogate - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
});
it('surrogate - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
});
it('combining surrogate - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
});
it('combining surrogate - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
});
it('fullwidth - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
});
it('fullwidth - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
});
it('combining fullwidth - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
});
it('combining fullwidth - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
});
});
describe('unicode within the match', () => {
it('combining - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
});
it('combining - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('testtest cafe\u0301', /cafe\u0301/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
});
it('surrogate - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('test a𝄞b', /a𝄞b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
});
it('surrogate - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('testtest a𝄞b', /a𝄞b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done);
});
it('combining surrogate - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('test a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
});
it('combining surrogate - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('testtest a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done);
});
it('fullwidth - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('test a1b', /a1b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
});
it('fullwidth - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('testtest a1b', /a1b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
});
it('combining fullwidth - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('test a¥\u0301b', /a¥\u0301b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
});
it('combining fullwidth - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
});
});
});
});
+92 -90
View File
@@ -4,10 +4,11 @@
*/
import { IMouseZoneManager } from './ui/Types';
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferLine } from './Types';
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult } from './Types';
import { MouseZone } from './ui/MouseZoneManager';
import { EventEmitter } from './EventEmitter';
import { EventEmitter } from './common/EventEmitter';
import { CHAR_DATA_ATTR_INDEX } from './Buffer';
import { getStringCellWidth } from './CharWidth';
/**
* The Linkifier applies links to rows shortly after they have been refreshed.
@@ -20,6 +21,13 @@ export class Linkifier extends EventEmitter implements ILinkifier {
*/
protected static readonly TIME_BEFORE_LINKIFY = 200;
/**
* Limit of the unwrapping line expansion (overscan) at the top and bottom
* of the actual viewport in ASCII characters.
* A limit of 2000 should match most sane urls.
*/
protected static readonly OVERSCAN_CHAR_LIMIT = 2000;
protected _linkMatchers: ILinkMatcher[] = [];
private _mouseZoneManager: IMouseZoneManager;
@@ -80,9 +88,37 @@ export class Linkifier extends EventEmitter implements ILinkifier {
*/
private _linkifyRows(): void {
this._rowsTimeoutId = null;
for (let i = this._rowsToLinkify.start; i <= this._rowsToLinkify.end; i++) {
this._linkifyRow(i);
const buffer = this._terminal.buffer;
// Ensure the start row exists
const absoluteRowIndexStart = buffer.ydisp + this._rowsToLinkify.start;
if (absoluteRowIndexStart >= buffer.lines.length) {
return;
}
// Invalidate bad end row values (if a resize happened)
const absoluteRowIndexEnd = buffer.ydisp + Math.min(this._rowsToLinkify.end, this._terminal.rows) + 1;
// Iterate over the range of unwrapped content strings within start..end
// (excluding).
// _doLinkifyRow gets full unwrapped lines with the start row as buffer offset
// for every matcher.
// The unwrapping is needed to also match content that got wrapped across
// several buffer lines. To avoid a worst case scenario where the whole buffer
// contains just a single unwrapped string we limit this line expansion beyond
// the viewport to +OVERSCAN_CHAR_LIMIT chars (overscan) at top and bottom.
// This comes with the tradeoff that matches longer than OVERSCAN_CHAR_LIMIT
// chars will not match anymore at the viewport borders.
const overscanLineLimit = Math.ceil(Linkifier.OVERSCAN_CHAR_LIMIT / this._terminal.cols);
const iterator = this._terminal.buffer.iterator(
false, absoluteRowIndexStart, absoluteRowIndexEnd, overscanLineLimit, overscanLineLimit);
while (iterator.hasNext()) {
const lineData: IBufferStringIteratorResult = iterator.next();
for (let i = 0; i < this._linkMatchers.length; i++) {
this._doLinkifyRow(lineData.range.first, lineData.content, this._linkMatchers[i]);
}
}
this._rowsToLinkify.start = null;
this._rowsToLinkify.end = null;
}
@@ -153,96 +189,61 @@ export class Linkifier extends EventEmitter implements ILinkifier {
return false;
}
/**
* Linkifies a row.
* @param rowIndex The index of the row to linkify.
*/
private _linkifyRow(rowIndex: number): void {
// Ensure the row exists
let absoluteRowIndex = this._terminal.buffer.ydisp + rowIndex;
if (absoluteRowIndex >= this._terminal.buffer.lines.length) {
return;
}
if (this._terminal.buffer.lines.get(absoluteRowIndex).isWrapped) {
// Only attempt to linkify rows that start in the viewport
if (rowIndex !== 0) {
return;
}
// If the first row is wrapped, backtrack to find the origin row and linkify that
let line: IBufferLine;
do {
rowIndex--;
absoluteRowIndex--;
line = this._terminal.buffer.lines.get(absoluteRowIndex);
if (!line) {
break;
}
} while (line.isWrapped);
}
// Construct full unwrapped line text
let text = this._terminal.buffer.translateBufferLineToString(absoluteRowIndex, false);
let currentIndex = absoluteRowIndex + 1;
while (currentIndex < this._terminal.buffer.lines.length &&
this._terminal.buffer.lines.get(currentIndex).isWrapped) {
text += this._terminal.buffer.translateBufferLineToString(currentIndex++, false);
}
for (let i = 0; i < this._linkMatchers.length; i++) {
this._doLinkifyRow(rowIndex, text, this._linkMatchers[i]);
}
}
/**
* Linkifies a row given a specific handler.
* @param rowIndex The row index to linkify.
* @param text The text of the row (excludes text in the row that's already
* linkified).
* @param rowIndex The row index to linkify (absolute index).
* @param text string content of the unwrapped row.
* @param matcher The link matcher for this line.
* @param offset The how much of the row has already been linkified.
* @return The link element(s) that were added.
*/
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher, offset: number = 0): void {
// Find the first match
const match = text.match(matcher.regex);
if (!match || match.length === 0) {
return;
}
const uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
// Get index, match.index is for the outer match which includes negated chars
const index = text.indexOf(uri);
// Get cell color
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ydisp + rowIndex);
const char = line.get(index);
const attr: number = char[CHAR_DATA_ATTR_INDEX];
const fg = (attr >> 9) & 0x1ff;
// Ensure the link is valid before registering
if (matcher.validationCallback) {
matcher.validationCallback(uri, isValid => {
// Discard link if the line has already changed
if (this._rowsTimeoutId) {
return;
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher): void {
// clone regex to do a global search on text
const rex = new RegExp(matcher.regex.source, matcher.regex.flags + 'g');
let match;
let stringIndex = -1;
while ((match = rex.exec(text)) !== null) {
const uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
if (!uri) {
// something matched but does not comply with the given matchIndex
// since this is most likely a bug the regex itself we simply do nothing here
// DEBUG: print match and throw
if ((<any>this._terminal).debug) {
console.log({match, matcher});
throw new Error('match found without corresponding matchIndex');
}
if (isValid) {
this._addLink(offset + index, rowIndex, uri, matcher, fg);
}
});
} else {
this._addLink(offset + index, rowIndex, uri, matcher, fg);
}
break;
}
// Recursively check for links in the rest of the text
const remainingStartIndex = index + uri.length;
const remainingText = text.substr(remainingStartIndex);
if (remainingText.length > 0) {
this._doLinkifyRow(rowIndex, remainingText, matcher, offset + remainingStartIndex);
// Get index, match.index is for the outer match which includes negated chars
// therefore we cannot use match.index directly, instead we search the position
// of the match group in text again
// also correct regex and string search offsets for the next loop run
stringIndex = text.indexOf(uri, stringIndex + 1);
rex.lastIndex = stringIndex + uri.length;
// get the buffer index as [absolute row, col] for the match
const bufferIndex = this._terminal.buffer.stringIndexToBufferIndex(rowIndex, stringIndex);
const line = this._terminal.buffer.lines.get(bufferIndex[0]);
const char = line.get(bufferIndex[1]);
let fg: number | undefined;
if (char) {
const attr: number = char[CHAR_DATA_ATTR_INDEX];
fg = (attr >> 9) & 0x1ff;
}
if (matcher.validationCallback) {
matcher.validationCallback(uri, isValid => {
// Discard link if the line has already changed
if (this._rowsTimeoutId) {
return;
}
if (isValid) {
this._addLink(bufferIndex[1], bufferIndex[0] - this._terminal.buffer.ydisp, uri, matcher, fg);
}
});
} else {
this._addLink(bufferIndex[1], bufferIndex[0] - this._terminal.buffer.ydisp, uri, matcher, fg);
}
}
}
@@ -255,10 +256,11 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @param fg The link color for hover event.
*/
private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher, fg: number): void {
const width = getStringCellWidth(uri);
const x1 = x % this._terminal.cols;
const y1 = y + Math.floor(x / this._terminal.cols);
let x2 = (x1 + uri.length) % this._terminal.cols;
let y2 = y1 + Math.floor((x1 + uri.length) / this._terminal.cols);
let x2 = (x1 + width) % this._terminal.cols;
let y2 = y1 + Math.floor((x1 + width) / this._terminal.cols);
if (x2 === 0) {
x2 = this._terminal.cols;
y2--;
+19 -6
View File
@@ -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]));
});
});
});
+12 -6
View File
@@ -3,11 +3,12 @@
* @license MIT
*/
import { ITerminal, ISelectionManager, IBuffer, CharData, XtermListener, IBufferLine } from './Types';
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 * as Browser from './core/Platform';
import { CharMeasure } from './ui/CharMeasure';
import { EventEmitter } from './EventEmitter';
import { EventEmitter } from './common/EventEmitter';
import { SelectionModel } from './SelectionModel';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer';
import { AltClickHandler } from './handlers/AltClickHandler';
@@ -288,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]);
}
/**
@@ -341,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;
}
+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 {
+171 -54
View File
@@ -7,7 +7,6 @@ import { assert, expect } from 'chai';
import { Terminal } from './Terminal';
import { MockViewport, MockCompositionHelper, MockRenderer } from './utils/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;
@@ -26,7 +25,7 @@ describe('term.js addons', () => {
beforeEach(() => {
term = new TestTerminal(termOptions);
term.refresh = () => {};
term.refresh = () => { };
(<any>term).renderer = new MockRenderer();
term.viewport = new MockViewport();
(<any>term)._compositionHelper = new MockCompositionHelper();
@@ -37,8 +36,8 @@ describe('term.js addons', () => {
};
(<any>term).element = {
classList: {
toggle: () => {},
remove: () => {}
toggle: () => { },
remove: () => { }
}
};
});
@@ -68,24 +67,142 @@ describe('term.js addons', () => {
});
});
describe('on', () => {
beforeEach(() => {
term.on('key', () => { });
term.on('keypress', () => { });
term.on('keydown', () => { });
});
describe('data', () => {
it('should emit a data event', (done) => {
term.on('data', () => {
done();
});
term.handler('fake');
});
});
describe(`keypress (including 'key' event)`, () => {
it('should receive a string and event object', (done) => {
let steps = 0;
const finish = () => {
if ((++steps) === 2) {
done();
}
};
const evKeyPress = <KeyboardEvent>{
preventDefault: () => { },
stopPropagation: () => { },
type: 'keypress',
keyCode: 13
};
term.on('keypress', (key, event) => {
assert.equal(typeof key, 'string');
expect(event).to.be.an.instanceof(Object);
finish();
});
term.on('key', (key, event) => {
assert.equal(typeof key, 'string');
expect(event).to.be.an.instanceof(Object);
finish();
});
term.keyPress(evKeyPress);
});
});
describe(`keydown (including 'key' event)`, () => {
it(`should receive an event object for 'keydown' and a string and event object for 'key'`, (done) => {
let steps = 0;
const finish = () => {
if ((++steps) === 2) {
done();
}
};
const evKeyDown = <KeyboardEvent>{
preventDefault: () => { },
stopPropagation: () => { },
type: 'keydown',
keyCode: 13
};
term.on('keydown', (event) => {
expect(event).to.be.an.instanceof(Object);
finish();
});
term.on('key', (key, event) => {
assert.equal(typeof key, 'string');
expect(event).to.be.an.instanceof(Object);
finish();
});
term.keyDown(evKeyDown);
});
});
describe('resize', () => {
it('should receive an object: {cols: number, rows: number}', (done) => {
term.on('resize', (data) => {
expect(data).to.have.keys(['cols', 'rows']);
assert.equal(typeof data.cols, 'number');
assert.equal(typeof data.rows, 'number');
done();
});
term.resize(1, 1);
});
});
describe('scroll', () => {
it('should receive a number', (done) => {
term.on('scroll', (ydisp) => {
assert.equal(typeof ydisp, 'number');
done();
});
term.scroll();
});
});
describe('title', () => {
it('should receive a string', (done) => {
term.on('title', (title) => {
assert.equal(typeof title, 'string');
done();
});
term.handleTitle('title');
});
});
});
describe('attachCustomKeyEventHandler', () => {
const evKeyDown = <KeyboardEvent>{
preventDefault: () => {},
stopPropagation: () => {},
preventDefault: () => { },
stopPropagation: () => { },
type: 'keydown',
keyCode: 77
};
const evKeyPress = <KeyboardEvent>{
preventDefault: () => {},
stopPropagation: () => {},
preventDefault: () => { },
stopPropagation: () => { },
type: 'keypress',
keyCode: 77
};
beforeEach(() => {
term.handler = () => {};
term.showCursor = () => {};
term.clearSelection = () => {};
term.handler = () => { };
term.showCursor = () => { };
term.clearSelection = () => { };
});
it('should process the keydown/keypress event based on what the handler returns', () => {
@@ -142,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', () => {
@@ -159,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', () => {
@@ -172,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));
}
});
});
@@ -305,8 +422,8 @@ describe('term.js addons', () => {
type: 'keydown',
key: 'a',
keyCode: 65,
preventDefault: () => {},
stopPropagation: () => {}
preventDefault: () => { },
stopPropagation: () => { }
};
term.buffer.ydisp = 0;
@@ -338,8 +455,8 @@ 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);
@@ -349,9 +466,9 @@ describe('term.js addons', () => {
});
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();
@@ -361,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();
@@ -379,11 +496,11 @@ describe('term.js addons', () => {
});
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;
@@ -404,9 +521,9 @@ 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();
@@ -419,9 +536,9 @@ describe('term.js addons', () => {
});
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();
@@ -431,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();
@@ -448,11 +565,11 @@ describe('term.js addons', () => {
});
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;
@@ -473,9 +590,9 @@ describe('term.js addons', () => {
let evKeyPress: any;
beforeEach(() => {
term.handler = () => {};
term.showCursor = () => {};
term.clearSelection = () => {};
term.handler = () => { };
term.showCursor = () => { };
term.clearSelection = () => { };
// term.compositionHelper = {
// isComposing: false,
// keydown: {
@@ -485,15 +602,15 @@ describe('term.js addons', () => {
// }
// };
evKeyDown = {
preventDefault: () => {},
stopPropagation: () => {},
preventDefault: () => { },
stopPropagation: () => { },
type: 'keydown',
altKey: null,
keyCode: null
};
evKeyPress = {
preventDefault: () => {},
stopPropagation: () => {},
preventDefault: () => { },
stopPropagation: () => { },
type: 'keypress',
altKey: null,
charCode: null,
+50 -22
View File
@@ -21,22 +21,22 @@
* 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 './EventEmitter';
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';
@@ -52,7 +52,6 @@ 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';
// 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();
}
/**
@@ -473,6 +475,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 +496,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 +807,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 +833,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 +988,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 +1157,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 +1177,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 +1232,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
@@ -1722,7 +1750,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 +1842,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 {
+29 -22
View File
@@ -7,11 +7,10 @@ import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions,
import { IColorSet, IRenderer } from './renderer/Types';
import { IMouseZoneManager } from './ui/Types';
import { ICharset } from './core/Types';
import { ICircularList } from './common/Types';
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
export type XtermListener = (...args: any[]) => void;
export type CharData = [number, string, number, number];
export type LineData = CharData[];
@@ -20,6 +19,9 @@ export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: bo
export type CharacterJoinerHandler = (text: string) => [number, number][];
// BufferIndex denotes a position in the buffer: [rowIndex, colIndex]
export type BufferIndex = [number, number];
export const enum LinkHoverEventTypes {
HOVER = 'linkhover',
TOOLTIP = 'linktooltip',
@@ -47,7 +49,6 @@ export interface IInputHandlingTerminal extends IEventEmitter {
wraparoundMode: boolean;
bracketedPasteMode: boolean;
curAttr: number;
savedCurAttr: number;
savedCols: number;
x10Mouse: boolean;
vt200Mouse: boolean;
@@ -244,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: { pageX: number, pageY: 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 {
@@ -266,6 +267,16 @@ export interface ITerminalOptions extends IPublicTerminalOptions {
useFlowControl?: boolean;
}
export interface IBufferStringIteratorResult {
range: {first: number, last: number};
content: string;
}
export interface IBufferStringIterator {
hasNext(): boolean;
next(): IBufferStringIteratorResult;
}
export interface IBuffer {
readonly lines: ICircularList<IBufferLine>;
ydisp: number;
@@ -278,11 +289,15 @@ 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;
}
export interface IBufferSet extends IEventEmitter {
@@ -291,20 +306,7 @@ export interface IBufferSet extends IEventEmitter {
active: IBuffer;
activateNormalBuffer(): void;
activateAltBuffer(): void;
}
export interface ICircularList<T> extends IEventEmitter {
length: number;
maxLength: number;
get(index: number): T;
set(index: number, value: T): void;
push(value: T): void;
pop(): T;
splice(start: number, deleteCount: number, ...items: T[]): void;
trimStart(count: number): void;
shiftElements(start: number, count: number, offset: number): void;
activateAltBuffer(fillAttr?: number): void;
}
export interface ISelectionManager {
@@ -515,10 +517,15 @@ 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;
}
export interface IBufferLineConstructor {
new(cols: number, fillCharData?: CharData, isWrapped?: boolean): IBufferLine;
}
+31 -10
View File
@@ -22,6 +22,7 @@ export class Viewport extends Disposable implements IViewport {
private _lastRecordedViewportHeight: number = 0;
private _lastRecordedBufferHeight: number = 0;
private _lastTouchY: number;
private _lastScrollTop: number = 0;
// Stores a partial line amount when scrolling, this is used to keep track of how much of a line
// is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a
@@ -97,18 +98,36 @@ export class Viewport extends Disposable implements IViewport {
* Updates dimensions and synchronizes the scroll area if necessary.
*/
public syncScrollArea(): void {
// If buffer height changed
if (this._lastRecordedBufferLength !== this._terminal.buffer.lines.length) {
// If buffer height changed
this._lastRecordedBufferLength = this._terminal.buffer.lines.length;
this._refresh();
} else if (this._lastRecordedViewportHeight !== (<any>this._terminal).renderer.dimensions.canvasHeight) {
// If viewport height changed
return;
}
// If viewport height changed
if (this._lastRecordedViewportHeight !== (<any>this._terminal).renderer.dimensions.canvasHeight) {
this._refresh();
} else {
// If size has changed, refresh viewport
if (this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
this._refresh();
}
return;
}
// If the buffer position doesn't match last scroll top
const newScrollTop = this._terminal.buffer.ydisp * this._currentRowHeight;
if (this._lastScrollTop !== newScrollTop) {
this._refresh();
return;
}
// If element's scroll top changed, this can happen when hiding the element
if (this._lastScrollTop !== this._viewportElement.scrollTop) {
this._refresh();
return;
}
// If row height changed
if (this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
this._refresh();
return;
}
}
@@ -118,6 +137,9 @@ export class Viewport extends Disposable implements IViewport {
* @param ev The scroll event.
*/
private _onScroll(ev: Event): void {
// Record current scroll top position
this._lastScrollTop = this._viewportElement.scrollTop;
// Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt
// which causes the terminal to scroll the buffer to the top
if (!this._viewportElement.offsetParent) {
@@ -130,8 +152,7 @@ export class Viewport extends Disposable implements IViewport {
return;
}
const newRow = Math.round(this._viewportElement.scrollTop / this._currentRowHeight);
const newRow = Math.round(this._lastScrollTop / this._currentRowHeight);
const diff = newRow - this._terminal.buffer.ydisp;
this._terminal.scrollLines(diff, true);
}
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { assert, expect } from 'chai';
import { assert } from 'chai';
import * as attach from './attach';
+4
View File
@@ -2,6 +2,10 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es6",
],
"rootDir": ".",
"outDir": "../../../lib/addons/attach/",
"sourceMap": true,
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { assert, expect } from 'chai';
import { assert } from 'chai';
import * as fit from './fit';
+8 -1
View File
@@ -2,12 +2,19 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"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';
+6 -5
View File
@@ -11,17 +11,18 @@ 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('fullscreen');
}
export function apply(terminalConstructor: typeof Terminal): void {
+8 -1
View File
@@ -2,12 +2,19 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es5"
],
"rootDir": ".",
"outDir": "../../../lib/addons/fullscreen/",
"sourceMap": true,
"removeComments": true,
"declaration": true,
"preserveWatchOutput": true
"preserveWatchOutput": true,
"types": [
"../../node_modules/@types/mocha"
]
},
"include": [
"**/*.ts",
+14 -2
View File
@@ -17,6 +17,18 @@ export interface ISearchAddonTerminal extends Terminal {
}
export interface ISearchHelper {
findNext(term: string): boolean;
findPrevious(term: string): boolean;
findNext(term: string, searchOptions: ISearchOptions): boolean;
findPrevious(term: string, searchOptions: ISearchOptions): boolean;
}
export interface ISearchOptions {
regex?: boolean;
wholeWord?: boolean;
caseSensitive?: boolean;
}
export interface ISearchResult {
term: string;
col: number;
row: number;
}
+89 -24
View File
@@ -3,13 +3,8 @@
* @license MIT
*/
import { ISearchHelper, ISearchAddonTerminal } from './Interfaces';
interface ISearchResult {
term: string;
col: number;
row: number;
}
import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces';
const nonWordCharacters = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?';
/**
* A class that knows how to search the terminal and how to display the results.
@@ -19,16 +14,16 @@ export class SearchHelper implements ISearchHelper {
// 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
// TODO: Support regex, case sensitivity, etc.
}
/**
* 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): boolean {
public findNext(term: string, searchOptions?: ISearchOptions): boolean {
if (!term || term.length === 0) {
return false;
}
@@ -38,12 +33,14 @@ export class SearchHelper implements ISearchHelper {
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];
if (this._terminal.getSelection().length !== 0) {
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);
result = this._findInLine(term, y, searchOptions);
if (result) {
break;
}
@@ -52,7 +49,7 @@ export class SearchHelper implements ISearchHelper {
// Search from the top to the current ydisp
if (!result) {
for (let y = 0; y < startRow; y++) {
result = this._findInLine(term, y);
result = this._findInLine(term, y, searchOptions);
if (result) {
break;
}
@@ -66,10 +63,11 @@ 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): boolean {
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean {
if (!term || term.length === 0) {
return false;
}
@@ -79,12 +77,14 @@ export class SearchHelper implements ISearchHelper {
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];
if (this._terminal.getSelection().length !== 0) {
startRow = this._terminal._core.selectionManager.selectionStart[1];
}
}
// Search from ydisp + 1 to end
for (let y = startRow - 1; y >= 0; y--) {
result = this._findInLine(term, y);
result = this._findInLine(term, y, searchOptions);
if (result) {
break;
}
@@ -93,7 +93,7 @@ export class SearchHelper implements ISearchHelper {
// Search from the top to the current ydisp
if (!result) {
for (let y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
result = this._findInLine(term, y);
result = this._findInLine(term, y, searchOptions);
if (result) {
break;
}
@@ -105,17 +105,59 @@ export class SearchHelper implements ISearchHelper {
}
/**
* Searches a line for a search term.
* @param term Tne search term.
* 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
* @param line entire string in which the potential whole word was found
* @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)));
}
/**
* Searches a line for a search term. Takes the provided terminal line and searches the text line, which may contain
* 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 The search term.
* @param y The line to search.
* @param searchOptions Search options.
* @return The search result if it was found.
*/
private _findInLine(term: string, y: number): ISearchResult {
const lowerStringLine = this._terminal._core.buffer.translateBufferLineToString(y, true).toLowerCase();
const lowerTerm = term.toLowerCase();
let searchIndex = lowerStringLine.indexOf(lowerTerm);
protected _findInLine(term: string, y: number, searchOptions: ISearchOptions = {}): ISearchResult {
if (this._terminal._core.buffer.lines.get(y).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];
}
} 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;
}
if (searchOptions.wholeWord && !this._isWholeWord(searchIndex, searchStringLine, term)) {
return;
}
const line = this._terminal._core.buffer.lines.get(y);
for (let i = 0; i < searchIndex; i++) {
const charData = line.get(i);
// Adjust the searchIndex to normalize emoji into single chars
@@ -138,6 +180,28 @@ export class SearchHelper implements ISearchHelper {
}
}
/**
* 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
* function is useful for getting the actual text underneath the raw selection
* position.
* @param line The line being translated.
* @param trimRight Whether to trim whitespace to the right.
*/
public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): string {
let lineString = '';
let lineWrapsToNext: boolean;
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).substring(0, this._terminal.cols);
lineIndex++;
} while (lineWrapsToNext);
return lineString;
}
/**
* Selects and scrolls to a result.
* @param result The result to select.
@@ -145,6 +209,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);
+219 -17
View File
@@ -2,21 +2,23 @@
* 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 { ISearchHelper } from './Interfaces';
import { ISearchOptions, ISearchResult } from './Interfaces';
class MockTerminalPlain {}
class MockTerminal {
private _core: any;
public searchHelper: ISearchHelper;
public searchHelper: TestSearchHelper;
public cols: number;
constructor(options: any) {
this._core = new (require('../../../lib/Terminal').Terminal)(options);
this.searchHelper = new SearchHelper(this as any);
this._core = new (require('../../../lib/Terminal')).Terminal(options);
this.searchHelper = new TestSearchHelper(this as any);
this.cols = options.cols;
}
get core(): any {
return this._core;
@@ -26,7 +28,13 @@ class MockTerminal {
}
}
describe('search addon', function(): void {
class TestSearchHelper extends SearchHelper {
public findInLine(term: string, y: number, searchOptions?: ISearchOptions): ISearchResult {
return this._findInLine(term, y, searchOptions);
}
}
describe('search addon', () => {
describe('apply', () => {
it('should register findNext and findPrevious', () => {
search.apply(<any>MockTerminalPlain);
@@ -34,16 +42,210 @@ describe('search addon', function(): void {
assert.equal(typeof (<any>MockTerminalPlain).prototype.findPrevious, 'function');
});
});
it('Searchhelper - should find correct position', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 3});
term.core.write('Hello World\r\ntest\n123....hello');
term.pushWriteData();
const hello0 = (term.searchHelper as any)._findInLine('Hello', 0);
const hello1 = (term.searchHelper as any)._findInLine('Hello', 1);
const hello2 = (term.searchHelper as any)._findInLine('Hello', 2);
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
expect(hello1).eql(undefined);
expect(hello2).eql({col: 11, row: 2, term: 'Hello'});
describe('find', () => {
it('Searchhelper - should find correct position', () => {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 3});
term.core.write('Hello World\r\ntest\n123....hello');
term.pushWriteData();
const hello0 = term.searchHelper.findInLine('Hello', 0);
const hello1 = term.searchHelper.findInLine('Hello', 1);
const hello2 = term.searchHelper.findInLine('Hello', 2);
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
expect(hello1).eql(undefined);
expect(hello2).eql({col: 11, row: 2, term: 'Hello'});
});
it('should find search term accross line wrap', () => {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 10, rows: 5});
term.core.write('texttextHellotext\r\n');
term.core.write('texttexttextHellotext goodbye');
term.pushWriteData();
/*
texttextHe
llotext
texttextte
xtHellotex
t (these spaces included intentionally)
goodbye
*/
const hello0 = term.searchHelper.findInLine('Hello', 0);
const hello1 = term.searchHelper.findInLine('Hello', 1);
const hello2 = term.searchHelper.findInLine('Hello', 2);
const hello3 = term.searchHelper.findInLine('Hello', 3);
const llo = term.searchHelper.findInLine('llo', 1);
const goodbye = term.searchHelper.findInLine('goodbye', 2);
expect(hello0).eql({col: 8, row: 0, term: 'Hello'});
expect(hello1).eql(undefined);
expect(hello2).eql({col: 2, row: 3, term: 'Hello'});
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);
const term = new MockTerminal({cols: 10, rows: 4});
term.core.write('abcdefghijklmnopqrstuvwxyz\r\n~/dev ');
/*
abcdefghij
klmnopqrst
uvwxyz
~/dev
*/
term.pushWriteData();
const searchOptions = {
regex: true,
wholeWord: false,
caseSensitive: false
};
const hello0 = term.searchHelper.findInLine('dee*', 0, searchOptions);
const hello1 = term.searchHelper.findInLine('jkk*', 0, searchOptions);
const hello2 = term.searchHelper.findInLine('mnn*', 1, searchOptions);
const tilda0 = term.searchHelper.findInLine('^~', 3, searchOptions);
const tilda1 = term.searchHelper.findInLine('^[~]', 3, searchOptions);
const tilda2 = term.searchHelper.findInLine('^\\~', 3, searchOptions);
expect(hello0).eql({col: 3, row: 0, term: 'de'});
expect(hello1).eql({col: 9, row: 0, term: 'jk'});
expect(hello2).eql(undefined);
expect(tilda0).eql({col: 0, row: 3, term: '~'});
expect(tilda1).eql({col: 0, row: 3, term: '~'});
expect(tilda2).eql({col: 0, row: 3, term: '~'});
});
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);
});
it('should respect case sensitive', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 4});
term.core.write('Hello World\r\n123....hello\r\nmoreTestHello');
term.pushWriteData();
const searchOptions = {
regex: false,
wholeWord: false,
caseSensitive: true
};
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
const hello1 = term.searchHelper.findInLine('Hello', 1, searchOptions);
const hello2 = term.searchHelper.findInLine('Hello', 2, searchOptions);
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
expect(hello1).eql(undefined);
expect(hello2).eql({col: 8, row: 2, term: 'Hello'});
});
it('should respect case sensitive + regex', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 4});
term.core.write('hellohello\r\nHelloHello');
term.pushWriteData();
/**
* hellohello
* HelloHello
*/
const searchOptions = {
regex: true,
wholeWord: false,
caseSensitive: true
};
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
const hello1 = term.searchHelper.findInLine('Hello$', 0, searchOptions);
const hello2 = term.searchHelper.findInLine('Hello', 1, searchOptions);
const hello3 = term.searchHelper.findInLine('Hello$', 1, searchOptions);
expect(hello0).eql(undefined);
expect(hello1).eql(undefined);
expect(hello2).eql({col: 0, row: 1, term: 'Hello'});
expect(hello3).eql({col: 5, row: 1, term: 'Hello'});
});
it('should respect whole-word search option', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 5});
term.core.write('Hello World\r\nWorld Hello\r\nWorldHelloWorld\r\nHelloWorld\r\nWorldHello');
term.pushWriteData();
const searchOptions = {
regex: false,
wholeWord: true,
caseSensitive: false
};
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
const hello1 = term.searchHelper.findInLine('Hello', 1, searchOptions);
const hello2 = term.searchHelper.findInLine('Hello', 2, searchOptions);
const hello3 = term.searchHelper.findInLine('Hello', 3, searchOptions);
const hello4 = term.searchHelper.findInLine('Hello', 4, searchOptions);
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
expect(hello1).eql({col: 6, row: 1, term: 'Hello'});
expect(hello2).eql(undefined);
expect(hello3).eql(undefined);
expect(hello4).eql(undefined);
});
it('should respect whole-word + case sensitive search options', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 5});
term.core.write('Hello World\r\nHelloWorld');
term.pushWriteData();
const searchOptions = {
regex: false,
wholeWord: true,
caseSensitive: true
};
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
const hello1 = term.searchHelper.findInLine('hello', 0, searchOptions);
const hello2 = term.searchHelper.findInLine('Hello', 1, searchOptions);
const hello3 = term.searchHelper.findInLine('hello', 1, searchOptions);
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
expect(hello1).eql(undefined);
expect(hello2).eql(undefined);
expect(hello3).eql(undefined);
});
it('should respect whole-word + regex search options', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 5});
term.core.write('Hello World Hello\r\nHelloWorldHello');
term.pushWriteData();
const searchOptions = {
regex: true,
wholeWord: true,
caseSensitive: false
};
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
const hello1 = term.searchHelper.findInLine('Hello$', 0, searchOptions);
const hello2 = term.searchHelper.findInLine('Hello', 1, searchOptions);
const hello3 = term.searchHelper.findInLine('Hello$', 1, searchOptions);
expect(hello0).eql({col: 0, row: 0, term: 'hello'});
expect(hello1).eql({col: 12, row: 0, term: 'hello'});
expect(hello2).eql(undefined);
expect(hello3).eql(undefined);
});
it('should respect all search options', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 5});
term.core.write('Hello World Hello\r\nHelloWorldHello');
term.pushWriteData();
const searchOptions = {
regex: true,
wholeWord: true,
caseSensitive: true
};
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
const hello1 = term.searchHelper.findInLine('Hello$', 0, searchOptions);
const hello2 = term.searchHelper.findInLine('hello', 0, searchOptions);
const hello3 = term.searchHelper.findInLine('hello$', 0, searchOptions);
const hello4 = term.searchHelper.findInLine('hello', 1, searchOptions);
const hello5 = term.searchHelper.findInLine('hello$', 1, searchOptions);
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
expect(hello1).eql({col: 12, row: 0, term: 'Hello'});
expect(hello2).eql(undefined);
expect(hello3).eql(undefined);
expect(hello4).eql(undefined);
expect(hello5).eql(undefined);
});
});
});
+13 -11
View File
@@ -5,42 +5,44 @@
import { SearchHelper } from './SearchHelper';
import { Terminal } from 'xterm';
import { ISearchAddonTerminal } from './Interfaces';
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.
*/
export function findNext(terminal: Terminal, term: string): boolean {
export function findNext(terminal: Terminal, term: string, searchOptions: ISearchOptions = {}): boolean {
const addonTerminal = <ISearchAddonTerminal>terminal;
if (!addonTerminal.__searchHelper) {
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
}
return addonTerminal.__searchHelper.findNext(term);
return addonTerminal.__searchHelper.findNext(term, searchOptions);
}
/**
* 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.
*/
export function findPrevious(terminal: Terminal, term: string): boolean {
export function findPrevious(terminal: Terminal, term: string, searchOptions: ISearchOptions): boolean {
const addonTerminal = <ISearchAddonTerminal>terminal;
if (!addonTerminal.__searchHelper) {
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
}
return addonTerminal.__searchHelper.findPrevious(term);
return addonTerminal.__searchHelper.findPrevious(term, searchOptions);
}
export function apply(terminalConstructor: typeof Terminal): void {
(<any>terminalConstructor.prototype).findNext = function(term: string): boolean {
return findNext(this, term);
(<any>terminalConstructor.prototype).findNext = function(term: string, searchOptions: ISearchOptions): boolean {
return findNext(this, term, searchOptions);
};
(<any>terminalConstructor.prototype).findPrevious = function(term: string): boolean {
return findPrevious(this, term);
(<any>terminalConstructor.prototype).findPrevious = function(term: string, searchOptions: ISearchOptions): boolean {
return findPrevious(this, term, searchOptions);
};
}
+7 -1
View File
@@ -2,12 +2,18 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"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';
+7 -1
View File
@@ -2,12 +2,18 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"es5"
],
"rootDir": ".",
"outDir": "../../../lib/addons/terminado/",
"sourceMap": true,
"removeComments": true,
"declaration": true,
"preserveWatchOutput": true
"preserveWatchOutput": true,
"types": [
"../../node_modules/@types/mocha"
]
},
"include": [
"**/*.ts",
+8 -1
View File
@@ -2,12 +2,19 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es5",
],
"rootDir": ".",
"outDir": "../../../lib/addons/webLinks/",
"sourceMap": true,
"removeComments": true,
"declaration": true,
"preserveWatchOutput": true
"preserveWatchOutput": true,
"types": [
"../../node_modules/@types/mocha"
]
},
"include": [
"**/*.ts",
+25 -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,28 @@ 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');
});
});
+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 + ')?';
+7 -1
View File
@@ -2,12 +2,18 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"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';
+7 -1
View File
@@ -2,12 +2,18 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"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;
+34 -16
View File
@@ -3,15 +3,15 @@
* @license MIT
*/
import { EventEmitter } from '../EventEmitter';
import { ICircularList } from '../Types';
import { EventEmitter } from './EventEmitter';
import { ICircularList } from './Types';
/**
* Represents a circular list; a list with a maximum size that wraps around when push is called,
* overriding values at the start of the list.
*/
export class CircularList<T> extends EventEmitter implements ICircularList<T> {
protected _array: T[];
protected _array: (T | undefined)[];
private _startIndex: number;
private _length: number;
@@ -36,7 +36,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
// Reconstruct array, starting at index 0. Only transfer values from the
// indexes 0 to length.
const newArray = new Array<T>(newMaxLength);
const newArray = new Array<T | undefined>(newMaxLength);
for (let i = 0; i < Math.min(newMaxLength, this.length); i++) {
newArray[i] = this._array[this._getCyclicIndex(i)];
}
@@ -66,7 +66,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
* @param index The index of the value to get.
* @return The value corresponding to the index.
*/
public get(index: number): T {
public get(index: number): T | undefined {
return this._array[this._getCyclicIndex(index)];
}
@@ -78,7 +78,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
* @param index The index to set.
* @param value The value to set.
*/
public set(index: number, value: T): void {
public set(index: number, value: T | undefined): void {
this._array[this._getCyclicIndex(index)] = value;
}
@@ -90,21 +90,39 @@ 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.
*/
public pop(): T {
public pop(): T | undefined {
return this._array[this._getCyclicIndex(this._length-- - 1)];
}
@@ -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;
}
}
@@ -5,7 +5,7 @@
import { XtermListener } from './Types';
import { IEventEmitter, IDisposable } from 'xterm';
import { Disposable } from './common/Lifecycle';
import { Disposable } from './Lifecycle';
export class EventEmitter extends Disposable implements IEventEmitter, IDisposable {
private _events: {[type: string]: XtermListener[]};
@@ -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 {
@@ -30,14 +30,15 @@ export class EventEmitter extends Disposable implements IEventEmitter, IDisposab
public addDisposableListener(type: string, handler: XtermListener): IDisposable {
// TODO: Rename addDisposableEventListener to more easily disambiguate from Dom listener
this.on(type, handler);
let disposed = false;
return {
dispose: () => {
if (!handler) {
if (disposed) {
// Already disposed
return;
}
this.off(type, handler);
handler = null;
disposed = true;
}
};
}
+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;
}
+19
View File
@@ -3,6 +3,10 @@
* @license MIT
*/
import { IEventEmitter } from 'xterm';
export type XtermListener = (...args: any[]) => void;
/**
* A keyboard event interface which does not depend on the DOM, KeyboardEvent implicitly extends
* this event.
@@ -16,3 +20,18 @@ export interface IKeyboardEvent {
key: string;
type: string;
}
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;
shiftElements(start: number, count: number, offset: 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.
+3 -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 || ''
};
+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/**/*"
]
}
+2 -2
View File
@@ -3,7 +3,8 @@
* @license MIT
*/
import { ITerminal, ICircularList, IBufferLine } from '../Types';
import { ITerminal, IBufferLine } from '../Types';
import { ICircularList } from '../common/Types';
import { C0 } from '../common/data/EscapeSequences';
const enum Direction {
@@ -32,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
+27 -3
View File
@@ -5,10 +5,11 @@
import { IRenderLayer, IColorSet, IRenderDimensions } from './Types';
import { CharData, ITerminal } from '../Types';
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from './atlas/Types';
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, IGlyphIdentifier } from './atlas/Types';
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;
@@ -22,6 +23,19 @@ export abstract class BaseRenderLayer implements IRenderLayer {
protected _charAtlas: BaseCharAtlas;
/**
* An object that's reused when drawing glyphs in order to reduce GC.
*/
private _currentGlyphIdentifier: IGlyphIdentifier = {
chars: '',
code: 0,
bg: 0,
fg: 0,
bold: false,
dim: false,
italic: false
};
constructor(
private _container: HTMLElement,
id: string,
@@ -38,6 +52,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
public dispose(): void {
this._container.removeChild(this._canvas);
if (this._charAtlas) {
this._charAtlas.dispose();
}
}
private _initCanvas(): void {
@@ -245,9 +262,16 @@ export abstract class BaseRenderLayer implements IRenderLayer {
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8 && fg !== INVERTED_DEFAULT_COLOR;
fg += drawInBrightColor ? 8 : 0;
this._currentGlyphIdentifier.chars = chars;
this._currentGlyphIdentifier.code = code;
this._currentGlyphIdentifier.bg = bg;
this._currentGlyphIdentifier.fg = fg;
this._currentGlyphIdentifier.bold = bold && terminal.options.enableBold;
this._currentGlyphIdentifier.dim = dim;
this._currentGlyphIdentifier.italic = italic;
const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
this._ctx,
{chars, code, bg, fg, bold: bold && terminal.options.enableBold, dim, italic},
this._currentGlyphIdentifier,
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop
);
@@ -275,7 +299,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
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 {
+15 -9
View File
@@ -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 {
+1 -1
View File
@@ -10,7 +10,7 @@ import { ColorManager } from './ColorManager';
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions, ICharacterJoinerRegistry } from './Types';
import { ITerminal, CharacterJoinerHandler } from '../Types';
import { LinkRenderLayer } from './LinkRenderLayer';
import { EventEmitter } from '../EventEmitter';
import { EventEmitter } from '../common/EventEmitter';
import { RenderDebouncer } from '../ui/RenderDebouncer';
import { ScreenDprMonitor } from '../ui/ScreenDprMonitor';
import { ITheme } from 'xterm';
+6 -5
View File
@@ -6,9 +6,10 @@
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, NULL_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
@@ -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[];
}
+4 -1
View File
@@ -4,10 +4,13 @@
*/
import { IGlyphIdentifier } from './Types';
import { IDisposable } from 'xterm';
export default abstract class BaseCharAtlas {
export default abstract class BaseCharAtlas implements IDisposable {
private _didWarmUp: boolean = false;
public dispose(): void { }
/**
* Perform any work needed to warm the cache before it can be used. May be called multiple times.
* Implement _doWarmUp instead if you only want to get called once.
+1 -1
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,
@@ -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(
@@ -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;
}
+106 -18
View File
@@ -3,13 +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 '../../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.
@@ -29,14 +29,29 @@ const TRANSPARENT_COLOR = {
// cache.
const FRAME_CACHE_DRAW_LIMIT = 100;
/**
* The number of milliseconds to wait before generating the ImageBitmap, this is to debounce/batch
* the operation as window.createImageBitmap is asynchronous.
*/
const GLYPH_BITMAP_COMMIT_DELAY = 100;
interface IGlyphCacheValue {
index: number;
isEmpty: boolean;
inBitmap: boolean;
}
function getGlyphCacheKey(glyph: IGlyphIdentifier): string {
const styleFlags = (glyph.bold ? 0 : 4) + (glyph.dim ? 0 : 2) + (glyph.italic ? 0 : 1);
return `${glyph.bg}_${glyph.fg}_${styleFlags}${glyph.chars}`;
export function getGlyphCacheKey(glyph: IGlyphIdentifier): number {
// Note that this only returns a valid key when code < 256
// Layout:
// 0b00000000000000000000000000000001: italic (1)
// 0b00000000000000000000000000000010: dim (1)
// 0b00000000000000000000000000000100: bold (1)
// 0b00000000000000000000111111111000: fg (9)
// 0b00000000000111111111000000000000: bg (9)
// 0b00011111111000000000000000000000: code (8)
// 0b11100000000000000000000000000000: unused (3)
return glyph.code << 21 | glyph.bg << 12 | glyph.fg << 3 | (glyph.bold ? 0 : 4) + (glyph.dim ? 0 : 2) + (glyph.italic ? 0 : 1);
}
export default class DynamicCharAtlas extends BaseCharAtlas {
@@ -57,6 +72,15 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
private _drawToCacheCount: number = 0;
// An array of glyph keys that are waiting on the bitmap to be generated.
private _glyphsWaitingOnBitmap: IGlyphCacheValue[] = [];
// The timeout that is used to batch bitmap generation so it's not requested for every new glyph.
private _bitmapCommitTimeout: number | null = null;
// The bitmap to draw from, this is much faster on other browsers than others.
private _bitmap: ImageBitmap | null = null;
constructor(document: Document, private _config: ICharAtlasConfig) {
super();
this._cacheCanvas = document.createElement('canvas');
@@ -82,6 +106,13 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
// document.body.appendChild(this._cacheCanvas);
}
public dispose(): void {
if (this._bitmapCommitTimeout !== null) {
window.clearTimeout(this._bitmapCommitTimeout);
this._bitmapCommitTimeout = null;
}
}
public beginFrame(): void {
this._drawToCacheCount = 0;
}
@@ -92,12 +123,22 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
x: number,
y: number
): boolean {
// Space is always an empty cell, special case this as it's so common
if (glyph.code === 32) {
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;
@@ -124,11 +165,12 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
return glyph.code < 256;
}
private _toCoordinates(index: number): [number, number] {
return [
(index % this._width) * this._config.scaledCharWidth,
Math.floor(index / this._width) * this._config.scaledCharHeight
];
private _toCoordinateX(index: number): number {
return (index % this._width) * this._config.scaledCharWidth;
}
private _toCoordinateY(index: number): number {
return Math.floor(index / this._width) * this._config.scaledCharHeight;
}
private _drawFromCache(
@@ -141,9 +183,10 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
if (cacheValue.isEmpty) {
return;
}
const [cacheX, cacheY] = this._toCoordinates(cacheValue.index);
const cacheX = this._toCoordinateX(cacheValue.index);
const cacheY = this._toCoordinateY(cacheValue.index);
ctx.drawImage(
this._cacheCanvas,
cacheValue.inBitmap ? this._bitmap : this._cacheCanvas,
cacheX,
cacheY,
this._config.scaledCharWidth,
@@ -230,13 +273,58 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
}
// copy the data from imageData to _cacheCanvas
const [x, y] = this._toCoordinates(index);
const x = this._toCoordinateX(index);
const y = this._toCoordinateY(index);
// putImageData doesn't do any blending, so it will overwrite any existing cache entry for us
this._cacheCtx.putImageData(imageData, x, y);
return {
// Add the glyph and queue it to the bitmap (if the browser supports it)
const cacheValue = {
index,
isEmpty
isEmpty,
inBitmap: false
};
this._addGlyphToBitmap(cacheValue);
return cacheValue;
}
private _addGlyphToBitmap(cacheValue: IGlyphCacheValue): void {
// Support is patchy for createImageBitmap at the moment, pass a canvas back
// if support is lacking as drawImage works there too. Firefox is also
// included here as ImageBitmap appears both buggy and has horrible
// performance (tested on v55).
if (!('createImageBitmap' in window) || isFirefox || isSafari) {
return;
}
// Add the glyph to the queue
this._glyphsWaitingOnBitmap.push(cacheValue);
// Check if bitmap generation timeout already exists
if (this._bitmapCommitTimeout !== null) {
return;
}
this._bitmapCommitTimeout = window.setTimeout(() => this._generateBitmap(), GLYPH_BITMAP_COMMIT_DELAY);
}
private _generateBitmap(): void {
const glyphsMovingToBitmap = this._glyphsWaitingOnBitmap;
this._glyphsWaitingOnBitmap = [];
window.createImageBitmap(this._cacheCanvas).then(bitmap => {
// Set bitmap
this._bitmap = bitmap;
// Mark all new glyphs as in bitmap, excluding glyphs that came in after
// the bitmap was requested
for (let i = 0; i < glyphsMovingToBitmap.length; i++) {
const value = glyphsMovingToBitmap[i];
// It doesn't matter if the value was already evicted, it will be
// released from memory after this block if so.
value.inBitmap = true;
}
});
this._bitmapCommitTimeout = null;
}
}
+28 -28
View File
@@ -9,57 +9,57 @@ import LRUMap from './LRUMap';
describe('LRUMap', () => {
it('can be used to store and retrieve values', () => {
const map = new LRUMap(10);
map.set('keya', 'valuea');
map.set('keyb', 'valueb');
map.set('keyc', 'valuec');
assert.strictEqual(map.get('keya'), 'valuea');
assert.strictEqual(map.get('keyb'), 'valueb');
assert.strictEqual(map.get('keyc'), 'valuec');
map.set(1, 'valuea');
map.set(2, 'valueb');
map.set(3, 'valuec');
assert.strictEqual(map.get(1), 'valuea');
assert.strictEqual(map.get(2), 'valueb');
assert.strictEqual(map.get(3), 'valuec');
});
it('maintains a size from insertions', () => {
const map = new LRUMap(10);
assert.strictEqual(map.size, 0);
map.set('a', 'value');
map.set(1, 'value');
assert.strictEqual(map.size, 1);
map.set('b', 'value');
map.set(2, 'value');
assert.strictEqual(map.size, 2);
});
it('deletes the oldest entry when the capacity is exceeded', () => {
const map = new LRUMap(4);
map.set('a', 'value');
map.set('b', 'value');
map.set('c', 'value');
map.set('d', 'value');
map.set('e', 'value');
assert.isNull(map.get('a'));
assert.isNotNull(map.get('b'));
assert.isNotNull(map.get('c'));
assert.isNotNull(map.get('d'));
assert.isNotNull(map.get('e'));
map.set(1, 'value');
map.set(2, 'value');
map.set(3, 'value');
map.set(4, 'value');
map.set(5, 'value');
assert.isNull(map.get(1));
assert.isNotNull(map.get(2));
assert.isNotNull(map.get(3));
assert.isNotNull(map.get(4));
assert.isNotNull(map.get(5));
assert.strictEqual(map.size, 4);
});
it('prevents a recently accessed entry from getting deleted', () => {
const map = new LRUMap(2);
map.set('a', 'value');
map.set('b', 'value');
map.get('a');
map.set(1, 'value');
map.set(2, 'value');
map.get(1);
// a would normally get deleted here, except that we called get()
map.set('c', 'value');
assert.isNotNull(map.get('a'));
map.set(3, 'value');
assert.isNotNull(map.get(1));
// b got deleted instead of a
assert.isNull(map.get('b'));
assert.isNotNull(map.get('c'));
assert.isNull(map.get(2));
assert.isNotNull(map.get(3));
});
it('supports mutation', () => {
const map = new LRUMap(10);
map.set('keya', 'oldvalue');
map.set('keya', 'newvalue');
map.set(1, 'oldvalue');
map.set(1, 'newvalue');
// mutation doesn't change the size
assert.strictEqual(map.size, 1);
assert.strictEqual(map.get('keya'), 'newvalue');
assert.strictEqual(map.get(1), 'newvalue');
});
});
+15 -4
View File
@@ -6,12 +6,12 @@
interface ILinkedListNode<T> {
prev: ILinkedListNode<T>;
next: ILinkedListNode<T>;
key: string;
key: number;
value: T;
}
export default class LRUMap<T> {
private _map: { [key: string]: ILinkedListNode<T> } = {};
private _map: { [key: number]: ILinkedListNode<T> } = {};
private _head: ILinkedListNode<T> = null;
private _tail: ILinkedListNode<T> = null;
private _nodePool: ILinkedListNode<T>[] = [];
@@ -68,7 +68,7 @@ export default class LRUMap<T> {
}
}
public get(key: string): T | null {
public get(key: number): T | null {
// This is unsafe: We're assuming our keyspace doesn't overlap with Object.prototype. However,
// it's faster than calling hasOwnProperty, and in our case, it would never overlap.
const node = this._map[key];
@@ -80,12 +80,23 @@ export default class LRUMap<T> {
return null;
}
/**
* Gets a value from a key without marking it as the most recently used item.
*/
public peekValue(key: number): T | null {
const node = this._map[key];
if (node !== undefined) {
return node.value;
}
return null;
}
public peek(): T | null {
const head = this._head;
return head === null ? null : head.value;
}
public set(key: string, value: T): void {
public set(key: number, value: T): void {
// This is unsafe: See note above.
let node = this._map[key];
if (node !== undefined) {
+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;
}
+64 -24
View File
@@ -4,12 +4,13 @@
*/
import { IRenderer, IRenderDimensions, IColorSet } from '../Types';
import { ITerminal, CharacterJoinerHandler } from '../../Types';
import { ILinkHoverEvent, ITerminal, CharacterJoinerHandler, LinkHoverEventTypes } from '../../Types';
import { ITheme } from 'xterm';
import { EventEmitter } from '../../EventEmitter';
import { EventEmitter } from '../../common/EventEmitter';
import { ColorManager } from '../ColorManager';
import { RenderDebouncer } from '../../ui/RenderDebouncer';
import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, DomRendererRowFactory } from './DomRendererRowFactory';
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';
@@ -79,6 +80,9 @@ export class DomRenderer extends EventEmitter implements IRenderer {
this._terminal.element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass);
this._terminal.screenElement.appendChild(this._rowContainer);
this._terminal.screenElement.appendChild(this._selectionContainer);
this._terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: ILinkHoverEvent) => this._onLinkHover(e));
this._terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: ILinkHoverEvent) => this._onLinkLeave(e));
}
public dispose(): void {
@@ -91,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) {
@@ -119,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 {
@@ -160,13 +167,19 @@ export class DomRenderer extends EventEmitter implements IRenderer {
`}`;
// Cursor
styles +=
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS} {` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS} {` +
` outline: 1px solid ${this.colorManager.colors.cursor.css};` +
` outline-offset: -1px;` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
` background-color: ${this.colorManager.colors.cursor.css};` +
` color: ${this.colorManager.colors.cursorAccent.css};` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS} {` +
` outline: 1px solid #fff;` +
` outline-offset: -1px;` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` +
` box-shadow: 1px 0 0 ${this.colorManager.colors.cursor.css} inset;` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` +
` box-shadow: 0 -1px 0 ${this.colorManager.colors.cursor.css} inset;` +
`}`;
// Selection
styles +=
@@ -187,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;
@@ -281,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;
}
@@ -319,7 +335,8 @@ export class DomRenderer extends EventEmitter implements IRenderer {
const row = y + terminal.buffer.ydisp;
const lineData = terminal.buffer.lines.get(row);
rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorX, terminal.charMeasure.width, terminal.cols));
const cursorStyle = terminal.options.cursorStyle;
rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, this.dimensions.actualCellWidth, terminal.cols));
}
this._terminal.emit('refresh', {start, end});
@@ -331,4 +348,27 @@ export class DomRenderer extends EventEmitter implements IRenderer {
public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return -1; }
public deregisterCharacterJoiner(joinerId: number): boolean { return false; }
private _onLinkHover(e: ILinkHoverEvent): void {
this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, true);
}
private _onLinkLeave(e: ILinkHoverEvent): void {
this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, false);
}
private _setCellUnderline(x: number, x2: number, y: number, y2: number, cols: number, enabled: boolean): void {
while (x !== x2 || y !== y2) {
const row = this._rowElements[y];
if (!row) {
return;
}
const span = <HTMLElement>row.children[x];
span.style.textDecoration = enabled ? 'underline' : 'none';
x = (x + 1) % cols;
if (x === 0) {
y++;
}
}
}
}
+36 -43
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', () => {
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
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>'
''
);
});
@@ -35,24 +35,25 @@ describe('DomRendererRowFactory', () => {
lineData.set(0, [DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)]);
// There should be no element for the following "empty" cell
lineData.set(1, [DEFAULT_ATTR, '', 0, undefined]);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span style="width: 10px;">語</span>'
);
});
it('should add class for cursor', () => {
const fragment = rowFactory.createRow(lineData, true, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-cursor"> </span>' +
'<span> </span>'
);
it('should add class for cursor and cursor style', () => {
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>`
);
}
});
it('should not render cells that go beyond the terminal\'s columns', () => {
lineData.set(0, [DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]);
lineData.set(1, [DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 1);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 1);
assert.equal(getFragmentHtml(fragment),
'<span>a</span>'
);
@@ -61,80 +62,72 @@ describe('DomRendererRowFactory', () => {
describe('attributes', () => {
it('should add class for bold', () => {
lineData.set(0, [DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
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>'
);
});
it('should add class for italic', () => {
lineData.set(0, [DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
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, 0, 5, 20);
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, 0, 5, 20);
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>`
);
}
});
it('should correctly invert colors', () => {
lineData.set(0, [(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
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)]);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
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)]);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
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)]);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
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>`
);
}
});
@@ -148,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;
}
+45 -20
View File
@@ -3,13 +3,17 @@
* @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 } 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';
export const CURSOR_CLASS = 'xterm-cursor';
export const CURSOR_STYLE_BLOCK_CLASS = 'xterm-cursor-block';
export const CURSOR_STYLE_BAR_CLASS = 'xterm-cursor-bar';
export const CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline';
export class DomRendererRowFactory {
constructor(
@@ -17,20 +21,29 @@ export class DomRendererRowFactory {
) {
}
public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorX: number, cellWidth: number, cols: number): DocumentFragment {
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];
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) {
@@ -48,6 +61,18 @@ export class DomRendererRowFactory {
if (isCursorRow && x === cursorX) {
charElement.classList.add(CURSOR_CLASS);
switch (cursorStyle) {
case 'bar':
charElement.classList.add(CURSOR_STYLE_BAR_CLASS);
break;
case 'underline':
charElement.classList.add(CURSOR_STYLE_UNDERLINE_CLASS);
break;
default:
charElement.classList.add(CURSOR_STYLE_BLOCK_CLASS);
break;
}
}
// If inverse flag is on, the foreground should become the background.
@@ -55,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;
}
@@ -76,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
@@ -4,7 +4,7 @@
*/
import { ICharMeasure, ITerminalOptions } from '../Types';
import { EventEmitter } from '../EventEmitter';
import { EventEmitter } from '../common/EventEmitter';
/**
* Utility class that measures the size of a character. Measurements are done in
+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;
+8 -8
View File
@@ -37,34 +37,34 @@ 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);
assert.equal(mouseHelper.getCoords({ pageX: 0, pageY: 0 }, document.createElement('div'), charMeasure, 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({ pageX: undefined, pageY: undefined }, 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({ pageX: CHAR_WIDTH / 2, pageY: 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({ pageX: CHAR_WIDTH, pageY: 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({ pageX: CHAR_WIDTH, pageY: 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({ pageX: CHAR_WIDTH + 1, pageY: 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({ pageX: -1, pageY: -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({ pageX: CHAR_WIDTH * 20, pageY: CHAR_HEIGHT * 20 }, document.createElement('div'), charMeasure, 10, 10);
assert.deepEqual(coords, [10, 10], 'coordinates should never come back as larger than the terminal');
});
});
+7 -3
View File
@@ -9,6 +9,10 @@ import { IRenderer } from '../renderer/Types';
export class MouseHelper {
constructor(private _renderer: IRenderer) {}
public setRenderer(renderer: IRenderer): void {
this._renderer = renderer;
}
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) {
@@ -48,7 +52,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: {pageX: number, pageY: 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 +85,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];
+23 -5
View File
@@ -4,10 +4,19 @@
*/
import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types';
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener, CharacterJoinerHandler, IBufferLine } from '../Types';
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferLine, IBufferStringIterator } from '../Types';
import { ICircularList, XtermListener } from '../common/Types';
import { Buffer } from '../Buffer';
import * as Browser from '../shared/utils/Browser';
import * as Browser from '../core/Platform';
import { ITheme, IDisposable, IMarker } from 'xterm';
import { Terminal } from '../Terminal';
export class TestTerminal extends Terminal {
writeSync(data: string): void {
this.writeBuffer.push(data);
this._innerWrite();
}
}
export class MockTerminal implements ITerminal {
markers: IMarker[];
@@ -112,7 +121,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 {
@@ -173,7 +182,6 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
wraparoundMode: boolean;
bracketedPasteMode: boolean;
curAttr: number;
savedCurAttr: number;
savedCols: number;
x10Mouse: boolean;
vt200Mouse: boolean;
@@ -295,11 +303,12 @@ 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);
}
getWrappedRangeForLine(y: number): { first: number; last: number; } {
throw new Error('Method not implemented.');
return Buffer.prototype.getWrappedRangeForLine.apply(this, arguments);
}
nextStop(x?: number): number {
throw new Error('Method not implemented.');
@@ -310,6 +319,15 @@ 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);
}
iterator(trimRight: boolean, startIndex?: number, endIndex?: number): IBufferStringIterator {
return Buffer.prototype.iterator.apply(this, arguments);
}
}
export class MockRenderer implements IRenderer {
+1 -1
View File
@@ -139,7 +139,7 @@
cursor: pointer;
}
.xterm.xterm-cursor-crosshair {
.xterm.column-select.focus {
/* Column selection mode */
cursor: crosshair;
}
+1
View File
@@ -5,6 +5,7 @@
"lib": [
"dom",
"es5",
"es6",
"scripthost",
"es2015.promise"
],

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