Compare commits

...
257 Commits
Author SHA1 Message Date
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 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
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 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
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 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 e736f1223f Merge pull request #1696 from Tyriar/posttest
Remove npm-run-all
2018-09-18 09:29:45 -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
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 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 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 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 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 4eede85ef6 fixes #1667 2018-09-08 17:26:19 +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 27c3663470 Bump version to 3.7.0 2018-09-08 02:38:05 -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
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
Daniel Imms 7c1b1e2eb8 Fix strict null checks in ./common
Part of #1319
2018-09-04 09:56:29 -07: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
Daniel Imms 92341c3e7b Merge branch 'master' into webpack_demo 2018-09-04 06:56:06 -07: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
jerch 3a310f1597 Merge pull request #1649 from jerch/fix_search_addon
Fix broken search addon
2018-09-02 22:00:25 +02:00
Jörg Breitbart 4d0217f816 Merge branch 'fix_search_addon' of https://github.com/jerch/xterm.js into fix_search_addon 2018-09-02 21:46:44 +02:00
Jörg Breitbart 559e932a2e remove comment and setTimeout 2018-09-02 21:46:20 +02:00
Jörg Breitbart aa427010e1 Merge branch 'master' into fix_search_addon 2018-09-02 21:38:42 +02:00
jerch 1aec57e8c0 Merge pull request #1645 from jerch/fix_convertEol
fix convertEol option
2018-09-02 21:19:19 +02:00
peacefullatom cbbf6bd654 Merge branch 'master' into add-tests-for-events 2018-09-02 22:50:45 +05:00
Daniel Imms 33b65bbfaa Merge branch 'master' into fix_convertEol 2018-09-02 10:42:02 -07:00
Daniel Imms 8e39650b5f Merge branch 'master' into fix_search_addon 2018-09-02 10:34:22 -07:00
Daniel Imms f148238117 Merge pull request #1638 from Tyriar/duplicate_tslint
Remove duplicate triple-equals rule
2018-09-02 10:32:23 -07:00
Jörg Breitbart 6057586ff3 fixes #1647 2018-09-02 17:52:56 +02: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
Daniel Imms 6a2c4b9823 Merge branch 'master' into duplicate_tslint 2018-09-01 09:58:58 -07:00
Daniel Imms e2cfd94005 Merge pull request #1607 from Tyriar/444_viewport_sync
Only sync viewport in an animation frame
2018-09-01 09:58:34 -07:00
Daniel Imms 9fe8ea8de2 Merge branch 'master' into duplicate_tslint 2018-09-01 09:50:57 -07:00
Daniel Imms 46311b87a2 Merge pull request #1602 from xtermjs/Tyriar-patch-1
Add link to running the demo from CONTRIBUTING.md
2018-09-01 09:48:31 -07:00
Daniel Imms 692ca1c9a3 Merge branch 'master' into duplicate_tslint 2018-09-01 09:47:26 -07:00
Daniel Imms bf5c1f0f78 Merge branch 'master' into 444_viewport_sync 2018-09-01 09:47:09 -07:00
Daniel Imms 467a93c8d9 Fix typo/grammar/line width 2018-09-01 09:34:01 -07:00
Daniel Imms 3841edadb5 Merge pull request #1644 from Tyriar/1642-disposable-jsdoc
Document addDisposableListener
2018-08-31 21:19:41 -07:00
Jörg Breitbart dc332480cc fix convertEol option 2018-08-31 23:02:05 +02:00
Daniel Imms a194263183 Document addDisposableListener
Part of #1642
2018-08-31 12:02:54 -07:00
jerch dd7081f948 Merge pull request #1632 from jerch/buffer_preparations
buffer redesign - `BufferLine` implementation and cleanups
2018-08-31 01:49:05 +02:00
Daniel Imms 999fce4e52 Remove test only functions from InputHandler.s 2018-08-30 16:22:30 -07:00
Jörg Breitbart 0d49d1f64d set DEFAULT_CELL explicit in ctor and remove the global const value 2018-08-30 21:12:55 +02:00
Jörg Breitbart 11df1e1d80 make sure DEFAULT_CELL gets not overwritten by accident 2018-08-30 21:03:30 +02:00
Jörg Breitbart a74ca25b09 cleanup spaces and comments in InputHandler 2018-08-30 20:29:23 +02:00
Jörg Breitbart 32231dcd41 remove static DefaultCell 2018-08-30 20:27:52 +02:00
Jörg Breitbart 04a847f0e2 merge with master 2018-08-30 18:12:48 +02:00
Jörg Breitbart 276e65fa0b move erase methods to InputHandler 2018-08-30 18:10:18 +02:00
Jörg Breitbart a1421b151e cleanup BufferLine.ts 2018-08-30 17:58:37 +02:00
Jörg Breitbart 71f5965f8e move to BufferLine.ts 2018-08-30 17:55:04 +02:00
Jörg Breitbart 59fc4af9a0 IBufferLine interface 2018-08-30 17:52:45 +02:00
Daniel Imms 381e6c1740 Move defaultCell to an exported member 2018-08-30 07:57:03 -07:00
Jörg Breitbart 9a54cf639b Merge branch 'master' into buffer_preparations 2018-08-29 23:19:14 +02:00
Jörg Breitbart 01a4ec21da make linter happy 2018-08-29 22:41:24 +02:00
Jörg Breitbart a65bd1a77c Merge branch 'buffer_preparations' of https://github.com/jerch/xterm.js into buffer_preparations 2018-08-29 22:37:19 +02:00
Jörg Breitbart 6439f6e856 regression tests for changes in InputHandler 2018-08-29 22:37:01 +02:00
Daniel Imms d24caf14a4 Merge branch 'master' into 444_viewport_sync 2018-08-29 10:27:20 -07:00
Daniel Imms 52f132a570 Clean up buffer tests 2018-08-28 14:29:18 -07:00
Jörg Breitbart 424d3257d1 set conditional blocks in brackets 2018-08-28 02:34:46 +02:00
Daniel Imms f47f095b89 Merge pull request #1636 from peacefullatom/fix-crosshair
Fix crosshair
2018-08-27 10:22:53 -07:00
Jörg Breitbart de26be56c1 change type of IPartialLineData 2018-08-27 17:11:40 +02:00
Jörg Breitbart f964a34b34 explicit member visibility 2018-08-27 17:01:30 +02:00
Jörg Breitbart 6576d513b0 fix index access to cell in winptyCompat 2018-08-27 16:53:58 +02:00
Daniel Imms 4af2d1ed41 Remove duplicate triple-equals rule 2018-08-27 07:26:00 -07:00
peacefullatom 9e27c231e5 Merge branch 'master' into fix-crosshair 2018-08-27 12:04:23 +05:00
Daniel Imms ae3684c897 Merge pull request #1634 from angersson/master
Add shellvault.io to README.md
2018-08-26 17:52:19 -07:00
Jörg Breitbart d4c621a182 more TerminalLine tests 2018-08-26 19:59:16 +02:00
Jörg Breitbart 9c68eb3b11 move line content functions to TerminalLine 2018-08-26 19:24:49 +02:00
Jörg Breitbart 77cb697990 remove <any> casts for .isWrapped 2018-08-26 16:04:52 +02:00
Jörg Breitbart cee432a66a TerminalLine ctor parameters; tests 2018-08-26 15:50:00 +02:00
Jörg Breitbart a553415216 remove blankLine from Terminal 2018-08-26 14:35:28 +02:00
Jörg Breitbart cd8477a942 move blankLine to TerminalLine 2018-08-26 14:27:38 +02:00
Austin Anderson 811b3cc326 Add Shellvault to readme 2018-08-25 21:36:58 -07:00
Jörg Breitbart a99cac45f7 fix import errors 2018-08-25 22:56:15 +02:00
Jörg Breitbart 3d34881c4e rebase to master 2018-08-25 22:43:15 +02:00
Jörg Breitbart 0340d91150 first step to buffer redesign: class TerminalLine; discourage low level index access to cells 2018-08-25 22:41:42 +02:00
Jörg Breitbart 5c3315251b first step to buffer redesign: class TerminalLine; discourage low level index access to cells 2018-08-25 20:58:50 +02:00
Daniel Imms e769e787a8 Merge pull request #1626 from peacefullatom/fix-underline-color
#957 Link underline should be the same as FG color
2018-08-24 14:49:40 -07:00
peacefullatom 96ed32698b fix 1629 Crosshair cursor doesn't go away after hitting alt+tab 2018-08-25 00:06:44 +05:00
peacefullatom e66928da20 Merge branch 'fix-underline-color' of https://github.com/peacefullatom/xterm.js into fix-underline-color 2018-08-24 22:48:50 +05:00
peacefullatom c762885c4b Merge branch 'master' of https://github.com/xtermjs/xterm.js into fix-underline-color 2018-08-24 22:36:12 +05:00
Yuriy Markov 63ced6f302 Merge branch 'master' into fix-underline-color 2018-08-24 22:35:39 +05:00
Daniel Imms 82f6547f10 Merge pull request #1628 from princjef/triple-equals
Enforce strict equality through tslint
2018-08-24 07:49:30 -07:00
Jeff Principe 841a0e010d Merge branch 'master' into triple-equals 2018-08-23 20:59:30 -07:00
Yuriy Markov 85f25e77f5 Merge branch 'master' into fix-underline-color 2018-08-24 01:46:30 +05:00
Daniel Imms 2adaed87d1 Merge pull request #1624 from princjef/first-line-render
Only render first line when requested
2018-08-23 09:53:33 -07:00
peacefullatom 4c93d8ddaa Merge branch 'master' into fix-underline-color 2018-08-23 16:25:32 +05:00
Jeff Principe b35fa9576e Enforce strict equality through tslint 2018-08-22 19:36:40 -07:00
Jeff Principe 091c42f7e6 only render first line when requested 2018-08-22 19:10:56 -07:00
peacefullatom 6601133912 #957 Link underline should be the same as FG color 2018-08-23 04:24:10 +05:00
Daniel Imms 9273e8175e Merge pull request #1621 from xtermjs/Tyriar-patch-2
Fix license
2018-08-21 15:59:27 -07:00
Daniel Imms cd75d6fb68 Merge pull request #1622 from Tyriar/1620_row_render
Prevent whole viewport from refreshing
2018-08-21 15:59:13 -07:00
Daniel Imms 01bfa1df4b Merge pull request #1619 from meefik/patch-1
Bug with receive the result from FileReader
2018-08-21 15:34:40 -07:00
Daniel Imms 123e074c16 Merge branch 'master' into 1620_row_render 2018-08-21 15:15:42 -07:00
Daniel Imms 929150a8e1 Prevent whole viewport from refreshing
Fixes #1620
2018-08-21 15:13:27 -07:00
Daniel Imms e3eb1b81b3 Fix license 2018-08-21 12:29:46 -07:00
Anton Skshidlevsky 04c4448f97 Bug with receive the result from FileReader
Incorrect variable "this" must be replace to fileReader.
2018-08-18 15:36:51 +03:00
Daniel Imms 0e45909c7e Merge pull request #1613 from shroudedcode/support-not-italic
Add support for the "Not italic" escape code
2018-08-14 11:33:39 -07:00
Niklas Higi 44366ffd3c Add support for the "Not italic" escape code 2018-08-14 12:29:12 +02:00
Daniel Imms 49cf1a2cea Merge pull request #1610 from princjef/typings-no-emit
Stop typescript from emitting output files in typings test
2018-08-10 19:47:18 -07:00
Jeff Principe 14d75364eb Stop typescript from emitting output files in typings test 2018-08-09 19:44:25 -07:00
Daniel Imms 2127128ef5 Only sync viewport in an animation frame
Fixes #444
2018-08-09 12:19:37 -07:00
Daniel Imms 95ebba6fdb Add link to running the demo from CONTRIBUTING.md 2018-08-07 19:32:30 -07:00
73 changed files with 3458 additions and 1173 deletions
+2 -1
View File
@@ -23,7 +23,7 @@ opening an issue, read these pointers.
- Mention precisely what went wrong. What did you expect to happen? What happened instead? Describe the
exact steps a maintainer has to take to make the problem occur.
- If the problem can not be reproduced in the [demo of xterm.js](README.md#demo), please provide an HTML document that demonstrates the problem.
- If the problem can not be reproduced in the [demo of xterm.js](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo), please provide an HTML document that demonstrates the problem.
- Be polite. Issues with an indignant or belligerent tone tend to be moved to the
bottom of the pile.
@@ -33,6 +33,7 @@ opening an issue, read these pointers.
- Make sure you have a [GitHub account](https://github.com/join)
- Fork [xterm.js](https://github.com/sourcelair/xterm.js/)
([how to fork a repo](https://help.github.com/articles/fork-a-repo))
- Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running
- Make your changes
- If your changes are easy to test or likely to regress, add tests. Tests go into `test`, directory.
- Follow the general code style of the rest of the project (see below).
+1
View File
@@ -1,3 +1,4 @@
Copyright (c) 2017-2018, The xterm.js authors (https://github.com/xtermjs/xterm.js)
Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com)
Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)
+3 -1
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)
@@ -169,6 +169,8 @@ computational environment for Jupyter, supporting interactive data science and s
- [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice.
- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js
- [**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.
[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'
+91 -68
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'],
@@ -186,9 +209,9 @@ function initOptions(term) {
fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
rendererType: ['dom', 'canvas']
};
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 +227,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 +241,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 +269,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 +277,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>
+2 -6
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) {
+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"
]
}
+6 -1
View File
@@ -4,6 +4,11 @@
],
"compilerOptions": {
"module": "commonjs",
"target": "es5"
"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']);
+12 -9
View File
@@ -1,7 +1,7 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
"version": "3.6.0",
"version": "3.8.0",
"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,25 @@
"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.0",
"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'",
"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",
@@ -61,6 +63,7 @@
"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\""
}
}
+249 -50
View File
@@ -3,11 +3,12 @@
* @license MIT
*/
import { assert } from 'chai';
import { assert, expect } from 'chai';
import { ITerminal } from './Types';
import { Buffer } 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;
const INIT_ROWS = 24;
@@ -36,13 +37,13 @@ describe('Buffer', () => {
describe('fillViewportRows', () => {
it('should fill the buffer with blank lines based on the size of the viewport', () => {
const blankLineChar = terminal.blankLine()[0];
const blankLineChar = BufferLine.blankLine(terminal.cols, DEFAULT_ATTR).get(0);
buffer.fillViewportRows();
assert.equal(buffer.lines.length, INIT_ROWS);
for (let y = 0; y < INIT_ROWS; y++) {
assert.equal(buffer.lines.get(y).length, INIT_COLS);
for (let x = 0; x < INIT_COLS; x++) {
assert.deepEqual(buffer.lines.get(y)[x], blankLineChar);
assert.deepEqual(buffer.lines.get(y).get(x), blankLineChar);
}
}
});
@@ -66,40 +67,40 @@ describe('Buffer', () => {
describe('wrapped', () => {
it('should return a range for the first row', () => {
buffer.fillViewportRows();
(<any> buffer.lines.get(1)).isWrapped = true;
buffer.lines.get(1).isWrapped = true;
assert.deepEqual(buffer.getWrappedRangeForLine(0), { first: 0, last: 1 });
});
it('should return a range for a middle row wrapping upwards', () => {
buffer.fillViewportRows();
(<any> buffer.lines.get(12)).isWrapped = true;
buffer.lines.get(12).isWrapped = true;
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 11, last: 12 });
});
it('should return a range for a middle row wrapping downwards', () => {
buffer.fillViewportRows();
(<any> buffer.lines.get(13)).isWrapped = true;
buffer.lines.get(13).isWrapped = true;
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 12, last: 13 });
});
it('should return a range for a middle row wrapping both ways', () => {
buffer.fillViewportRows();
(<any> buffer.lines.get(11)).isWrapped = true;
(<any> buffer.lines.get(12)).isWrapped = true;
(<any> buffer.lines.get(13)).isWrapped = true;
(<any> buffer.lines.get(14)).isWrapped = true;
buffer.lines.get(11).isWrapped = true;
buffer.lines.get(12).isWrapped = true;
buffer.lines.get(13).isWrapped = true;
buffer.lines.get(14).isWrapped = true;
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 10, last: 14 });
});
it('should return a range for the last row', () => {
buffer.fillViewportRows();
(<any> buffer.lines.get(23)).isWrapped = true;
buffer.lines.get(23).isWrapped = true;
assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 1), { first: 22, last: 23 });
});
it('should return a range for a row that wraps upward to first row', () => {
buffer.fillViewportRows();
(<any> buffer.lines.get(1)).isWrapped = true;
buffer.lines.get(1).isWrapped = true;
assert.deepEqual(buffer.getWrappedRangeForLine(1), { first: 0, last: 1 });
});
it('should return a range for a row that wraps downward to last row', () => {
buffer.fillViewportRows();
(<any> buffer.lines.get(buffer.lines.length - 1)).isWrapped = true;
buffer.lines.get(buffer.lines.length - 1).isWrapped = true;
assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 2), { first: 22, last: 23 });
});
});
@@ -154,11 +155,11 @@ describe('Buffer', () => {
assert.equal(buffer.lines.maxLength, INIT_ROWS);
buffer.y = INIT_ROWS - 1;
buffer.fillViewportRows();
buffer.lines.get(5)[0][1] = 'a';
buffer.lines.get(INIT_ROWS - 1)[0][1] = 'b';
buffer.lines.get(5).get(0)[1] = 'a';
buffer.lines.get(INIT_ROWS - 1).get(0)[1] = 'b';
buffer.resize(INIT_COLS, INIT_ROWS - 5);
assert.equal(buffer.lines.get(0)[0][1], 'a');
assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5)[0][1], 'b');
assert.equal(buffer.lines.get(0).get(0)[1], 'a');
assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5).get(0)[1], 'b');
});
});
});
@@ -179,7 +180,7 @@ describe('Buffer', () => {
buffer.fillViewportRows();
// Create 10 extra blank lines
for (let i = 0; i < 10; i++) {
buffer.lines.push(terminal.blankLine());
buffer.lines.push(BufferLine.blankLine(terminal.cols, DEFAULT_ATTR));
}
// Set cursor to the bottom of the buffer
buffer.y = INIT_ROWS - 1;
@@ -199,7 +200,7 @@ describe('Buffer', () => {
buffer.fillViewportRows();
// Create 10 extra blank lines
for (let i = 0; i < 10; i++) {
buffer.lines.push(terminal.blankLine());
buffer.lines.push(BufferLine.blankLine(terminal.cols, DEFAULT_ATTR));
}
// Set cursor to the bottom of the buffer
buffer.y = INIT_ROWS - 1;
@@ -272,34 +273,34 @@ describe('Buffer', () => {
describe ('translateBufferLineToString', () => {
it('should handle selecting a section of ascii text', () => {
buffer.lines.set(0, [
[ null, 'a', 1, 'a'.charCodeAt(0)],
[ null, 'b', 1, 'b'.charCodeAt(0)],
[ null, 'c', 1, 'c'.charCodeAt(0)],
[ null, 'd', 1, 'd'.charCodeAt(0)]
]);
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)]);
buffer.lines.set(0, line);
const str = buffer.translateBufferLineToString(0, true, 0, 2);
assert.equal(str, 'ab');
});
it('should handle a cut-off double width character by including it', () => {
buffer.lines.set(0, [
[ null, '語', 2, 35486 ],
[ null, '', 0, null],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
const line = new BufferLine();
line.push([ null, '語', 2, 35486 ]);
line.push([ null, '', 0, null]);
line.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
buffer.lines.set(0, line);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
assert.equal(str1, '語');
});
it('should handle a zero width character in the middle of the string by not including it', () => {
buffer.lines.set(0, [
[ null, '語', 2, '語'.charCodeAt(0) ],
[ null, '', 0, null],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
const line = new BufferLine();
line.push([ null, '語', 2, '語'.charCodeAt(0) ]);
line.push([ null, '', 0, null]);
line.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
buffer.lines.set(0, line);
const str0 = buffer.translateBufferLineToString(0, true, 0, 1);
assert.equal(str0, '語');
@@ -312,10 +313,10 @@ describe('Buffer', () => {
});
it('should handle single width emojis', () => {
buffer.lines.set(0, [
[ null, '😁', 1, '😁'.charCodeAt(0) ],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
const line = new BufferLine();
line.push([ null, '😁', 1, '😁'.charCodeAt(0) ]);
line.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
buffer.lines.set(0, line);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
assert.equal(str1, '😁');
@@ -325,10 +326,10 @@ describe('Buffer', () => {
});
it('should handle double width emojis', () => {
buffer.lines.set(0, [
[ null, '😁', 2, '😁'.charCodeAt(0) ],
[ null, '', 0, null]
]);
const line = new BufferLine();
line.push([ null, '😁', 2, '😁'.charCodeAt(0) ]);
line.push([ null, '', 0, null]);
buffer.lines.set(0, line);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
assert.equal(str1, '😁');
@@ -336,14 +337,212 @@ describe('Buffer', () => {
const str2 = buffer.translateBufferLineToString(0, true, 0, 2);
assert.equal(str2, '😁');
buffer.lines.set(0, [
[ null, '😁', 2, '😁'.charCodeAt(0) ],
[ null, '', 0, null],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
const line2 = new BufferLine();
line2.push([ null, '😁', 2, '😁'.charCodeAt(0) ]);
line2.push([ null, '', 0, null]);
line2.push([ 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();
});
});
});
+105 -12
View File
@@ -4,9 +4,10 @@
*/
import { CircularList } from './common/CircularList';
import { LineData, CharData, ITerminal, IBuffer } from './Types';
import { EventEmitter } from './EventEmitter';
import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from './Types';
import { EventEmitter } from './common/EventEmitter';
import { IMarker } from 'xterm';
import { BufferLine } from './BufferLine';
export const DEFAULT_ATTR = (0 << 18) | (257 << 9) | (256 << 0);
export const CHAR_DATA_ATTR_INDEX = 0;
@@ -27,7 +28,7 @@ export const NULL_CELL_CODE = 32;
* - scroll position
*/
export class Buffer implements IBuffer {
public lines: CircularList<LineData>;
public lines: CircularList<IBufferLine>;
public ydisp: number;
public ybase: number;
public y: number;
@@ -84,7 +85,7 @@ export class Buffer implements IBuffer {
if (this.lines.length === 0) {
let i = this._terminal.rows;
while (i--) {
this.lines.push(this._terminal.blankLine());
this.lines.push(BufferLine.blankLine(this._terminal.cols, DEFAULT_ATTR));
}
}
}
@@ -97,7 +98,7 @@ export class Buffer implements IBuffer {
this.ybase = 0;
this.y = 0;
this.x = 0;
this.lines = new CircularList<LineData>(this._getCorrectBufferLength(this._terminal.rows));
this.lines = new CircularList<IBufferLine>(this._getCorrectBufferLength(this._terminal.rows));
this.scrollTop = 0;
this.scrollBottom = this._terminal.rows - 1;
this.setupTabStops();
@@ -146,7 +147,7 @@ export class Buffer implements IBuffer {
} else {
// Add a blank line if there is no buffer left at the top to scroll to, or if there
// are blank lines after the cursor
this.lines.push(this._terminal.blankLine(undefined, undefined, newCols));
this.lines.push(BufferLine.blankLine(newCols, DEFAULT_ATTR));
}
}
}
@@ -193,6 +194,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
@@ -223,7 +254,7 @@ export class Buffer implements IBuffer {
let endIndex = endCol;
for (let i = 0; i < line.length; i++) {
const char = line[i];
const char = line.get(i);
lineString += char[CHAR_DATA_CHAR_INDEX];
// Adjust start and end cols for wide characters if they affect their
// column indexes
@@ -268,11 +299,11 @@ export class Buffer implements IBuffer {
let first = y;
let last = y;
// Scan upwards for wrapped lines
while (first > 0 && (<any>this.lines.get(first)).isWrapped) {
while (first > 0 && this.lines.get(first).isWrapped) {
first--;
}
// Scan downwards for wrapped lines
while (last + 1 < this.lines.length && (<any>this.lines.get(last + 1)).isWrapped) {
while (last + 1 < this.lines.length && this.lines.get(last + 1).isWrapped) {
last++;
}
return { first, last };
@@ -283,7 +314,7 @@ export class Buffer implements IBuffer {
* @param i The index to start setting up tab stops from.
*/
public setupTabStops(i?: number): void {
if (i != null) {
if (i !== null && i !== undefined) {
if (!this.tabs[i]) {
i = this.prevStop(i);
}
@@ -302,7 +333,7 @@ export class Buffer implements IBuffer {
* @param x The position to move the cursor to the previous tab stop.
*/
public prevStop(x?: number): number {
if (x == null) {
if (x === null || x === undefined) {
x = this.x;
}
while (!this.tabs[--x] && x > 0);
@@ -314,7 +345,7 @@ export class Buffer implements IBuffer {
* @param x The position to move the cursor one tab stop forward.
*/
public nextStop(x?: number): number {
if (x == null) {
if (x === null || x === undefined) {
x = this.x;
}
while (!this.tabs[++x] && x < this._terminal.cols);
@@ -339,6 +370,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 {
@@ -365,3 +400,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};
}
}
+97
View File
@@ -0,0 +1,97 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import * as chai from 'chai';
import { BufferLine } from './BufferLine';
import { CharData, IBufferLine } from './Types';
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, CHAR_DATA_ATTR_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer';
class TestBufferLine extends BufferLine {
public toArray(): CharData[] {
return this._data;
}
}
describe('BufferLine', function(): void {
it('ctor', function(): void {
let line: IBufferLine = new TestBufferLine();
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.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.isWrapped).equals(true);
line = new TestBufferLine(10, [123, 'a', 456, 789], true);
chai.expect(line.length).equals(10);
chai.expect(line.pop()).eql([123, 'a', 456, 789]);
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]]);
});
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]]);
});
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]]);
});
});
+83
View File
@@ -0,0 +1,83 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { CharData, IBufferLine } from './Types';
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer';
/**
* Class representing a terminal line.
*/
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);
}
protected _data: CharData[];
public isWrapped = false;
public length: number;
constructor(cols?: number, ch?: 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 (isWrapped) {
this.isWrapped = true;
}
}
public get(index: number): CharData {
return this._data[index];
}
public set(index: number, data: CharData): void {
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();
}
}
/** delete n cells at pos, right side is filled with fill (stable length) */
public deleteCells(pos: number, n: number, fill: CharData): void {
while (n--) {
this.splice(pos, 1);
this.push(fill);
}
}
/** replace cells from pos to pos + n - 1 with fill */
public replaceCells(start: number, end: number, fill: CharData): void {
while (start < end && start < this.length) {
this.set(start++, fill); // Note: fill is not cloned (resembles old behavior)
}
}
}
+1 -1
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
+79
View File
@@ -0,0 +1,79 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { TestTerminal } from './utils/TestUtils.test';
import { assert } from 'chai';
import { getStringCellWidth } 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
});
+22
View File
@@ -169,3 +169,25 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
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;
for (let i = 0; i < s.length; ++i) {
let code = s.charCodeAt(i);
if (0xD800 <= code && code <= 0xDBFF) {
const low = s.charCodeAt(i + 1);
if (isNaN(low)) {
return result;
}
code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
}
if (0xDC00 <= code && code <= 0xDFFF) {
continue;
}
result += wcwidth(code);
}
return result;
}
+392 -1
View File
@@ -3,9 +3,102 @@
* @license MIT
*/
import { assert } from 'chai';
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 { Terminal } from './Terminal';
import { IBufferLine } from './Types';
// TODO: This and the sections related to this object in associated tests can be
// removed safely after InputHandler refactors are finished
class OldInputHandler extends InputHandler {
public eraseInLine(params: number[]): void {
switch (params[0]) {
case 0:
this.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
break;
case 1:
this.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y);
break;
case 2:
this.eraseLine(this._terminal.buffer.y);
break;
}
}
public eraseInDisplay(params: number[]): void {
let j;
switch (params[0]) {
case 0:
this.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
j = this._terminal.buffer.y + 1;
for (; j < this._terminal.rows; j++) {
this.eraseLine(j);
}
break;
case 1:
this.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y);
j = this._terminal.buffer.y;
while (j--) {
this.eraseLine(j);
}
break;
case 2:
j = this._terminal.rows;
while (j--) this.eraseLine(j);
break;
case 3:
// Clear scrollback (everything not in viewport)
const scrollBackSize = this._terminal.buffer.lines.length - this._terminal.rows;
if (scrollBackSize > 0) {
this._terminal.buffer.lines.trimStart(scrollBackSize);
this._terminal.buffer.ybase = Math.max(this._terminal.buffer.ybase - scrollBackSize, 0);
this._terminal.buffer.ydisp = Math.max(this._terminal.buffer.ydisp - scrollBackSize, 0);
// Force a scroll event to refresh viewport
this._terminal.emit('scroll', 0);
}
break;
}
}
/**
* Erase in the identified line everything from "x" to the end of the line (right).
* @param x The column from which to start erasing to the end of the line.
* @param y The line in which to operate.
*/
public eraseRight(x: number, y: number): void {
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y);
if (!line) {
return;
}
line.replaceCells(x, this._terminal.cols, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
this._terminal.updateRange(y);
}
/**
* Erase in the identified line everything from "x" to the start of the line (left).
* @param x The column from which to start erasing to the start of the line.
* @param y The line in which to operate.
*/
public eraseLeft(x: number, y: number): void {
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y);
if (!line) {
return;
}
line.replaceCells(0, x + 1, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
this._terminal.updateRange(y);
}
/**
* Erase all content in the given line
* @param y The line to erase all of its contents.
*/
public eraseLine(y: number): void {
this.eraseRight(0, y);
}
}
describe('InputHandler', () => {
describe('save and restore cursor', () => {
@@ -84,4 +177,302 @@ describe('InputHandler', () => {
assert.equal(terminal.bracketedPasteMode, false);
});
});
describe('regression tests', function(): void {
type CharData = [number, string, number, number];
function lineContent(line: IBufferLine): string {
let content = '';
for (let i = 0; i < line.length; ++i) content += line.get(i)[CHAR_DATA_CHAR_INDEX];
return content;
}
function termContent(term: Terminal): string[] {
const result = [];
for (let i = 0; i < term.rows; ++i) result.push(lineContent(term.buffer.lines.get(i)));
return result;
}
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();
}
}
// insert some data in first and second line
inputHandler.parse(Array(term.cols - 9).join('a'));
inputHandler.parse('1234567890');
inputHandler.parse(Array(term.cols - 9).join('a'));
inputHandler.parse('1234567890');
const line1: IBufferLine = term.buffer.lines.get(0); // line for old variant
const line2: IBufferLine = term.buffer.lines.get(1); // line for new variant
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '1234567890');
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '1234567890');
// insert one char from params = [0]
term.buffer.y = 0;
term.buffer.x = 70;
insertChars([0]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 123456789');
term.buffer.y = 1;
term.buffer.x = 70;
inputHandler.insertChars([0]);
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 123456789');
expect(lineContent(line2)).equals(lineContent(line1));
// insert one char from params = [1]
term.buffer.y = 0;
term.buffer.x = 70;
insertChars([1]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 12345678');
term.buffer.y = 1;
term.buffer.x = 70;
inputHandler.insertChars([1]);
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 12345678');
expect(lineContent(line2)).equals(lineContent(line1));
// insert two chars from params = [2]
term.buffer.y = 0;
term.buffer.x = 70;
insertChars([2]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 123456');
term.buffer.y = 1;
term.buffer.x = 70;
inputHandler.insertChars([2]);
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 123456');
expect(lineContent(line2)).equals(lineContent(line1));
// insert 10 chars from params = [10]
term.buffer.y = 0;
term.buffer.x = 70;
insertChars([10]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' ');
term.buffer.y = 1;
term.buffer.x = 70;
inputHandler.insertChars([10]);
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' ');
expect(lineContent(line2)).equals(lineContent(line1));
});
it('deleteChars', function(): void {
const term = new Terminal();
const inputHandler = new InputHandler(term);
// old variant of the method
function deleteChars(params: number[]): void {
let param: number = params[0];
if (param < 1) {
param = 1;
}
// make buffer local for faster access
const buffer = term.buffer;
const row = buffer.y + buffer.ybase;
const ch: CharData = [term.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm
while (param--) {
buffer.lines.get(row).splice(buffer.x, 1);
buffer.lines.get(row).push(ch);
}
term.updateRange(buffer.y);
}
// insert some data in first and second line
inputHandler.parse(Array(term.cols - 9).join('a'));
inputHandler.parse('1234567890');
inputHandler.parse(Array(term.cols - 9).join('a'));
inputHandler.parse('1234567890');
const line1: IBufferLine = term.buffer.lines.get(0); // line for old variant
const line2: IBufferLine = term.buffer.lines.get(1); // line for new variant
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '1234567890');
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '1234567890');
// delete one char from params = [0]
term.buffer.y = 0;
term.buffer.x = 70;
deleteChars([0]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '234567890 ');
term.buffer.y = 1;
term.buffer.x = 70;
inputHandler.deleteChars([0]);
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '234567890 ');
expect(lineContent(line2)).equals(lineContent(line1));
// insert one char from params = [1]
term.buffer.y = 0;
term.buffer.x = 70;
deleteChars([1]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '34567890 ');
term.buffer.y = 1;
term.buffer.x = 70;
inputHandler.deleteChars([1]);
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '34567890 ');
expect(lineContent(line2)).equals(lineContent(line1));
// insert two chars from params = [2]
term.buffer.y = 0;
term.buffer.x = 70;
deleteChars([2]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '567890 ');
term.buffer.y = 1;
term.buffer.x = 70;
inputHandler.deleteChars([2]);
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '567890 ');
expect(lineContent(line2)).equals(lineContent(line1));
// insert 10 chars from params = [10]
term.buffer.y = 0;
term.buffer.x = 70;
deleteChars([10]);
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' ');
term.buffer.y = 1;
term.buffer.x = 70;
inputHandler.deleteChars([10]);
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' ');
expect(lineContent(line2)).equals(lineContent(line1));
});
it('eraseInLine', function(): void {
const term = new Terminal();
const inputHandler = new InputHandler(term);
const oldInputHandler = new OldInputHandler(term);
// fill 6 lines to test 3 different states
inputHandler.parse(Array(term.cols + 1).join('a'));
inputHandler.parse(Array(term.cols + 1).join('a'));
inputHandler.parse(Array(term.cols + 1).join('a'));
inputHandler.parse(Array(term.cols + 1).join('a'));
inputHandler.parse(Array(term.cols + 1).join('a'));
inputHandler.parse(Array(term.cols + 1).join('a'));
// params[0] - right erase
term.buffer.y = 0;
term.buffer.x = 70;
oldInputHandler.eraseInLine([0]);
expect(lineContent(term.buffer.lines.get(0))).equals(Array(71).join('a') + ' ');
term.buffer.y = 1;
term.buffer.x = 70;
inputHandler.eraseInLine([0]);
expect(lineContent(term.buffer.lines.get(1))).equals(Array(71).join('a') + ' ');
// params[1] - left erase
term.buffer.y = 2;
term.buffer.x = 70;
oldInputHandler.eraseInLine([1]);
expect(lineContent(term.buffer.lines.get(2))).equals(Array(71).join(' ') + ' aaaaaaaaa');
term.buffer.y = 3;
term.buffer.x = 70;
inputHandler.eraseInLine([1]);
expect(lineContent(term.buffer.lines.get(3))).equals(Array(71).join(' ') + ' aaaaaaaaa');
// params[1] - left erase
term.buffer.y = 4;
term.buffer.x = 70;
oldInputHandler.eraseInLine([2]);
expect(lineContent(term.buffer.lines.get(4))).equals(Array(term.cols + 1).join(' '));
term.buffer.y = 5;
term.buffer.x = 70;
inputHandler.eraseInLine([2]);
expect(lineContent(term.buffer.lines.get(5))).equals(Array(term.cols + 1).join(' '));
});
it('eraseInDisplay', function(): void {
const termOld = new Terminal();
const inputHandlerOld = new OldInputHandler(termOld);
const termNew = new Terminal();
const inputHandlerNew = new InputHandler(termNew);
// fill display with a's
for (let i = 0; i < termOld.rows; ++i) inputHandlerOld.parse(Array(termOld.cols + 1).join('a'));
for (let i = 0; i < termNew.rows; ++i) inputHandlerNew.parse(Array(termOld.cols + 1).join('a'));
const data = [];
for (let i = 0; i < termOld.rows; ++i) data.push(Array(termOld.cols + 1).join('a'));
expect(termContent(termOld)).eql(data);
expect(termContent(termOld)).eql(termContent(termNew));
// params [0] - right and below erase
termOld.buffer.y = 5;
termOld.buffer.x = 40;
inputHandlerOld.eraseInDisplay([0]);
termNew.buffer.y = 5;
termNew.buffer.x = 40;
inputHandlerNew.eraseInDisplay([0]);
expect(termContent(termNew)).eql(termContent(termOld));
// reset
termOld.buffer.y = 0;
termOld.buffer.x = 0;
termNew.buffer.y = 0;
termNew.buffer.x = 0;
for (let i = 0; i < termOld.rows; ++i) inputHandlerOld.parse(Array(termOld.cols + 1).join('a'));
for (let i = 0; i < termNew.rows; ++i) inputHandlerNew.parse(Array(termOld.cols + 1).join('a'));
// params [1] - left and above
termOld.buffer.y = 5;
termOld.buffer.x = 40;
inputHandlerOld.eraseInDisplay([1]);
termNew.buffer.y = 5;
termNew.buffer.x = 40;
inputHandlerNew.eraseInDisplay([1]);
expect(termContent(termNew)).eql(termContent(termOld));
// reset
termOld.buffer.y = 0;
termOld.buffer.x = 0;
termNew.buffer.y = 0;
termNew.buffer.x = 0;
for (let i = 0; i < termOld.rows; ++i) inputHandlerOld.parse(Array(termOld.cols + 1).join('a'));
for (let i = 0; i < termNew.rows; ++i) inputHandlerNew.parse(Array(termOld.cols + 1).join('a'));
// params [2] - whole screen
termOld.buffer.y = 5;
termOld.buffer.x = 40;
inputHandlerOld.eraseInDisplay([2]);
termNew.buffer.y = 5;
termNew.buffer.x = 40;
inputHandlerNew.eraseInDisplay([2]);
expect(termContent(termNew)).eql(termContent(termOld));
});
});
it('convertEol setting', function(): void {
// not converting
let s = '';
const termNotConverting = new Terminal({cols: 15, rows: 10});
(termNotConverting as any)._inputHandler.parse('Hello\nWorld');
for (let i = 0; i < termNotConverting.cols; ++i) {
s += termNotConverting.buffer.lines.get(0).get(i)[CHAR_DATA_CHAR_INDEX];
}
expect(s).equals('Hello ');
s = '';
for (let i = 0; i < termNotConverting.cols; ++i) {
s += termNotConverting.buffer.lines.get(1).get(i)[CHAR_DATA_CHAR_INDEX];
}
expect(s).equals(' World ');
// converting
s = '';
const termConverting = new Terminal({cols: 15, rows: 10, convertEol: true});
(termConverting as any)._inputHandler.parse('Hello\nWorld');
for (let i = 0; i < termConverting.cols; ++i) {
s += termConverting.buffer.lines.get(0).get(i)[CHAR_DATA_CHAR_INDEX];
}
expect(s).equals('Hello ');
s = '';
for (let i = 0; i < termConverting.cols; ++i) {
s += termConverting.buffer.lines.get(1).get(i)[CHAR_DATA_CHAR_INDEX];
}
expect(s).equals('World ');
});
});
+82 -82
View File
@@ -4,7 +4,7 @@
* @license MIT
*/
import { CharData, IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types';
import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types';
import { C0, C1 } from './common/data/EscapeSequences';
import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer';
@@ -13,6 +13,7 @@ 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`.
@@ -116,7 +117,7 @@ export class InputHandler extends Disposable implements IInputHandler {
private _surrogateHigh: string;
constructor(
private _terminal: IInputHandlingTerminal,
protected _terminal: IInputHandlingTerminal,
private _parser: IEscapeSequenceParser = new EscapeSequenceParser())
{
super();
@@ -381,18 +382,20 @@ export class InputHandler extends Disposable implements IInputHandler {
// since they always follow a cell consuming char
// therefore we can test for buffer.x to avoid overflow left
if (!chWidth && buffer.x) {
if (bufferRow[buffer.x - 1]) {
if (!bufferRow[buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) {
const chMinusOne = bufferRow.get(buffer.x - 1);
if (chMinusOne) {
if (!chMinusOne[CHAR_DATA_WIDTH_INDEX]) {
// found empty cell after fullwidth, need to go 2 cells back
// it is save to step 2 cells back here
// since an empty cell is only set by fullwidth chars
if (bufferRow[buffer.x - 2]) {
bufferRow[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char;
bufferRow[buffer.x - 2][CHAR_DATA_CODE_INDEX] = code;
const chMinusTwo = bufferRow.get(buffer.x - 2);
if (chMinusTwo) {
chMinusTwo[CHAR_DATA_CHAR_INDEX] += char;
chMinusTwo[CHAR_DATA_CODE_INDEX] = code;
}
} else {
bufferRow[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char;
bufferRow[buffer.x - 1][CHAR_DATA_CODE_INDEX] = code;
chMinusOne[CHAR_DATA_CHAR_INDEX] += char;
chMinusOne[CHAR_DATA_CODE_INDEX] = code;
}
}
continue;
@@ -412,7 +415,7 @@ export class InputHandler extends Disposable implements IInputHandler {
} else {
// The line already exists (eg. the initial viewport), mark it as a
// wrapped line
(<any>buffer.lines.get(buffer.y)).isWrapped = true;
buffer.lines.get(buffer.y).isWrapped = true;
}
// row changed, get it again
bufferRow = buffer.lines.get(buffer.y + buffer.ybase);
@@ -435,10 +438,11 @@ export class InputHandler extends Disposable implements IInputHandler {
// 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
&& bufferRow[this._terminal.cols - 2]
&& bufferRow[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) {
bufferRow[this._terminal.cols - 2] = [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
&& 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
@@ -447,11 +451,11 @@ export class InputHandler extends Disposable implements IInputHandler {
}
// write current char to buffer and advance cursor
bufferRow[buffer.x++] = [curAttr, char, chWidth, code];
bufferRow.set(buffer.x++, [curAttr, char, chWidth, code]);
// fullwidth char - also set next cell to placeholder stub and advance cursor
if (chWidth === 2) {
bufferRow[buffer.x++] = [curAttr, '', 0, undefined];
bufferRow.set(buffer.x++, [curAttr, '', 0, undefined]);
}
}
this._terminal.updateRange(buffer.y);
@@ -473,7 +477,7 @@ export class InputHandler extends Disposable implements IInputHandler {
// make buffer local for faster access
const buffer = this._terminal.buffer;
if (this._terminal.convertEol) {
if (this._terminal.options.convertEol) {
buffer.x = 0;
}
buffer.y++;
@@ -546,20 +550,12 @@ export class InputHandler extends Disposable implements IInputHandler {
* Insert Ps (Blank) Character(s) (default = 1) (ICH).
*/
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;
const ch: CharData = [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm
while (param-- && j < this._terminal.cols) {
buffer.lines.get(row).splice(j++, 0, ch);
buffer.lines.get(row).pop();
}
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).insertCells(
this._terminal.buffer.x,
params[0] || 1,
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
);
this._terminal.updateRange(this._terminal.buffer.y);
}
/**
@@ -719,6 +715,21 @@ export class InputHandler extends Disposable implements IInputHandler {
}
}
/**
* Helper method to erase cells in a terminal row.
* The cell gets replaced with the eraseChar of the terminal.
* @param y row index
* @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(
start,
end,
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
);
}
/**
* CSI Ps J Erase in Display (ED).
* Ps = 0 -> Erase Below (default).
@@ -735,22 +746,30 @@ export class InputHandler extends Disposable implements IInputHandler {
let j;
switch (params[0]) {
case 0:
this._terminal.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
j = this._terminal.buffer.y + 1;
j = this._terminal.buffer.y;
this._terminal.updateRange(j);
this._eraseInBufferLine(j++, this._terminal.buffer.x, this._terminal.cols);
for (; j < this._terminal.rows; j++) {
this._terminal.eraseLine(j);
this._eraseInBufferLine(j, 0, this._terminal.cols);
}
this._terminal.updateRange(j);
break;
case 1:
this._terminal.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y);
j = this._terminal.buffer.y;
this._terminal.updateRange(j);
this._eraseInBufferLine(j, 0, this._terminal.buffer.x + 1);
while (j--) {
this._terminal.eraseLine(j);
this._eraseInBufferLine(j, 0, this._terminal.cols);
}
this._terminal.updateRange(0);
break;
case 2:
j = this._terminal.rows;
while (j--) this._terminal.eraseLine(j);
this._terminal.updateRange(j - 1);
while (j--) {
this._eraseInBufferLine(j, 0, this._terminal.cols);
}
this._terminal.updateRange(0);
break;
case 3:
// Clear scrollback (everything not in viewport)
@@ -780,15 +799,16 @@ export class InputHandler extends Disposable implements IInputHandler {
public eraseInLine(params: number[]): void {
switch (params[0]) {
case 0:
this._terminal.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
this._eraseInBufferLine(this._terminal.buffer.y, this._terminal.buffer.x, this._terminal.cols);
break;
case 1:
this._terminal.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y);
this._eraseInBufferLine(this._terminal.buffer.y, 0, this._terminal.buffer.x + 1);
break;
case 2:
this._terminal.eraseLine(this._terminal.buffer.y);
this._eraseInBufferLine(this._terminal.buffer.y, 0, this._terminal.cols);
break;
}
this._terminal.updateRange(this._terminal.buffer.y);
}
/**
@@ -812,7 +832,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, this._terminal.blankLine(true));
buffer.lines.splice(row, 0, BufferLine.blankLine(this._terminal.cols, this._terminal.eraseAttr()));
}
// this.maxRange();
@@ -842,7 +862,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, this._terminal.blankLine(true));
buffer.lines.splice(j, 0, BufferLine.blankLine(this._terminal.cols, this._terminal.eraseAttr()));
}
// this.maxRange();
@@ -855,22 +875,12 @@ export class InputHandler extends Disposable implements IInputHandler {
* Delete Ps Character(s) (default = 1) (DCH).
*/
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;
const ch: CharData = [this._terminal.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);
}
this._terminal.updateRange(buffer.y);
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).deleteCells(
this._terminal.buffer.x,
params[0] || 1,
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
);
this._terminal.updateRange(this._terminal.buffer.y);
}
/**
@@ -884,7 +894,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, this._terminal.blankLine());
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, BufferLine.blankLine(this._terminal.cols, DEFAULT_ATTR));
}
// this.maxRange();
this._terminal.updateRange(buffer.scrollTop);
@@ -903,7 +913,7 @@ export class InputHandler extends Disposable implements IInputHandler {
while (param--) {
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1);
buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, this._terminal.blankLine());
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, BufferLine.blankLine(this._terminal.cols, DEFAULT_ATTR));
}
// this.maxRange();
this._terminal.updateRange(buffer.scrollTop);
@@ -916,21 +926,11 @@ export class InputHandler extends Disposable implements IInputHandler {
* Erase Ps Character(s) (default = 1) (ECH).
*/
public eraseChars(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;
const ch: CharData = [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm
while (param-- && j < this._terminal.cols) {
buffer.lines.get(row)[j++] = ch;
}
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).replaceCells(
this._terminal.buffer.x,
this._terminal.buffer.x + (params[0] || 1),
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
);
}
/**
@@ -982,17 +982,14 @@ export class InputHandler extends Disposable implements IInputHandler {
* CSI Ps b Repeat the preceding graphic character Ps times (REP).
*/
public repeatPrecedingCharacter(params: number[]): void {
let param = params[0] || 1;
// make buffer local for faster access
const buffer = this._terminal.buffer;
const line = buffer.lines.get(buffer.ybase + buffer.y);
const ch = line[buffer.x - 1] || [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
while (param--) {
line[buffer.x++] = ch;
}
line.replaceCells(buffer.x,
buffer.x + (params[0] || 1),
line.get(buffer.x - 1) || [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
);
// FIXME: no updateRange here?
}
/**
@@ -1631,6 +1628,9 @@ export class InputHandler extends Disposable implements IInputHandler {
// not bold nor faint
flags &= ~FLAGS.BOLD;
flags &= ~FLAGS.DIM;
} else if (p === 23) {
// not italic
flags &= ~FLAGS.ITALIC;
} else if (p === 24) {
// not underlined
flags &= ~FLAGS.UNDERLINE;
+102 -5
View File
@@ -5,10 +5,11 @@
import { assert } from 'chai';
import { IMouseZoneManager, IMouseZone } from './ui/Types';
import { ILinkMatcher, LineData, ITerminal } from './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';
class TestLinkifier extends Linkifier {
constructor(terminal: ITerminal) {
@@ -41,15 +42,16 @@ describe('Linkifier', () => {
beforeEach(() => {
terminal = new MockTerminal();
terminal.cols = 100;
terminal.rows = 10;
terminal.buffer = new MockBuffer();
(<MockBuffer>terminal.buffer).setLines(new CircularList<LineData>(20));
(<MockBuffer>terminal.buffer).setLines(new CircularList<IBufferLine>(20));
terminal.buffer.ydisp = 0;
linkifier = new TestLinkifier(terminal);
mouseZoneManager = new TestMouseZoneManager();
});
function stringToRow(text: string): LineData {
const result: LineData = [];
function stringToRow(text: string): IBufferLine {
const result = new BufferLine();
for (let i = 0; i < text.length; i++) {
result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
}
@@ -63,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(() => {
@@ -237,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);
});
});
});
});
+101 -91
View File
@@ -4,9 +4,11 @@
*/
import { IMouseZoneManager } from './ui/Types';
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, LineData } 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.
@@ -19,12 +21,19 @@ 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;
private _rowsTimeoutId: number;
private _nextLinkMatcherId = 0;
private _rowsToLinkify: {start: number, end: number};
private _rowsToLinkify: { start: number, end: number };
constructor(
protected _terminal: ITerminal
@@ -79,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;
}
@@ -152,90 +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 ((<any>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: LineData;
do {
rowIndex--;
absoluteRowIndex--;
line = this._terminal.buffer.lines.get(absoluteRowIndex);
if (!line) {
break;
}
} while ((<any>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 &&
(<any>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);
// 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);
}
});
} else {
this._addLink(offset + index, rowIndex, uri, matcher);
}
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);
}
}
}
@@ -245,12 +253,14 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @param y The row the link is on.
* @param uri The URI of the link.
* @param matcher The link matcher for the link.
* @param fg The link color for hover event.
*/
private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher): void {
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--;
@@ -268,17 +278,17 @@ export class Linkifier extends EventEmitter implements ILinkifier {
window.open(uri, '_blank');
},
e => {
this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(x1, y1, x2, y2));
this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
this._terminal.element.classList.add('xterm-cursor-pointer');
},
e => {
this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x1, y1, x2, y2));
this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
if (matcher.hoverTooltipCallback) {
matcher.hoverTooltipCallback(e, uri);
}
},
() => {
this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(x1, y1, x2, y2));
this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
this._terminal.element.classList.remove('xterm-cursor-pointer');
if (matcher.hoverLeaveCallback) {
matcher.hoverLeaveCallback();
@@ -293,7 +303,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
));
}
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number): ILinkHoverEvent {
return { x1, y1, x2, y2, cols: this._terminal.cols };
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number): ILinkHoverEvent {
return { x1, y1, x2, y2, cols: this._terminal.cols, fg };
}
}
+19 -13
View File
@@ -8,8 +8,9 @@ import { CharMeasure } from './ui/CharMeasure';
import { SelectionManager, SelectionMode } from './SelectionManager';
import { SelectionModel } from './SelectionModel';
import { BufferSet } from './BufferSet';
import { LineData, CharData, ITerminal, IBuffer } from './Types';
import { ITerminal, IBuffer, IBufferLine } from './Types';
import { MockTerminal } from './utils/TestUtils.test';
import { BufferLine } from './BufferLine';
class TestMockTerminal extends MockTerminal {
emit(event: string, data: any): void {}
@@ -52,16 +53,18 @@ describe('SelectionManager', () => {
selectionManager = new TestSelectionManager(terminal, null);
});
function stringToRow(text: string): LineData {
const result: LineData = [];
function stringToRow(text: string): IBufferLine {
const result = new BufferLine();
for (let i = 0; i < text.length; i++) {
result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
}
return result;
}
function stringArrayToRow(chars: string[]): LineData {
return chars.map(c => <CharData>[0, c, 1, c.charCodeAt(0)]);
function stringArrayToRow(chars: string[]): IBufferLine {
const line = new BufferLine();
chars.map(c => line.push([0, c, 1, c.charCodeAt(0)]));
return line;
}
describe('_selectWordAt', () => {
@@ -97,7 +100,8 @@ describe('SelectionManager', () => {
});
it('should expand selection for wide characters', () => {
// Wide characters use a special format
buffer.lines.set(0, [
const line = new BufferLine();
const data: [number, string, number, number][] = [
[null, '中', 2, '中'.charCodeAt(0)],
[null, '', 0, null],
[null, '文', 2, '文'.charCodeAt(0)],
@@ -113,7 +117,9 @@ describe('SelectionManager', () => {
[null, 'f', 1, 'f'.charCodeAt(0)],
[null, 'o', 1, 'o'.charCodeAt(0)],
[null, 'o', 1, 'o'.charCodeAt(0)]
]);
];
for (let i = 0; i < data.length; ++i) line.push(data[i]);
buffer.lines.set(0, line);
// Ensure wide characters take up 2 columns
selectionManager.selectWordAt([0, 0]);
assert.equal(selectionManager.selectionText, '中文');
@@ -186,7 +192,7 @@ describe('SelectionManager', () => {
it('should expand upwards or downards for wrapped lines', () => {
buffer.lines.set(0, stringToRow(' foo'));
buffer.lines.set(1, stringToRow('bar '));
(<any>buffer.lines.get(1)).isWrapped = true;
buffer.lines.get(1).isWrapped = true;
selectionManager.selectWordAt([1, 1]);
assert.equal(selectionManager.selectionText, 'foobar');
selectionManager.model.clearSelection();
@@ -200,10 +206,10 @@ describe('SelectionManager', () => {
buffer.lines.set(2, stringToRow('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'));
buffer.lines.set(3, stringToRow('cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'));
buffer.lines.set(4, stringToRow('bar '));
(<any>buffer.lines.get(1)).isWrapped = true;
(<any>buffer.lines.get(2)).isWrapped = true;
(<any>buffer.lines.get(3)).isWrapped = true;
(<any>buffer.lines.get(4)).isWrapped = true;
buffer.lines.get(1).isWrapped = true;
buffer.lines.get(2).isWrapped = true;
buffer.lines.get(3).isWrapped = true;
buffer.lines.get(4).isWrapped = true;
selectionManager.selectWordAt([78, 0]);
assert.equal(selectionManager.selectionText, expectedText);
selectionManager.model.clearSelection();
@@ -339,7 +345,7 @@ describe('SelectionManager', () => {
it('should select the entire wrapped line', () => {
buffer.lines.set(0, stringToRow('foo'));
const line2 = stringToRow('bar');
(<any>line2).isWrapped = true;
line2.isWrapped = true;
buffer.lines.set(1, line2);
selectionManager.selectLineAt(0);
assert.equal(selectionManager.selectionText, 'foobar', 'The selected text is correct');
+22 -21
View File
@@ -3,11 +3,12 @@
* @license MIT
*/
import { ITerminal, ISelectionManager, IBuffer, CharData, XtermListener } 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 { 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';
@@ -204,7 +205,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
const bufferLine = this._buffer.lines.get(i);
const lineText = this._buffer.translateBufferLineToString(i, true);
if ((<any>bufferLine).isWrapped) {
if (bufferLine.isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
@@ -215,7 +216,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
if (start[1] !== end[1]) {
const bufferLine = this._buffer.lines.get(end[1]);
const lineText = this._buffer.translateBufferLineToString(end[1], true, 0, end[0]);
if ((<any>bufferLine).isWrapped) {
if (bufferLine.isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
@@ -500,7 +501,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
// If the mouse is over the second half of a wide character, adjust the
// selection to cover the whole character
const char = line[this._model.selectionStart[0]];
const char = line.get(this._model.selectionStart[0]);
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
this._model.selectionStart[0]++;
}
@@ -590,7 +591,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
// selection. Note that selections at the very end of the line will never
// have a character.
if (this._model.selectionEnd[1] < this._buffer.lines.length) {
const char = this._buffer.lines.get(this._model.selectionEnd[1])[this._model.selectionEnd[0]];
const char = this._buffer.lines.get(this._model.selectionEnd[1]).get(this._model.selectionEnd[0]);
if (char && char[CHAR_DATA_WIDTH_INDEX] === 0) {
this._model.selectionEnd[0]++;
}
@@ -661,10 +662,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* latter takes into account wide characters.
* @param coords The coordinates to find the 2 index for.
*/
private _convertViewportColToCharacterIndex(bufferLine: any, coords: [number, number]): number {
private _convertViewportColToCharacterIndex(bufferLine: IBufferLine, coords: [number, number]): number {
let charIndex = coords[0];
for (let i = 0; coords[0] >= i; i++) {
const char = bufferLine[i];
const char = bufferLine.get(i);
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
// Wide characters aren't included in the line string so decrement the
// index so the index is back on the wide character.
@@ -733,24 +734,24 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
// Consider the initial position, skip it and increment the wide char
// variable
if (bufferLine[startCol][CHAR_DATA_WIDTH_INDEX] === 0) {
if (bufferLine.get(startCol)[CHAR_DATA_WIDTH_INDEX] === 0) {
leftWideCharCount++;
startCol--;
}
if (bufferLine[endCol][CHAR_DATA_WIDTH_INDEX] === 2) {
if (bufferLine.get(endCol)[CHAR_DATA_WIDTH_INDEX] === 2) {
rightWideCharCount++;
endCol++;
}
// Adjust the end index for characters whose length are > 1 (emojis)
if (bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length > 1) {
rightLongCharOffset += bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length - 1;
endIndex += bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length - 1;
if (bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length > 1) {
rightLongCharOffset += bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length - 1;
endIndex += bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length - 1;
}
// Expand the string in both directions until a space is hit
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine[startCol - 1])) {
const char = bufferLine[startCol - 1];
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.get(startCol - 1))) {
const char = bufferLine.get(startCol - 1);
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
// If the next character is a wide char, record it and skip the column
leftWideCharCount++;
@@ -764,8 +765,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
startIndex--;
startCol--;
}
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine[endCol + 1])) {
const char = bufferLine[endCol + 1];
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.get(endCol + 1))) {
const char = bufferLine.get(endCol + 1);
if (char[CHAR_DATA_WIDTH_INDEX] === 2) {
// If the next character is a wide char, record it and skip the column
rightWideCharCount++;
@@ -808,9 +809,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
// Recurse upwards if the line is wrapped and the word wraps to the above line
if (followWrappedLinesAbove) {
if (start === 0 && bufferLine[0][CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
if (start === 0 && bufferLine.get(0)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
const previousBufferLine = this._buffer.lines.get(coords[1] - 1);
if (previousBufferLine && (<any>bufferLine).isWrapped && previousBufferLine[this._terminal.cols - 1][CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
if (previousBufferLine && bufferLine.isWrapped && previousBufferLine.get(this._terminal.cols - 1)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
const previousLineWordPosition = this._getWordAt([this._terminal.cols - 1, coords[1] - 1], false, true, false);
if (previousLineWordPosition) {
const offset = this._terminal.cols - previousLineWordPosition.start;
@@ -823,9 +824,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
// Recurse downwards if the line is wrapped and the word wraps to the next line
if (followWrappedLinesBelow) {
if (start + length === this._terminal.cols && bufferLine[this._terminal.cols - 1][CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
if (start + length === this._terminal.cols && bufferLine.get(this._terminal.cols - 1)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
const nextBufferLine = this._buffer.lines.get(coords[1] + 1);
if (nextBufferLine && (<any>nextBufferLine).isWrapped && nextBufferLine[0][CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
if (nextBufferLine && nextBufferLine.isWrapped && nextBufferLine.get(0)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
const nextLineWordPosition = this._getWordAt([0, coords[1] + 1], false, false, true);
if (nextLineWordPosition) {
length += nextLineWordPosition.length;
+2 -3
View File
@@ -67,7 +67,7 @@ function terminalToString(term: Terminal): string {
for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
lineText = '';
for (let cell = 0; cell < term.cols; ++cell) {
lineText += term.buffer.lines.get(line)[cell][CHAR_DATA_CHAR_INDEX];
lineText += term.buffer.lines.get(line).get(cell)[CHAR_DATA_CHAR_INDEX];
}
// rtrim empty cells as xterm does
lineText = lineText.replace(/\s+$/, '');
@@ -105,6 +105,7 @@ if (os.platform() !== 'win32') {
// omit stack trace for escape sequence files
Error.stackTraceLimit = 0;
const files = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')});
// for (let i = 0; i < files.length; ++i) console.debug(i, files[i]);
// only successful tests for now
const skip = [
10, 16, 17, 19, 32, 33, 34, 35, 36, 39,
@@ -163,7 +164,5 @@ describe('typings', () => {
const fixtureDir = path.join(__dirname, '..', 'fixtures', 'typings-test');
const result = cp.spawnSync(tsc, { cwd: fixtureDir });
assert.equal(result.status, 0, `build did not succeed:\nstdout: ${result.stdout.toString()}\nstderr: ${result.stderr.toString()}\n`);
// Clean up
fs.unlinkSync(path.join(fixtureDir, 'typings-test.js'));
});
});
+262 -143
View File
@@ -6,7 +6,8 @@
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 } from './Buffer';
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;
@@ -25,7 +26,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();
@@ -36,8 +37,8 @@ describe('term.js addons', () => {
};
(<any>term).element = {
classList: {
toggle: () => {},
remove: () => {}
toggle: () => { },
remove: () => { }
}
};
});
@@ -67,24 +68,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', () => {
@@ -141,7 +260,7 @@ describe('term.js addons', () => {
assert.equal(term.buffer.lines.length, term.rows);
assert.deepEqual(term.buffer.lines.get(0), promptLine);
for (let i = 1; i < term.rows; i++) {
assert.deepEqual(term.buffer.lines.get(i), term.blankLine());
assert.deepEqual(term.buffer.lines.get(i), BufferLine.blankLine(term.cols, DEFAULT_ATTR));
}
});
it('should clear a buffer larger than rows', () => {
@@ -158,7 +277,7 @@ describe('term.js addons', () => {
assert.equal(term.buffer.lines.length, term.rows);
assert.deepEqual(term.buffer.lines.get(0), promptLine);
for (let i = 1; i < term.rows; i++) {
assert.deepEqual(term.buffer.lines.get(i), term.blankLine());
assert.deepEqual(term.buffer.lines.get(i), BufferLine.blankLine(term.cols, DEFAULT_ATTR));
}
});
it('should not break the prompt when cleared twice', () => {
@@ -171,7 +290,7 @@ describe('term.js addons', () => {
assert.equal(term.buffer.lines.length, term.rows);
assert.deepEqual(term.buffer.lines.get(0), promptLine);
for (let i = 1; i < term.rows; i++) {
assert.deepEqual(term.buffer.lines.get(i), term.blankLine());
assert.deepEqual(term.buffer.lines.get(i), BufferLine.blankLine(term.cols, DEFAULT_ATTR));
}
});
});
@@ -304,8 +423,8 @@ describe('term.js addons', () => {
type: 'keydown',
key: 'a',
keyCode: 65,
preventDefault: () => {},
stopPropagation: () => {}
preventDefault: () => { },
stopPropagation: () => { }
};
term.buffer.ydisp = 0;
@@ -337,62 +456,62 @@ describe('term.js addons', () => {
describe('scroll() function', () => {
describe('when scrollback > 0', () => {
it('should create a new line and scroll', () => {
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
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.y = INIT_ROWS - 1; // Move cursor to last line
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
assert.equal(term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX], 'b');
assert.equal(term.buffer.lines.get(INIT_ROWS)[0][CHAR_DATA_CHAR_INDEX], ' ');
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
assert.equal(term.buffer.lines.get(INIT_ROWS).get(0)[CHAR_DATA_CHAR_INDEX], ' ');
});
it('should properly scroll inside a scroll region (scrollTop set)', () => {
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
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.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c');
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
});
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
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.y = 3;
term.buffer.scrollBottom = 3;
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a', '\'a\' should be pushed to the scrollback');
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'b');
assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'c');
assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], 'd');
assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
assert.equal(term.buffer.lines.get(5)[0][CHAR_DATA_CHAR_INDEX], 'e');
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a', '\'a\' should be pushed to the scrollback');
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
assert.equal(term.buffer.lines.get(5).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
});
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
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.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.buffer.scrollBottom = 3;
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'd');
assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], 'e');
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
});
});
@@ -403,65 +522,65 @@ describe('term.js addons', () => {
});
it('should create a new line and shift everything up', () => {
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX] = 'c';
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.y = INIT_ROWS - 1; // Move cursor to last line
assert.equal(term.buffer.lines.length, INIT_ROWS);
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS);
// 'a' gets pushed out of buffer
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'b');
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], ' ');
assert.equal(term.buffer.lines.get(INIT_ROWS - 2)[0][CHAR_DATA_CHAR_INDEX], 'c');
assert.equal(term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX], ' ');
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], ' ');
assert.equal(term.buffer.lines.get(INIT_ROWS - 2).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], ' ');
});
it('should properly scroll inside a scroll region (scrollTop set)', () => {
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
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.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c');
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
});
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
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.y = 3;
term.buffer.scrollBottom = 3;
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'b');
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c');
assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'd');
assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], 'e');
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
});
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
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.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.buffer.scrollBottom = 3;
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'd');
assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], 'e');
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
});
});
});
@@ -472,9 +591,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: {
@@ -484,15 +603,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,
@@ -654,11 +773,11 @@ describe('term.js addons', () => {
const high = String.fromCharCode(0xD800);
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.write(high + String.fromCharCode(i));
const tchar = term.buffer.lines.get(0)[0];
const tchar = term.buffer.lines.get(0).get(0);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
term.reset();
}
});
@@ -667,9 +786,9 @@ describe('term.js addons', () => {
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1;
term.write(high + String.fromCharCode(i));
expect(term.buffer.lines.get(0)[term.buffer.x - 1][CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
expect(term.buffer.lines.get(0)[term.buffer.x - 1][CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX]).eql(' ');
expect(term.buffer.lines.get(0).get(term.buffer.x - 1)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
expect(term.buffer.lines.get(0).get(term.buffer.x - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql(' ');
term.reset();
}
});
@@ -679,10 +798,10 @@ describe('term.js addons', () => {
term.buffer.x = term.cols - 1;
term.wraparoundMode = true;
term.write('a' + high + String.fromCharCode(i));
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX]).eql('a');
expect(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
expect(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(term.buffer.lines.get(1)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a');
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
term.reset();
}
});
@@ -693,9 +812,9 @@ describe('term.js addons', () => {
term.wraparoundMode = false;
term.write('a' + high + String.fromCharCode(i));
// auto wraparound mode should cut off the rest of the line
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX]).eql('a');
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX].length).eql(1);
expect(term.buffer.lines.get(1)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a');
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(1);
expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
term.reset();
}
});
@@ -704,11 +823,11 @@ describe('term.js addons', () => {
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.write(high);
term.write(String.fromCharCode(i));
const tchar = term.buffer.lines.get(0)[0];
const tchar = term.buffer.lines.get(0).get(0);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
term.reset();
}
});
@@ -717,30 +836,30 @@ describe('term.js addons', () => {
describe('unicode - combining characters', () => {
it('café', () => {
term.write('cafe\u0301');
expect(term.buffer.lines.get(0)[3][CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
expect(term.buffer.lines.get(0)[3][CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(term.buffer.lines.get(0)[3][CHAR_DATA_WIDTH_INDEX]).eql(1);
expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_WIDTH_INDEX]).eql(1);
});
it('café - end of line', () => {
term.buffer.x = term.cols - 1 - 3;
term.write('cafe\u0301');
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_WIDTH_INDEX]).eql(1);
expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX].length).eql(1);
expect(term.buffer.lines.get(0)[1][CHAR_DATA_WIDTH_INDEX]).eql(1);
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_WIDTH_INDEX]).eql(1);
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX].length).eql(1);
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_WIDTH_INDEX]).eql(1);
});
it('multiple combined é', () => {
term.wraparoundMode = true;
term.write(Array(100).join('e\u0301'));
for (let i = 0; i < term.cols; ++i) {
const tchar = term.buffer.lines.get(0)[i];
const tchar = term.buffer.lines.get(0).get(i);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
}
const tchar = term.buffer.lines.get(1)[0];
const tchar = term.buffer.lines.get(1).get(0);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
@@ -749,12 +868,12 @@ describe('term.js addons', () => {
term.wraparoundMode = true;
term.write(Array(100).join('\uD800\uDC00\u0301'));
for (let i = 0; i < term.cols; ++i) {
const tchar = term.buffer.lines.get(0)[i];
const tchar = term.buffer.lines.get(0).get(i);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
}
const tchar = term.buffer.lines.get(1)[0];
const tchar = term.buffer.lines.get(1).get(0);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
@@ -777,7 +896,7 @@ describe('term.js addons', () => {
term.wraparoundMode = true;
term.write(Array(50).join('¥'));
for (let i = 0; i < term.cols; ++i) {
const tchar = term.buffer.lines.get(0)[i];
const tchar = term.buffer.lines.get(0).get(i);
if (i % 2) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -788,7 +907,7 @@ describe('term.js addons', () => {
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
}
}
const tchar = term.buffer.lines.get(1)[0];
const tchar = term.buffer.lines.get(1).get(0);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
@@ -798,7 +917,7 @@ describe('term.js addons', () => {
term.buffer.x = 1;
term.write(Array(50).join('¥'));
for (let i = 1; i < term.cols - 1; ++i) {
const tchar = term.buffer.lines.get(0)[i];
const tchar = term.buffer.lines.get(0).get(i);
if (!(i % 2)) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -809,11 +928,11 @@ describe('term.js addons', () => {
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
}
}
let tchar = term.buffer.lines.get(0)[term.cols - 1];
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
tchar = term.buffer.lines.get(1)[0];
tchar = term.buffer.lines.get(1).get(0);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
@@ -823,7 +942,7 @@ describe('term.js addons', () => {
term.buffer.x = 1;
term.write(Array(50).join('¥\u0301'));
for (let i = 1; i < term.cols - 1; ++i) {
const tchar = term.buffer.lines.get(0)[i];
const tchar = term.buffer.lines.get(0).get(i);
if (!(i % 2)) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -834,11 +953,11 @@ describe('term.js addons', () => {
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
}
}
let tchar = term.buffer.lines.get(0)[term.cols - 1];
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
tchar = term.buffer.lines.get(1)[0];
tchar = term.buffer.lines.get(1).get(0);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
@@ -847,7 +966,7 @@ describe('term.js addons', () => {
term.wraparoundMode = true;
term.write(Array(50).join('¥\u0301'));
for (let i = 0; i < term.cols; ++i) {
const tchar = term.buffer.lines.get(0)[i];
const tchar = term.buffer.lines.get(0).get(i);
if (i % 2) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -858,7 +977,7 @@ describe('term.js addons', () => {
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
}
}
const tchar = term.buffer.lines.get(1)[0];
const tchar = term.buffer.lines.get(1).get(0);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
@@ -868,7 +987,7 @@ describe('term.js addons', () => {
term.buffer.x = 1;
term.write(Array(50).join('\ud843\ude6d\u0301'));
for (let i = 1; i < term.cols - 1; ++i) {
const tchar = term.buffer.lines.get(0)[i];
const tchar = term.buffer.lines.get(0).get(i);
if (!(i % 2)) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -879,11 +998,11 @@ describe('term.js addons', () => {
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
}
}
let tchar = term.buffer.lines.get(0)[term.cols - 1];
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
tchar = term.buffer.lines.get(1)[0];
tchar = term.buffer.lines.get(1).get(0);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
@@ -892,7 +1011,7 @@ describe('term.js addons', () => {
term.wraparoundMode = true;
term.write(Array(50).join('\ud843\ude6d\u0301'));
for (let i = 0; i < term.cols; ++i) {
const tchar = term.buffer.lines.get(0)[i];
const tchar = term.buffer.lines.get(0).get(i);
if (i % 2) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -903,7 +1022,7 @@ describe('term.js addons', () => {
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
}
}
const tchar = term.buffer.lines.get(1)[0];
const tchar = term.buffer.lines.get(1).get(0);
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
@@ -918,10 +1037,10 @@ describe('term.js addons', () => {
term.insertMode = true;
term.write('abcde');
expect(term.buffer.lines.get(0).length).eql(term.cols);
expect(term.buffer.lines.get(0)[10][CHAR_DATA_CHAR_INDEX]).eql('a');
expect(term.buffer.lines.get(0)[14][CHAR_DATA_CHAR_INDEX]).eql('e');
expect(term.buffer.lines.get(0)[15][CHAR_DATA_CHAR_INDEX]).eql('0');
expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql('4');
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('a');
expect(term.buffer.lines.get(0).get(14)[CHAR_DATA_CHAR_INDEX]).eql('e');
expect(term.buffer.lines.get(0).get(15)[CHAR_DATA_CHAR_INDEX]).eql('0');
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql('4');
});
it('fullwidth - insert', () => {
term.write(Array(9).join('0123456789').slice(-80));
@@ -930,11 +1049,11 @@ describe('term.js addons', () => {
term.insertMode = true;
term.write('¥¥¥');
expect(term.buffer.lines.get(0).length).eql(term.cols);
expect(term.buffer.lines.get(0)[10][CHAR_DATA_CHAR_INDEX]).eql('¥');
expect(term.buffer.lines.get(0)[11][CHAR_DATA_CHAR_INDEX]).eql('');
expect(term.buffer.lines.get(0)[14][CHAR_DATA_CHAR_INDEX]).eql('¥');
expect(term.buffer.lines.get(0)[15][CHAR_DATA_CHAR_INDEX]).eql('');
expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql('3');
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('¥');
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('');
expect(term.buffer.lines.get(0).get(14)[CHAR_DATA_CHAR_INDEX]).eql('¥');
expect(term.buffer.lines.get(0).get(15)[CHAR_DATA_CHAR_INDEX]).eql('');
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql('3');
});
it('fullwidth - right border', () => {
term.write(Array(41).join('¥'));
@@ -943,14 +1062,14 @@ describe('term.js addons', () => {
term.insertMode = true;
term.write('a');
expect(term.buffer.lines.get(0).length).eql(term.cols);
expect(term.buffer.lines.get(0)[10][CHAR_DATA_CHAR_INDEX]).eql('a');
expect(term.buffer.lines.get(0)[11][CHAR_DATA_CHAR_INDEX]).eql('¥');
expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql(' '); // fullwidth char got replaced
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('a');
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('¥');
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(' '); // fullwidth char got replaced
term.write('b');
expect(term.buffer.lines.get(0).length).eql(term.cols);
expect(term.buffer.lines.get(0)[11][CHAR_DATA_CHAR_INDEX]).eql('b');
expect(term.buffer.lines.get(0)[12][CHAR_DATA_CHAR_INDEX]).eql('¥');
expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql(''); // empty cell after fullwidth
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('b');
expect(term.buffer.lines.get(0).get(12)[CHAR_DATA_CHAR_INDEX]).eql('¥');
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(''); // empty cell after fullwidth
});
});
});
+14 -83
View File
@@ -21,13 +21,13 @@
* http://linux.die.net/man/7/urxvt
*/
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, LineData, CharacterJoinerHandler } from './Types';
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler } 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 { 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 { C0 } from './common/data/EscapeSequences';
@@ -52,6 +52,7 @@ import { DomRenderer } from './renderer/dom/DomRenderer';
import { IKeyboardEvent } from './common/Types';
import { evaluateKeyboardEvent } from './core/input/Keyboard';
import { KeyboardResultType, ICharset } from './core/Types';
import { BufferLine } from './BufferLine';
// Let it work inside Node.js for automated testing purposes.
const document = (typeof window !== 'undefined') ? window.document : null;
@@ -133,7 +134,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// TODO: This can be changed to an enum or boolean, 0 and 1 seem to be the only options
public cursorState: number;
public cursorHidden: boolean;
public convertEol: boolean;
private _customKeyEventHandler: CustomKeyEventHandler;
@@ -251,7 +251,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
private _setup(): void {
Object.keys(DEFAULT_OPTIONS).forEach((key) => {
if (this.options[key] == null) {
if (this.options[key] === null || this.options[key] === undefined) {
this.options[key] = DEFAULT_OPTIONS[key];
}
});
@@ -503,10 +503,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Binds the desired focus behavior on a given terminal object.
*/
private _onTextAreaFocus(): void {
private _onTextAreaFocus(ev: KeyboardEvent): void {
if (this.sendFocus) {
this.handler(C0.ESC + '[I');
}
this.updateCursorStyle(ev);
this.element.classList.add('focus');
this.showCursor();
this.emit('focus');
@@ -679,7 +680,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.textarea.setAttribute('autocapitalize', 'off');
this.textarea.setAttribute('spellcheck', 'false');
this.textarea.tabIndex = 0;
this.register(addDisposableDomListener(this.textarea, 'focus', () => this._onTextAreaFocus()));
this.register(addDisposableDomListener(this.textarea, 'focus', (ev: KeyboardEvent) => this._onTextAreaFocus(ev)));
this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur()));
this._helperContainer.appendChild(this.textarea);
@@ -960,9 +961,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// 1, and 2 - with 64 added
switch ((<any>ev).overrideType || ev.type) {
case 'mousedown':
button = ev.button != null
button = ev.button !== null && ev.button !== undefined
? +ev.button
: ev.which != null
: ev.which !== null && ev.which !== undefined
? ev.which - 1
: null;
@@ -1169,7 +1170,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
* @param isWrapped Whether the new line is wrapped from the previous line.
*/
public scroll(isWrapped?: boolean): void {
const newLine = this.blankLine(undefined, isWrapped);
const newLine = BufferLine.blankLine(this.cols, DEFAULT_ATTR, isWrapped);
const topRow = this.buffer.ybase + this.buffer.scrollTop;
const bottomRow = this.buffer.ybase + this.buffer.scrollBottom;
@@ -1587,7 +1588,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
if (ev.charCode) {
key = ev.charCode;
} else if (ev.which == null) {
} else if (ev.which === null || ev.which === undefined) {
key = ev.keyCode;
} else if (ev.which !== 0 && ev.charCode !== 0) {
key = ev.which;
@@ -1707,41 +1708,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this._refreshEnd = this.rows - 1;
}
/**
* Erase in the identified line everything from "x" to the end of the line (right).
* @param x The column from which to start erasing to the end of the line.
* @param y The line in which to operate.
*/
public eraseRight(x: number, y: number): void {
const line = this.buffer.lines.get(this.buffer.ybase + y);
if (!line) {
return;
}
const ch: CharData = [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm
for (; x < this.cols; x++) {
line[x] = ch;
}
this.updateRange(y);
}
/**
* Erase in the identified line everything from "x" to the start of the line (left).
* @param x The column from which to start erasing to the start of the line.
* @param y The line in which to operate.
*/
public eraseLeft(x: number, y: number): void {
const line = this.buffer.lines.get(this.buffer.ybase + y);
if (!line) {
return;
}
const ch: CharData = [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm
x++;
while (x--) {
line[x] = ch;
}
this.updateRange(y);
}
/**
* Clear the entire buffer, making the prompt line the new first line.
*/
@@ -1756,47 +1722,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.buffer.ybase = 0;
this.buffer.y = 0;
for (let i = 1; i < this.rows; i++) {
this.buffer.lines.push(this.blankLine());
this.buffer.lines.push(BufferLine.blankLine(this.cols, DEFAULT_ATTR));
}
this.refresh(0, this.rows - 1);
this.emit('scroll', this.buffer.ydisp);
}
/**
* Erase all content in the given line
* @param y The line to erase all of its contents.
*/
public eraseLine(y: number): void {
this.eraseRight(0, y);
}
/**
* Return the data array of a blank line
* @param cur First bunch of data for each "blank" character.
* @param isWrapped Whether the new line is wrapped from the previous line.
* @param cols The number of columns in the terminal, if this is not
* set, the terminal's current column count would be used.
*/
public blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData {
const attr = cur ? this.eraseAttr() : DEFAULT_ATTR;
const ch: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // width defaults to 1 halfwidth character
const line: LineData = [];
// TODO: It is not ideal that this is a property on an array, a buffer line
// class should be added that will hold this data and other useful functions.
if (isWrapped) {
(<any>line).isWrapped = isWrapped;
}
cols = cols || this.cols;
for (let i = 0; i < cols; i++) {
line[i] = ch;
}
return line;
}
/**
* If cur return the back color xterm feature attribute. Else return default attribute.
* @param cur
@@ -1883,7 +1814,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// blankLine(true) is xterm/linux behavior
const scrollRegionHeight = this.buffer.scrollBottom - this.buffer.scrollTop;
this.buffer.lines.shiftElements(this.buffer.y + this.buffer.ybase, scrollRegionHeight, 1);
this.buffer.lines.set(this.buffer.y + this.buffer.ybase, this.blankLine(true));
this.buffer.lines.set(this.buffer.y + this.buffer.ybase, BufferLine.blankLine(this.cols, this.eraseAttr()));
this.updateRange(this.buffer.scrollTop);
this.updateRange(this.buffer.scrollBottom);
} else {
@@ -1932,7 +1863,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
public matchColor(r1: number, g1: number, b1: number): number {
const hash = (r1 << 16) | (g1 << 8) | b1;
if (matchColorCache[hash] != null) {
if (matchColorCache[hash] !== null && matchColorCache[hash] !== undefined) {
return matchColorCache[hash];
}
+34 -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',
@@ -66,15 +68,10 @@ export interface IInputHandlingTerminal extends IEventEmitter {
bell(): void;
focus(): void;
convertEol: boolean;
updateRange(y: number): void;
scroll(isWrapped?: boolean): void;
setgLevel(g: number): void;
eraseAttr(): number;
eraseRight(x: number, y: number): void;
eraseLine(y: number): void;
eraseLeft(x: number, y: number): void;
blankLine(cur?: boolean, isWrapped?: boolean): LineData;
is(term: string): boolean;
setgCharset(g: number, charset: ICharset): void;
resize(x: number, y: number): void;
@@ -204,6 +201,7 @@ export interface ILinkHoverEvent {
x2: number;
y2: number;
cols: number;
fg: number;
}
export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
@@ -233,7 +231,6 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
cancel(ev: Event, force?: boolean): boolean | void;
log(text: string): void;
showCursor(): void;
blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData;
}
export interface IBufferAccessor {
@@ -271,8 +268,18 @@ 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<LineData>;
readonly lines: ICircularList<IBufferLine>;
ydisp: number;
ybase: number;
y: number;
@@ -288,6 +295,8 @@ export interface IBuffer {
getWrappedRangeForLine(y: number): { first: number, last: number };
nextStop(x?: number): number;
prevStop(x?: number): number;
stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[];
iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator;
}
export interface IBufferSet extends IEventEmitter {
@@ -299,19 +308,6 @@ export interface IBufferSet extends IEventEmitter {
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;
}
export interface ISelectionManager {
selectionText: string;
selectionStart: [number, number];
@@ -511,3 +507,19 @@ export interface IEscapeSequenceParser extends IDisposable {
setErrorHandler(callback: (state: IParsingState) => IParsingState): void;
clearErrorHandler(): void;
}
/**
* Interface for a line in the terminal buffer.
*/
export interface IBufferLine {
length: number;
isWrapped: boolean;
get(index: number): CharData;
set(index: number, value: CharData): void;
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;
}
+56 -14
View File
@@ -22,12 +22,16 @@ 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
// quick fix and could have a more robust solution in place that reset the value when needed.
private _wheelPartialScroll: number = 0;
private _refreshAnimationFrame: number | null = null;
private _ignoreNextScrollEvent: boolean = false;
/**
* Creates a new Viewport.
* @param _terminal The terminal this viewport belongs to.
@@ -62,6 +66,12 @@ export class Viewport extends Disposable implements IViewport {
* necessary.
*/
private _refresh(): void {
if (this._refreshAnimationFrame === null) {
this._refreshAnimationFrame = requestAnimationFrame(() => this._innerRefresh());
}
}
private _innerRefresh(): void {
if (this._charMeasure.height > 0) {
this._currentRowHeight = this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio;
this._lastRecordedViewportHeight = this._viewportElement.offsetHeight;
@@ -71,30 +81,53 @@ export class Viewport extends Disposable implements IViewport {
this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px';
}
}
// Sync scrollTop
const scrollTop = this._terminal.buffer.ydisp * this._currentRowHeight;
if (this._viewportElement.scrollTop !== scrollTop) {
// Ignore the next scroll event which will be triggered by setting the scrollTop as we do not
// want this event to scroll the terminal
this._ignoreNextScrollEvent = true;
this._viewportElement.scrollTop = scrollTop;
}
this._refreshAnimationFrame = null;
}
/**
* 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
this._refresh();
} else {
// If size has changed, refresh viewport
if (this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
this._refresh();
}
return;
}
// Sync scrollTop
const scrollTop = this._terminal.buffer.ydisp * this._currentRowHeight;
if (this._viewportElement.scrollTop !== scrollTop) {
this._viewportElement.scrollTop = scrollTop;
// If viewport height changed
if (this._lastRecordedViewportHeight !== (<any>this._terminal).renderer.dimensions.canvasHeight) {
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;
}
}
@@ -104,13 +137,22 @@ 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) {
return;
}
const newRow = Math.round(this._viewportElement.scrollTop / this._currentRowHeight);
// Ignore the event if it was flagged to ignore (when the source of the event is from Viewport)
if (this._ignoreNextScrollEvent) {
this._ignoreNextScrollEvent = false;
return;
}
const newRow = Math.round(this._lastScrollTop / this._currentRowHeight);
const diff = newRow - this._terminal.buffer.ydisp;
this._terminal.scrollLines(diff, true);
}
+1 -1
View File
@@ -53,7 +53,7 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean
const fileReader = new FileReader();
fileReader.addEventListener('load', () => {
str = myTextDecoder.decode(this.result);
str = myTextDecoder.decode(fileReader.result);
displayData(str);
});
fileReader.readAsArrayBuffer(ev.data);
+4
View File
@@ -2,6 +2,10 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es6",
],
"rootDir": ".",
"outDir": "../../../lib/addons/attach/",
"sourceMap": true,
+4
View File
@@ -2,6 +2,10 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es6",
],
"rootDir": ".",
"outDir": "../../../lib/addons/fit/",
"sourceMap": true,
+4
View File
@@ -2,6 +2,10 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es6",
],
"rootDir": ".",
"outDir": "../../../lib/addons/fullscreen/",
"sourceMap": true,
+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;
}
+80 -20
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 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;
}
@@ -43,7 +38,7 @@ export class SearchHelper implements ISearchHelper {
// 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 +47,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;
}
@@ -67,9 +62,10 @@ 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 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;
}
@@ -84,7 +80,7 @@ export class SearchHelper implements ISearchHelper {
// 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 +89,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,19 +101,61 @@ export class SearchHelper implements ISearchHelper {
}
/**
* Searches a line for a 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 Tne 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[i];
const charData = line.get(i);
// Adjust the searchIndex to normalize emoji into single chars
const char = charData[1/*CHAR_DATA_CHAR_INDEX*/];
if (char.length > 1) {
@@ -138,6 +176,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);
lineIndex++;
} while (lineWrapsToNext);
return lineString;
}
/**
* Selects and scrolls to a result.
* @param result The result to select.
+248
View File
@@ -0,0 +1,248 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert, expect } from 'chai';
import * as search from './search';
import { SearchHelper } from './SearchHelper';
import { ISearchOptions, ISearchResult } from './Interfaces';
class MockTerminalPlain {}
class MockTerminal {
private _core: any;
public searchHelper: TestSearchHelper;
public cols: number;
constructor(options: 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;
}
pushWriteData(): void {
this._core._innerWrite();
}
}
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);
assert.equal(typeof (<any>MockTerminalPlain).prototype.findNext, 'function');
assert.equal(typeof (<any>MockTerminalPlain).prototype.findPrevious, 'function');
});
});
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'});
});
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);
});
});
});
+11 -9
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 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 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);
};
}
+4
View File
@@ -2,6 +2,10 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es6",
],
"rootDir": ".",
"outDir": "../../../lib/addons/search/",
"sourceMap": true,
+4
View File
@@ -2,6 +2,10 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es6",
],
"rootDir": ".",
"outDir": "../../../lib/addons/terminado/",
"sourceMap": true,
+4
View File
@@ -2,6 +2,10 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es6",
],
"rootDir": ".",
"outDir": "../../../lib/addons/webLinks/",
"sourceMap": true,
+4
View File
@@ -2,6 +2,10 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es6",
],
"rootDir": ".",
"outDir": "../../../lib/addons/winptyCompat/",
"sourceMap": true,
+2 -2
View File
@@ -30,11 +30,11 @@ export function winptyCompatInit(terminal: Terminal): void {
// wrapped.
addonTerminal.on('linefeed', () => {
const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1);
const lastChar = line[addonTerminal.cols - 1];
const lastChar = line.get(addonTerminal.cols - 1);
if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE) {
const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y);
(<any>nextLine).isWrapped = true;
nextLine.isWrapped = true;
}
});
}
+4
View File
@@ -2,6 +2,10 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es6",
],
"rootDir": ".",
"outDir": "../../../lib/addons/zmodem/",
"sourceMap": true,
+7 -7
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;
}
@@ -104,7 +104,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
* 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)];
}
@@ -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[]};
@@ -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;
}
};
}
+17
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,16 @@ export interface IKeyboardEvent {
key: string;
type: string;
}
export interface ICircularList<T> extends IEventEmitter {
length: number;
maxLength: number;
get(index: number): T | undefined;
set(index: number, value: T): void;
push(value: T): void;
pop(): T | undefined;
splice(start: number, deleteCount: number, ...items: T[]): void;
trimStart(count: number): void;
shiftElements(start: number, count: number, offset: number): void;
}
+6 -5
View File
@@ -3,7 +3,8 @@
* @license MIT
*/
import { ITerminal, ICircularList, LineData } from '../Types';
import { ITerminal, IBufferLine } from '../Types';
import { ICircularList } from '../common/Types';
import { C0 } from '../common/data/EscapeSequences';
const enum Direction {
@@ -18,7 +19,7 @@ export class AltClickHandler {
private _startCol: number;
private _endRow: number;
private _endCol: number;
private _lines: ICircularList<LineData>;
private _lines: ICircularList<IBufferLine>;
constructor(
private _mouseEvent: MouseEvent,
@@ -138,7 +139,7 @@ export class AltClickHandler {
for (let i = 0; i < Math.abs(startRow - endRow); i++) {
const direction = this._verticalDirection() === Direction.UP ? -1 : 1;
if ((<any>this._lines.get(startRow + (direction * i))).isWrapped) {
if (this._lines.get(startRow + (direction * i)).isWrapped) {
wrappedRows++;
}
}
@@ -152,12 +153,12 @@ export class AltClickHandler {
*/
private _wrappedRowsForRow(currentRow: number): number {
let rowCount = 0;
let lineWraps = (<any>this._lines.get(currentRow)).isWrapped;
let lineWraps = this._lines.get(currentRow).isWrapped;
while (lineWraps && currentRow >= 0 && currentRow < this._terminal.rows) {
rowCount++;
currentRow--;
lineWraps = (<any>this._lines.get(currentRow)).isWrapped;
lineWraps = this._lines.get(currentRow).isWrapped;
}
return rowCount;
+25 -2
View File
@@ -5,7 +5,7 @@
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';
@@ -22,6 +22,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 +51,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 +261,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
);
+31 -19
View File
@@ -1,11 +1,12 @@
import { assert } from 'chai';
import { LineData, CharData } from '../Types';
import { MockTerminal, MockBuffer } from '../utils/TestUtils.test';
import { CircularList } from '../common/CircularList';
import { ICharacterJoinerRegistry } from './Types';
import { CharacterJoinerRegistry } from './CharacterJoinerRegistry';
import { BufferLine } from '../BufferLine';
import { IBufferLine } from '../Types';
describe('CharacterJoinerRegistry', () => {
let registry: ICharacterJoinerRegistry;
@@ -14,22 +15,25 @@ describe('CharacterJoinerRegistry', () => {
const terminal = new MockTerminal();
terminal.cols = 16;
terminal.buffer = new MockBuffer();
const lines = new CircularList<LineData>(7);
lines.set(0, lineData('a -> b -> c -> d'));
lines.set(1, lineData('a -> b => c -> d'));
lines.set(2, [...lineData('a -> b -', 0xFFFFFFFF), ...lineData('> c -> d', 0)]);
lines.set(3, lineData('no joined ranges'));
lines.set(4, []);
lines.set(5, [...lineData('a', 0x11111111), ...lineData(' -> b -> c -> '), ...lineData('d', 0x22222222)]);
lines.set(6, [
...lineData('wi'),
[0, '¥', 2, '¥'.charCodeAt(0)],
[0, '', 0, null],
...lineData('deemo'),
[0, '\xf0\x9f\x98\x81', 1, 128513],
[0, ' ', 1, ' '.charCodeAt(0)],
...lineData('jiabc')
]);
const lines = new CircularList<IBufferLine>(7);
lines.set(0, lineData([['a -> b -> c -> d']]));
lines.set(1, lineData([['a -> b => c -> d']]));
lines.set(2, lineData([['a -> b -', 0xFFFFFFFF], ['> c -> d', 0]]));
lines.set(3, lineData([['no joined ranges']]));
lines.set(4, new BufferLine());
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]);
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)]);
sub = lineData([['jiabc']]);
for (let i = 0; i < sub.length; ++i) line6.push(sub.get(i));
lines.set(6, line6);
(<MockBuffer>terminal.buffer).setLines(lines);
terminal.buffer.ydisp = 0;
registry = new CharacterJoinerRegistry(terminal);
@@ -257,8 +261,16 @@ describe('CharacterJoinerRegistry', () => {
});
});
function lineData(line: string, attr: number = 0): LineData {
return line.split('').map<CharData>(char => [attr, char, 1, char.charCodeAt(0)]);
type IPartialLineData = ([string] | [string, number]);
function lineData(data: IPartialLineData[]): IBufferLine {
const tline = new BufferLine();
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)]));
}
return tline;
}
function substringJoiner(substring: string): (sequence: string) => [number, number][] {
+6 -6
View File
@@ -1,5 +1,5 @@
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
import { ITerminal, LineData } from '../Types';
import { ITerminal, IBufferLine } from '../Types';
import { ICharacterJoinerRegistry, ICharacterJoiner } from './Types';
export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
@@ -51,10 +51,10 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
let rangeStartColumn = 0;
let currentStringIndex = 0;
let rangeStartStringIndex = 0;
let rangeAttr = line[0][CHAR_DATA_ATTR_INDEX] >> 9;
let rangeAttr = line.get(0)[CHAR_DATA_ATTR_INDEX] >> 9;
for (let x = 0; x < this._terminal.cols; x++) {
const charData = line[x];
const charData = line.get(x);
const chars = charData[CHAR_DATA_CHAR_INDEX];
const width = charData[CHAR_DATA_WIDTH_INDEX];
const attr = charData[CHAR_DATA_ATTR_INDEX] >> 9;
@@ -115,7 +115,7 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
* @param startIndex Start position of the range to search in the string (inclusive)
* @param endIndex End position of the range to search in the string (exclusive)
*/
private _getJoinedRanges(line: string, startIndex: number, endIndex: number, lineData: LineData, startCol: number): [number, number][] {
private _getJoinedRanges(line: string, startIndex: number, endIndex: number, lineData: IBufferLine, startCol: number): [number, number][] {
const text = line.substring(startIndex, endIndex);
// At this point we already know that there is at least one joiner so
// we can just pull its value and assign it directly rather than
@@ -140,7 +140,7 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
* @param line Cell data for the relevant line in the terminal
* @param startCol Offset within the line to start from
*/
private _stringRangesToCellRanges(ranges: [number, number][], line: LineData, startCol: number): void {
private _stringRangesToCellRanges(ranges: [number, number][], line: IBufferLine, startCol: number): void {
let currentRangeIndex = 0;
let currentRangeStarted = false;
let currentStringIndex = 0;
@@ -152,7 +152,7 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
}
for (let x = startCol; x < this._terminal.cols; x++) {
const charData = line[x];
const charData = line.get(x);
const width = charData[CHAR_DATA_WIDTH_INDEX];
const length = charData[CHAR_DATA_CHAR_INDEX].length;
+1 -1
View File
@@ -127,7 +127,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
return;
}
const charData = terminal.buffer.lines.get(cursorY)[terminal.buffer.x];
const charData = terminal.buffer.lines.get(cursorY).get(terminal.buffer.x);
if (!charData) {
return;
}
+10 -1
View File
@@ -6,6 +6,7 @@
import { ILinkHoverEvent, ITerminal, ILinkifierAccessor, LinkHoverEventTypes } from '../Types';
import { IColorSet, IRenderDimensions } from './Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { INVERTED_DEFAULT_COLOR } from './atlas/Types';
export class LinkRenderLayer extends BaseRenderLayer {
private _state: ILinkHoverEvent = null;
@@ -39,7 +40,15 @@ export class LinkRenderLayer extends BaseRenderLayer {
}
private _onLinkHover(e: ILinkHoverEvent): void {
this._ctx.fillStyle = this._colors.foreground.css;
if (e.fg === INVERTED_DEFAULT_COLOR) {
this._ctx.fillStyle = this._colors.background.css;
} else if (e.fg < 256) {
// 256 color support
this._ctx.fillStyle = this._colors.ansi[e.fg].css;
} else {
this._ctx.fillStyle = this._colors.foreground.css;
}
if (e.y1 === e.y2) {
// Single line link
this.fillBottomLineAtCells(e.x1, e.y1, e.x2 - e.x1);
+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';
+2 -2
View File
@@ -71,7 +71,7 @@ export class TextRenderLayer extends BaseRenderLayer {
const line = terminal.buffer.lines.get(row);
const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : [];
for (let x = 0; x < terminal.cols; x++) {
const charData = line[x];
const charData = line.get(x);
let code: number = <number>charData[CHAR_DATA_CODE_INDEX];
// Can either represent character(s) for a single cell or multiple cells
@@ -124,7 +124,7 @@ export class TextRenderLayer extends BaseRenderLayer {
// get removed, and `a` would not re-render because it thinks it's
// already in the correct state.
// this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA;
if (lastCharX < line.length - 1 && line[lastCharX + 1][CHAR_DATA_CODE_INDEX] === NULL_CELL_CODE) {
if (lastCharX < line.length - 1 && line.get(lastCharX + 1)[CHAR_DATA_CODE_INDEX] === NULL_CELL_CODE) {
width = 2;
// this._clearChar(x + 1, y);
// The overlapping char's char data will force a clear and render when the
+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.
+98 -14
View File
@@ -10,6 +10,7 @@ import BaseCharAtlas from './BaseCharAtlas';
import { DEFAULT_ANSI_COLORS } from '../ColorManager';
import { clearColor } from '../../shared/atlas/CharAtlasGenerator';
import LRUMap from './LRUMap';
import { isFirefox, isSafari } from '../../shared/utils/Browser';
// 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 +30,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}`;
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 +73,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 +107,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,9 +124,14 @@ 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;
}
const glyphKey = getGlyphCacheKey(glyph);
const cacheValue = this._cacheMap.get(glyphKey);
if (cacheValue != null) {
if (cacheValue !== null && cacheValue !== undefined) {
this._drawFromCache(ctx, cacheValue, x, y);
return true;
} else if (this._canCache(glyph) && this._drawToCacheCount < FRAME_CACHE_DRAW_LIMIT) {
@@ -124,11 +161,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 +179,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 +269,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 -1
View File
@@ -53,7 +53,7 @@ export default class StaticCharAtlas extends BaseCharAtlas {
y: number
): boolean {
// we're not warmed up yet
if (this._texture == null) {
if (this._texture === null || this._texture === undefined) {
return false;
}
+37 -8
View File
@@ -4,12 +4,12 @@
*/
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';
const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
const ROW_CONTAINER_CLASS = 'xterm-rows';
@@ -79,6 +79,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 {
@@ -160,13 +163,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 +=
@@ -319,7 +328,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, terminal.charMeasure.width, terminal.cols));
}
this._terminal.emit('refresh', {start, end});
@@ -331,4 +341,23 @@ 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 span = <HTMLElement>this._rowElements[y].children[x];
span.style.textDecoration = enabled ? 'underline' : 'none';
x = (x + 1) % cols;
if (x === 0) {
y++;
}
}
}
}
+36 -33
View File
@@ -6,14 +6,15 @@
import jsdom = require('jsdom');
import { assert } from 'chai';
import { DomRendererRowFactory } from './DomRendererRowFactory';
import { LineData } from '../../Types';
import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../../Buffer';
import { FLAGS } from '../Types';
import { BufferLine } from '../../BufferLine';
import { IBufferLine } from '../../Types';
describe('DomRendererRowFactory', () => {
let dom: jsdom.JSDOM;
let rowFactory: DomRendererRowFactory;
let lineData: LineData;
let lineData: IBufferLine;
beforeEach(() => {
dom = new jsdom.JSDOM('');
@@ -23,7 +24,7 @@ describe('DomRendererRowFactory', () => {
describe('createRow', () => {
it('should create an element for every character in the row', () => {
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span> </span>' +
'<span> </span>'
@@ -31,27 +32,29 @@ describe('DomRendererRowFactory', () => {
});
it('should set correct attributes for double width characters', () => {
lineData[0] = [DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)];
lineData.set(0, [DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)]);
// There should be no element for the following "empty" cell
lineData[1] = [DEFAULT_ATTR, '', 0, undefined];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
lineData.set(1, [DEFAULT_ATTR, '', 0, undefined]);
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>` +
'<span> </span>'
);
}
});
it('should not render cells that go beyond the terminal\'s columns', () => {
lineData[0] = [DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)];
lineData[1] = [DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 1);
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, undefined, 0, 5, 1);
assert.equal(getFragmentHtml(fragment),
'<span>a</span>'
);
@@ -59,8 +62,8 @@ describe('DomRendererRowFactory', () => {
describe('attributes', () => {
it('should add class for bold', () => {
lineData[0] = [DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
lineData.set(0, [DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-bold">a</span>' +
'<span> </span>'
@@ -68,8 +71,8 @@ describe('DomRendererRowFactory', () => {
});
it('should add class for italic', () => {
lineData[0] = [DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
lineData.set(0, [DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-italic">a</span>' +
'<span> </span>'
@@ -79,8 +82,8 @@ describe('DomRendererRowFactory', () => {
it('should add classes for 256 foreground colors', () => {
const defaultAttrNoFgColor = (0 << 9) | (256 << 0);
for (let i = 0; i < 256; i++) {
lineData[0] = [defaultAttrNoFgColor | (i << 9), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
lineData.set(0, [defaultAttrNoFgColor | (i << 9), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-fg-${i}">a</span>` +
'<span> </span>'
@@ -91,8 +94,8 @@ describe('DomRendererRowFactory', () => {
it('should add classes for 256 background colors', () => {
const defaultAttrNoBgColor = (257 << 9) | (0 << 0);
for (let i = 0; i < 256; i++) {
lineData[0] = [defaultAttrNoBgColor | (i << 0), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
lineData.set(0, [defaultAttrNoBgColor | (i << 0), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-bg-${i}">a</span>` +
'<span> </span>'
@@ -101,8 +104,8 @@ describe('DomRendererRowFactory', () => {
});
it('should correctly invert colors', () => {
lineData[0] = [(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
lineData.set(0, [(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-1 xterm-bg-2">a</span>' +
'<span> </span>'
@@ -110,8 +113,8 @@ describe('DomRendererRowFactory', () => {
});
it('should correctly invert default fg color', () => {
lineData[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) | (257 << 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>'
@@ -119,8 +122,8 @@ describe('DomRendererRowFactory', () => {
});
it('should correctly invert default bg color', () => {
lineData[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) | (256 << 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>'
@@ -129,8 +132,8 @@ describe('DomRendererRowFactory', () => {
it('should turn bold fg text bright', () => {
for (let i = 0; i < 8; i++) {
lineData[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) | (256 << 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>'
@@ -146,8 +149,8 @@ describe('DomRendererRowFactory', () => {
return element.innerHTML;
}
function createEmptyLineData(cols: number): LineData {
const lineData: LineData = [];
function createEmptyLineData(cols: number): IBufferLine {
const lineData = new BufferLine();
for (let i = 0; i < cols; i++) {
lineData.push([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
}
+18 -3
View File
@@ -3,13 +3,16 @@
* @license MIT
*/
import { LineData } from '../../Types';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../../Buffer';
import { FLAGS } from '../Types';
import { IBufferLine } from '../../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,7 +20,7 @@ export class DomRendererRowFactory {
) {
}
public createRow(lineData: LineData, 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;
@@ -27,7 +30,7 @@ export class DomRendererRowFactory {
continue;
}
const charData = lineData[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];
@@ -48,6 +51,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.
+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
+10 -5
View File
@@ -22,11 +22,16 @@ export class RenderDebouncer implements IDisposable {
}
}
public refresh(rowStart?: number, rowEnd?: number): void {
rowStart = rowStart || 0;
rowEnd = rowEnd || this._terminal.rows - 1;
this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart;
this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd;
public refresh(rowStart: number, rowEnd: number): void {
// Get the min/max row start/end for the arg values
rowStart = rowStart !== null && rowStart !== undefined ? rowStart : 0;
rowEnd = rowEnd !== null && rowEnd !== undefined ? rowEnd : this._terminal.rows - 1;
// Check whether the row start/end values have already been set
const isRowStartSet = this._rowStart !== undefined && this._rowStart !== null;
const isRowEndSet = this._rowEnd !== undefined && this._rowEnd !== null;
// Set the properties to the updated values
this._rowStart = isRowStartSet ? Math.min(this._rowStart, rowStart) : rowStart;
this._rowEnd = isRowEndSet ? Math.max(this._rowEnd, rowEnd) : rowEnd;
if (this._animationFrame) {
return;
+1 -1
View File
@@ -11,7 +11,7 @@ export class MouseHelper {
public static getCoordsRelativeToElement(event: {pageX: number, pageY: number}, element: HTMLElement): [number, number] {
// Ignore browsers that don't support MouseEvent.pageX
if (event.pageX == null) {
if (event.pageX === null || event.pageX === undefined) {
return null;
}
+20 -16
View File
@@ -4,10 +4,19 @@
*/
import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types';
import { LineData, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener, CharacterJoinerHandler } from '../Types';
import { Buffer, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../Buffer';
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 { 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[];
@@ -145,14 +154,6 @@ export class MockTerminal implements ITerminal {
refresh(start: number, end: number): void {
throw new Error('Method not implemented.');
}
blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData {
const line: LineData = [];
cols = cols || this.cols;
for (let i = 0; i < cols; i++) {
line.push([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
}
return line;
}
registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; }
deregisterCharacterJoiner(joinerId: number): void { }
}
@@ -228,9 +229,6 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
eraseLeft(x: number, y: number): void {
throw new Error('Method not implemented.');
}
blankLine(cur?: boolean, isWrapped?: boolean): [number, string, number, number][] {
throw new Error('Method not implemented.');
}
prevStop(x?: number): number {
throw new Error('Method not implemented.');
}
@@ -295,7 +293,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
export class MockBuffer implements IBuffer {
isCursorInViewport: boolean;
lines: ICircularList<[number, string, number, number][]>;
lines: ICircularList<IBufferLine>;
ydisp: number;
ybase: number;
hasScrollback: boolean;
@@ -310,7 +308,7 @@ export class MockBuffer implements IBuffer {
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.');
@@ -318,9 +316,15 @@ export class MockBuffer implements IBuffer {
prevStop(x?: number): number {
throw new Error('Method not implemented.');
}
setLines(lines: ICircularList<[number, string, number, number][]>): void {
setLines(lines: ICircularList<IBufferLine>): void {
this.lines = lines;
}
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
View File
@@ -5,6 +5,7 @@
"lib": [
"dom",
"es5",
"es6",
"scripthost",
"es2015.promise"
],
+3 -6
View File
@@ -39,6 +39,7 @@
"one-variable-per-declaration": true,
"no-unsafe-finally": true,
"no-var-keyword": true,
"prefer-const": true,
"quotemark": [
true,
"single"
@@ -59,10 +60,7 @@
"esSpecCompliant": true
}
],
"triple-equals": [
true,
"allow-null-check"
],
"triple-equals": true,
"typedef-whitespace": [
true,
{
@@ -110,7 +108,6 @@
},
"prefer-const-enum": [
true
],
"prefer-const": true
]
}
}
+19 -9
View File
@@ -39,6 +39,16 @@ declare module 'xterm' {
*/
bellStyle?: 'none' /*| 'visual'*/ | 'sound' /*| 'both'*/;
/**
* When enabled the cursor will be set to the beginning of the next line
* with every new line. This equivalent to sending '\r\n' for each '\n'.
* Normally the termios settings of the underlying PTY deals with the
* translation of '\n' to '\r\n' and this setting should not be used. If you
* deal with data from a non-PTY related source, this settings might be
* useful.
*/
convertEol?: boolean;
/**
* The number of columns in the terminal.
*/
@@ -134,19 +144,13 @@ declare module 'xterm' {
macOptionClickForcesSelection?: boolean;
/**
* (EXPERIMENTAL) The type of renderer to use, this allows using the
* fallback DOM renderer when canvas is too slow for the environment. The
* following features do not work when the DOM renderer is used:
* The type of renderer to use, this allows using the fallback DOM renderer
* when canvas is too slow for the environment. The following features do
* not work when the DOM renderer is used:
*
* - Links
* - Line height
* - Letter spacing
* - Cursor blink
* - Cursor style
*
* This option is marked as experiemental because it will eventually be
* moved to an addon. You can only set this option in the constructor (not
* setOption).
*/
rendererType?: RendererType;
@@ -425,6 +429,12 @@ declare module 'xterm' {
*/
emit(type: string, data?: any): void;
/**
* Adds an event listener to the Terminal, returning an IDisposable that can
* be used to conveniently remove the event listener.
* @param type The type of event.
* @param handler The event handler.
*/
addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable;
/**
-19
View File
@@ -1,19 +0,0 @@
const path = require('path');
module.exports = {
entry: './demo/main.js',
output: {
path: path.resolve(__dirname, 'demo/dist'),
filename: 'bundle.js'
},
devtool: 'source-map',
module: {
rules: [
{
test: /\.js$/,
use: ["source-map-loader"],
enforce: "pre"
}
]
}
};
+696 -303
View File
File diff suppressed because it is too large Load Diff