Compare commits

..
164 Commits
Author SHA1 Message Date
Daniel Imms 27c3663470 Bump version to 3.7.0 2018-09-08 02:38:05 -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
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 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
Daniel Imms 86800d2d71 Bump version to 3.6.0 2018-08-10 19:28:06 -07:00
Daniel Imms 41fdc9071a Update AUTHORS 2018-08-10 19:27:16 -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
Daniel Imms ff63d3eb23 Merge pull request #1601 from segevfiner/source-map-loader
Add source-map-loader to webpack config
2018-08-07 14:11:08 -07:00
Daniel Imms 05b8ef1727 Merge pull request #1599 from 7PH/master
#1591: Fix bold inverted text displayed with wrong color
2018-08-07 14:07:32 -07:00
Segev Finer 458f2d8bf1 Add source-map-loader to webpack config
This makes source maps work in the demo
2018-08-07 22:39:09 +03:00
PowerHat 87a84b608c #1591: Fix bold inverted text displayed with wrong color 2018-08-06 22:41:57 +02:00
Daniel Imms dd7a9c3b7a Merge pull request #1594 from Tyriar/1593_column_select
Fix bug in column selection rendering
2018-08-06 12:32:35 -07:00
Daniel Imms d1ca0d7839 Merge branch 'master' into 1593_column_select 2018-08-06 11:33:59 -07:00
Daniel Imms 3419480b98 Merge pull request #1586 from Tyriar/1579_emit_pos_directly
Report device status directly via emit
2018-08-06 11:26:15 -07:00
Daniel Imms a003a0c585 Merge remote-tracking branch 'origin/master' into 1579_emit_pos_directly 2018-08-06 11:21:35 -07:00
Daniel Imms e3dfe4bc32 Merge branch 'master' into 1593_column_select 2018-08-06 11:17:52 -07:00
Daniel Imms c9ec7aa215 Fix flickering when dragging below viewport 2018-08-06 11:17:28 -07:00
Daniel Imms 7675be7da3 Fix dragging when in column selection mode 2018-08-06 11:13:56 -07:00
Daniel Imms 1a03b703f1 Merge pull request #1578 from Tyriar/1567_npe
Fix NPE in InputHandler.parse
2018-08-06 07:31:28 -07:00
Daniel Imms 9165581266 Merge branch 'master' into 1593_column_select 2018-08-06 07:24:46 -07:00
Daniel Imms 6a770296f9 Add tests for Dispoable 2018-08-06 07:22:32 -07:00
Daniel Imms cbfb99a5cf Move _isDisposed to Disposable class 2018-08-06 07:16:51 -07:00
Daniel Imms bae3f64c10 Merge branch 'master' into 1567_npe 2018-08-06 07:14:33 -07:00
Daniel Imms 7c7102d8a6 Merge pull request #1556 from xtermjs/Tyriar-patch-1
Add pointer to .d.ts and mention addon CSS
2018-08-06 07:13:21 -07:00
Daniel Imms 8e9ba23227 Merge pull request #1598 from xtermjs/remove_travis
Remove Travis CI badge
2018-08-06 07:12:35 -07:00
Daniel Imms 1d91e3c5bf Merge pull request #1597 from Tyriar/1596_a11y_npe
Fix NPE in AccessibilityManager
2018-08-06 07:12:23 -07:00
Daniel Imms 45d4622920 Merge pull request #1590 from Tyriar/1589_swap_renderer
Allow renderer to be swapped in and out at runtime
2018-08-06 07:08:40 -07:00
Daniel Imms b7da4b6bfe Merge pull request #1588 from Tyriar/1584_use_const
Use const for null cell
2018-08-06 07:08:21 -07:00
Daniel Imms fdf51c119b Merge pull request #1585 from Tyriar/1571_dbl_click_wrapped
Make double click select word aware of wrapped lines
2018-08-06 07:06:45 -07:00
Daniel Imms 6de8c91059 Merge pull request #1583 from Tyriar/remove_jsdoc_types
Remove jsdoc types
2018-08-06 07:06:29 -07:00
Daniel Imms 490f0ad329 Remove Travis CI badge 2018-08-03 19:20:21 -07:00
Daniel Imms 882aea357f Fix NPE in AccessibilityManager
Fixes #1596
2018-08-03 11:10:51 -07:00
Daniel Imms d8eedfa1c7 Fix bug in column selection rendering
Fixes #1593
2018-07-31 10:56:37 -07:00
Daniel Imms 90e57c9b3c Merge branch 'master' into 1571_dbl_click_wrapped 2018-07-29 19:12:07 -07:00
Daniel Imms a269e7f5d9 Merge pull request #1569 from Tyriar/1505_deprecate_emit
Deprecate Terminal.emit
2018-07-29 19:11:15 -07:00
Daniel Imms 4a3c1a4cc6 Store theme in right place 2018-07-29 14:25:55 -07:00
Daniel Imms 804cb36adc Retain theme when renderers are swapped 2018-07-29 14:17:25 -07:00
Daniel Imms 5aaf9ef11b Allow renderer to be swapped in and out at runtime
Fixes #1589
2018-07-29 13:50:45 -07:00
Daniel Imms c65dd1f476 Use const for null cell
Fixes #1584
2018-07-29 13:05:27 -07:00
Daniel Imms 05f136b3da Add link to GitHub dependents to real world uses 2018-07-28 12:42:11 -07:00
Daniel Imms 1c4cb8c620 Reinstate IInputHandlingTerminal
Fixes #1587
2018-07-27 11:46:09 -07:00
Daniel Imms fec66c7dac Remove Terminal.send 2018-07-27 11:32:11 -07:00
Daniel Imms e82f78fc84 Merge branch 'master' into 1571_dbl_click_wrapped 2018-07-26 12:28:37 -07:00
Daniel Imms 67d6ad57c3 Add tests 2018-07-26 12:23:34 -07:00
Daniel Imms 48f1c4667a Fix select to word upwards 2018-07-26 11:30:49 -07:00
Daniel Imms 6a9da97163 Cover select to word downwards 2018-07-26 11:15:34 -07:00
Daniel Imms 7b68cb9fe7 Support double click select word over wrapped lines
Part of #1571
2018-07-26 11:09:29 -07:00
Daniel Imms 6691f80906 Merge pull request #1581 from Tyriar/exp_api
Add note in readme about API/experimental API
2018-07-25 17:53:22 -07:00
Daniel Imms bdfe877c8f Remove jsdoc types
They're redundant in TS/tsdoc
2018-07-24 08:59:20 -07:00
Daniel Imms ee3668eb03 Report device status directly via emit
Fixes #1579
2018-07-24 08:40:51 -07:00
Daniel Imms 4a443c4a45 Move API section to getting started, above addons 2018-07-24 06:30:18 -07:00
Daniel Imms d964c23c43 Add note in readme about API/experimental API 2018-07-24 06:27:05 -07:00
Daniel Imms 20d4c64ffe Make write/_innerWrite aware of terminal disposal 2018-07-23 15:37:04 -07:00
Daniel Imms bf3709f4c5 Fix NPE in InputHandler.parse
Fixes #1567
2018-07-23 15:29:45 -07:00
Daniel Imms 8f22d8dda4 Merge pull request #1574 from vincentwoo/patch-1
refreshRowElements argument order was flipped
2018-07-19 08:14:08 -07:00
Vincent Woo 0f6a8d3991 refreshRowElements argument order was flipped
This is potentially a fix for #1573 but I have not tested.
2018-07-18 17:55:59 -07:00
Daniel Imms 5b8da8c89a Merge pull request #1570 from nikonso/fix-1509
Prevent Uncaught TypeError  - Cannot read property 'isWrapped' on Linkifier
2018-07-17 08:43:08 -07:00
Bruno Ribeito f2e5ea5783 Remove unnecessary get line 2018-07-16 21:11:35 +01:00
Bruno Ribeito cb888c343e break instead of returning 2018-07-15 19:48:16 +01:00
Bruno Ribeito 18f37440dc Prevent Uncaught TypeError
- Cannot read property 'isWrapped' on Linkifier
2018-07-15 19:36:49 +01:00
Daniel Imms 3582f8733d Deprecate emit
Part of #1505
2018-07-14 10:46:55 -07:00
Daniel Imms b84dba2e8d Merge pull request #1566 from Tyriar/1565_charset_fix
Fix charset drawing
2018-07-12 08:45:21 -07:00
Daniel Imms 61fc4ca8a6 Fix charset drawing
Fixes #1565
2018-07-12 08:27:57 -07:00
Daniel Imms bbd234b951 Merge branch 'master' into Tyriar-patch-1 2018-07-10 18:26:31 -07:00
Daniel Imms 0545cfa121 Merge pull request #1563 from Tyriar/1562_yarn
Use yarn
2018-07-10 18:26:12 -07:00
Daniel Imms 26969b725e Merge branch 'master' into 1562_yarn 2018-07-10 17:47:23 -07:00
Daniel Imms eb31732af0 Merge pull request #1564 from Tyriar/remove_travis
Remove TravisCI
2018-07-10 17:47:05 -07:00
Daniel Imms 9c08f78017 Remove TravisCI 2018-07-10 17:33:37 -07:00
Daniel Imms 54dcbeb6cf Use yarn. 2018-07-10 17:25:39 -07:00
Daniel Imms a52aab4618 Merge pull request #1460 from princjef/character-joiner
Add optional character joiner
2018-07-10 17:01:20 -07:00
Daniel Imms 5f5a49995a Merge pull request #1555 from jerch/fix_out_of_bound_deopt
fix out of bound deopt in EscapeSequenceParser.parse
2018-07-10 17:00:58 -07:00
Daniel Imms ac7239732d Merge pull request #1558 from xtermjs/Tyriar-patch-2
Move developer documentation to the wiki
2018-07-10 17:00:25 -07:00
Daniel Imms 68d537fb28 Merge pull request #1561 from roke22/master
Add project that use xterm to the README file
2018-07-10 17:00:14 -07:00
Daniel Imms 7ce071c575 Remove extra list item 2018-07-10 08:24:59 -07:00
Jose Anton 42f1aba00c Add project that use xterm to the README file 2018-07-10 16:20:58 +02:00
Daniel Imms dfb1b4db95 Move third party deps note to wiki 2018-07-09 09:25:35 -07:00
Daniel Imms 6900bbc07a Move developer documentation to the wiki 2018-07-09 09:24:07 -07:00
Daniel Imms d4b507ddb1 Inline private member to ctor 2018-07-09 07:05:12 -07:00
Daniel Imms 1bad71f13a Merge branch 'master' into character-joiner 2018-07-09 06:18:03 -07:00
Jeff Principe 63b3a6f323 Factor logic into registry, make matches cell-based and document 2018-07-08 11:30:54 -07:00
Jörg Breitbart ba7976f3b1 do while loop 2018-07-07 21:20:03 +02:00
Daniel Imms b5b350fdcf Add pointer to .d.ts and mention addon CSS
Part of #1554
2018-07-07 11:12:29 -07:00
Jörg Breitbart 87e292414d fix out of bound deopt in EscapeSequenceParser.parse 2018-07-07 12:46:51 +02:00
Daniel Imms 5e3ebbde5e Merge branch 'master' into character-joiner 2018-07-03 11:50:27 -07:00
Daniel Imms 6d9d2a3304 Merge remote-tracking branch 'origin/master' into pr/princjef/1460 2018-07-03 11:50:07 -07:00
Jeff Principe eabb25c796 add ability to register/deregister character joiner 2018-06-02 10:24:52 -07:00
61 changed files with 8263 additions and 767 deletions
-19
View File
@@ -1,19 +0,0 @@
language: node_js
os:
- linux
node_js:
- 6
before_install:
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test ; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -qq update ; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -qq install g++-4.8 ; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export CXX=g++-4.8 ; fi
- npm install -g npm@5.1.0
env:
matrix:
- NPM_COMMAND=tsc
- NPM_COMMAND=lint
- NPM_COMMAND=test-coverage
notifications:
email: false
script: npm run $NPM_COMMAND
+4
View File
@@ -61,6 +61,7 @@ irokas <akasidiari@inf.uth.gr>
Jakob Gillich <jakob@gillich.me>
Jan Kuri <jkuri88@gmail.com>
Jean Bruenn <himself@jeanbruenn.info>
Jeff Principe <princjef@gmail.com>
Jeremy Danyow <jedanyow@microsoft.com>
Jianhui Zhao <jianhuizhao329@gmail.com>
Joao Moreno <jomo@microsoft.com>
@@ -70,6 +71,7 @@ Jon Austin <jon.i.austin@gmail.com>
Jon Kohler <jon@nutanix.com>
Jon Masters <jon.masters@sky.com>
Jörg Breitbart <jerch@rockborn.de>
Jose Anton <roke22@gmail.com>
jpoth <poth.john@gmail.com>
Justin Luk <jluk@users.noreply.github.com>
Justin Mecham <justin@mecham.me>
@@ -103,6 +105,7 @@ Peng Xiao <pengxiao@outlook.com>
Peter Baumgarten <me@peterbaumgarten.com>
Philip Olson <philip.olson@protonmail.ch>
PowerHat <31401273+7PH@users.noreply.github.com>
PowerHat <b.raymond@protonmail.com>
pro-src <34285059+pro-src@users.noreply.github.com>
pro-src <rodneyd.teal@gmail.com>
Rick Baker <rick@ricktbaker.com>
@@ -111,6 +114,7 @@ Saad Malik <simfox3@gmail.com>
Samuel Williams <samuel.williams@oriontransfer.co.nz>
Saswat Das <saswatds@users.noreply.github.com>
Saul Costa <saul@codevolve.com>
Segev Finer <segev208@gmail.com>
Shuanglei Tao <tsl0922@gmail.com>
sitzmar <sitzmar@gmail.com>
Steven Silvester <steven.silvester@ieee.org>
+2 -5
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).
@@ -50,7 +51,3 @@ By contributing code to xterm.js you
holder has explicitly granted the right to use it like this,
through a compatible open source license or through a direct
agreement with you.)
### Third party dependencies
We prefer to not include any non-dev third party dependencies in order to keep our code minimal, performant and secure. If you plan on adding a dependency on a third party library it's a good idea to discuss the need in an issue with the maintainers first.
+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/)
+22 -90
View File
@@ -1,6 +1,9 @@
# [![xterm.js logo](logo-full.png)](https://xtermjs.org)
[![Travis CI build status](https://api.travis-ci.org/xtermjs/xterm.js.svg)](https://travis-ci.org/xtermjs/xterm.js) [![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) [![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)
[![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)
[![Coverage Status](https://coveralls.io/repos/github/xtermjs/xterm.js/badge.svg?branch=master)](https://coveralls.io/github/xtermjs/xterm.js?branch=master)
[![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/xterm/badge?style=rounded)](https://www.jsdelivr.com/package/npm/xterm)
Xterm.js is a terminal front-end component written in JavaScript that works in the browser.
@@ -57,6 +60,12 @@ The proposed way to load xterm.js is via the ES6 module syntax.
import { Terminal } from 'xterm';
```
### API
The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API.
Note that some APIs are marked *experimental*, these are added so we can experiment with new ideas without committing to support it like a normal semver API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs.
### Addons
Addons are JavaScript modules that extend the `Terminal` prototype with new methods and attributes to provide additional functionality. There are a handful available in the main repository in the `src/addons` directory and you can even write your own, by using xterm.js' public API.
@@ -74,6 +83,8 @@ var xterm = new Terminal(); // Instantiate the terminal
xterm.fit(); // Use the `fit` method, provided by the `fit` addon
```
You will also need to include the addon's CSS file if it has one in the folder.
#### Importing Addons in TypeScript
There are currently no typings for addons if they are accessed via extending Terminal prototype, so you will need to upcast if using TypeScript, eg. `(<any>xterm).fit()`.
@@ -107,6 +118,10 @@ Since xterm.js is typically implemented as a developer tool, only modern browser
Xterm.js works seamlessly in Electron apps and may even work on earlier versions of the browsers but these are the browsers we strive to keep working.
## API
The current full API documentation is available in the [TypeScript declaration file on the repository](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), switch the tag (press `w` when viewing the file) to point at the specific version tag you're using.
## Real-world uses
Xterm.js is used in several world-class applications to provide great terminal experiences.
@@ -153,105 +168,22 @@ computational environment for Jupyter, supporting interactive data science and s
- [**genact**](https://github.com/svenstaro/genact): A nonsense activity generator.
- [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice.
- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js
- [**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.
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list. Note: Please add any new contributions to the end of the list only.
## Demo
Xterm.js ships with a barebones demo implementation, designed for the development and evaluation of the library only. Exposing the demo to the public as is would introduce security risks for the host.
Below you can find instructions on how to run the demo on different platforms.
### SourceLair
SourceLair will run the demo and builder in parallel automatically. Just make sure to choose the "Node.js" project type, when cloning the xterm.js repo (or just use this shortcut; https://lair.io/xtermjs/xtermjs).
Then open your project's [Public URL](https://help.sourcelair.com/projects/the-public-url/) to access the demo.
### Docker
First, make sure you have Docker Engine 1.13.0 (or newer) and Docker Compose 1.10.0 (or newer).
Xterm.js [provides a pre-built Docker image](https://hub.docker.com/r/xtermjs/xterm.js/) to help run the demo easily (Git tags are built as [tagged Docker images](https://hub.docker.com/r/xtermjs/xterm.js/tags/) too).
To run the just demo (with no editing access). run the following command in your terminal:
```
docker run -p 3000:3000 xtermjs/xterm.js
```
Then open http://0.0.0.0:3000 in a web browser to access the demo.
To run the demo and builder in parallel, run the following command in your terminal:
```
docker-compose up
```
Then open http://0.0.0.0:3000 in a web browser to access the demo. If you prefer a different port than `3000` to access the xterm.js demo, then set the `XTERMJS_PORT` environment variable to the desired port.
### Foreman (or other Procfile runner)
First, be sure that a C++ compiler such as GCC-C++ or Clang is installed, then run the following commands in your terminal:
```
npm install
foreman start # Replace foreman with "honcho", "forego" etc. depending on your runner
```
Then open http://0.0.0.0:3000 in a web browser to access the demo.
### Linux or macOS
First, be sure that a C++ compiler such as GCC-C++ or Clang is installed, then run the following commands in your terminal:
```
npm install
npm start # Run this in its own terminal
npm run watch # Run this in its own terminal
```
Then open http://0.0.0.0:3000 in a web browser to access the demo.
### Windows
First, ensure [node-gyp](https://github.com/nodejs/node-gyp) is installed and configured correctly, then run the following commands in your terminal:
```
npm install
npm start # Run this in its own terminal
npm run watch # Run this in its own terminal
```
Then open http://127.0.0.1:3000 in a web browser to access the demo.
*Note: Do not use ConEmu, as it seems to break the demo for some reason.*
## Testing
Tests are run using the following npm scripts:
- `npm test`: This will run both unit tests and the linter
- `npm run test-suite <file>`: This will run all tests within a particular file, &lt;file&gt; is the test file name excluding the extension (eg. "Linkifier.test")
- `npm run test-debug`: This will run unit tests with `--inspect-brk`, this can then be automatically debugged using [VS Code auto attach](https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_auto-attach-feature) or manually attached to by a debugger
- `npm run test-coverage`: This will run all unit tests and produce a coverage report
- `npm run lint`: This will run the linter only
## Releases
Xterm.js follows a monthly release cycle roughly.
The existing releases are available at this GitHub repo's [Releases](https://github.com/sourcelair/xterm.js/releases), while the roadmap is available as [Milestones](https://github.com/sourcelair/xterm.js/milestones).
## Development and Contribution
## Contributing
Xterm.js is maintained by [SourceLair](https://www.sourcelair.com/) and a few external contributors, but we would love to receive contributions from everyone!
To contribute either code, documentation or issues to xterm.js please read the [Contributing document](CONTRIBUTING.md) beforehand. The development of xterm.js does not require any special tool. All you need is an editor that supports JavaScript/TypeScript and a browser. You will need Node.js installed locally to get all the features working in the demo.
### Code structure
`src/` is roughly split up into areas of functionality such as `renderer/` that handles all rendering and `utils/` which provides general utility functions. The `shared/` folder contains code that can be used from either the main thread or a web worker thread, all code inside a `shared/` folder should only ever import other code from a `shared/` folder to minimize the amount of code ran when launching a web worker.
You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and setup xterm.js for development.
## License Agreement
+3 -4
View File
@@ -174,9 +174,7 @@ function initOptions(term) {
'termName',
'useFlowControl',
// Complex option
'theme',
// Only in constructor
'rendererType'
'theme'
];
var stringOptions = {
bellSound: null,
@@ -185,7 +183,8 @@ function initOptions(term) {
experimentalCharAtlas: ['none', 'static', 'dynamic'],
fontFamily: null,
fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900']
fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
rendererType: ['dom', 'canvas']
};
var options = Object.keys(term._core.options);
var booleanOptions = [];
+2 -1
View File
@@ -4,6 +4,7 @@
],
"compilerOptions": {
"module": "commonjs",
"target": "es5"
"target": "es5",
"noEmit": true
}
}
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
"version": "3.5.1",
"version": "3.7.0",
"main": "lib/public/Terminal.js",
"types": "typings/xterm.d.ts",
"repository": "https://github.com/xtermjs/xterm.js",
@@ -35,6 +35,7 @@
"npm-run-all": "^4.1.2",
"nyc": "^11.8.0",
"sorcery": "^0.10.0",
"source-map-loader": "^0.2.3",
"tslint": "^5.9.1",
"tslint-consistent-codestyle": "^1.13.0",
"typescript": "2.8.3",
+5 -2
View File
@@ -72,7 +72,7 @@ export class AccessibilityManager extends Disposable {
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
this.register(this._renderRowsDebouncer);
this.register(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows)));
this.register(this._terminal.addDisposableListener('resize', data => this._onResize(data.rows)));
this.register(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
this.register(this._terminal.addDisposableListener('scroll', data => this._refreshRows()));
// Line feed is an issue as the prompt won't be read out after a command is run
@@ -157,7 +157,7 @@ export class AccessibilityManager extends Disposable {
e.stopImmediatePropagation();
}
private _onResize(cols: number, rows: number): void {
private _onResize(rows: number): void {
// Remove bottom boundary listener
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
@@ -259,6 +259,9 @@ export class AccessibilityManager extends Disposable {
if (!this._terminal.renderer.dimensions.actualCellHeight) {
return;
}
if (this._rowElements.length !== this._terminal.rows) {
this._onResize(this._terminal.rows);
}
for (let i = 0; i < this._terminal.rows; i++) {
this._refreshRowDimensions(this._rowElements[i]);
}
+49 -48
View File
@@ -5,9 +5,10 @@
import { assert } from 'chai';
import { ITerminal } from './Types';
import { Buffer } from './Buffer';
import { Buffer, DEFAULT_ATTR } from './Buffer';
import { CircularList } from './common/CircularList';
import { MockTerminal } 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,11 +337,11 @@ 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');
+17 -12
View File
@@ -4,9 +4,10 @@
*/
import { CircularList } from './common/CircularList';
import { LineData, CharData, ITerminal, IBuffer } from './Types';
import { CharData, ITerminal, IBuffer, IBufferLine } from './Types';
import { EventEmitter } from './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;
@@ -15,6 +16,10 @@ export const CHAR_DATA_WIDTH_INDEX = 2;
export const CHAR_DATA_CODE_INDEX = 3;
export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
export const NULL_CELL_CHAR = ' ';
export const NULL_CELL_WIDTH = 1;
export const NULL_CELL_CODE = 32;
/**
* This class represents a terminal buffer (an internal state of the terminal), where the
* following information is stored (in high-level):
@@ -23,7 +28,7 @@ export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
* - scroll position
*/
export class Buffer implements IBuffer {
public lines: CircularList<LineData>;
public lines: CircularList<IBufferLine>;
public ydisp: number;
public ybase: number;
public y: number;
@@ -80,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));
}
}
}
@@ -93,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();
@@ -117,7 +122,7 @@ export class Buffer implements IBuffer {
if (this.lines.length > 0) {
// Deal with columns increasing (we don't do anything when columns reduce)
if (this._terminal.cols < newCols) {
const ch: CharData = [DEFAULT_ATTR, ' ', 1, 32]; // does xterm use the default attr?
const ch: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // does xterm use the default attr?
for (let i = 0; i < this.lines.length; i++) {
while (this.lines.get(i).length < newCols) {
this.lines.get(i).push(ch);
@@ -142,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));
}
}
}
@@ -219,7 +224,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
@@ -264,11 +269,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 };
@@ -279,7 +284,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);
}
@@ -298,7 +303,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);
@@ -310,7 +315,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);
+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 -4
View File
@@ -18,7 +18,7 @@ export class BufferSet extends EventEmitter implements IBufferSet {
/**
* Create a new BufferSet for the given terminal.
* @param {Terminal} terminal - The terminal the BufferSet will belong to
* @param _terminal - The terminal the BufferSet will belong to
*/
constructor(private _terminal: ITerminal) {
super();
@@ -35,7 +35,6 @@ export class BufferSet extends EventEmitter implements IBufferSet {
/**
* Returns the alt Buffer of the BufferSet
* @returns {Buffer}
*/
public get alt(): Buffer {
return this._alt;
@@ -43,7 +42,6 @@ export class BufferSet extends EventEmitter implements IBufferSet {
/**
* Returns the normal Buffer of the BufferSet
* @returns {Buffer}
*/
public get active(): Buffer {
return this._activeBuffer;
@@ -51,7 +49,6 @@ export class BufferSet extends EventEmitter implements IBufferSet {
/**
* Returns the currently active Buffer of the BufferSet
* @returns {Buffer}
*/
public get normal(): Buffer {
return this._normal;
+1 -1
View File
@@ -61,7 +61,7 @@ export class CompositionHelper {
/**
* Handles the compositionupdate event, updating the composition view.
* @param {CompositionEvent} ev The event.
* @param ev The event.
*/
public compositionupdate(ev: CompositionEvent): void {
this._compositionView.textContent = ev.data;
+2 -2
View File
@@ -375,8 +375,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
// shortcut for most chars (print action)
if (currentState === ParserState.GROUND && code > 0x1f && code < 0x80) {
print = (~print) ? print : i;
do code = data.charCodeAt(++i);
while (i < l && code > 0x1f && code < 0x80);
do i++;
while (i < l && data.charCodeAt(i) > 0x1f && data.charCodeAt(i) < 0x80);
i--;
continue;
}
+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 ');
});
});
+120 -117
View File
@@ -4,15 +4,16 @@
* @license MIT
*/
import { CharData, IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer } 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 } from './Buffer';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer';
import { FLAGS } from './renderer/Types';
import { wcwidth } from './CharWidth';
import { EscapeSequenceParser } from './EscapeSequenceParser';
import { ICharset } from './core/Types';
import { Disposable } from './common/Lifecycle';
import { BufferLine } from './BufferLine';
/**
* Map collect to glevel. Used in `selectCharset`.
@@ -40,7 +41,7 @@ class RequestTerminfo implements IDcsHandler {
}
unhook(): void {
// invalid: DCS 0 + r Pt ST
this._terminal.send(`${C0.ESC}P0+r${this._data}${C0.ESC}\\`);
this._terminal.handler(`${C0.ESC}P0+r${this._data}${C0.ESC}\\`);
}
}
@@ -68,25 +69,25 @@ class DECRQSS implements IDcsHandler {
switch (this._data) {
// valid: DCS 1 $ r Pt ST (xterm)
case '"q': // DECSCA
return this._terminal.send(`${C0.ESC}P1$r0"q${C0.ESC}\\`);
return this._terminal.handler(`${C0.ESC}P1$r0"q${C0.ESC}\\`);
case '"p': // DECSCL
return this._terminal.send(`${C0.ESC}P1$r61"p${C0.ESC}\\`);
return this._terminal.handler(`${C0.ESC}P1$r61"p${C0.ESC}\\`);
case 'r': // DECSTBM
const pt = '' + (this._terminal.buffer.scrollTop + 1) +
';' + (this._terminal.buffer.scrollBottom + 1) + 'r';
return this._terminal.send(`${C0.ESC}P1$r${pt}${C0.ESC}\\`);
return this._terminal.handler(`${C0.ESC}P1$r${pt}${C0.ESC}\\`);
case 'm': // SGR
// TODO: report real settings instead of 0m
return this._terminal.send(`${C0.ESC}P1$r0m${C0.ESC}\\`);
return this._terminal.handler(`${C0.ESC}P1$r0m${C0.ESC}\\`);
case ' q': // DECSCUSR
const STYLES: {[key: string]: number} = {'block': 2, 'underline': 4, 'bar': 6};
let style = STYLES[this._terminal.getOption('cursorStyle')];
style -= this._terminal.getOption('cursorBlink');
return this._terminal.send(`${C0.ESC}P1$r${style} q${C0.ESC}\\`);
return this._terminal.handler(`${C0.ESC}P1$r${style} q${C0.ESC}\\`);
default:
// invalid: DCS 0 $ r Pt ST (xterm)
this._terminal.error('Unknown DCS $q %s', this._data);
this._terminal.send(`${C0.ESC}P0$r${this._data}${C0.ESC}\\`);
this._terminal.handler(`${C0.ESC}P0$r${this._data}${C0.ESC}\\`);
}
}
}
@@ -116,7 +117,7 @@ export class InputHandler extends Disposable implements IInputHandler {
private _surrogateHigh: string;
constructor(
private _terminal: any, // TODO: reestablish IInputHandlingTerminal here
protected _terminal: IInputHandlingTerminal,
private _parser: IEscapeSequenceParser = new EscapeSequenceParser())
{
super();
@@ -129,16 +130,16 @@ export class InputHandler extends Disposable implements IInputHandler {
* custom fallback handlers
*/
this._parser.setCsiHandlerFallback((collect: string, params: number[], flag: number) => {
this._terminal.error('Unknown CSI code: ', collect, params, String.fromCharCode(flag));
this._terminal.error('Unknown CSI code: ', { collect, params, flag: String.fromCharCode(flag) });
});
this._parser.setEscHandlerFallback((collect: string, flag: number) => {
this._terminal.error('Unknown ESC code: ', collect, String.fromCharCode(flag));
this._terminal.error('Unknown ESC code: ', { collect, flag: String.fromCharCode(flag) });
});
this._parser.setExecuteHandlerFallback((code: number) => {
this._terminal.error('Unknown EXECUTE code: ', code);
this._terminal.error('Unknown EXECUTE code: ', { code });
});
this._parser.setOscHandlerFallback((identifier: number, data: string) => {
this._terminal.error('Unknown OSC code: ', identifier, data);
this._terminal.error('Unknown OSC code: ', { identifier, data });
});
/**
@@ -296,10 +297,17 @@ export class InputHandler extends Disposable implements IInputHandler {
}
public parse(data: string): void {
// Ensure the terminal is not disposed
if (!this._terminal) {
return;
}
let buffer = this._terminal.buffer;
const cursorStartX = buffer.x;
const cursorStartY = buffer.y;
if (this._terminal.debug) {
// TODO: Consolidate debug/logging #1560
if ((<any>this._terminal).debug) {
this._terminal.log('data: ' + data);
}
@@ -374,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;
@@ -405,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);
@@ -428,23 +438,24 @@ 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, ' ', 1, 32 /* ' '.charCodeAt(0) */ ];
&& chMinusTwo
&& chMinusTwo[CHAR_DATA_WIDTH_INDEX] === 2) {
bufferRow.set(this._terminal.cols - 2, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
}
// insert empty cell at cursor
bufferRow.splice(buffer.x, 0, [curAttr, ' ', 1, 32 /* ' '.charCodeAt(0) */ ]);
bufferRow.splice(buffer.x, 0, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
}
}
// 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);
@@ -466,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++;
@@ -539,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(), ' ', 1, 32]; // 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);
}
/**
@@ -712,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).
@@ -728,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)
@@ -773,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);
}
/**
@@ -805,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();
@@ -835,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();
@@ -848,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(), ' ', 1, 32]; // 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);
}
/**
@@ -877,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);
@@ -896,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);
@@ -909,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(), ' ', 1, 32]; // 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]
);
}
/**
@@ -975,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, ' ', 1, 32];
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?
}
/**
@@ -1032,24 +1036,24 @@ export class InputHandler extends Disposable implements IInputHandler {
if (!collect) {
if (this._terminal.is('xterm') || this._terminal.is('rxvt-unicode') || this._terminal.is('screen')) {
this._terminal.send(C0.ESC + '[?1;2c');
this._terminal.handler(C0.ESC + '[?1;2c');
} else if (this._terminal.is('linux')) {
this._terminal.send(C0.ESC + '[?6c');
this._terminal.handler(C0.ESC + '[?6c');
}
} else if (collect === '>') {
// xterm and urxvt
// seem to spit this
// out around ~370 times (?).
if (this._terminal.is('xterm')) {
this._terminal.send(C0.ESC + '[>0;276;0c');
this._terminal.handler(C0.ESC + '[>0;276;0c');
} else if (this._terminal.is('rxvt-unicode')) {
this._terminal.send(C0.ESC + '[>85;95;0c');
this._terminal.handler(C0.ESC + '[>85;95;0c');
} else if (this._terminal.is('linux')) {
// not supported by linux console.
// linux console echoes parameters.
this._terminal.send(params[0] + 'c');
this._terminal.handler(params[0] + 'c');
} else if (this._terminal.is('screen')) {
this._terminal.send(C0.ESC + '[>83;40003;0c');
this._terminal.handler(C0.ESC + '[>83;40003;0c');
}
}
}
@@ -1624,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;
@@ -1712,15 +1719,13 @@ export class InputHandler extends Disposable implements IInputHandler {
switch (params[0]) {
case 5:
// status report
this._terminal.send(C0.ESC + '[0n');
this._terminal.emit('data', `${C0.ESC}[0n`);
break;
case 6:
// cursor position
this._terminal.send(C0.ESC + '['
+ (this._terminal.buffer.y + 1)
+ ';'
+ (this._terminal.buffer.x + 1)
+ 'R');
const y = this._terminal.buffer.y + 1;
const x = this._terminal.buffer.x + 1;
this._terminal.emit('data', `${C0.ESC}[${y};${x}R`);
break;
}
} else if (collect === '?') {
@@ -1729,27 +1734,25 @@ export class InputHandler extends Disposable implements IInputHandler {
switch (params[0]) {
case 6:
// cursor position
this._terminal.send(C0.ESC + '[?'
+ (this._terminal.buffer.y + 1)
+ ';'
+ (this._terminal.buffer.x + 1)
+ 'R');
const y = this._terminal.buffer.y + 1;
const x = this._terminal.buffer.x + 1;
this._terminal.emit('data', `${C0.ESC}[?${y};${x}R`);
break;
case 15:
// no printer
// this.send(C0.ESC + '[?11n');
// this.handler(C0.ESC + '[?11n');
break;
case 25:
// dont support user defined keys
// this.send(C0.ESC + '[?21n');
// this.handler(C0.ESC + '[?21n');
break;
case 26:
// north american keyboard
// this.send(C0.ESC + '[?27;1;0;0n');
// this.handler(C0.ESC + '[?27;1;0;0n');
break;
case 53:
// no dec locator/mouse
// this.send(C0.ESC + '[?50n');
// this.handler(C0.ESC + '[?50n');
break;
}
}
+5 -4
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 { CircularList } from './common/CircularList';
import { BufferLine } from './BufferLine';
class TestLinkifier extends Linkifier {
constructor(terminal: ITerminal) {
@@ -42,14 +43,14 @@ describe('Linkifier', () => {
terminal = new MockTerminal();
terminal.cols = 100;
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)]);
}
+29 -13
View File
@@ -4,9 +4,10 @@
*/
import { IMouseZoneManager } from './ui/Types';
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal } from './Types';
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferLine } from './Types';
import { MouseZone } from './ui/MouseZoneManager';
import { EventEmitter } from './EventEmitter';
import { CHAR_DATA_ATTR_INDEX } from './Buffer';
/**
* The Linkifier applies links to rows shortly after they have been refreshed.
@@ -24,7 +25,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
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
@@ -163,23 +164,31 @@ export class Linkifier extends EventEmitter implements ILinkifier {
return;
}
if ((<any>this._terminal.buffer.lines.get(absoluteRowIndex)).isWrapped) {
if (this._terminal.buffer.lines.get(absoluteRowIndex).isWrapped) {
// Only attempt to linkify rows that start in the viewport
if (rowIndex !== 0) {
return;
}
// If the first row is wrapped, backtrack to find the origin row and linkify that
let line: IBufferLine;
do {
rowIndex--;
absoluteRowIndex--;
} while ((<any>this._terminal.buffer.lines.get(absoluteRowIndex)).isWrapped);
line = this._terminal.buffer.lines.get(absoluteRowIndex);
if (!line) {
break;
}
} while (line.isWrapped);
}
// Construct full unwrapped line text
let text = this._terminal.buffer.translateBufferLineToString(absoluteRowIndex, false);
let currentIndex = absoluteRowIndex + 1;
while (currentIndex < this._terminal.buffer.lines.length &&
(<any>this._terminal.buffer.lines.get(currentIndex)).isWrapped) {
this._terminal.buffer.lines.get(currentIndex).isWrapped) {
text += this._terminal.buffer.translateBufferLineToString(currentIndex++, false);
}
@@ -208,6 +217,12 @@ export class Linkifier extends EventEmitter implements ILinkifier {
// Get index, match.index is for the outer match which includes negated chars
const index = text.indexOf(uri);
// Get cell color
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ydisp + rowIndex);
const char = line.get(index);
const attr: number = char[CHAR_DATA_ATTR_INDEX];
const fg = (attr >> 9) & 0x1ff;
// Ensure the link is valid before registering
if (matcher.validationCallback) {
matcher.validationCallback(uri, isValid => {
@@ -216,11 +231,11 @@ export class Linkifier extends EventEmitter implements ILinkifier {
return;
}
if (isValid) {
this._addLink(offset + index, rowIndex, uri, matcher);
this._addLink(offset + index, rowIndex, uri, matcher, fg);
}
});
} else {
this._addLink(offset + index, rowIndex, uri, matcher);
this._addLink(offset + index, rowIndex, uri, matcher, fg);
}
// Recursively check for links in the rest of the text
@@ -237,8 +252,9 @@ 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 x1 = x % this._terminal.cols;
const y1 = y + Math.floor(x / this._terminal.cols);
let x2 = (x1 + uri.length) % this._terminal.cols;
@@ -260,17 +276,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();
@@ -285,7 +301,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 };
}
}
+50 -8
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, '中文');
@@ -183,6 +189,42 @@ describe('SelectionManager', () => {
selectionManager.selectWordAt([15, 0]);
assert.equal(selectionManager.selectionText, 'ij"');
});
it('should expand upwards or downards for wrapped lines', () => {
buffer.lines.set(0, stringToRow(' foo'));
buffer.lines.set(1, stringToRow('bar '));
buffer.lines.get(1).isWrapped = true;
selectionManager.selectWordAt([1, 1]);
assert.equal(selectionManager.selectionText, 'foobar');
selectionManager.model.clearSelection();
selectionManager.selectWordAt([78, 0]);
assert.equal(selectionManager.selectionText, 'foobar');
});
it('should expand both upwards and downwards for word wrapped over many lines', () => {
const expectedText = 'fooaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccbar';
buffer.lines.set(0, stringToRow(' foo'));
buffer.lines.set(1, stringToRow('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'));
buffer.lines.set(2, stringToRow('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'));
buffer.lines.set(3, stringToRow('cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'));
buffer.lines.set(4, stringToRow('bar '));
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();
selectionManager.selectWordAt([40, 1]);
assert.equal(selectionManager.selectionText, expectedText);
selectionManager.model.clearSelection();
selectionManager.selectWordAt([40, 2]);
assert.equal(selectionManager.selectionText, expectedText);
selectionManager.model.clearSelection();
selectionManager.selectWordAt([40, 3]);
assert.equal(selectionManager.selectionText, expectedText);
selectionManager.model.clearSelection();
selectionManager.selectWordAt([1, 4]);
assert.equal(selectionManager.selectionText, expectedText);
});
describe('emoji', () => {
it('should treat a single emoji as a word when wrapped in spaces', () => {
buffer.lines.set(0, stringToRow(' ⚽ a')); // The a is here to prevent the space being trimmed in selectionText
@@ -303,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');
+94 -30
View File
@@ -3,13 +3,13 @@
* @license MIT
*/
import { ITerminal, ISelectionManager, IBuffer, CharData, XtermListener } from './Types';
import { ITerminal, ISelectionManager, IBuffer, CharData, XtermListener, IBufferLine } from './Types';
import { MouseHelper } from './utils/MouseHelper';
import * as Browser from './shared/utils/Browser';
import { CharMeasure } from './ui/CharMeasure';
import { EventEmitter } from './EventEmitter';
import { SelectionModel } from './SelectionModel';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer';
import { AltClickHandler } from './handlers/AltClickHandler';
/**
@@ -204,7 +204,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 +215,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);
@@ -451,8 +451,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* Removes the listeners that are registered when mousedown is triggered.
*/
private _removeMouseDownListeners(): void {
this._terminal.element.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
this._terminal.element.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
if (this._terminal.element.ownerDocument) {
this._terminal.element.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
this._terminal.element.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
}
clearInterval(this._dragScrollIntervalTimer);
this._dragScrollIntervalTimer = null;
}
@@ -498,7 +500,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]++;
}
@@ -574,18 +576,21 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._dragScrollAmount = this._getMouseEventScrollAmount(event);
// If the cursor was above or below the viewport, make sure it's at the
// start or end of the viewport respectively.
if (this._dragScrollAmount > 0) {
this._model.selectionEnd[0] = this._terminal.cols;
} else if (this._dragScrollAmount < 0) {
this._model.selectionEnd[0] = 0;
// start or end of the viewport respectively. This should only happen when
// NOT in column select mode.
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
if (this._dragScrollAmount > 0) {
this._model.selectionEnd[0] = this._terminal.cols;
} else if (this._dragScrollAmount < 0) {
this._model.selectionEnd[0] = 0;
}
}
// If the character is a wide character include the cell to the right in the
// 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]++;
}
@@ -607,10 +612,19 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
if (this._dragScrollAmount) {
this._terminal.scrollLines(this._dragScrollAmount, false);
// Re-evaluate selection
// If the cursor was above or below the viewport, make sure it's at the
// start or end of the viewport respectively. This should only happen when
// NOT in column select mode.
if (this._dragScrollAmount > 0) {
this._model.selectionEnd = [this._terminal.cols - 1, Math.min(this._terminal.buffer.ydisp + this._terminal.rows, this._terminal.buffer.lines.length - 1)];
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
this._model.selectionEnd[0] = this._terminal.cols;
}
this._model.selectionEnd[1] = Math.min(this._terminal.buffer.ydisp + this._terminal.rows, this._terminal.buffer.lines.length - 1);
} else {
this._model.selectionEnd = [0, this._terminal.buffer.ydisp];
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
this._model.selectionEnd[0] = 0;
}
this._model.selectionEnd[1] = this._terminal.buffer.ydisp;
}
this.refresh();
}
@@ -647,10 +661,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.
@@ -677,7 +691,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* Gets positional information for the word at the coordinated specified.
* @param coords The coordinates to get the word at.
*/
private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): IWordPosition {
private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean, followWrappedLinesAbove: boolean = true, followWrappedLinesBelow: boolean = true): IWordPosition {
// Ensure coords are within viewport (eg. not within scroll bar)
if (coords[0] >= this._terminal.cols) {
return null;
@@ -719,24 +733,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++;
@@ -750,8 +764,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++;
@@ -772,7 +786,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
// Calculate the start _column_, converting the the string indexes back to
// column coordinates.
const start =
let start =
startIndex // The index of the selection's start char in the line string
+ charOffset // The difference between the initial char's column and index
- leftWideCharCount // The number of wide chars left of the initial char
@@ -780,7 +794,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
// Calculate the length in _columns_, converting the the string indexes back
// to column coordinates.
const length = Math.min(this._terminal.cols, // Disallow lengths larger than the terminal cols
let length = Math.min(this._terminal.cols, // Disallow lengths larger than the terminal cols
endIndex // The index of the selection's end char in the line string
- startIndex // The index of the selection's start char in the line string
+ leftWideCharCount // The number of wide chars left of the initial char
@@ -792,6 +806,34 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
return null;
}
// Recurse upwards if the line is wrapped and the word wraps to the above line
if (followWrappedLinesAbove) {
if (start === 0 && bufferLine.get(0)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
const previousBufferLine = this._buffer.lines.get(coords[1] - 1);
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;
start -= offset;
length += offset;
}
}
}
}
// Recurse downwards if the line is wrapped and the word wraps to the next line
if (followWrappedLinesBelow) {
if (start + length === this._terminal.cols && bufferLine.get(this._terminal.cols - 1)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
const nextBufferLine = this._buffer.lines.get(coords[1] + 1);
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;
}
}
}
}
return { start, length };
}
@@ -803,6 +845,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
protected _selectWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): void {
const wordPosition = this._getWordAt(coords, allowWhitespaceOnlySelection);
if (wordPosition) {
// Adjust negative start value
while (wordPosition.start < 0) {
wordPosition.start += this._terminal.cols;
coords[1]--;
}
this._model.selectionStart = [wordPosition.start, coords[1]];
this._model.selectionStartLength = wordPosition.length;
}
@@ -815,7 +862,24 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
private _selectToWordAt(coords: [number, number]): void {
const wordPosition = this._getWordAt(coords, true);
if (wordPosition) {
this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), coords[1]];
let endRow = coords[1];
// Adjust negative start value
while (wordPosition.start < 0) {
wordPosition.start += this._terminal.cols;
endRow--;
}
// Adjust wrapped length value, this only needs to happen when values are reversed as in that
// case we're interested in the start of the word, not the end
if (!this._model.areSelectionValuesReversed()) {
while (wordPosition.start + wordPosition.length > this._terminal.cols) {
wordPosition.length -= this._terminal.cols;
endRow++;
}
}
this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : wordPosition.start + wordPosition.length, endRow];
}
}
+5
View File
@@ -128,6 +128,11 @@ describe('SelectionManager', () => {
model.selectionEnd = [3, 2];
assert.deepEqual(model.finalSelectionEnd, [4, 2]);
});
it('should return the end on a different row when start + length overflows onto a following row', () => {
model.selectionStart = [78, 2];
model.selectionStartLength = 4;
assert.deepEqual(model.finalSelectionEnd, [2, 3]);
});
it('should return selection end if selection end is after selection start + length', () => {
model.selectionStart = [2, 2];
model.selectionStartLength = 2;
+6 -2
View File
@@ -76,9 +76,13 @@ export class SelectionModel {
return null;
}
// Use the selection start if the end doesn't exist or they're reversed
// Use the selection start + length if the end doesn't exist or they're reversed
if (!this.selectionEnd || this.areSelectionValuesReversed()) {
return [this.selectionStart[0] + this.selectionStartLength, this.selectionStart[1]];
const startPlusLength = this.selectionStart[0] + this.selectionStartLength;
if (startPlusLength > this._terminal.cols) {
return [startPlusLength % this._terminal.cols, this.selectionStart[1] + Math.floor(startPlusLength / this._terminal.cols)];
}
return [startPlusLength, this.selectionStart[1]];
}
// Ensure the the word/line is selected after a double/triple click
+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'));
});
});
+125 -124
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;
@@ -141,7 +142,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 +159,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 +172,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));
}
});
});
@@ -337,62 +338,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 +404,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');
});
});
});
@@ -654,11 +655,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 +668,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 +680,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 +694,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 +705,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 +718,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 +750,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 +778,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 +789,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 +799,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 +810,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 +824,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 +835,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 +848,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 +859,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 +869,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 +880,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 +893,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 +904,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 +919,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 +931,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 +944,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
});
});
});
+104 -141
View File
@@ -21,11 +21,11 @@
* http://linux.die.net/man/7/urxvt
*/
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, LineData } 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 } from './Buffer';
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 { Viewport } from './Viewport';
@@ -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;
@@ -72,7 +73,7 @@ const WRITE_BATCH_SIZE = 300;
/**
* The set of options that only have an effect when set in the Terminal constructor.
*/
const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows', 'rendererType'];
const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows'];
const DEFAULT_OPTIONS: ITerminalOptions = {
cols: 80,
@@ -133,9 +134,7 @@ 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 _sendDataQueue: string;
private _customKeyEventHandler: CustomKeyEventHandler;
// modes
@@ -207,6 +206,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
public mouseHelper: MouseHelper;
private _accessibilityManager: AccessibilityManager;
private _screenDprMonitor: ScreenDprMonitor;
private _theme: ITheme;
public cols: number;
public rows: number;
@@ -214,7 +214,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Creates a new `Terminal` object.
*
* @param {object} options An object containing a set of options, the available options are:
* @param options An object containing a set of options, the available options are:
* - `cursorBlink` (boolean): Whether the terminal cursor blinks
* - `cols` (number): The number of columns of the terminal (horizontal size)
* - `rows` (number): The number of rows of the terminal (vertical size)
@@ -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];
}
});
@@ -270,7 +270,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.cursorState = 0;
this.cursorHidden = false;
this._sendDataQueue = '';
this._customKeyEventHandler = null;
// modes
@@ -348,7 +347,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Retrieves an option's value from the terminal.
* @param {string} key The option key.
* @param key The option key.
*/
public getOption(key: string): any {
if (!(key in DEFAULT_OPTIONS)) {
@@ -360,8 +359,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Sets an option on the terminal.
* @param {string} key The option key.
* @param {any} value The option value.
* @param key The option key.
* @param value The option value.
*/
public setOption(key: string, value: any): void {
if (!(key in DEFAULT_OPTIONS)) {
@@ -370,6 +369,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
if (CONSTRUCTOR_ONLY_OPTIONS.indexOf(key) !== -1) {
console.error(`Option "${key}" can only be set in the constructor`);
}
if (this.options[key] === value) {
return;
}
switch (key) {
case 'bellStyle':
if (!value) {
@@ -396,6 +398,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
console.warn(`${key} cannot be less than 1, value: ${value}`);
return;
}
case 'rendererType':
if (!value) {
value = 'canvas';
}
break;
case 'tabStopWidth':
if (value < 1) {
console.warn(`${key} cannot be less than 1, value: ${value}`);
@@ -455,6 +462,18 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.renderer.onResize(this.cols, this.rows);
this.refresh(0, this.rows - 1);
}
case 'rendererType':
if (this.renderer) {
this.unregister(this.renderer);
this.renderer.dispose();
this.renderer = null;
}
this._setupRenderer();
this.renderer.onCharSizeChanged();
if (this._theme) {
this.renderer.setTheme(this._theme);
}
break;
case 'scrollback':
this.buffers.resize(this.cols, this.rows);
if (this.viewport) {
@@ -484,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.send(C0.ESC + '[I');
this.handler(C0.ESC + '[I');
}
this.updateCursorStyle(ev);
this.element.classList.add('focus');
this.showCursor();
this.emit('focus');
@@ -510,7 +530,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.textarea.value = '';
this.refresh(this.buffer.y, this.buffer.y);
if (this.sendFocus) {
this.send(C0.ESC + '[O');
this.handler(C0.ESC + '[O');
}
this.element.classList.remove('focus');
this.emit('blur');
@@ -602,7 +622,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Opens the terminal within an element.
*
* @param {HTMLElement} parent The element to create the terminal within.
* @param parent The element to create the terminal within.
*/
public open(parent: HTMLElement): void {
this._parent = parent || this._parent;
@@ -660,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);
@@ -674,12 +694,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// Performance: Add viewport and helper elements from the fragment
this.element.appendChild(fragment);
switch (this.options.rendererType) {
case 'canvas': this.renderer = new Renderer(this, this.options.theme); break;
case 'dom': this.renderer = new DomRenderer(this, this.options.theme); break;
default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`);
}
this.register(this.renderer);
this._setupRenderer();
this._theme = this.options.theme;
this.options.theme = null;
this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure);
this.viewport.onThemeChanged(this.renderer.colorManager.colors);
@@ -736,11 +752,21 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
private _setupRenderer(): void {
switch (this.options.rendererType) {
case 'canvas': this.renderer = new Renderer(this, this.options.theme); break;
case 'dom': this.renderer = new DomRenderer(this, this.options.theme); break;
default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`);
}
this.register(this.renderer);
}
/**
* Sets the theme on the renderer. The renderer must have been initialized.
* @param theme The theme to ste.
* @param theme The theme to set.
*/
private _setTheme(theme: ITheme): void {
this._theme = theme;
const colors = this.renderer.setTheme(theme);
if (this.viewport) {
this.viewport.onThemeChanged(colors);
@@ -860,7 +886,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
else if (button === 3) return;
else data += '0';
data += '~[' + pos.x + ',' + pos.y + ']\r';
self.send(data);
self.handler(data);
return;
}
@@ -873,7 +899,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
else if (button === 1) button = 4;
else if (button === 2) button = 6;
else if (button === 3) button = 3;
self.send(C0.ESC + '['
self.handler(C0.ESC + '['
+ button
+ ';'
+ (button === 3 ? 4 : 0)
@@ -893,14 +919,14 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
pos.y -= 32;
pos.x++;
pos.y++;
self.send(C0.ESC + '[' + button + ';' + pos.x + ';' + pos.y + 'M');
self.handler(C0.ESC + '[' + button + ';' + pos.x + ';' + pos.y + 'M');
return;
}
if (self.sgrMouse) {
pos.x -= 32;
pos.y -= 32;
self.send(C0.ESC + '[<'
self.handler(C0.ESC + '[<'
+ (((button & 3) === 3 ? button & ~3 : button) - 32)
+ ';'
+ pos.x
@@ -916,7 +942,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
encode(data, pos.x);
encode(data, pos.y);
self.send(C0.ESC + '[M' + String.fromCharCode.apply(String, data));
self.handler(C0.ESC + '[M' + String.fromCharCode.apply(String, data));
}
function getButton(ev: MouseEvent): number {
@@ -935,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;
@@ -1065,7 +1091,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
for (let i = 0; i < Math.abs(amount); i++) {
data += sequence;
}
this.send(data);
this.handler(data);
}
return;
}
@@ -1098,8 +1124,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Tells the renderer to refresh terminal content between two rows (inclusive) at the next
* opportunity.
* @param {number} start The row to start from (between 0 and this.rows - 1).
* @param {number} end The row to end at (between start and this.rows - 1).
* @param start The row to start from (between 0 and this.rows - 1).
* @param end The row to end at (between start and this.rows - 1).
*/
public refresh(start: number, end: number): void {
if (this.renderer) {
@@ -1109,8 +1135,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Queues linkification for the specified rows.
* @param {number} start The row to start from (between 0 and this.rows - 1).
* @param {number} end The row to end at (between start and this.rows - 1).
* @param start The row to start from (between 0 and this.rows - 1).
* @param end The row to end at (between start and this.rows - 1).
*/
private _queueLinkification(start: number, end: number): void {
if (this.linkifier) {
@@ -1144,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;
@@ -1202,8 +1228,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Scroll the display of the terminal
* @param {number} disp The number of lines to scroll down (negative scroll up).
* @param {boolean} suppressScrollEvent Don't emit the scroll event as scrollLines. This is used
* @param disp The number of lines to scroll down (negative scroll up).
* @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used
* to avoid unwanted events being handled by the viewport when the event was triggered from the
* viewport originally.
*/
@@ -1234,7 +1260,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Scroll the display of the terminal by a number of pages.
* @param {number} pageCount The number of pages to scroll (negative scrolls up).
* @param pageCount The number of pages to scroll (negative scrolls up).
*/
public scrollPages(pageCount: number): void {
this.scrollLines(pageCount * (this.rows - 1));
@@ -1263,9 +1289,14 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Writes text to the terminal.
* @param {string} data The text to write to the terminal.
* @param data The text to write to the terminal.
*/
public write(data: string): void {
// Ensure the terminal isn't disposed
if (this._isDisposed) {
return;
}
// Ignore falsy data values (including the empty string)
if (!data) {
return;
@@ -1279,7 +1310,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
if (this.options.useFlowControl && !this._xoffSentToCatchUp && this.writeBuffer.length >= WRITE_BUFFER_PAUSE_THRESHOLD) {
// XOFF - stop pty pipe
// XON will be triggered by emulator before processing data chunk
this.send(C0.DC3);
this.handler(C0.DC3);
this._xoffSentToCatchUp = true;
}
@@ -1294,6 +1325,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
protected _innerWrite(): void {
// Ensure the terminal isn't disposed
if (this._isDisposed) {
this.writeBuffer = [];
}
const writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE);
while (writeBatch.length > 0) {
const data = writeBatch.shift();
@@ -1301,7 +1337,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// If XOFF was sent in order to catch up with the pty process, resume it if
// the writeBuffer is empty to allow more data to come in.
if (this._xoffSentToCatchUp && writeBatch.length === 0 && this.writeBuffer.length === 0) {
this.send(C0.DC1);
this.handler(C0.DC1);
this._xoffSentToCatchUp = false;
}
@@ -1329,7 +1365,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Writes text to the terminal, followed by a break line character (\n).
* @param {string} data The text to write to the terminal.
* @param data The text to write to the terminal.
*/
public writeln(data: string): void {
this.write(data + '\r\n');
@@ -1374,6 +1410,18 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
}
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
const joinerId = this.renderer.registerCharacterJoiner(handler);
this.refresh(0, this.rows - 1);
return joinerId;
}
public deregisterCharacterJoiner(joinerId: number): void {
if (this.renderer.deregisterCharacterJoiner(joinerId)) {
this.refresh(0, this.rows - 1);
}
}
public get markers(): IMarker[] {
return this.buffer.markers;
}
@@ -1430,7 +1478,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
* Handle a keydown event
* Key Resources:
* - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent
* @param {KeyboardEvent} ev The keydown event to be handled.
* @param ev The keydown event to be handled.
*/
protected _keyDown(event: KeyboardEvent): boolean {
if (this._customKeyEventHandler && this._customKeyEventHandler(event) === false) {
@@ -1527,7 +1575,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
* Handle a keypress event.
* Key Resources:
* - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent
* @param {KeyboardEvent} ev The keypress event to be handled.
* @param ev The keypress event to be handled.
*/
protected _keyPress(ev: KeyboardEvent): boolean {
let key;
@@ -1540,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;
@@ -1564,21 +1612,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
return true;
}
/**
* Send data for handling to the terminal
* @param {string} data
*/
public send(data: string): void {
if (!this._sendDataQueue) {
setTimeout(() => {
this.handler(this._sendDataQueue);
this._sendDataQueue = '';
}, 1);
}
this._sendDataQueue += data;
}
/**
* Ring the bell.
* Note: We could do sweet things with webaudio here
@@ -1619,8 +1652,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Resizes the terminal.
*
* @param {number} x The number of columns to resize to.
* @param {number} y The number of rows to resize to.
* @param x The number of columns to resize to.
* @param y The number of rows to resize to.
*/
public resize(x: number, y: number): void {
if (isNaN(x) || isNaN(y)) {
@@ -1654,7 +1687,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Updates the range of rows to refresh
* @param {number} y The number of rows to refresh next.
* @param y The number of rows to refresh next.
*/
public updateRange(y: number): void {
if (y < this._refreshStart) this._refreshStart = y;
@@ -1675,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 {number} x The column from which to start erasing to the end of the line.
* @param {number} 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(), ' ', 1, 32 /* ' '.charCodeAt(0) */]; // 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 {number} x The column from which to start erasing to the start of the line.
* @param {number} 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(), ' ', 1, 32 /* ' '.charCodeAt(0) */]; // xterm
x++;
while (x--) {
line[x] = ch;
}
this.updateRange(y);
}
/**
* Clear the entire buffer, making the prompt line the new first line.
*/
@@ -1724,56 +1722,21 @@ 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 {number} 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 {boolean} cur First bunch of data for each "blank" character.
* @param {boolean} isWrapped Whether the new line is wrapped from the previous line.
* @param {boolean} 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, ' ', 1, 32 /* ' '.charCodeAt(0) */]; // 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
*/
public ch(cur?: boolean): CharData {
if (cur) {
return [this.eraseAttr(), ' ', 1, 32 /* ' '.charCodeAt(0) */];
return [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
}
return [DEFAULT_ATTR, ' ', 1, 32 /* ' '.charCodeAt(0) */];
return [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
}
/**
@@ -1786,7 +1749,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Emit the 'data' event and populate the given data.
* @param {string} data The data to populate in the event.
* @param data The data to populate in the event.
*/
public handler(data: string): void {
// Prevents all events to pty process if stdin is disabled
@@ -1808,7 +1771,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Emit the 'title' event and populate the given title.
* @param {string} title The title to populate in the event.
* @param title The title to populate in the event.
*/
public handleTitle(title: string): void {
/**
@@ -1851,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 {
@@ -1900,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];
}
+25 -9
View File
@@ -18,6 +18,8 @@ export type LineData = CharData[];
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
export type CharacterJoinerHandler = (text: string) => [number, number][];
export const enum LinkHoverEventTypes {
HOVER = 'linkhover',
TOOLTIP = 'linktooltip',
@@ -45,6 +47,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
wraparoundMode: boolean;
bracketedPasteMode: boolean;
curAttr: number;
savedCurAttr: number;
savedCols: number;
x10Mouse: boolean;
vt200Mouse: boolean;
@@ -63,17 +66,11 @@ 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;
send(data: string): void;
setgCharset(g: number, charset: ICharset): void;
resize(x: number, y: number): void;
log(text: string, data?: any): void;
@@ -84,6 +81,10 @@ export interface IInputHandlingTerminal extends IEventEmitter {
error(text: string, data?: any): void;
setOption(key: string, value: any): void;
tabSet(): void;
handler(data: string): void;
handleTitle(title: string): void;
index(): void;
reverseIndex(): void;
}
export interface IViewport extends IDisposable {
@@ -198,6 +199,7 @@ export interface ILinkHoverEvent {
x2: number;
y2: number;
cols: number;
fg: number;
}
export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
@@ -223,12 +225,10 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
* @param data The data to populate in the event.
*/
handler(data: string): void;
send(data: string): void;
scrollLines(disp: number, suppressScrollEvent?: boolean): void;
cancel(ev: Event, force?: boolean): boolean | void;
log(text: string): void;
showCursor(): void;
blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData;
}
export interface IBufferAccessor {
@@ -267,7 +267,7 @@ export interface ITerminalOptions extends IPublicTerminalOptions {
}
export interface IBuffer {
readonly lines: ICircularList<LineData>;
readonly lines: ICircularList<IBufferLine>;
ydisp: number;
ybase: number;
y: number;
@@ -506,3 +506,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;
}
+27 -6
View File
@@ -28,6 +28,9 @@ export class Viewport extends Disposable implements IViewport {
// 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 +65,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,6 +80,17 @@ 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;
}
/**
@@ -90,12 +110,6 @@ export class Viewport extends Disposable implements IViewport {
this._refresh();
}
}
// Sync scrollTop
const scrollTop = this._terminal.buffer.ydisp * this._currentRowHeight;
if (this._viewportElement.scrollTop !== scrollTop) {
this._viewportElement.scrollTop = scrollTop;
}
}
/**
@@ -110,6 +124,13 @@ export class Viewport extends Disposable implements IViewport {
return;
}
// 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._viewportElement.scrollTop / 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);
+1 -1
View File
@@ -117,7 +117,7 @@ export class SearchHelper implements ISearchHelper {
if (searchIndex >= 0) {
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) {
+49
View File
@@ -0,0 +1,49 @@
/**
* 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 { ISearchHelper } from './Interfaces';
class MockTerminalPlain {}
class MockTerminal {
private _core: any;
public searchHelper: ISearchHelper;
constructor(options: any) {
this._core = new (require('../../../lib/Terminal').Terminal)(options);
this.searchHelper = new SearchHelper(this as any);
}
get core(): any {
return this._core;
}
pushWriteData(): void {
this._core._innerWrite();
}
}
describe('search addon', function(): void {
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');
});
});
it('Searchhelper - should find correct position', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 3});
term.core.write('Hello World\r\ntest\n123....hello');
term.pushWriteData();
const hello0 = (term.searchHelper as any)._findInLine('Hello', 0);
const hello1 = (term.searchHelper as any)._findInLine('Hello', 1);
const hello2 = (term.searchHelper as any)._findInLine('Hello', 2);
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
expect(hello1).eql(undefined);
expect(hello2).eql({col: 11, row: 2, term: 'Hello'});
});
});
+6 -3
View File
@@ -6,6 +6,9 @@
import { Terminal } from 'xterm';
import { IWinptyCompatAddonTerminal } from './Interfaces';
const CHAR_DATA_CODE_INDEX = 3;
const NULL_CELL_CODE = 32;
export function winptyCompatInit(terminal: Terminal): void {
const addonTerminal = <IWinptyCompatAddonTerminal>terminal;
@@ -27,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[3] !== 32 /* ' ' */) {
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;
}
});
}
+45
View File
@@ -0,0 +1,45 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { Disposable } from './Lifecycle';
class TestDisposable extends Disposable {
public get isDisposed(): boolean {
return this._isDisposed;
}
}
describe('Disposable', () => {
describe('register', () => {
it('should register disposables', () => {
const d = new TestDisposable();
const d2 = {
dispose: () => { throw new Error(); }
};
d.register(d2);
assert.throws(() => d.dispose());
});
});
describe('unregister', () => {
it('should unregister disposables', () => {
const d = new TestDisposable();
const d2 = {
dispose: () => { throw new Error(); }
};
d.register(d2);
d.unregister(d2);
assert.doesNotThrow(() => d.dispose());
});
});
describe('dispose', () => {
it('should set is disposed flag', () => {
const d = new TestDisposable();
assert.isFalse(d.isDisposed);
d.dispose();
assert.isTrue(d.isDisposed);
});
});
});
+14
View File
@@ -11,6 +11,7 @@ import { IDisposable } from 'xterm';
*/
export abstract class Disposable implements IDisposable {
protected _disposables: IDisposable[] = [];
protected _isDisposed: boolean = false;
constructor() {
}
@@ -19,6 +20,7 @@ export abstract class Disposable implements IDisposable {
* Disposes the object, triggering the `dispose` method on all registered IDisposables.
*/
public dispose(): void {
this._isDisposed = true;
this._disposables.forEach(d => d.dispose());
this._disposables.length = 0;
}
@@ -30,4 +32,16 @@ export abstract class Disposable implements IDisposable {
public register<T extends IDisposable>(d: T): void {
this._disposables.push(d);
}
/**
* Unregisters a disposable object if it has been registered, if not do
* nothing.
* @param d The disposable to unregister.
*/
public unregister<T extends IDisposable>(d: T): void {
const index = this._disposables.indexOf(d);
if (index !== -1) {
this._disposables.splice(index, 1);
}
}
}
+6 -6
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ITerminal, ICircularList, LineData } from '../Types';
import { ITerminal, ICircularList, IBufferLine } from '../Types';
import { C0 } from '../common/data/EscapeSequences';
const enum Direction {
@@ -18,7 +18,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,
@@ -50,7 +50,7 @@ export class AltClickHandler {
*/
public move(): void {
if (this._mouseEvent.altKey && this._endCol !== undefined && this._endRow !== undefined) {
this._terminal.send(this._arrowSequences());
this._terminal.handler(this._arrowSequences());
}
}
@@ -138,7 +138,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 +152,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;
+3 -3
View File
@@ -35,7 +35,7 @@ export function bracketTextForPaste(text: string, bracketedPasteMode: boolean):
/**
* Binds copy functionality to the given terminal.
* @param {ClipboardEvent} ev The original copy event to be handled
* @param ev The original copy event to be handled
*/
export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManager: ISelectionManager): void {
if (term.browser.isMSIE) {
@@ -50,8 +50,8 @@ export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManage
/**
* Redirect the clipboard's data to the terminal's input handler.
* @param {ClipboardEvent} ev The original paste event to be handled
* @param {Terminal} term The terminal on which to apply the handled paste event
* @param ev The original paste event to be handled
* @param term The terminal on which to apply the handled paste event
*/
export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void {
ev.stopPropagation();
+6
View File
@@ -65,6 +65,12 @@ export class Terminal implements ITerminalApi {
public deregisterLinkMatcher(matcherId: number): void {
this._core.deregisterLinkMatcher(matcherId);
}
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
return this._core.registerCharacterJoiner(handler);
}
public deregisterCharacterJoiner(joinerId: number): void {
this._core.deregisterCharacterJoiner(joinerId);
}
public addMarker(cursorYOffset: number): IMarker {
return this._core.addMarker(cursorYOffset);
}
+19 -14
View File
@@ -36,6 +36,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._container.appendChild(this._canvas);
}
public dispose(): void {
this._container.removeChild(this._canvas);
}
private _initCanvas(): void {
this._ctx = this._canvas.getContext('2d', {alpha: this._alpha});
// Draw the background if this is an opaque layer
@@ -224,12 +228,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
}
/**
* Draws a character at a cell. If possible this will draw using the character
* atlas to reduce draw time.
* Draws one or more characters at a cell. If possible this will draw using
* the character atlas to reduce draw time.
* @param terminal The terminal.
* @param char The character.
* @param chars The character or characters.
* @param code The character code.
* @param width The width of the character.
* @param width The width of the characters.
* @param x The column to draw at.
* @param y The row to draw at.
* @param fg The foreground color, in the format stored within the attributes.
@@ -237,33 +241,34 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* This is used to validate whether a cached image can be used.
* @param bold Whether the text is bold.
*/
protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean, italic: boolean): void {
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8;
protected drawChars(terminal: ITerminal, chars: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean, italic: boolean): void {
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8 && fg !== INVERTED_DEFAULT_COLOR;
fg += drawInBrightColor ? 8 : 0;
const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
this._ctx,
{char, code, bg, fg, bold: bold && terminal.options.enableBold, dim, italic},
{chars, code, bg, fg, bold: bold && terminal.options.enableBold, dim, italic},
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop
);
if (!atlasDidDraw) {
this._drawUncachedChar(terminal, char, width, fg, x, y, bold && terminal.options.enableBold, dim, italic);
this._drawUncachedChars(terminal, chars, width, fg, x, y, bold && terminal.options.enableBold, dim, italic);
}
}
/**
* Draws a character at a cell. The character will be clipped to
* ensure that it fits with the cell, including the cell to the right if it's
* a wide character.
* Draws one or more characters at one or more cells. The character(s) will be
* clipped to ensure that they fit with the cell(s), including the cell to the
* right if the last character is a wide character.
* @param terminal The terminal.
* @param char The character.
* @param chars The character.
* @param width The width of the character.
* @param fg The foreground color, in the format stored within the attributes.
* @param x The column to draw at.
* @param y The row to draw at.
*/
private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void {
private _drawUncachedChars(terminal: ITerminal, chars: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void {
this._ctx.save();
this._ctx.font = this._getFont(terminal, bold, italic);
this._ctx.textBaseline = 'top';
@@ -285,7 +290,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
}
// Draw the character
this._ctx.fillText(
char,
chars,
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop);
this._ctx.restore();
@@ -0,0 +1,290 @@
import { assert } from 'chai';
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;
beforeEach(() => {
const terminal = new MockTerminal();
terminal.cols = 16;
terminal.buffer = new MockBuffer();
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);
});
it('has no joiners upon creation', () => {
assert.deepEqual(registry.getJoinedCharacters(0), []);
});
it('returns ranges matched by the registered joiners', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
assert.deepEqual(
registry.getJoinedCharacters(0),
[[2, 4], [7, 9], [12, 14]]
);
});
it('processes the input using all provided joiners', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
assert.deepEqual(
registry.getJoinedCharacters(1),
[[2, 4], [12, 14]]
);
registry.registerCharacterJoiner(substringJoiner('=>'));
assert.deepEqual(
registry.getJoinedCharacters(1),
[[2, 4], [7, 9], [12, 14]]
);
});
it('removes deregistered joiners from future calls', () => {
const joiner1 = registry.registerCharacterJoiner(substringJoiner('->'));
const joiner2 = registry.registerCharacterJoiner(substringJoiner('=>'));
assert.deepEqual(
registry.getJoinedCharacters(1),
[[2, 4], [7, 9], [12, 14]]
);
registry.deregisterCharacterJoiner(joiner1);
assert.deepEqual(
registry.getJoinedCharacters(1),
[[7, 9]]
);
registry.deregisterCharacterJoiner(joiner2);
assert.deepEqual(
registry.getJoinedCharacters(1),
[]
);
});
it('doesn\'t process joins on differently-styled characters', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
assert.deepEqual(
registry.getJoinedCharacters(2),
[[2, 4], [12, 14]]
);
});
it('returns an empty list of ranges if there is nothing to be joined', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
assert.deepEqual(
registry.getJoinedCharacters(3),
[]
);
});
it('returns an empty list of ranges if the line is empty', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
assert.deepEqual(
registry.getJoinedCharacters(4),
[]
);
});
it('returns false when trying to deregister a joiner that does not exist', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
assert.deepEqual(registry.deregisterCharacterJoiner(123), false);
assert.deepEqual(
registry.getJoinedCharacters(0),
[[2, 4], [7, 9], [12, 14]]
);
});
it('doesn\'t process same-styled ranges that only have one character', () => {
registry.registerCharacterJoiner(substringJoiner('a'));
registry.registerCharacterJoiner(substringJoiner('b'));
registry.registerCharacterJoiner(substringJoiner('d'));
assert.deepEqual(
registry.getJoinedCharacters(5),
[[5, 6]]
);
});
it('handles ranges that extend all the way to the end of the line', () => {
registry.registerCharacterJoiner(substringJoiner('-> d'));
assert.deepEqual(
registry.getJoinedCharacters(2),
[[12, 16]]
);
});
it('handles adjacent ranges', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
registry.registerCharacterJoiner(substringJoiner('> c '));
assert.deepEqual(
registry.getJoinedCharacters(2),
[[2, 4], [8, 12], [12, 14]]
);
});
it('handles fullwidth characters in the middle of ranges', () => {
registry.registerCharacterJoiner(substringJoiner('wi¥de'));
assert.deepEqual(
registry.getJoinedCharacters(6),
[[0, 6]]
);
});
it('handles fullwidth characters at the end of ranges', () => {
registry.registerCharacterJoiner(substringJoiner('wi¥'));
assert.deepEqual(
registry.getJoinedCharacters(6),
[[0, 4]]
);
});
it('handles emojis in the middle of ranges', () => {
registry.registerCharacterJoiner(substringJoiner('emo\xf0\x9f\x98\x81 ji'));
assert.deepEqual(
registry.getJoinedCharacters(6),
[[6, 13]]
);
});
it('handles emojis at the end of ranges', () => {
registry.registerCharacterJoiner(substringJoiner('emo\xf0\x9f\x98\x81 '));
assert.deepEqual(
registry.getJoinedCharacters(6),
[[6, 11]]
);
});
it('handles ranges after wide and emoji characters', () => {
registry.registerCharacterJoiner(substringJoiner('abc'));
assert.deepEqual(
registry.getJoinedCharacters(6),
[[13, 16]]
);
});
describe('range merging', () => {
it('inserts a new range before the existing ones', () => {
registry.registerCharacterJoiner(() => [[1, 2], [2, 3]]);
registry.registerCharacterJoiner(() => [[0, 1]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
[[0, 1], [1, 2], [2, 3]]
);
});
it('inserts in between two ranges', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[2, 4]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
[[0, 2], [2, 4], [4, 6]]
);
});
it('inserts after the last range', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[6, 8]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
[[0, 2], [4, 6], [6, 8]]
);
});
it('extends the beginning of a range', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[3, 5]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
[[0, 2], [3, 6]]
);
});
it('extends the end of a range', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[1, 4]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
[[0, 4], [4, 6]]
);
});
it('extends the last range', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[5, 7]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
[[0, 2], [4, 7]]
);
});
it('connects two ranges', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[1, 5]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
[[0, 6]]
);
});
it('connects more than two ranges', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6], [8, 10], [12, 14]]);
registry.registerCharacterJoiner(() => [[1, 10]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
[[0, 10], [12, 14]]
);
});
});
});
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][] {
return (sequence: string): [number, number][] => {
const ranges: [number, number][] = [];
let searchIndex = 0;
let matchIndex = -1;
while ((matchIndex = sequence.indexOf(substring, searchIndex)) !== -1) {
const matchEndIndex = matchIndex + substring.length;
searchIndex = matchEndIndex;
ranges.push([matchIndex, matchEndIndex]);
}
return ranges;
};
}
+276
View File
@@ -0,0 +1,276 @@
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
import { ITerminal, IBufferLine } from '../Types';
import { ICharacterJoinerRegistry, ICharacterJoiner } from './Types';
export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
private _characterJoiners: ICharacterJoiner[] = [];
private _nextCharacterJoinerId: number = 0;
constructor(private _terminal: ITerminal) {
}
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
const joiner: ICharacterJoiner = {
id: this._nextCharacterJoinerId++,
handler
};
this._characterJoiners.push(joiner);
return joiner.id;
}
public deregisterCharacterJoiner(joinerId: number): boolean {
for (let i = 0; i < this._characterJoiners.length; i++) {
if (this._characterJoiners[i].id === joinerId) {
this._characterJoiners.splice(i, 1);
return true;
}
}
return false;
}
public getJoinedCharacters(row: number): [number, number][] {
if (this._characterJoiners.length === 0) {
return [];
}
const line = this._terminal.buffer.lines.get(row);
if (line.length === 0) {
return [];
}
const ranges: [number, number][] = [];
const lineStr = this._terminal.buffer.translateBufferLineToString(row, true);
// Because some cells can be represented by multiple javascript characters,
// we track the cell and the string indexes separately. This allows us to
// translate the string ranges we get from the joiners back into cell ranges
// for use when rendering
let rangeStartColumn = 0;
let currentStringIndex = 0;
let rangeStartStringIndex = 0;
let rangeAttr = line.get(0)[CHAR_DATA_ATTR_INDEX] >> 9;
for (let x = 0; x < this._terminal.cols; 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;
if (width === 0) {
// If this character is of width 0, skip it.
continue;
}
// End of range
if (attr !== rangeAttr) {
// If we ended up with a sequence of more than one character,
// look for ranges to join.
if (x - rangeStartColumn > 1) {
const joinedRanges = this._getJoinedRanges(
lineStr,
rangeStartStringIndex,
currentStringIndex,
line,
rangeStartColumn
);
for (let i = 0; i < joinedRanges.length; i++) {
ranges.push(joinedRanges[i]);
}
}
// Reset our markers for a new range.
rangeStartColumn = x;
rangeStartStringIndex = currentStringIndex;
rangeAttr = attr;
}
currentStringIndex += chars.length;
}
// Process any trailing ranges.
if (this._terminal.cols - rangeStartColumn > 1) {
const joinedRanges = this._getJoinedRanges(
lineStr,
rangeStartStringIndex,
currentStringIndex,
line,
rangeStartColumn
);
for (let i = 0; i < joinedRanges.length; i++) {
ranges.push(joinedRanges[i]);
}
}
return ranges;
}
/**
* Given a segment of a line of text, find all ranges of text that should be
* joined in a single rendering unit. Ranges are internally converted to
* column ranges, rather than string ranges.
* @param line String representation of the full line of text
* @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: 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
// merging it into an empty array, which incurs unnecessary writes.
const joinedRanges: [number, number][] = this._characterJoiners[0].handler(text);
for (let i = 1; i < this._characterJoiners.length; i++) {
// We merge any overlapping ranges across the different joiners
const joinerRanges = this._characterJoiners[i].handler(text);
for (let j = 0; j < joinerRanges.length; j++) {
CharacterJoinerRegistry._mergeRanges(joinedRanges, joinerRanges[j]);
}
}
this._stringRangesToCellRanges(joinedRanges, lineData, startCol);
return joinedRanges;
}
/**
* Modifies the provided ranges in-place to adjust for variations between
* string length and cell width so that the range represents a cell range,
* rather than the string range the joiner provides.
* @param ranges String ranges containing start (inclusive) and end (exclusive) index
* @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: IBufferLine, startCol: number): void {
let currentRangeIndex = 0;
let currentRangeStarted = false;
let currentStringIndex = 0;
let currentRange = ranges[currentRangeIndex];
// If we got through all of the ranges, stop searching
if (!currentRange) {
return;
}
for (let x = startCol; x < this._terminal.cols; x++) {
const charData = line.get(x);
const width = charData[CHAR_DATA_WIDTH_INDEX];
const length = charData[CHAR_DATA_CHAR_INDEX].length;
// We skip zero-width characters when creating the string to join the text
// so we do the same here
if (width === 0) {
continue;
}
// Adjust the start of the range
if (!currentRangeStarted && currentRange[0] <= currentStringIndex) {
currentRange[0] = x;
currentRangeStarted = true;
}
// Adjust the end of the range
if (currentRange[1] <= currentStringIndex) {
currentRange[1] = x;
// We're finished with this range, so we move to the next one
currentRange = ranges[++currentRangeIndex];
// If there are no more ranges left, stop searching
if (!currentRange) {
break;
}
// Ranges can be on adjacent characters. Because the end index of the
// ranges are exclusive, this means that the index for the start of a
// range can be the same as the end index of the previous range. To
// account for the start of the next range, we check here just in case.
if (currentRange[0] <= currentStringIndex) {
currentRange[0] = x;
currentRangeStarted = true;
} else {
currentRangeStarted = false;
}
}
// Adjust the string index based on the character length to line up with
// the column adjustment
currentStringIndex += length;
}
// If there is still a range left at the end, it must extend all the way to
// the end of the line.
if (currentRange) {
currentRange[1] = this._terminal.cols;
}
}
/**
* Merges the range defined by the provided start and end into the list of
* existing ranges. The merge is done in place on the existing range for
* performance and is also returned.
* @param ranges Existing range list
* @param newRange Tuple of two numbers representing the new range to merge in.
* @returns The ranges input with the new range merged in place
*/
private static _mergeRanges(ranges: [number, number][], newRange: [number, number]): [number, number][] {
let inRange = false;
for (let i = 0; i < ranges.length; i++) {
const range = ranges[i];
if (!inRange) {
if (newRange[1] <= range[0]) {
// Case 1: New range is before the search range
ranges.splice(i, 0, newRange);
return ranges;
}
if (newRange[1] <= range[1]) {
// Case 2: New range is either wholly contained within the
// search range or overlaps with the front of it
range[0] = Math.min(newRange[0], range[0]);
return ranges;
}
if (newRange[0] < range[1]) {
// Case 3: New range either wholly contains the search range
// or overlaps with the end of it
range[0] = Math.min(newRange[0], range[0]);
inRange = true;
}
// Case 4: New range starts after the search range
continue;
} else {
if (newRange[1] <= range[0]) {
// Case 5: New range extends from previous range but doesn't
// reach the current one
ranges[i - 1][1] = newRange[1];
return ranges;
}
if (newRange[1] <= range[1]) {
// Case 6: New range extends from prvious range into the
// current range
ranges[i - 1][1] = Math.max(newRange[1], range[1]);
ranges.splice(i, 1);
inRange = false;
return ranges;
}
// Case 7: New range extends from previous range past the
// end of the current range
ranges.splice(i, 1);
i--;
}
}
if (inRange) {
// Case 8: New range extends past the last existing range
ranges[ranges.length - 1][1] = newRange[1];
} else {
// Case 9: New range starts after the last existing range
ranges.push(newRange);
}
return ranges;
}
}
+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);
+23 -7
View File
@@ -7,13 +7,14 @@ import { TextRenderLayer } from './TextRenderLayer';
import { SelectionRenderLayer } from './SelectionRenderLayer';
import { CursorRenderLayer } from './CursorRenderLayer';
import { ColorManager } from './ColorManager';
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Types';
import { ITerminal } from '../Types';
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions, ICharacterJoinerRegistry } from './Types';
import { ITerminal, CharacterJoinerHandler } from '../Types';
import { LinkRenderLayer } from './LinkRenderLayer';
import { EventEmitter } from '../EventEmitter';
import { RenderDebouncer } from '../ui/RenderDebouncer';
import { ScreenDprMonitor } from '../ui/ScreenDprMonitor';
import { ITheme } from 'xterm';
import { CharacterJoinerRegistry } from '../renderer/CharacterJoinerRegistry';
export class Renderer extends EventEmitter implements IRenderer {
private _renderDebouncer: RenderDebouncer;
@@ -23,6 +24,7 @@ export class Renderer extends EventEmitter implements IRenderer {
private _screenDprMonitor: ScreenDprMonitor;
private _isPaused: boolean = false;
private _needsFullRefresh: boolean = false;
private _characterJoinerRegistry: ICharacterJoinerRegistry;
public colorManager: ColorManager;
public dimensions: IRenderDimensions;
@@ -31,12 +33,13 @@ export class Renderer extends EventEmitter implements IRenderer {
super();
const allowTransparency = this._terminal.options.allowTransparency;
this.colorManager = new ColorManager(document, allowTransparency);
this._characterJoinerRegistry = new CharacterJoinerRegistry(_terminal);
if (theme) {
this.colorManager.setTheme(theme);
}
this._renderLayers = [
new TextRenderLayer(this._terminal.screenElement, 0, this.colorManager.colors, allowTransparency),
new TextRenderLayer(this._terminal.screenElement, 0, this.colorManager.colors, this._characterJoinerRegistry, allowTransparency),
new SelectionRenderLayer(this._terminal.screenElement, 1, this.colorManager.colors),
new LinkRenderLayer(this._terminal.screenElement, 2, this.colorManager.colors, this._terminal),
new CursorRenderLayer(this._terminal.screenElement, 3, this.colorManager.colors)
@@ -67,12 +70,17 @@ export class Renderer extends EventEmitter implements IRenderer {
// Detect whether IntersectionObserver is detected and enable renderer pause
// and resume based on terminal visibility if so
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver(e => this.onIntersectionChange(e[0]), {threshold: 0});
const observer = new IntersectionObserver(e => this.onIntersectionChange(e[0]), { threshold: 0 });
observer.observe(this._terminal.element);
this.register({ dispose: () => observer.disconnect() });
}
}
public dispose(): void {
super.dispose();
this._renderLayers.forEach(l => l.dispose());
}
public onIntersectionChange(entry: IntersectionObserverEntry): void {
this._isPaused = entry.intersectionRatio === 0;
if (!this._isPaused && this._needsFullRefresh) {
@@ -171,8 +179,8 @@ export class Renderer extends EventEmitter implements IRenderer {
/**
* Queues a refresh between two rows (inclusive), to be done on next animation
* frame.
* @param {number} start The start row.
* @param {number} end The end row.
* @param start The start row.
* @param end The end row.
*/
public refreshRows(start: number, end: number): void {
if (this._isPaused) {
@@ -188,7 +196,7 @@ export class Renderer extends EventEmitter implements IRenderer {
*/
private _renderRows(start: number, end: number): void {
this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end));
this._terminal.emit('refresh', {start, end});
this._terminal.emit('refresh', { start, end });
}
/**
@@ -250,4 +258,12 @@ export class Renderer extends EventEmitter implements IRenderer {
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
}
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
return this._characterJoinerRegistry.registerCharacterJoiner(handler);
}
public deregisterCharacterJoiner(joinerId: number): boolean {
return this._characterJoinerRegistry.deregisterCharacterJoiner(joinerId);
}
}
+1 -1
View File
@@ -72,7 +72,7 @@ export class SelectionRenderLayer extends BaseRenderLayer {
this._ctx.fillStyle = this._colors.selection.css;
if (columnSelectMode) {
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
const startCol = start[0];
const width = end[0] - startCol;
const height = viewportCappedEndRow - viewportCappedStartRow + 1;
this.fillCells(startCol, viewportCappedStartRow, width, height);
+63 -17
View File
@@ -3,8 +3,8 @@
* @license MIT
*/
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer';
import { FLAGS, IColorSet, IRenderDimensions } from './Types';
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, NULL_CELL_CODE } from '../Buffer';
import { FLAGS, IColorSet, IRenderDimensions, ICharacterJoinerRegistry } from './Types';
import { CharData, ITerminal } from '../Types';
import { INVERTED_DEFAULT_COLOR } from './atlas/Types';
import { GridCache } from './GridCache';
@@ -22,10 +22,12 @@ export class TextRenderLayer extends BaseRenderLayer {
private _characterWidth: number;
private _characterFont: string;
private _characterOverlapCache: { [key: string]: boolean } = {};
private _characterJoinerRegistry: ICharacterJoinerRegistry;
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, alpha: boolean) {
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, characterJoinerRegistry: ICharacterJoinerRegistry, alpha: boolean) {
super(container, 'text', zIndex, alpha, colors);
this._state = new GridCache<CharData>();
this._characterJoinerRegistry = characterJoinerRegistry;
}
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
@@ -52,9 +54,10 @@ export class TextRenderLayer extends BaseRenderLayer {
terminal: ITerminal,
firstRow: number,
lastRow: number,
joinerRegistry: ICharacterJoinerRegistry | null,
callback: (
code: number,
char: string,
chars: string,
width: number,
x: number,
y: number,
@@ -66,22 +69,54 @@ export class TextRenderLayer extends BaseRenderLayer {
for (let y = firstRow; y <= lastRow; y++) {
const row = y + terminal.buffer.ydisp;
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 code: number = <number>charData[CHAR_DATA_CODE_INDEX];
const char: string = charData[CHAR_DATA_CHAR_INDEX];
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
// if indicated by a character joiner.
let chars: string = charData[CHAR_DATA_CHAR_INDEX];
const attr: number = charData[CHAR_DATA_ATTR_INDEX];
let width: number = charData[CHAR_DATA_WIDTH_INDEX];
// If true, indicates that the current character(s) to draw were joined.
let isJoined = false;
let lastCharX = x;
// The character to the left is a wide character, drawing is owned by
// the char at x-1
if (width === 0) {
continue;
}
// If the character is an overlapping char and the character to the right is a
// space, take ownership of the cell to the right.
if (this._isOverlapping(charData)) {
// Process any joined character ranges as needed. Because of how the
// ranges are produced, we know that they are valid for the characters
// and attributes of our input.
if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
isJoined = true;
const range = joinedRanges.shift();
// We already know the exact start and end column of the joined range,
// so we get the string and width representing it directly
chars = terminal.buffer.translateBufferLineToString(
row,
true,
range[0],
range[1]
);
width = range[1] - range[0];
code = Infinity;
// Skip over the cells occupied by this range in the loop
lastCharX = range[1] - 1;
}
// If the character is an overlapping char and the character to the
// right is a space, take ownership of the cell to the right. We skip
// this check for joined characters because their rendering likely won't
// yield the same result as rendering the last character individually.
if (!isJoined && this._isOverlapping(charData)) {
// If the character is overlapping, we want to force a re-render on every
// frame. This is specifically to work around the case where two
// overlaping chars `a` and `b` are adjacent, the cursor is moved to b and a
@@ -89,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 (x < line.length - 1 && line[x + 1][CHAR_DATA_CODE_INDEX] === 32 /*' '*/) {
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
@@ -116,7 +151,18 @@ export class TextRenderLayer extends BaseRenderLayer {
}
}
callback(code, char, width, x, y, fg, bg, flags);
callback(
code,
chars,
width,
x,
y,
fg,
bg,
flags
);
x = lastCharX;
}
}
}
@@ -134,7 +180,7 @@ export class TextRenderLayer extends BaseRenderLayer {
ctx.save();
this._forEachCell(terminal, firstRow, lastRow, (code, char, width, x, y, fg, bg, flags) => {
this._forEachCell(terminal, firstRow, lastRow, null, (code, chars, width, x, y, fg, bg, flags) => {
// libvte and xterm both draw the background (but not foreground) of invisible characters,
// so we should too.
let nextFillStyle = null; // null represents default background color
@@ -176,7 +222,7 @@ export class TextRenderLayer extends BaseRenderLayer {
}
private _drawForeground(terminal: ITerminal, firstRow: number, lastRow: number): void {
this._forEachCell(terminal, firstRow, lastRow, (code, char, width, x, y, fg, bg, flags) => {
this._forEachCell(terminal, firstRow, lastRow, this._characterJoinerRegistry, (code, chars, width, x, y, fg, bg, flags) => {
if (flags & FLAGS.INVISIBLE) {
return;
}
@@ -190,11 +236,11 @@ export class TextRenderLayer extends BaseRenderLayer {
} else {
this._ctx.fillStyle = this._colors.foreground.css;
}
this.fillBottomLineAtCells(x, y);
this.fillBottomLineAtCells(x, y, width);
this._ctx.restore();
}
this.drawChar(
terminal, char, code,
this.drawChars(
terminal, chars, code,
width, x, y,
fg, bg,
!!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM), !!(flags & FLAGS.ITALIC)
+25 -2
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ITerminal } from '../Types';
import { ITerminal, CharacterJoinerHandler } from '../Types';
import { IEventEmitter, ITheme, IDisposable } from 'xterm';
import { IColorSet } from '../shared/Types';
@@ -40,6 +40,8 @@ export interface IRenderer extends IEventEmitter, IDisposable {
onOptionsChanged(): void;
clear(): void;
refreshRows(start: number, end: number): void;
registerCharacterJoiner(handler: CharacterJoinerHandler): number;
deregisterCharacterJoiner(joinerId: number): boolean;
}
export interface IColorManager {
@@ -64,7 +66,7 @@ export interface IRenderDimensions {
actualCellHeight: number;
}
export interface IRenderLayer {
export interface IRenderLayer extends IDisposable {
/**
* Called when the terminal loses focus.
*/
@@ -101,6 +103,16 @@ export interface IRenderLayer {
*/
onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number], columnSelectMode: boolean): void;
/**
* Registers a handler to join characters to render as a group
*/
registerCharacterJoiner?(joiner: ICharacterJoiner): void;
/**
* Deregisters the specified character joiner handler
*/
deregisterCharacterJoiner?(joinerId: number): void;
/**
* Resize the render layer.
*/
@@ -111,3 +123,14 @@ export interface IRenderLayer {
*/
reset(terminal: ITerminal): void;
}
export interface ICharacterJoiner {
id: number;
handler: CharacterJoinerHandler;
}
export interface ICharacterJoinerRegistry {
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
deregisterCharacterJoiner(joinerId: number): boolean;
getJoinedCharacters(row: number): [number, number][];
}
+3 -3
View File
@@ -36,7 +36,7 @@ interface IGlyphCacheValue {
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.char}`;
return `${glyph.bg}_${glyph.fg}_${styleFlags}${glyph.chars}`;
}
export default class DynamicCharAtlas extends BaseCharAtlas {
@@ -94,7 +94,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
): boolean {
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) {
@@ -216,7 +216,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
this._tmpCtx.globalAlpha = DIM_OPACITY;
}
// Draw the character
this._tmpCtx.fillText(glyph.char, 0, 0);
this._tmpCtx.fillText(glyph.chars, 0, 0);
this._tmpCtx.restore();
// clear the background from the character to avoid issues with drawing over the previous
+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;
}
+1 -1
View File
@@ -7,7 +7,7 @@ export const INVERTED_DEFAULT_COLOR = -1;
export const DIM_OPACITY = 0.5;
export interface IGlyphIdentifier {
char: string;
chars: string;
code: number;
bg: number;
fg: number;
+16 -4
View File
@@ -4,7 +4,7 @@
*/
import { IRenderer, IRenderDimensions, IColorSet } from '../Types';
import { ITerminal } from '../../Types';
import { ITerminal, CharacterJoinerHandler } from '../../Types';
import { ITheme } from 'xterm';
import { EventEmitter } from '../../EventEmitter';
import { ColorManager } from '../ColorManager';
@@ -52,7 +52,7 @@ export class DomRenderer extends EventEmitter implements IRenderer {
this._rowContainer.classList.add(ROW_CONTAINER_CLASS);
this._rowContainer.style.lineHeight = 'normal';
this._rowContainer.setAttribute('aria-hidden', 'true');
this._refreshRowElements(this._terminal.rows, this._terminal.cols);
this._refreshRowElements(this._terminal.cols, this._terminal.rows);
this._selectionContainer = document.createElement('div');
this._selectionContainer.classList.add(SELECTION_CLASS);
this._selectionContainer.setAttribute('aria-hidden', 'true');
@@ -81,6 +81,15 @@ export class DomRenderer extends EventEmitter implements IRenderer {
this._terminal.screenElement.appendChild(this._selectionContainer);
}
public dispose(): void {
this._terminal.element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass);
this._terminal.screenElement.removeChild(this._rowContainer);
this._terminal.screenElement.removeChild(this._selectionContainer);
this._terminal.screenElement.removeChild(this._themeStyleElement);
this._terminal.screenElement.removeChild(this._dimensionsStyleElement);
super.dispose();
}
private _updateDimensions(): void {
this.dimensions.scaledCharWidth = this._terminal.charMeasure.width * window.devicePixelRatio;
this.dimensions.scaledCharHeight = this._terminal.charMeasure.height * window.devicePixelRatio;
@@ -241,14 +250,14 @@ export class DomRenderer extends EventEmitter implements IRenderer {
// Create the selections
const documentFragment = document.createDocumentFragment();
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
if (columnSelectMode) {
documentFragment.appendChild(
this._createSelectionElement(viewportCappedStartRow, startCol, end[0], viewportCappedEndRow - viewportStartRow + 1)
this._createSelectionElement(viewportCappedStartRow, start[0], end[0], viewportCappedEndRow - viewportCappedStartRow + 1)
);
} else {
// Draw first row
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
// Draw middle rows
@@ -319,4 +328,7 @@ export class DomRenderer extends EventEmitter implements IRenderer {
private get _terminalSelector(): string {
return `.${TERMINAL_CLASS_PREFIX}${this._terminalClass}`;
}
public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return -1; }
public deregisterCharacterJoiner(joinerId: number): boolean { return false; }
}
+19 -18
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 } from '../../Buffer';
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('');
@@ -31,9 +32,9 @@ 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];
lineData.set(1, [DEFAULT_ATTR, '', 0, undefined]);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span style="width: 10px;">語</span>'
@@ -49,8 +50,8 @@ describe('DomRendererRowFactory', () => {
});
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)];
lineData.set(0, [DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]);
lineData.set(1, [DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 1);
assert.equal(getFragmentHtml(fragment),
'<span>a</span>'
@@ -59,7 +60,7 @@ describe('DomRendererRowFactory', () => {
describe('attributes', () => {
it('should add class for bold', () => {
lineData[0] = [DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)];
lineData.set(0, [DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-bold">a</span>' +
@@ -68,7 +69,7 @@ describe('DomRendererRowFactory', () => {
});
it('should add class for italic', () => {
lineData[0] = [DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)];
lineData.set(0, [DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-italic">a</span>' +
@@ -79,7 +80,7 @@ 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)];
lineData.set(0, [defaultAttrNoFgColor | (i << 9), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-fg-${i}">a</span>` +
@@ -91,7 +92,7 @@ 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)];
lineData.set(0, [defaultAttrNoBgColor | (i << 0), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-bg-${i}">a</span>` +
@@ -101,7 +102,7 @@ describe('DomRendererRowFactory', () => {
});
it('should correctly invert colors', () => {
lineData[0] = [(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)];
lineData.set(0, [(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-1 xterm-bg-2">a</span>' +
@@ -110,7 +111,7 @@ describe('DomRendererRowFactory', () => {
});
it('should correctly invert default fg color', () => {
lineData[0] = [(FLAGS.INVERSE << 18) | (257 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)];
lineData.set(0, [(FLAGS.INVERSE << 18) | (257 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-1 xterm-bg-15">a</span>' +
@@ -119,7 +120,7 @@ describe('DomRendererRowFactory', () => {
});
it('should correctly invert default bg color', () => {
lineData[0] = [(FLAGS.INVERSE << 18) | (1 << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)];
lineData.set(0, [(FLAGS.INVERSE << 18) | (1 << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-0 xterm-bg-1">a</span>' +
@@ -129,7 +130,7 @@ 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)];
lineData.set(0, [(FLAGS.BOLD << 18) | (i << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-bold xterm-fg-${i + 8}">a</span>` +
@@ -146,10 +147,10 @@ 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, ' ', 1, 32 /* ' '.charCodeAt(0) */]);
lineData.push([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
}
return lineData;
}
+3 -3
View File
@@ -3,9 +3,9 @@
* @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';
@@ -17,7 +17,7 @@ export class DomRendererRowFactory {
) {
}
public createRow(lineData: LineData, isCursorRow: boolean, cursorX: number, cellWidth: number, cols: number): DocumentFragment {
public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorX: number, cellWidth: number, cols: number): DocumentFragment {
const fragment = this._document.createDocumentFragment();
let colCount = 0;
@@ -27,7 +27,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];
+2 -2
View File
@@ -22,8 +22,8 @@ export const isLinux = platform.indexOf('Linux') >= 0;
/**
* Return if the given array contains the given element
* @param {Array} array The array to search for the given element.
* @param {Object} el The element to look for into the array
* @param arr The array to search for the given element.
* @param el The element to look for into the array
*/
function contains(arr: any[], el: any): boolean {
return arr.indexOf(el) >= 0;
+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;
}
+19 -20
View File
@@ -4,7 +4,7 @@
*/
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 } from '../Types';
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener, CharacterJoinerHandler, IBufferLine } from '../Types';
import { Buffer } from '../Buffer';
import * as Browser from '../shared/utils/Browser';
import { ITheme, IDisposable, IMarker } from 'xterm';
@@ -84,9 +84,6 @@ export class MockTerminal implements ITerminal {
write(data: string): void {
throw new Error('Method not implemented.');
}
send(data: string): void {
throw new Error('Method not implemented.');
}
bracketedPasteMode: boolean;
mouseHelper: IMouseHelper;
renderer: IRenderer;
@@ -148,14 +145,8 @@ 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, ' ', 1, 32]);
}
return line;
}
registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; }
deregisterCharacterJoiner(joinerId: number): void { }
}
export class MockCharMeasure implements ICharMeasure {
@@ -229,18 +220,12 @@ 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.');
}
is(term: string): boolean {
throw new Error('Method not implemented.');
}
send(data: string): void {
throw new Error('Method not implemented.');
}
setgCharset(g: number, charset: { [key: string]: string; }): void {
throw new Error('Method not implemented.');
}
@@ -283,11 +268,23 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
tabSet(): void {
throw new Error('Method not implemented.');
}
handler(data: string): void {
throw new Error('Method not implemented.');
}
handleTitle(title: string): void {
throw new Error('Method not implemented.');
}
index(): void {
throw new Error('Method not implemented.');
}
reverseIndex(): void {
throw new Error('Method not implemented.');
}
}
export class MockBuffer implements IBuffer {
isCursorInViewport: boolean;
lines: ICircularList<[number, string, number, number][]>;
lines: ICircularList<IBufferLine>;
ydisp: number;
ybase: number;
hasScrollback: boolean;
@@ -310,7 +307,7 @@ 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;
}
}
@@ -344,6 +341,8 @@ export class MockRenderer implements IRenderer {
onWindowResize(devicePixelRatio: number): void {}
clear(): void {}
refreshRows(start: number, end: number): void {}
registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; }
deregisterCharacterJoiner(): boolean { return true; }
}
export class MockViewport implements IViewport {
+1 -4
View File
@@ -59,10 +59,7 @@
"esSpecCompliant": true
}
],
"triple-equals": [
true,
"allow-null-check"
],
"triple-equals": true,
"typedef-whitespace": [
true,
{
+61
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.
*/
@@ -416,8 +426,21 @@ declare module 'xterm' {
*/
off(type: 'blur' | 'focus' | 'linefeed' | 'selection' | 'data' | 'key' | 'keypress' | 'keydown' | 'refresh' | 'resize' | 'scroll' | 'title' | string, listener: (...args: any[]) => void): void;
/**
* Emits an event on the terminal.
* @param type The type of event
* @param data data associated with the event.
* @deprecated This is being removed from the API with no replacement, see
* issue #1505.
*/
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;
/**
@@ -470,6 +493,44 @@ declare module 'xterm' {
*/
deregisterLinkMatcher(matcherId: number): void;
/**
* (EXPERIMENTAL) Registers a character joiner, allowing custom sequences of
* characters to be rendered as a single unit. This is useful in particular
* for rendering ligatures and graphemes, among other things.
*
* Each registered character joiner is called with a string of text
* representing a portion of a line in the terminal that can be rendered as
* a single unit. The joiner must return a sorted array, where each entry is
* itself an array of length two, containing the start (inclusive) and end
* (exclusive) index of a substring of the input that should be rendered as
* a single unit. When multiple joiners are provided, the results of each
* are collected. If there are any overlapping substrings between them, they
* are combined into one larger unit that is drawn together.
*
* All character joiners that are registered get called every time a line is
* rendered in the terminal, so it is essential for the handler function to
* run as quickly as possible to avoid slowdowns when rendering. Similarly,
* joiners should strive to return the smallest possible substrings to
* render together, since they aren't drawn as optimally as individual
* characters.
*
* NOTE: character joiners are only used by the canvas renderer.
*
* @param handler The function that determines character joins. It is called
* with a string of text that is eligible for joining and returns an array
* where each entry is an array containing the start (inclusive) and end
* (exclusive) indexes of ranges that should be rendered as a single unit.
* @return The ID of the new joiner, this can be used to deregister
*/
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
/**
* (EXPERIMENTAL) Deregisters the character joiner if one was registered.
* NOTE: character joiners are only used by the canvas renderer.
* @param joinerId The character joiner's ID (returned after register)
*/
deregisterCharacterJoiner(joinerId: number): void;
/**
* (EXPERIMENTAL) Adds a marker to the normal buffer and returns it. If the
* alt buffer is active, undefined is returned.
+10 -1
View File
@@ -6,5 +6,14 @@ module.exports = {
path: path.resolve(__dirname, 'demo/dist'),
filename: 'bundle.js'
},
devtool: 'source-map'
devtool: 'source-map',
module: {
rules: [
{
test: /\.js$/,
use: ["source-map-loader"],
enforce: "pre"
}
]
}
};
+6002
View File
File diff suppressed because it is too large Load Diff