Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea07bf8f69 | ||
|
|
0b1711fb79 | ||
|
|
bb526aaa88 | ||
|
|
a28d9f1456 | ||
|
|
fb6b173fe7 | ||
|
|
1361ff8e22 | ||
|
|
0731f28615 | ||
|
|
f7b58d0718 | ||
|
|
d46003843b | ||
|
|
13619d427a | ||
|
|
f03d00a497 | ||
|
|
3d20c2f266 | ||
|
|
6f749b196a | ||
|
|
f5ba386c01 | ||
|
|
0079677e43 | ||
|
|
106eda7067 | ||
|
|
f13455965c | ||
|
|
5bc0ad879b | ||
|
|
72e85a352a | ||
|
|
49c1ca3f57 | ||
|
|
bcff69b79c | ||
|
|
3f7fec8c91 | ||
|
|
3479614c73 | ||
|
|
34f075a8cc | ||
|
|
ff165e8627 | ||
|
|
cdcbf60e44 | ||
|
|
2d4e617c95 | ||
|
|
6ea7758ab1 | ||
|
|
604959a319 | ||
|
|
6a3b39b428 | ||
|
|
9e80a12462 | ||
|
|
0e0ecc2d6a | ||
|
|
f872ced863 | ||
|
|
9a30b98880 | ||
|
|
f36bd24e1e | ||
|
|
23dd3d7a67 | ||
|
|
79965708ab | ||
|
|
80a83ff8d9 | ||
|
|
2e9177a032 | ||
|
|
bbb5daa575 | ||
|
|
739bd05037 | ||
|
|
f7197c10df | ||
|
|
e56f710cd1 | ||
|
|
18f3cc77e5 | ||
|
|
e3ded61122 | ||
|
|
f8e3fa5a99 | ||
|
|
05d308a37a | ||
|
|
cd5827e69b | ||
|
|
20048d31a1 | ||
|
|
598a729fc0 | ||
|
|
6ed5c9e64d | ||
|
|
1232c00dd1 | ||
|
|
5d87b54390 | ||
|
|
6733c6ce1f | ||
|
|
ba2d0e8df2 | ||
|
|
1722248065 | ||
|
|
dd0fcb6320 | ||
|
|
b965d0a0af | ||
|
|
48dab494db | ||
|
|
42eacba09a | ||
|
|
e2c993ebcd | ||
|
|
634f4e29aa | ||
|
|
59f75555bc | ||
|
|
cb5a452c90 | ||
|
|
58b9f712df | ||
|
|
f09918f0d2 | ||
|
|
4e35904392 | ||
|
|
82b3beb921 | ||
|
|
2806288489 | ||
|
|
0d498fcd52 | ||
|
|
83efc9afc4 | ||
|
|
393b411acc | ||
|
|
4effe1f1d9 | ||
|
|
de82bd00ae | ||
|
|
d222eec9f7 | ||
|
|
2cda163bcf | ||
|
|
9025d91f12 | ||
|
|
6810de85e1 | ||
|
|
4626e19b75 | ||
|
|
d75e0b99d5 | ||
|
|
73b4bfb5f8 | ||
|
|
bbafdd3d06 | ||
|
|
8ede1fc9d2 | ||
|
|
95cb6f30bb | ||
|
|
c94fdaecb2 | ||
|
|
e2b43a4ced | ||
|
|
1fd087e5d3 | ||
|
|
6df6aad303 | ||
|
|
5140c83649 | ||
|
|
1b6ce6dab6 | ||
|
|
1cc0d43aa4 | ||
|
|
0af4b17c20 | ||
|
|
2275601bf7 | ||
|
|
7cfc2ffa32 | ||
|
|
9e95024a9d | ||
|
|
87b76aadb7 | ||
|
|
adc657898c | ||
|
|
43b47abaa7 | ||
|
|
2ebc926f4e | ||
|
|
af58643ee7 | ||
|
|
9a358b2258 | ||
|
|
25fc01cb81 | ||
|
|
d3fe3a1960 | ||
|
|
01453e9d40 | ||
|
|
27f0eaf71d | ||
|
|
9bd52820e7 | ||
|
|
86c5aa023f | ||
|
|
4df8d3aada | ||
|
|
8fab079ad6 | ||
|
|
e59d1f7a98 | ||
|
|
5f817e88e7 | ||
|
|
38de6cb46a | ||
|
|
e287edb6fe | ||
|
|
29545a90c6 | ||
|
|
e849e3f206 | ||
|
|
c3011cd3ea | ||
|
|
e4770de9f2 | ||
|
|
99061c672c | ||
|
|
27fcd7c260 | ||
|
|
4c0e7b2462 | ||
|
|
decd81bc1e | ||
|
|
73970ec5a2 | ||
|
|
c62923fb1d | ||
|
|
c13b86a885 | ||
|
|
2564592c09 | ||
|
|
4b0cf2ffce | ||
|
|
fab039ddf3 | ||
|
|
3c0e142f73 | ||
|
|
80f7a37118 | ||
|
|
11de0d0ca0 | ||
|
|
5e815de82a | ||
|
|
0536862b8f | ||
|
|
b520e2fd35 | ||
|
|
740d4a45f9 | ||
|
|
cb53cdd728 | ||
|
|
2b92996d7c | ||
|
|
b8944beea9 | ||
|
|
67435edad7 | ||
|
|
fc9a0f15db | ||
|
|
8a57489966 | ||
|
|
3a8c1b3fcd | ||
|
|
0c2f079670 | ||
|
|
d266d995ab | ||
|
|
0ea2e9cfd6 | ||
|
|
9b8cb03c55 | ||
|
|
729f646a28 | ||
|
|
85c892c218 | ||
|
|
7f5e72dbdb | ||
|
|
479fb0e782 | ||
|
|
8ba11a2bd0 | ||
|
|
abc3f3b94d | ||
|
|
67bc0456e0 | ||
|
|
3e8ddd1966 | ||
|
|
6223342890 | ||
|
|
9ddabb3fee | ||
|
|
af0030b580 | ||
|
|
b93271d9cd | ||
|
|
934ca6508f | ||
|
|
7e210c7e6b | ||
|
|
e4970266d8 | ||
|
|
0a5d7a7bd2 | ||
|
|
31aab8ea6f | ||
|
|
857d9bcf2a | ||
|
|
7463166aec | ||
|
|
f80159fedc | ||
|
|
3180a6f6ec | ||
|
|
35d26f1d27 | ||
|
|
ca03f8928d | ||
|
|
0b3d1e50a8 | ||
|
|
401886c7c3 | ||
|
|
4eac7661c2 | ||
|
|
b8163cfbbf | ||
|
|
a9f82907ee | ||
|
|
57f4ae5320 | ||
|
|
d1712424c8 | ||
|
|
16f865987d | ||
|
|
f071531494 | ||
|
|
618768c7ed | ||
|
|
cfe22e6fd2 | ||
|
|
3961dd5a44 | ||
|
|
471e332036 | ||
|
|
467e8c563a | ||
|
|
5971630f64 | ||
|
|
f7cc1058c7 | ||
|
|
645a5920da | ||
|
|
baccca843c | ||
|
|
19381454ad | ||
|
|
f95bc64971 | ||
|
|
0c7c307de3 | ||
|
|
1c84881d00 | ||
|
|
f1252abbbe | ||
|
|
11d6ca2aa1 | ||
|
|
eb59b90790 | ||
|
|
4c3aa97917 | ||
|
|
ba54f25b8c | ||
|
|
af268f4968 | ||
|
|
f93b24acc7 | ||
|
|
6854def13c | ||
|
|
63d63c27e7 | ||
|
|
ef1e2ab9e2 | ||
|
|
7b46940772 | ||
|
|
6f5f68df1a | ||
|
|
dc165175cf | ||
|
|
32f1782c84 | ||
|
|
8811d96a8b | ||
|
|
d11d74688e | ||
|
|
8e79d53d78 | ||
|
|
6dbc476cdc | ||
|
|
57921e3fe0 | ||
|
|
75b74ac4ff | ||
|
|
1c659841fa | ||
|
|
9573357778 | ||
|
|
f11362a80d | ||
|
|
a01fbb636a | ||
|
|
320fb55ddc | ||
|
|
4b170ca43f | ||
|
|
c6e7fb74ab | ||
|
|
b05814e6a5 | ||
|
|
07882c42dc | ||
|
|
a59b2f8433 | ||
|
|
7f2e94c30c | ||
|
|
95aed8fcb0 | ||
|
|
663d3b24cd | ||
|
|
a87303f375 | ||
|
|
259858fac2 | ||
|
|
a4514ee98a | ||
|
|
39151e4354 | ||
|
|
d27b7bfa7a | ||
|
|
ce61795c38 | ||
|
|
528f06bc62 | ||
|
|
5360277594 | ||
|
|
b957130787 | ||
|
|
f88dcd160c | ||
|
|
7d8f698096 | ||
|
|
d3f5a06517 | ||
|
|
9cc878cea7 | ||
|
|
087acf8a9a | ||
|
|
7612180276 | ||
|
|
201e3e4dab | ||
|
|
55f51ad169 | ||
|
|
405d1ee52d | ||
|
|
5f9ad5c19e | ||
|
|
280b698a50 | ||
|
|
8d12881a1c | ||
|
|
c5cfa409b1 | ||
|
|
2a0f44b0fb | ||
|
|
7d0e585c44 | ||
|
|
a5ced5d909 | ||
|
|
0a99f65f01 | ||
|
|
c33814a7a6 | ||
|
|
0950369a5c | ||
|
|
346b5177bc | ||
|
|
9246d524cb | ||
|
|
6480933861 | ||
|
|
a889fef752 | ||
|
|
6075498c24 | ||
|
|
ee1da73a69 | ||
|
|
29ac6a153e | ||
|
|
264757e964 | ||
|
|
e88a9ffd89 | ||
|
|
d56a611a19 | ||
|
|
28ed7772c6 | ||
|
|
a047359f81 | ||
|
|
b81c165bea | ||
|
|
4405f5e163 | ||
|
|
2012c8c91d | ||
|
|
f719a4e91b | ||
|
|
5d33727ae4 | ||
|
|
1343b83fc3 | ||
|
|
9e47ec9af2 | ||
|
|
9c459aacc7 | ||
|
|
72724abac8 | ||
|
|
9a86eeb333 | ||
|
|
46f6fd18b0 | ||
|
|
603f22a7bb | ||
|
|
13c401cbaa | ||
|
|
b812991072 | ||
|
|
2b2431824f | ||
|
|
7147787441 | ||
|
|
ec3bf1139d | ||
|
|
d0b603d033 | ||
|
|
2621be819e | ||
|
|
f380153f42 | ||
|
|
fd91c5e1a8 | ||
|
|
f61d852f3c | ||
|
|
5bc1112151 | ||
|
|
59cc0b675e | ||
|
|
db8ded2a02 | ||
|
|
4b7279146a | ||
|
|
c4a2f7911d | ||
|
|
db9fe53af3 | ||
|
|
d3865ad23f | ||
|
|
8b1067d180 | ||
|
|
a53a0acd65 | ||
|
|
42724c7f42 | ||
|
|
616f069538 | ||
|
|
11cec31da6 | ||
|
|
d9991b25a4 | ||
|
|
cb6533f3a6 | ||
|
|
54e7f65d4e | ||
|
|
ec61f3acf0 | ||
|
|
910aa1ad7f | ||
|
|
24bed01fa4 | ||
|
|
f7d6ab5f2a | ||
|
|
0716cffd7d | ||
|
|
e29ab29441 | ||
|
|
9f271de803 | ||
|
|
43c796a781 | ||
|
|
25152e4441 | ||
|
|
3846fe0a7d | ||
|
|
b3b2bd1faf | ||
|
|
2c3d98abc8 | ||
|
|
0dc3dd0336 | ||
|
|
c4762e7228 | ||
|
|
35b32b721e | ||
|
|
ccf1a047d3 | ||
|
|
db0338cbcf | ||
|
|
06742eb507 | ||
|
|
0fab4ff5d1 | ||
|
|
d2b0d514f8 | ||
|
|
ab40908fa4 | ||
|
|
a5c48c891e | ||
|
|
70e35de56b | ||
|
|
c323e0ad4a | ||
|
|
4d32826891 | ||
|
|
13088f55a0 | ||
|
|
ea008411e4 | ||
|
|
e63fdf580b | ||
|
|
597c6939c8 | ||
|
|
293ae18a11 | ||
|
|
32b34cbe4c | ||
|
|
2f6d05f99d | ||
|
|
ad3ae67e70 | ||
|
|
b594407cd5 | ||
|
|
207c4cf961 | ||
|
|
fe11837a61 | ||
|
|
fee912023f | ||
|
|
a95ac990ff | ||
|
|
aad8439542 | ||
|
|
1a777edffe | ||
|
|
537018f71f | ||
|
|
67ca3a9334 | ||
|
|
5fb30979c1 | ||
|
|
cf863d4849 | ||
|
|
47814847f9 | ||
|
|
9ec8974545 | ||
|
|
c887c6e0f6 | ||
|
|
6c8949bba1 | ||
|
|
508d3ab68c | ||
|
|
6dad013783 | ||
|
|
0532e5cbf0 | ||
|
|
abcdf3e252 | ||
|
|
3baa6b9237 | ||
|
|
23e6a475ee | ||
|
|
65256c8751 | ||
|
|
b36d8780fd | ||
|
|
70fda99415 | ||
|
|
2221f70ff0 | ||
|
|
daad8d9c04 | ||
|
|
03d26e39c7 | ||
|
|
64d3c089a2 | ||
|
|
938c011e63 | ||
|
|
66f6b88333 | ||
|
|
35f746c925 | ||
|
|
944da28089 | ||
|
|
8f3e2b645a | ||
|
|
82b2a5305f | ||
|
|
6a2852ee26 | ||
|
|
526d6c4da9 | ||
|
|
46d62f4972 | ||
|
|
5cafcdb695 | ||
|
|
2674258ead | ||
|
|
b38be3a1d0 | ||
|
|
18a9f05626 | ||
|
|
bf34980c43 | ||
|
|
d2f8f9dc1a | ||
|
|
df13469b6e | ||
|
|
584ec681af | ||
|
|
0c5b5cca4a | ||
|
|
64663bf4a8 | ||
|
|
fe8334a873 | ||
|
|
c74fd40e2a | ||
|
|
0f8ff67451 | ||
|
|
8b0032c210 | ||
|
|
d9254ef2d9 | ||
|
|
66be05a962 | ||
|
|
0e85a09dd3 | ||
|
|
0c4ba9fd67 | ||
|
|
aca81c764a | ||
|
|
6b3ddb97d1 | ||
|
|
26ccf2a316 | ||
|
|
361894194a | ||
|
|
8c2db8ddb8 | ||
|
|
5546baa927 | ||
|
|
d814009721 | ||
|
|
08fd050c3f | ||
|
|
0b096d7d55 | ||
|
|
44cfb2dc87 | ||
|
|
b0624cadb7 | ||
|
|
045f361b0d | ||
|
|
cb77b9dc1a | ||
|
|
0723dac596 | ||
|
|
365f9b4c1b | ||
|
|
2cf90d90f7 | ||
|
|
da14f64d1b | ||
|
|
ef1b1c8ffc | ||
|
|
adde8ab2c3 | ||
|
|
22cc8079dc | ||
|
|
6d25706300 | ||
|
|
81419c980b | ||
|
|
097e21f641 | ||
|
|
b68180b97b | ||
|
|
d9059d18c0 | ||
|
|
5225a6e27a | ||
|
|
9055bace96 | ||
|
|
eb25e88754 | ||
|
|
0cb8ecc6f7 | ||
|
|
8cf76cc6e9 | ||
|
|
3c3a646d42 | ||
|
|
ff29fd69a3 | ||
|
|
91994a399c | ||
|
|
bef7137c92 | ||
|
|
11f62bab34 | ||
|
|
b66776a047 | ||
|
|
2c151f444c | ||
|
|
d8f8b060cb | ||
|
|
ea9376019c | ||
|
|
008d1df106 | ||
|
|
ec91a6c7f4 | ||
|
|
4b2ae6a79f | ||
|
|
2ec756fd6b | ||
|
|
f0d5b40121 | ||
|
|
31a0996b2f | ||
|
|
0bbd8f0a16 | ||
|
|
b1619e42fd | ||
|
|
4cfec2fc09 | ||
|
|
3d721a95bb | ||
|
|
dc3a136704 | ||
|
|
235ae2fbb9 | ||
|
|
1f518b0b45 | ||
|
|
ab34d53d6b | ||
|
|
aafb5333b9 | ||
|
|
5e2791f981 | ||
|
|
1ecc5674ac | ||
|
|
c9ad3a595a | ||
|
|
43558fa1dc | ||
|
|
15a867d2b7 | ||
|
|
43cb4f471f | ||
|
|
012051c1db | ||
|
|
b5483898da | ||
|
|
c7b4c2beaf | ||
|
|
8693d067e3 | ||
|
|
78d5fc9506 | ||
|
|
7ac4f1a9be | ||
|
|
670fb01fbf | ||
|
|
ec9608d54b | ||
|
|
dcffaf1c8c | ||
|
|
efac5293c0 | ||
|
|
c7563346a2 | ||
|
|
69906532b1 | ||
|
|
9404fae725 | ||
|
|
3542f15575 | ||
|
|
15d79143e2 | ||
|
|
f6dafee748 | ||
|
|
356d86ae74 | ||
|
|
e673c6ec97 | ||
|
|
3e5fcf7004 | ||
|
|
96d2a2bc68 | ||
|
|
b401992178 | ||
|
|
4c34351383 | ||
|
|
202b54afbb | ||
|
|
26ebc3d944 | ||
|
|
d0f36e9939 | ||
|
|
4c99c032ca | ||
|
|
6198556e50 | ||
|
|
514eba0e4e | ||
|
|
7279ee0f5f | ||
|
|
ccadf3fcca | ||
|
|
cb80ce0d2c | ||
|
|
9aae43961f | ||
|
|
b810687163 | ||
|
|
28d4ec779d | ||
|
|
cf4da6c748 | ||
|
|
e569b5669a | ||
|
|
695a9523fe | ||
|
|
01b8e28a7d | ||
|
|
e221030a2b | ||
|
|
88dc697392 | ||
|
|
99a270216c | ||
|
|
d06c920a2d | ||
|
|
5452986ce3 | ||
|
|
ae69841174 | ||
|
|
e6fc80c13b | ||
|
|
a10ee8f58c | ||
|
|
3f69da2455 | ||
|
|
a14a0834e9 | ||
|
|
c4f431849b | ||
|
|
ade3389cea | ||
|
|
6873ba0909 | ||
|
|
c3f31b5f65 | ||
|
|
3b62aa444b | ||
|
|
fb2b10b565 | ||
|
|
345553012f | ||
|
|
fddc13d98e | ||
|
|
aeb9f798c9 | ||
|
|
8fd44266a2 | ||
|
|
ef4155fbf6 | ||
|
|
92f27d3144 | ||
|
|
5183332f6e | ||
|
|
ee1de0980f | ||
|
|
c53183d94b | ||
|
|
1ee774d01d | ||
|
|
1c030f57ef | ||
|
|
c8bb321654 | ||
|
|
a1d71c9198 | ||
|
|
55cb43d70b | ||
|
|
551159e0c3 | ||
|
|
3bf31aa478 | ||
|
|
1316682343 | ||
|
|
7167b06bf8 | ||
|
|
34582f9f65 | ||
|
|
8dc9ec5733 | ||
|
|
e8adf8ad2b | ||
|
|
1306431e7c | ||
|
|
f7bc0fba4d | ||
|
|
0f3ee21d9d | ||
|
|
a489037ec4 | ||
|
|
c48388af14 | ||
|
|
2207d356f2 | ||
|
|
6b5ced0078 | ||
|
|
c7e8498166 | ||
|
|
2b4c019f13 | ||
|
|
335850380e |
@@ -0,0 +1 @@
|
||||
* text=auto
|
||||
@@ -13,3 +13,7 @@ npm-debug.log
|
||||
.env
|
||||
build/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
|
||||
# Directories needed for code coverage
|
||||
/coverage/
|
||||
|
||||
+10
-8
@@ -1,18 +1,20 @@
|
||||
language: node_js
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
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:
|
||||
global:
|
||||
- CXX=g++-4.8
|
||||
matrix:
|
||||
- NPM_COMMAND=lint
|
||||
- NPM_COMMAND=test
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
||||
notifications:
|
||||
email: false
|
||||
script: npm run $NPM_COMMAND
|
||||
after_success: npm run coveralls
|
||||
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/bower_components": true,
|
||||
"**/dist": true,
|
||||
"**/build": true,
|
||||
"**/lib": true
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
List of xterm.js contributors. Updated before every release.
|
||||
|
||||
Aleksandr Andrienko <aandrienko@codenvy.com>
|
||||
Aleksandr Andriienko <oandriie@redhat.com>
|
||||
Alessandro Nadalin <alessandro.nadalin@gmail.com>
|
||||
Alexander Olsson <noseglid@gmail.com>
|
||||
Alexey Kontsevoy <alexey@gravitational.com>
|
||||
Anish Athalye <me@anishathalye.com>
|
||||
Anthony Lapenna <lapenna.anthony@gmail.com>
|
||||
Antonis Kalipetis <akalipetis@sourcelair.com>
|
||||
Anton Skshidlevsky <meefik@gmail.com>
|
||||
Anton Yurovskykh <anton.yurovskykh@gmail.com>
|
||||
Artem Arbatskiy <artyom.arbatskiy@gmail.com>
|
||||
Austin Robertson <austinrobertson@gmail.com>
|
||||
ayapi <colors.aya@gmail.com>
|
||||
Ben Hall <ben@benhall.me.uk>
|
||||
@@ -12,30 +18,57 @@ Benjamin Fischer <benjamin.fischer@rwth-aachen.de>
|
||||
Bill Church <billchurch@users.noreply.github.com>
|
||||
Bob Reid <bobreid@Bobs-MacBook-Pro.local>
|
||||
bottleofwater <nison.mael+bottleofwater@gmail.com>
|
||||
Brian Mock <brian@mockbrian.com>
|
||||
Carson Anderson <carson@betterservers.com>
|
||||
CHaBou <chabup@delean.fr>
|
||||
Christian Budde Christensen <budde377@gmail.com>
|
||||
Christof Marti <chrmarti@microsoft.com>
|
||||
Christopher Jeffrey <chjjeffrey@gmail.com>
|
||||
coderaiser <mnemonic.enemy@gmail.com>
|
||||
Daniel Imms <daimms@microsoft.com>
|
||||
Daniel Risacher <drisacher@gmail.com>
|
||||
Dan Kaplun <dbkaplun@twitch.tv>
|
||||
Darin Morrison <freebroccolo@users.noreply.github.com>
|
||||
Edgar Andrés Margffoy Tuay <andfoy@gmail.com>
|
||||
Elliot Saba <staticfloat@gmail.com>
|
||||
Gary Ritchie <gary@rstudio.com>
|
||||
hiro-su <h.sugipon@gmail.com>
|
||||
Ian Lewis <ianlewis@google.com>
|
||||
imoses <ido@twiggle.com>
|
||||
InDieTasten <indietasten@gmail.com>
|
||||
irokas <akasidiari@inf.uth.gr>
|
||||
Jean Bruenn <himself@jeanbruenn.info>
|
||||
Jeremy Danyow <jedanyow@microsoft.com>
|
||||
Joao Moreno <jomo@microsoft.com>
|
||||
Joao Moreno <mail@joaomoreno.com>
|
||||
Jon Masters <jon.masters@sky.com>
|
||||
Jörg Breitbart <jerch@rockborn.de>
|
||||
Justin Mecham <justin@mecham.me>
|
||||
Kirill Merkushev <lanwen@yandex.ru>
|
||||
Lucian Buzzo <lucian.buzzo@gmail.com>
|
||||
Maël Nison <nison.mael@gmail.com>
|
||||
Martin Chloride <i@martincl2.me>
|
||||
Martin Wang <jiahaow@ca.ibm.com>
|
||||
Matt Bierner <matb@microsoft.com>
|
||||
Michael Irwin <mikesir87@gmail.com>
|
||||
Mikko Karvonen <mikko.karvonen@arm.com>
|
||||
mofux <t.zilz@mofux.org>
|
||||
Nicolas Ramz <nicolas.ramz@gmail.com>
|
||||
Oleksandr Andriienko <oandriie@redhat.com>
|
||||
Paris Kasidiaris <pariskasidiaris@gmail.com>
|
||||
Paris Kasidiaris <paris@sourcelair.com>
|
||||
runarberg <runar@greenqloud.com>
|
||||
Saswat Das <saswatds@users.noreply.github.com>
|
||||
Saul Costa <saul@codevolve.com>
|
||||
Shuanglei Tao <tsl0922@gmail.com>
|
||||
Steven Silvester <steven.silvester@ieee.org>
|
||||
stuicey <stuicey@users.noreply.github.com>
|
||||
t-amqi <umberos@gmail.com>
|
||||
Thanasis Daglis <thanasis@sourcelair.com>
|
||||
thomas <thomas@kreativkonzentrat.de>
|
||||
Thomas Zilz <t.zilz@mofux.org>
|
||||
Tine Jozelj <tine.jozelj@outlook.com>
|
||||
Tyler Jewell <tjewell@codenvy.com>
|
||||
Vincent Woo <me@vincentwoo.com>
|
||||
yutaka <shigure@refy.net>
|
||||
YuviPanda <yuvipanda@gmail.com>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# xterm.js
|
||||
# [](https://xtermjs.org)
|
||||
|
||||
[](https://travis-ci.org/sourcelair/xterm.js) [](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
[](https://travis-ci.org/sourcelair/xterm.js) [](https://coveralls.io/github/sourcelair/xterm.js) [](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
|
||||
Xterm.js is a terminal front-end component written in JavaScript that works in the browser.
|
||||
|
||||
@@ -30,6 +30,19 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
||||
- [**Codenvy**](http://www.codenvy.com): Cloud workspaces for development teams.
|
||||
- [**CoderPad**](https://coderpad.io): Online interviewing platform for programmers. Run code in many programming languages, with results displayed by `xterm.js`.
|
||||
- [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using `xterm.js`, socket.io, and ssh2.
|
||||
- [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE.
|
||||
- [**Cloud Commander**](https://cloudcmd.io "Cloud Commander"): Orthodox web file manager with console and editor.
|
||||
- [**Codevolve**](https://www.codevolve.com "Codevolve"): Online platform for interactive coding and web development courses. Live container-backed terminal uses `xterm.js`.
|
||||
- [**RStudio**](https://www.rstudio.com/products/RStudio "RStudio"): RStudio is an integrated development environment (IDE) for R.
|
||||
- [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor.
|
||||
- [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy and run in the cloud.
|
||||
- [**Gravitational Teleport**](https://github.com/gravitational/teleport): Gravitational Teleport is a modern SSH server for remotely accessing clusters of Linux servers via SSH or HTTPS.
|
||||
- [**Hexlet**](https://en.hexlet.io): Practical programming courses (JavaScript, PHP, Unix, databases, functional programming). A steady path from the first line of code to the first job.
|
||||
- [**Selenoid UI**](https://github.com/aerokube/selenoid-ui): Simple UI for the scallable golang implementation of Selenium Hub named Selenoid. We use XTerm for streaming logs over websockets from docker containers.
|
||||
- [**Portainer**](https://portainer.io): Simple management UI for Docker.
|
||||
- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising `xterm.js`, SJCL & websockets.
|
||||
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible
|
||||
computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages.
|
||||
|
||||
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.
|
||||
|
||||
@@ -48,14 +61,29 @@ Xterm.js works seamlessly in Electron apps and may even work on earlier versions
|
||||
|
||||
## Demo
|
||||
|
||||
To launch the demo simply run:
|
||||
### Linux or macOS
|
||||
|
||||
Run the following commands:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm start
|
||||
$ npm install
|
||||
$ npm start
|
||||
```
|
||||
|
||||
Then open http://0.0.0.0:3000 in a web browser (use http://127.0.0.1:3000 if running under Windows).
|
||||
Then open http://0.0.0.0:3000 in a web browser.
|
||||
|
||||
### Windows
|
||||
|
||||
First, ensure [node-gyp](https://github.com/nodejs/node-gyp) is installed and configured correctly, then run these commands.
|
||||
|
||||
Note: Do not use ConEmu, as it seems to break the demo for some reason.
|
||||
|
||||
```
|
||||
> npm install
|
||||
> npm start
|
||||
```
|
||||
|
||||
Then open http://127.0.0.1:3000 in a web browser.
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -71,13 +99,13 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
|
||||
<div id="terminal"></div>
|
||||
<script>
|
||||
var term = new Terminal();
|
||||
term.open(document.getElementById('#terminal'));
|
||||
term.open(document.getElementById('terminal'));
|
||||
term.write('Hello from \033[1;3;31mxterm.js\033[0m $ ')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
|
||||
Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
|
||||
|
||||
## Addons
|
||||
|
||||
@@ -106,32 +134,12 @@ The existing releases are available at this GitHub repo's [Releases](https://git
|
||||
|
||||
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) before.
|
||||
|
||||
The development of xterm.js does not require any special tool. All you need is an editor that supports JavaScript and a browser (if you would like to run the demo you will need Node.js to get all features).
|
||||
|
||||
It is recommended though to use a development tool that uses xterm.js internally, to develop for xterm.js. [Eating our own dogfood](https://en.wikipedia.org/wiki/Eating_your_own_dog_food) has been proved extremely beneficial for this project. Known tools that use xterm.js internally are:
|
||||
|
||||
#### [SourceLair](https://www.sourcelair.com)
|
||||
|
||||
Visit https://lair.io/sourcelair/xterm and follow the instructions. All development will happen in your browser.
|
||||
|
||||
#### [Visual Studio Code](http://code.visualstudio.com/)
|
||||
|
||||
[Download Visual Studio Code](http://code.visualstudio.com/Download), clone xterm.js and you are all set.
|
||||
|
||||
#### [Eclipse Che](http://www.eclipse.org/che)
|
||||
|
||||
You can start Eclipse Che with `docker run eclipse/che start`.
|
||||
|
||||
#### [Codenvy](http://www.codenvy.io)
|
||||
|
||||
You can create a trial account or install an enterprise version with `docker run codenvy/cli start`.
|
||||
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.
|
||||
|
||||
## License Agreement
|
||||
|
||||
If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work.
|
||||
|
||||
Copyright (c) 2014-2016, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)
|
||||
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)
|
||||
|
||||
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm.js",
|
||||
"version": "2.3.1",
|
||||
"version": "2.9.2",
|
||||
"ignore": ["demo", "test", ".gitignore"],
|
||||
"main": [
|
||||
"dist/xterm.js",
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ app.ws('/terminals/:pid', function (ws, req) {
|
||||
term.write(msg);
|
||||
});
|
||||
ws.on('close', function () {
|
||||
process.kill(term.pid);
|
||||
term.kill();
|
||||
console.log('Closed terminal ' + term.pid);
|
||||
// Clean things up
|
||||
delete terminals[term.pid];
|
||||
|
||||
@@ -5,20 +5,39 @@
|
||||
<link rel="stylesheet" href="/build/xterm.css" />
|
||||
<link rel="stylesheet" href="/build/addons/fullscreen/fullscreen.css" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
|
||||
<script src="/build/xterm.js" ></script>
|
||||
<script src="/build/addons/attach/attach.js" ></script>
|
||||
<script src="/build/addons/fit/fit.js" ></script>
|
||||
<script src="/build/addons/fullscreen/fullscreen.js" ></script>
|
||||
<script src="/build/addons/search/search.js" ></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>xterm.js: xterm, in the browser</h1>
|
||||
<div id="terminal-container"></div>
|
||||
<div>
|
||||
<h2>Actions</h2>
|
||||
<p>
|
||||
<label>Find next <input id="find-next"/></label>
|
||||
<label>Find previous <input id="find-previous"/></label>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Options</h2>
|
||||
<p>
|
||||
<label><input type="checkbox" id="option-cursor-blink"> cursorBlink</label>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
cursorStyle
|
||||
<select id="option-cursor-style">
|
||||
<option value="block">block</option>
|
||||
<option value="underline">underline</option>
|
||||
<option value="bar">bar</option>
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label>scrollback <input type="number" id="option-scrollback" value="1000" /></label>
|
||||
</p>
|
||||
|
||||
@@ -7,8 +7,13 @@ var term,
|
||||
charHeight;
|
||||
|
||||
var terminalContainer = document.getElementById('terminal-container'),
|
||||
actionElements = {
|
||||
findNext: document.querySelector('#find-next'),
|
||||
findPrevious: document.querySelector('#find-previous')
|
||||
},
|
||||
optionElements = {
|
||||
cursorBlink: document.querySelector('#option-cursor-blink'),
|
||||
cursorStyle: document.querySelector('#option-cursor-style'),
|
||||
scrollback: document.querySelector('#option-scrollback'),
|
||||
tabstopwidth: document.querySelector('#option-tabstopwidth')
|
||||
},
|
||||
@@ -29,9 +34,25 @@ function setTerminalSize () {
|
||||
colsElement.addEventListener('change', setTerminalSize);
|
||||
rowsElement.addEventListener('change', setTerminalSize);
|
||||
|
||||
actionElements.findNext.addEventListener('keypress', function (e) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
term.findNext(actionElements.findNext.value);
|
||||
}
|
||||
});
|
||||
actionElements.findPrevious.addEventListener('keypress', function (e) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
term.findPrevious(actionElements.findPrevious.value);
|
||||
}
|
||||
});
|
||||
|
||||
optionElements.cursorBlink.addEventListener('change', function () {
|
||||
term.setOption('cursorBlink', optionElements.cursorBlink.checked);
|
||||
});
|
||||
optionElements.cursorStyle.addEventListener('change', function () {
|
||||
term.setOption('cursorStyle', optionElements.cursorStyle.value);
|
||||
});
|
||||
optionElements.scrollback.addEventListener('change', function () {
|
||||
term.setOption('scrollback', parseInt(optionElements.scrollback.value, 10));
|
||||
});
|
||||
|
||||
Vendored
+7
-2
@@ -32,6 +32,9 @@
|
||||
var exports = {};
|
||||
|
||||
exports.proposeGeometry = function (term) {
|
||||
if (!term.element.parentElement) {
|
||||
return null;
|
||||
}
|
||||
var parentElementStyle = window.getComputedStyle(term.element.parentElement),
|
||||
parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')),
|
||||
parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17),
|
||||
@@ -53,7 +56,7 @@
|
||||
subjectRow.innerHTML = 'W'; // Common character for measuring width, although on monospace
|
||||
characterWidth = subjectRow.getBoundingClientRect().width;
|
||||
subjectRow.style.display = ''; // Revert style before calculating height, since they differ.
|
||||
characterHeight = parseInt(subjectRow.offsetHeight);
|
||||
characterHeight = subjectRow.getBoundingClientRect().height;
|
||||
subjectRow.innerHTML = contentBuffer;
|
||||
|
||||
rows = parseInt(availableHeight / characterHeight);
|
||||
@@ -66,7 +69,9 @@
|
||||
exports.fit = function (term) {
|
||||
var geometry = exports.proposeGeometry(term);
|
||||
|
||||
term.resize(geometry.cols, geometry.rows);
|
||||
if (geometry) {
|
||||
term.resize(geometry.cols, geometry.rows);
|
||||
}
|
||||
};
|
||||
|
||||
Xterm.prototype.proposeGeometry = function () {
|
||||
|
||||
Vendored
+56
-37
@@ -41,6 +41,9 @@
|
||||
font-family: courier-new, courier, monospace;
|
||||
font-feature-settings: "liga" 0;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.terminal.focus,
|
||||
@@ -71,63 +74,55 @@
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.terminal:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar) .terminal-cursor {
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
.terminal a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.terminal a:hover {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.terminal a.xterm-invalid-link:hover {
|
||||
cursor: text;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.terminal .terminal-cursor {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.terminal:not(.focus) .terminal-cursor {
|
||||
outline: 1px solid #fff;
|
||||
outline-offset: -1px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.terminal:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus.xterm-cursor-blink .terminal-cursor {
|
||||
animation: xterm-cursor-blink 1.2s infinite step-end;
|
||||
.terminal.xterm-cursor-style-block.focus:not(.xterm-cursor-blink-on) .terminal-cursor {
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
@keyframes xterm-cursor-blink {
|
||||
0% {
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
}
|
||||
50% {
|
||||
background-color: transparent;
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
|
||||
.terminal.xterm-cursor-style-bar .terminal-cursor,
|
||||
.terminal.xterm-cursor-style-underline .terminal-cursor {
|
||||
position: relative;
|
||||
}
|
||||
.terminal.xterm-cursor-style-bar .terminal-cursor::before,
|
||||
.terminal.xterm-cursor-style-underline .terminal-cursor::before {
|
||||
content: "";
|
||||
display: block;
|
||||
.terminal.focus.xterm-cursor-style-bar:not(.xterm-cursor-blink-on) .terminal-cursor::before,
|
||||
.terminal.focus.xterm-cursor-style-underline:not(.xterm-cursor-blink-on) .terminal-cursor::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-color: #fff;
|
||||
}
|
||||
.terminal.xterm-cursor-style-bar .terminal-cursor::before {
|
||||
|
||||
.terminal.focus.xterm-cursor-style-bar:not(.xterm-cursor-blink-on) .terminal-cursor::before {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
}
|
||||
.terminal.xterm-cursor-style-underline .terminal-cursor::before {
|
||||
|
||||
.terminal.focus.xterm-cursor-style-underline:not(.xterm-cursor-blink-on) .terminal-cursor::before {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
}
|
||||
.terminal.xterm-cursor-style-bar.focus.xterm-cursor-blink .terminal-cursor::before,
|
||||
.terminal.xterm-cursor-style-underline.focus.xterm-cursor-blink .terminal-cursor::before {
|
||||
animation: xterm-cursor-non-bar-blink 1.2s infinite step-end;
|
||||
}
|
||||
@keyframes xterm-cursor-non-bar-blink {
|
||||
0% { background-color: #fff; }
|
||||
50% { background-color: transparent; }
|
||||
}
|
||||
|
||||
.terminal .composition-view {
|
||||
background: #000;
|
||||
@@ -148,7 +143,8 @@
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.terminal .xterm-wide-char {
|
||||
.terminal .xterm-wide-char,
|
||||
.terminal .xterm-normal-char {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@@ -174,6 +170,25 @@
|
||||
left: -9999em;
|
||||
}
|
||||
|
||||
.terminal.enable-mouse-events {
|
||||
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.terminal .xterm-selection {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.terminal .xterm-selection div {
|
||||
position: absolute;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/*
|
||||
* Determine default colors for xterm.js
|
||||
*/
|
||||
@@ -189,6 +204,10 @@
|
||||
text-decoration: blink;
|
||||
}
|
||||
|
||||
.terminal .xterm-blink.xterm-underline {
|
||||
text-decoration: blink underline;
|
||||
}
|
||||
|
||||
.terminal .xterm-hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
Vendored
+1890
-628
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+107
-24
@@ -1,39 +1,57 @@
|
||||
/**
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
const browserify = require('browserify');
|
||||
const buffer = require('vinyl-buffer');
|
||||
const coveralls = require('gulp-coveralls');
|
||||
const fs = require('fs-extra');
|
||||
const gulp = require('gulp');
|
||||
const istanbul = require('gulp-istanbul');
|
||||
const merge = require('merge-stream');
|
||||
const mocha = require('gulp-mocha');
|
||||
const sorcery = require('sorcery');
|
||||
const source = require('vinyl-source-stream');
|
||||
const sourcemaps = require('gulp-sourcemaps');
|
||||
const ts = require('gulp-typescript');
|
||||
const tsify = require('tsify');
|
||||
|
||||
const util = require('gulp-util');
|
||||
|
||||
let buildDir = process.env.BUILD_DIR || 'build';
|
||||
let tsProject = ts.createProject('tsconfig.json');
|
||||
let tsProjectSearchAddon = ts.createProject('./src/addons/search/tsconfig.json');
|
||||
let srcDir = tsProject.config.compilerOptions.rootDir;
|
||||
let outDir = tsProject.config.compilerOptions.outDir;
|
||||
|
||||
// Under some environments like TravisCI, this comes out at absolute which can
|
||||
// break the build. This ensures that the outDir is absolute.
|
||||
if (outDir.indexOf(__dirname) !== 0) {
|
||||
outDir = `${__dirname}/${outDir}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile TypeScript sources to JavaScript files and create a source map file for each TypeScript
|
||||
* file compiled.
|
||||
*/
|
||||
gulp.task('tsc', function () {
|
||||
// Remove the lib/ directory to prevent confusion if files were deleted in src/
|
||||
fs.emptyDirSync('lib');
|
||||
// Remove the ${outDir}/ directory to prevent confusion if files were deleted in ${srcDir}/
|
||||
fs.emptyDirSync(`${outDir}`);
|
||||
|
||||
// Build all TypeScript files (including tests) to lib/, based on the configuration defined in
|
||||
// Build all TypeScript files (including tests) to ${outDir}/, based on the configuration defined in
|
||||
// `tsconfig.json`.
|
||||
let tsProject = ts.createProject('tsconfig.json');
|
||||
let tsResult = tsProject.src().pipe(sourcemaps.init()).pipe(tsProject());
|
||||
let tsc = tsResult.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest('lib'));
|
||||
let tsc = tsResult.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(outDir));
|
||||
|
||||
// Copy all addons from src/ to lib/
|
||||
let copyAddons = gulp.src('src/addons/**/*').pipe(gulp.dest('lib/addons'));
|
||||
fs.emptyDirSync(`${outDir}/addons/search`);
|
||||
let tsResultSearchAddon = tsProjectSearchAddon.src().pipe(sourcemaps.init()).pipe(tsProjectSearchAddon());
|
||||
let tscSearchAddon = tsResultSearchAddon.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(`${outDir}/addons/search`));
|
||||
|
||||
// Copy stylesheets from src/ to lib/
|
||||
let copyStylesheets = gulp.src('src/**/*.css').pipe(gulp.dest('lib'));
|
||||
// Copy all addons from ${srcDir}/ to ${outDir}/
|
||||
let copyAddons = gulp.src([`${srcDir}/addons/**/*`, `!${srcDir}/addons/search`, `!${srcDir}/addons/search/**`]).pipe(gulp.dest(`${outDir}/addons`));
|
||||
|
||||
return merge(tsc, copyAddons, copyStylesheets);
|
||||
// Copy stylesheets from ${srcDir}/ to ${outDir}/
|
||||
let copyStylesheets = gulp.src(`${srcDir}/**/*.css`).pipe(gulp.dest(outDir));
|
||||
|
||||
return merge(tsc, tscSearchAddon, copyAddons, copyStylesheets);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -47,13 +65,12 @@ gulp.task('browserify', ['tsc'], function() {
|
||||
let browserifyOptions = {
|
||||
basedir: buildDir,
|
||||
debug: true,
|
||||
entries: ['../lib/xterm.js'],
|
||||
entries: [`${outDir}/xterm.js`],
|
||||
standalone: 'Terminal',
|
||||
cache: {},
|
||||
packageCache: {}
|
||||
};
|
||||
let bundleStream = browserify(browserifyOptions)
|
||||
.plugin(tsify)
|
||||
.bundle()
|
||||
.pipe(source('xterm.js'))
|
||||
.pipe(buffer())
|
||||
@@ -61,27 +78,93 @@ gulp.task('browserify', ['tsc'], function() {
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest(buildDir));
|
||||
|
||||
// Copy all add-ons from lib/ to buildDir
|
||||
let copyAddons = gulp.src('lib/addons/**/*').pipe(gulp.dest(`${buildDir}/addons`));
|
||||
// Copy stylesheets from ${outDir}/ to ${buildDir}/
|
||||
let copyStylesheets = gulp.src(`${outDir}/**/*.css`).pipe(gulp.dest(buildDir));
|
||||
|
||||
// Copy stylesheets from src/ to lib/
|
||||
let copyStylesheets = gulp.src('lib/**/*.css').pipe(gulp.dest(buildDir));
|
||||
|
||||
return merge(bundleStream, copyAddons, copyStylesheets);
|
||||
return merge(bundleStream, copyStylesheets);
|
||||
});
|
||||
|
||||
gulp.task('browserify-addons', ['tsc'], function() {
|
||||
let searchOptions = {
|
||||
basedir: `${buildDir}/addons/search`,
|
||||
debug: true,
|
||||
entries: [`${outDir}/addons/search/search.js`],
|
||||
cache: {},
|
||||
packageCache: {}
|
||||
};
|
||||
let searchBundle = browserify(searchOptions)
|
||||
.bundle()
|
||||
.pipe(source('./addons/search/search.js'))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({loadMaps: true, sourceRoot: ''}))
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest(buildDir));
|
||||
|
||||
// Copy all add-ons from outDir to buildDir
|
||||
let copyAddons = gulp.src([
|
||||
// Copy JS addons
|
||||
`${outDir}/addons/**/*`,
|
||||
// Exclude TS addons from copy as they are being built via browserify
|
||||
`!${outDir}/addons/search`,
|
||||
`!${outDir}/addons/search/**`
|
||||
]).pipe(gulp.dest(`${buildDir}/addons`));
|
||||
|
||||
return merge(searchBundle, copyAddons);
|
||||
});
|
||||
|
||||
gulp.task('instrument-test', function () {
|
||||
return gulp.src([`${outDir}/**/*.js`])
|
||||
// Covering files
|
||||
.pipe(istanbul())
|
||||
// Force `require` to return covered files
|
||||
.pipe(istanbul.hookRequire());
|
||||
});
|
||||
|
||||
gulp.task('mocha', ['instrument-test'], function () {
|
||||
return gulp.src([`${outDir}/*test.js`, `${outDir}/**/*test.js`], {read: false})
|
||||
.pipe(mocha())
|
||||
.once('error', () => process.exit(1))
|
||||
.pipe(istanbul.writeReports());
|
||||
});
|
||||
|
||||
/**
|
||||
* Run single test file by file name(without file extension). Example of the command:
|
||||
* gulp mocha-test --test InputHandler.test
|
||||
*/
|
||||
gulp.task('mocha-test', ['instrument-test'], function () {
|
||||
let testName = util.env.test;
|
||||
util.log("Run test by Name: " + testName);
|
||||
return gulp.src([`${outDir}/${testName}.js`, `${outDir}/**/${testName}.js`], {read: false})
|
||||
.pipe(mocha())
|
||||
.once('error', () => process.exit(1))
|
||||
.pipe(istanbul.writeReports());
|
||||
});
|
||||
|
||||
/**
|
||||
* Use `sorcery` to resolve the source map chain and point back to the TypeScript files.
|
||||
* (Without this task the source maps produced for the JavaScript bundle points into the
|
||||
* compiled JavaScript files in lib/).
|
||||
* compiled JavaScript files in ${outDir}/).
|
||||
*/
|
||||
gulp.task('sorcery', ['browserify'], function () {
|
||||
var chain = sorcery.loadSync(`${buildDir}/xterm.js`);
|
||||
var map = chain.apply();
|
||||
let chain = sorcery.loadSync(`${buildDir}/xterm.js`);
|
||||
chain.apply();
|
||||
chain.writeSync();
|
||||
});
|
||||
|
||||
gulp.task('build', ['sorcery']);
|
||||
gulp.task('sorcery-addons', ['browserify-addons'], function () {
|
||||
var chain = sorcery.loadSync(`${buildDir}/addons/search/search.js`);
|
||||
chain.apply();
|
||||
chain.writeSync();
|
||||
});
|
||||
|
||||
/**
|
||||
* Submit coverage results to coveralls.io
|
||||
*/
|
||||
gulp.task('coveralls', function () {
|
||||
gulp.src('coverage/**/lcov.info')
|
||||
.pipe(coveralls());
|
||||
});
|
||||
|
||||
gulp.task('build', ['sorcery', 'sorcery-addons']);
|
||||
gulp.task('test', ['mocha']);
|
||||
gulp.task('default', ['build']);
|
||||
|
||||
Generated
+12552
File diff suppressed because it is too large
Load Diff
+14
-10
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "xterm",
|
||||
"description": "Full xterm terminal, in your browser",
|
||||
"version": "2.3.1",
|
||||
"version": "2.9.2",
|
||||
"ignore": [
|
||||
"demo",
|
||||
"test",
|
||||
@@ -36,40 +36,44 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/chai": "^3.4.34",
|
||||
"@types/jsdom": "^11.0.1",
|
||||
"@types/mocha": "^2.2.33",
|
||||
"@types/node": "^6.0.41",
|
||||
"browserify": "^13.1.0",
|
||||
"browserify": "^13.3.0",
|
||||
"chai": "3.5.0",
|
||||
"docdash": "0.4.0",
|
||||
"express": "4.13.4",
|
||||
"express-ws": "2.0.0-rc.1",
|
||||
"fs-extra": "^1.0.0",
|
||||
"glob": "^7.0.5",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-cli": "^1.2.2",
|
||||
"gulp-coveralls": "^0.1.4",
|
||||
"gulp-istanbul": "^1.1.1",
|
||||
"gulp-mocha": "^3.0.1",
|
||||
"gulp-sourcemaps": "1.9.1",
|
||||
"gulp-typescript": "^3.1.3",
|
||||
"gulp-util": "3.0.8",
|
||||
"jsdoc": "3.4.3",
|
||||
"jsdom": "^11.1.0",
|
||||
"merge-stream": "^1.0.1",
|
||||
"mocha": "2.5.3",
|
||||
"node-pty": "^0.4.1",
|
||||
"nodemon": "1.10.2",
|
||||
"sleep": "^3.0.1",
|
||||
"sorcery": "^0.10.0",
|
||||
"tsify": "^3.0.0",
|
||||
"tslint": "^4.0.2",
|
||||
"typescript": "^2.0.3",
|
||||
"typescript": "~2.2.0",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prestart": "npm run build",
|
||||
"start": "node demo/app",
|
||||
"dev": "nodemon -e js,ts --watch src --watch demo --exec npm start",
|
||||
"dev": "nodemon -e js,ts,css --watch src --watch demo --exec npm start",
|
||||
"lint": "tslint src/*.ts src/**/*.ts",
|
||||
"test": "mocha --recursive ./lib",
|
||||
"test": "gulp test",
|
||||
"build:docs": "jsdoc -c jsdoc.json",
|
||||
"build": "gulp build",
|
||||
"prepublish": "npm run build"
|
||||
"prepublish": "npm run build",
|
||||
"coveralls": "gulp coveralls"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* @license MIT
|
||||
*/
|
||||
import { assert } from 'chai';
|
||||
import { ITerminal } from './Interfaces';
|
||||
import { Buffer } from './Buffer';
|
||||
import { CircularList } from './utils/CircularList';
|
||||
import { MockTerminal } from './utils/TestUtils';
|
||||
|
||||
const INIT_COLS = 80;
|
||||
const INIT_ROWS = 24;
|
||||
|
||||
describe('Buffer', () => {
|
||||
let terminal: ITerminal;
|
||||
let buffer: Buffer;
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = new MockTerminal();
|
||||
terminal.cols = INIT_COLS;
|
||||
terminal.rows = INIT_ROWS;
|
||||
terminal.scrollback = 1000;
|
||||
buffer = new Buffer(terminal);
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create a CircularList with max length equal to scrollback, for its lines', () => {
|
||||
assert.instanceOf(buffer.lines, CircularList);
|
||||
assert.equal(buffer.lines.maxLength, terminal.scrollback);
|
||||
});
|
||||
it('should set the Buffer\'s scrollBottom value equal to the terminal\'s rows -1', () => {
|
||||
assert.equal(buffer.scrollBottom, terminal.rows - 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fillViewportRows', () => {
|
||||
it('should fill the buffer with blank lines based on the size of the viewport', () => {
|
||||
const blankLineChar = terminal.blankLine()[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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('resize', () => {
|
||||
describe('column size is reduced', () => {
|
||||
it('should not trim the data in the buffer', () => {
|
||||
buffer.fillViewportRows();
|
||||
buffer.resize(INIT_COLS / 2, INIT_ROWS);
|
||||
assert.equal(buffer.lines.length, INIT_ROWS);
|
||||
for (let i = 0; i < INIT_ROWS; i++) {
|
||||
assert.equal(buffer.lines.get(i).length, INIT_COLS);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('column size is increased', () => {
|
||||
it('should add pad columns', () => {
|
||||
buffer.fillViewportRows();
|
||||
buffer.resize(INIT_COLS + 10, INIT_ROWS);
|
||||
assert.equal(buffer.lines.length, INIT_ROWS);
|
||||
for (let i = 0; i < INIT_ROWS; i++) {
|
||||
assert.equal(buffer.lines.get(i).length, INIT_COLS + 10);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('row size reduced', () => {
|
||||
it('should trim blank lines from the end', () => {
|
||||
buffer.fillViewportRows();
|
||||
buffer.resize(INIT_COLS, INIT_ROWS - 10);
|
||||
assert.equal(buffer.lines.length, INIT_ROWS - 10);
|
||||
});
|
||||
|
||||
it('should move the viewport down when it\'s at the end', () => {
|
||||
buffer.fillViewportRows();
|
||||
// Set cursor y to have 5 blank lines below it
|
||||
buffer.y = INIT_ROWS - 5 - 1;
|
||||
buffer.resize(INIT_COLS, INIT_ROWS - 10);
|
||||
// Trim 5 rows
|
||||
assert.equal(buffer.lines.length, INIT_ROWS - 5);
|
||||
// Shift the viewport down 5 rows
|
||||
assert.equal(buffer.ydisp, 5);
|
||||
assert.equal(buffer.ybase, 5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('row size increased', () => {
|
||||
describe('empty buffer', () => {
|
||||
it('should add blank lines to end', () => {
|
||||
buffer.fillViewportRows();
|
||||
assert.equal(buffer.ydisp, 0);
|
||||
buffer.resize(INIT_COLS, INIT_ROWS + 10);
|
||||
assert.equal(buffer.ydisp, 0);
|
||||
assert.equal(buffer.lines.length, INIT_ROWS + 10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filled buffer', () => {
|
||||
it('should show more of the buffer above', () => {
|
||||
buffer.fillViewportRows();
|
||||
// Create 10 extra blank lines
|
||||
for (let i = 0; i < 10; i++) {
|
||||
buffer.lines.push(terminal.blankLine());
|
||||
}
|
||||
// Set cursor to the bottom of the buffer
|
||||
buffer.y = INIT_ROWS - 1;
|
||||
// Scroll down 10 lines
|
||||
buffer.ybase = 10;
|
||||
buffer.ydisp = 10;
|
||||
assert.equal(buffer.lines.length, INIT_ROWS + 10);
|
||||
buffer.resize(INIT_COLS, INIT_ROWS + 5);
|
||||
// Should be should 5 more lines
|
||||
assert.equal(buffer.ydisp, 5);
|
||||
assert.equal(buffer.ybase, 5);
|
||||
// Should not trim the buffer
|
||||
assert.equal(buffer.lines.length, INIT_ROWS + 10);
|
||||
});
|
||||
|
||||
it('should show more of the buffer below when the viewport is at the top of the buffer', () => {
|
||||
buffer.fillViewportRows();
|
||||
// Create 10 extra blank lines
|
||||
for (let i = 0; i < 10; i++) {
|
||||
buffer.lines.push(terminal.blankLine());
|
||||
}
|
||||
// Set cursor to the bottom of the buffer
|
||||
buffer.y = INIT_ROWS - 1;
|
||||
// Scroll down 10 lines
|
||||
buffer.ybase = 10;
|
||||
buffer.ydisp = 0;
|
||||
assert.equal(buffer.lines.length, INIT_ROWS + 10);
|
||||
buffer.resize(INIT_COLS, INIT_ROWS + 5);
|
||||
// The viewport should remain at the top
|
||||
assert.equal(buffer.ydisp, 0);
|
||||
// The buffer ybase should move up 5 lines
|
||||
assert.equal(buffer.ybase, 5);
|
||||
// Should not trim the buffer
|
||||
assert.equal(buffer.lines.length, INIT_ROWS + 10);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('row and column increased', () => {
|
||||
it('should resize properly', () => {
|
||||
buffer.fillViewportRows();
|
||||
buffer.resize(INIT_COLS + 5, INIT_ROWS + 5);
|
||||
assert.equal(buffer.lines.length, INIT_ROWS + 5);
|
||||
for (let i = 0; i < INIT_ROWS + 5; i++) {
|
||||
assert.equal(buffer.lines.get(i).length, INIT_COLS + 5);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, IBuffer } from './Interfaces';
|
||||
import { CircularList } from './utils/CircularList';
|
||||
|
||||
/**
|
||||
* This class represents a terminal buffer (an internal state of the terminal), where the
|
||||
* following information is stored (in high-level):
|
||||
* - text content of this particular buffer
|
||||
* - cursor position
|
||||
* - scroll position
|
||||
*/
|
||||
export class Buffer implements IBuffer {
|
||||
private _lines: CircularList<[number, string, number][]>;
|
||||
|
||||
public ydisp: number;
|
||||
public ybase: number;
|
||||
public y: number;
|
||||
public x: number;
|
||||
public scrollBottom: number;
|
||||
public scrollTop: number;
|
||||
public tabs: any;
|
||||
public savedY: number;
|
||||
public savedX: number;
|
||||
|
||||
/**
|
||||
* Create a new Buffer.
|
||||
* @param {Terminal} _terminal - The terminal the Buffer will belong to
|
||||
* @param {number} ydisp - The scroll position of the Buffer in the viewport
|
||||
* @param {number} ybase - The scroll position of the y cursor (ybase + y = the y position within the Buffer)
|
||||
* @param {number} y - The cursor's y position after ybase
|
||||
* @param {number} x - The cursor's x position after ybase
|
||||
*/
|
||||
constructor(
|
||||
private _terminal: ITerminal
|
||||
) {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
public get lines(): CircularList<[number, string, number][]> {
|
||||
return this._lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the buffer's viewport with blank lines.
|
||||
*/
|
||||
public fillViewportRows(): void {
|
||||
if (this._lines.length === 0) {
|
||||
let i = this._terminal.rows;
|
||||
while (i--) {
|
||||
this.lines.push(this._terminal.blankLine());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the buffer to it's initial state, discarding all previous data.
|
||||
*/
|
||||
public clear(): void {
|
||||
this.ydisp = 0;
|
||||
this.ybase = 0;
|
||||
this.y = 0;
|
||||
this.x = 0;
|
||||
this.scrollBottom = 0;
|
||||
this.scrollTop = 0;
|
||||
this.tabs = {};
|
||||
this._lines = new CircularList<[number, string, number][]>(this._terminal.scrollback);
|
||||
this.scrollBottom = this._terminal.rows - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes the buffer, adjusting its data accordingly.
|
||||
* @param newCols The new number of columns.
|
||||
* @param newRows The new number of rows.
|
||||
*/
|
||||
public resize(newCols: number, newRows: number): void {
|
||||
// Don't resize the buffer if it's empty and hasn't been used yet.
|
||||
if (this._lines.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Deal with columns increasing (we don't do anything when columns reduce)
|
||||
if (this._terminal.cols < newCols) {
|
||||
const ch: [number, string, number] = [this._terminal.defAttr, ' ', 1]; // does xterm use the default attr?
|
||||
for (let i = 0; i < this._lines.length; i++) {
|
||||
// TODO: This should be removed, with tests setup for the case that was
|
||||
// causing the underlying bug, see https://github.com/sourcelair/xterm.js/issues/824
|
||||
if (this._lines.get(i) === undefined) {
|
||||
this._lines.set(i, this._terminal.blankLine(undefined, undefined, newCols));
|
||||
}
|
||||
while (this._lines.get(i).length < newCols) {
|
||||
this._lines.get(i).push(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resize rows in both directions as needed
|
||||
let addToY = 0;
|
||||
if (this._terminal.rows < newRows) {
|
||||
for (let y = this._terminal.rows; y < newRows; y++) {
|
||||
if (this._lines.length < newRows + this.ybase) {
|
||||
if (this.ybase > 0 && this._lines.length <= this.ybase + this.y + addToY + 1) {
|
||||
// There is room above the buffer and there are no empty elements below the line,
|
||||
// scroll up
|
||||
this.ybase--;
|
||||
addToY++;
|
||||
if (this.ydisp > 0) {
|
||||
// Viewport is at the top of the buffer, must increase downwards
|
||||
this.ydisp--;
|
||||
}
|
||||
} 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // (this._terminal.rows >= newRows)
|
||||
for (let y = this._terminal.rows; y > newRows; y--) {
|
||||
if (this._lines.length > newRows + this.ybase) {
|
||||
if (this._lines.length > this.ybase + this.y + 1) {
|
||||
// The line is a blank line below the cursor, remove it
|
||||
this._lines.pop();
|
||||
} else {
|
||||
// The line is the cursor, scroll down
|
||||
this.ybase++;
|
||||
this.ydisp++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure that the cursor stays on screen
|
||||
if (this.y >= newRows) {
|
||||
this.y = newRows - 1;
|
||||
}
|
||||
if (addToY) {
|
||||
this.y += addToY;
|
||||
}
|
||||
|
||||
if (this.x >= newCols) {
|
||||
this.x = newCols - 1;
|
||||
}
|
||||
|
||||
this.scrollTop = 0;
|
||||
this.scrollBottom = newRows - 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* @license MIT
|
||||
*/
|
||||
import { assert } from 'chai';
|
||||
import { ITerminal } from './Interfaces';
|
||||
import { BufferSet } from './BufferSet';
|
||||
import { Buffer } from './Buffer';
|
||||
import { MockTerminal } from './utils/TestUtils';
|
||||
|
||||
describe('BufferSet', () => {
|
||||
let terminal: ITerminal;
|
||||
let bufferSet: BufferSet;
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = new MockTerminal();
|
||||
terminal.cols = 80;
|
||||
terminal.rows = 24;
|
||||
terminal.scrollback = 1000;
|
||||
bufferSet = new BufferSet(terminal);
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create two different buffers: alt and normal', () => {
|
||||
assert.instanceOf(bufferSet.normal, Buffer);
|
||||
assert.instanceOf(bufferSet.alt, Buffer);
|
||||
assert.notEqual(bufferSet.normal, bufferSet.alt);
|
||||
});
|
||||
});
|
||||
|
||||
describe('activateNormalBuffer', () => {
|
||||
beforeEach(() => {
|
||||
bufferSet.activateNormalBuffer();
|
||||
});
|
||||
|
||||
it('should set the normal buffer as the currently active buffer', () => {
|
||||
assert.equal(bufferSet.active, bufferSet.normal);
|
||||
});
|
||||
});
|
||||
|
||||
describe('activateAltBuffer', () => {
|
||||
beforeEach(() => {
|
||||
bufferSet.activateAltBuffer();
|
||||
});
|
||||
|
||||
it('should set the alt buffer as the currently active buffer', () => {
|
||||
assert.equal(bufferSet.active, bufferSet.alt);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, IBufferSet } from './Interfaces';
|
||||
import { Buffer } from './Buffer';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
|
||||
/**
|
||||
* The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and
|
||||
* provides also utilities for working with them.
|
||||
*/
|
||||
export class BufferSet extends EventEmitter implements IBufferSet {
|
||||
private _normal: Buffer;
|
||||
private _alt: Buffer;
|
||||
private _activeBuffer: Buffer;
|
||||
|
||||
/**
|
||||
* Create a new BufferSet for the given terminal.
|
||||
* @param {Terminal} terminal - The terminal the BufferSet will belong to
|
||||
*/
|
||||
constructor(private _terminal: ITerminal) {
|
||||
super();
|
||||
this._normal = new Buffer(this._terminal);
|
||||
this._normal.fillViewportRows();
|
||||
this._alt = new Buffer(this._terminal);
|
||||
this._activeBuffer = this._normal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the alt Buffer of the BufferSet
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
public get alt(): Buffer {
|
||||
return this._alt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the normal Buffer of the BufferSet
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
public get active(): Buffer {
|
||||
return this._activeBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently active Buffer of the BufferSet
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
public get normal(): Buffer {
|
||||
return this._normal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the normal Buffer of the BufferSet as its currently active Buffer
|
||||
*/
|
||||
public activateNormalBuffer(): void {
|
||||
// The alt buffer should always be cleared when we switch to the normal
|
||||
// buffer. This frees up memory since the alt buffer should always be new
|
||||
// when activated.
|
||||
this._alt.clear();
|
||||
|
||||
this._activeBuffer = this._normal;
|
||||
this.emit('activate', this._normal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the alt Buffer of the BufferSet as its currently active Buffer
|
||||
*/
|
||||
public activateAltBuffer(): void {
|
||||
// Since the alt buffer is always cleared when the normal buffer is
|
||||
// activated, we want to fill it when switching to it.
|
||||
this._alt.fillViewportRows();
|
||||
|
||||
this._activeBuffer = this._alt;
|
||||
this.emit('activate', this._alt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes both normal and alt buffers, adjusting their data accordingly.
|
||||
* @param newCols The new number of columns.
|
||||
* @param newRows The new number of rows.
|
||||
*/
|
||||
public resize(newCols: number, newRows: number): void {
|
||||
this._normal.resize(newCols, newRows);
|
||||
this._alt.resize(newCols, newRows);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* C0 control codes
|
||||
* See = https://en.wikipedia.org/wiki/C0_and_C1_control_codes
|
||||
|
||||
+4
-5
@@ -2,12 +2,14 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IEventEmitter } from './Interfaces';
|
||||
|
||||
interface ListenerType {
|
||||
(): void;
|
||||
listener?: () => void;
|
||||
};
|
||||
|
||||
export class EventEmitter {
|
||||
export class EventEmitter implements IEventEmitter {
|
||||
private _events: {[type: string]: ListenerType[]};
|
||||
|
||||
constructor() {
|
||||
@@ -53,14 +55,11 @@ export class EventEmitter {
|
||||
return this.on(type, on);
|
||||
}
|
||||
|
||||
public emit(type): void {
|
||||
public emit(type: string, ...args: any[]): void {
|
||||
if (!this._events[type]) {
|
||||
return;
|
||||
}
|
||||
|
||||
let args = Array.prototype.slice.call(arguments, 1);
|
||||
let obj = this._events[type];
|
||||
|
||||
for (let i = 0; i < obj.length; i++) {
|
||||
obj[i].apply(this, args);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
import { assert } from 'chai';
|
||||
import { InputHandler } from './InputHandler';
|
||||
import { wcwidth } from './InputHandler';
|
||||
|
||||
describe('InputHandler', () => {
|
||||
describe('save and restore cursor', () => {
|
||||
let terminal = { buffer: { x: 1, y: 2 } };
|
||||
let inputHandler = new InputHandler(terminal);
|
||||
// Save cursor position
|
||||
inputHandler.saveCursor([]);
|
||||
assert.equal(terminal.buffer.x, 1);
|
||||
assert.equal(terminal.buffer.y, 2);
|
||||
// Change cursor position
|
||||
terminal.buffer.x = 10;
|
||||
terminal.buffer.y = 20;
|
||||
// Restore cursor position
|
||||
inputHandler.restoreCursor([]);
|
||||
assert.equal(terminal.buffer.x, 1);
|
||||
assert.equal(terminal.buffer.y, 2);
|
||||
});
|
||||
describe('setCursorStyle', () => {
|
||||
it('should call Terminal.setOption with correct params', () => {
|
||||
let options = {};
|
||||
@@ -47,3 +63,139 @@ describe('InputHandler', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const old_wcwidth = (function(opts) {
|
||||
// extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c
|
||||
// combining characters
|
||||
const COMBINING = [
|
||||
[0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489],
|
||||
[0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2],
|
||||
[0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603],
|
||||
[0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670],
|
||||
[0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED],
|
||||
[0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A],
|
||||
[0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902],
|
||||
[0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D],
|
||||
[0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981],
|
||||
[0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD],
|
||||
[0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C],
|
||||
[0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D],
|
||||
[0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC],
|
||||
[0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD],
|
||||
[0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C],
|
||||
[0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D],
|
||||
[0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0],
|
||||
[0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48],
|
||||
[0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC],
|
||||
[0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD],
|
||||
[0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D],
|
||||
[0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6],
|
||||
[0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E],
|
||||
[0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC],
|
||||
[0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35],
|
||||
[0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E],
|
||||
[0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97],
|
||||
[0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030],
|
||||
[0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039],
|
||||
[0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F],
|
||||
[0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753],
|
||||
[0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD],
|
||||
[0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD],
|
||||
[0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922],
|
||||
[0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B],
|
||||
[0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34],
|
||||
[0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42],
|
||||
[0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF],
|
||||
[0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063],
|
||||
[0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],
|
||||
[0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],
|
||||
[0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],
|
||||
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB],
|
||||
[0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],
|
||||
[0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169],
|
||||
[0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD],
|
||||
[0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F],
|
||||
[0xE0100, 0xE01EF]
|
||||
];
|
||||
// binary search
|
||||
function bisearch(ucs) {
|
||||
let min = 0;
|
||||
let max = COMBINING.length - 1;
|
||||
let mid;
|
||||
if (ucs < COMBINING[0][0] || ucs > COMBINING[max][1])
|
||||
return false;
|
||||
while (max >= min) {
|
||||
mid = Math.floor((min + max) / 2);
|
||||
if (ucs > COMBINING[mid][1])
|
||||
min = mid + 1;
|
||||
else if (ucs < COMBINING[mid][0])
|
||||
max = mid - 1;
|
||||
else
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function wcwidth(ucs) {
|
||||
// test for 8-bit control characters
|
||||
if (ucs === 0)
|
||||
return opts.nul;
|
||||
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0))
|
||||
return opts.control;
|
||||
// binary search in table of non-spacing characters
|
||||
if (bisearch(ucs))
|
||||
return 0;
|
||||
// if we arrive here, ucs is not a combining or C0/C1 control character
|
||||
if (isWide(ucs)) {
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
function isWide(ucs) {
|
||||
return (
|
||||
ucs >= 0x1100 && (
|
||||
ucs <= 0x115f || // Hangul Jamo init. consonants
|
||||
ucs === 0x2329 ||
|
||||
ucs === 0x232a ||
|
||||
(ucs >= 0x2e80 && ucs <= 0xa4cf && ucs !== 0x303f) || // CJK..Yi
|
||||
(ucs >= 0xac00 && ucs <= 0xd7a3) || // Hangul Syllables
|
||||
(ucs >= 0xf900 && ucs <= 0xfaff) || // CJK Compat Ideographs
|
||||
(ucs >= 0xfe10 && ucs <= 0xfe19) || // Vertical forms
|
||||
(ucs >= 0xfe30 && ucs <= 0xfe6f) || // CJK Compat Forms
|
||||
(ucs >= 0xff00 && ucs <= 0xff60) || // Fullwidth Forms
|
||||
(ucs >= 0xffe0 && ucs <= 0xffe6) ||
|
||||
(ucs >= 0x20000 && ucs <= 0x2fffd) ||
|
||||
(ucs >= 0x30000 && ucs <= 0x3fffd)));
|
||||
}
|
||||
return wcwidth;
|
||||
})({nul: 0, control: 0}); // configurable options
|
||||
|
||||
describe('wcwidth', () => {
|
||||
it('same as old implementation for BMP and individual higher', (done) => {
|
||||
for (let i = 0; i < 65536; ++i)
|
||||
assert.equal(wcwidth(i), old_wcwidth(i));
|
||||
// test some individual higher to fullfill branching
|
||||
assert.equal(wcwidth(0x10A01), old_wcwidth(0x10A01));
|
||||
assert.equal(wcwidth(0x30000), old_wcwidth(0x30000));
|
||||
assert.equal(wcwidth(0x3fffe), old_wcwidth(0x3fffe));
|
||||
done();
|
||||
}).timeout(3000);
|
||||
/*
|
||||
it('new is at least 5 times faster', () => {
|
||||
let start_new = new Date().getTime();
|
||||
let x = 0;
|
||||
for (let runs = 0; runs < 1; ++runs)
|
||||
for (let i = 0; i < 65536; ++i)
|
||||
x = wcwidth(i);
|
||||
let end_new = new Date().getTime();
|
||||
let start_old = new Date().getTime();
|
||||
let y = 0;
|
||||
for (let runs = 0; runs < 1; ++runs)
|
||||
for (let i = 0; i < 65536; ++i)
|
||||
y = old_wcwidth(i);
|
||||
let end_old = new Date().getTime();
|
||||
// console.log((end_new - start_new));
|
||||
// console.log((end_old - start_old));
|
||||
assert.equal(((end_new - start_new) * 5 < (end_old - start_old)), true);
|
||||
});
|
||||
*/
|
||||
});
|
||||
|
||||
+338
-289
@@ -27,44 +27,48 @@ export class InputHandler implements IInputHandler {
|
||||
char = this._terminal.charset[char];
|
||||
}
|
||||
|
||||
let row = this._terminal.y + this._terminal.ybase;
|
||||
let row = this._terminal.buffer.y + this._terminal.buffer.ybase;
|
||||
|
||||
// insert combining char in last cell
|
||||
// FIXME: needs handling after cursor jumps
|
||||
if (!ch_width && this._terminal.x) {
|
||||
if (!ch_width && this._terminal.buffer.x) {
|
||||
// dont overflow left
|
||||
if (this._terminal.lines.get(row)[this._terminal.x - 1]) {
|
||||
if (!this._terminal.lines.get(row)[this._terminal.x - 1][2]) {
|
||||
if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1]) {
|
||||
if (!this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][2]) {
|
||||
|
||||
// found empty cell after fullwidth, need to go 2 cells back
|
||||
if (this._terminal.lines.get(row)[this._terminal.x - 2])
|
||||
this._terminal.lines.get(row)[this._terminal.x - 2][1] += char;
|
||||
if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2])
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][1] += char;
|
||||
|
||||
} else {
|
||||
this._terminal.lines.get(row)[this._terminal.x - 1][1] += char;
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][1] += char;
|
||||
}
|
||||
this._terminal.updateRange(this._terminal.y);
|
||||
this._terminal.updateRange(this._terminal.buffer.y);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// goto next line if ch would overflow
|
||||
// TODO: needs a global min terminal width of 2
|
||||
if (this._terminal.x + ch_width - 1 >= this._terminal.cols) {
|
||||
if (this._terminal.buffer.x + ch_width - 1 >= this._terminal.cols) {
|
||||
// autowrap - DECAWM
|
||||
if (this._terminal.wraparoundMode) {
|
||||
this._terminal.x = 0;
|
||||
this._terminal.y++;
|
||||
if (this._terminal.y > this._terminal.scrollBottom) {
|
||||
this._terminal.y--;
|
||||
this._terminal.scroll();
|
||||
this._terminal.buffer.x = 0;
|
||||
this._terminal.buffer.y++;
|
||||
if (this._terminal.buffer.y > this._terminal.buffer.scrollBottom) {
|
||||
this._terminal.buffer.y--;
|
||||
this._terminal.scroll(true);
|
||||
} else {
|
||||
// The line already exists (eg. the initial viewport), mark it as a
|
||||
// wrapped line
|
||||
this._terminal.buffer.lines.get(this._terminal.buffer.y).isWrapped = true;
|
||||
}
|
||||
} else {
|
||||
if (ch_width === 2) // FIXME: check for xterm behavior
|
||||
return;
|
||||
}
|
||||
}
|
||||
row = this._terminal.y + this._terminal.ybase;
|
||||
row = this._terminal.buffer.y + this._terminal.buffer.ybase;
|
||||
|
||||
// insert mode: move characters to right
|
||||
if (this._terminal.insertMode) {
|
||||
@@ -72,25 +76,26 @@ export class InputHandler implements IInputHandler {
|
||||
for (let moves = 0; moves < ch_width; ++moves) {
|
||||
// remove last cell, if it's width is 0
|
||||
// we have to adjust the second last cell as well
|
||||
const removed = this._terminal.lines.get(this._terminal.y + this._terminal.ybase).pop();
|
||||
const removed = this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).pop();
|
||||
if (removed[2] === 0
|
||||
&& this._terminal.lines.get(row)[this._terminal.cols - 2]
|
||||
&& this._terminal.lines.get(row)[this._terminal.cols - 2][2] === 2)
|
||||
this._terminal.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1];
|
||||
&& this._terminal.buffer.lines.get(row)[this._terminal.cols - 2]
|
||||
&& this._terminal.buffer.lines.get(row)[this._terminal.cols - 2][2] === 2) {
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1];
|
||||
}
|
||||
|
||||
// insert empty cell at cursor
|
||||
this._terminal.lines.get(row).splice(this._terminal.x, 0, [this._terminal.curAttr, ' ', 1]);
|
||||
this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 0, [this._terminal.curAttr, ' ', 1]);
|
||||
}
|
||||
}
|
||||
|
||||
this._terminal.lines.get(row)[this._terminal.x] = [this._terminal.curAttr, char, ch_width];
|
||||
this._terminal.x++;
|
||||
this._terminal.updateRange(this._terminal.y);
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, ch_width];
|
||||
this._terminal.buffer.x++;
|
||||
this._terminal.updateRange(this._terminal.buffer.y);
|
||||
|
||||
// fullwidth char - set next cell width to zero and advance cursor
|
||||
if (ch_width === 2) {
|
||||
this._terminal.lines.get(row)[this._terminal.x] = [this._terminal.curAttr, '', 0];
|
||||
this._terminal.x++;
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0];
|
||||
this._terminal.buffer.x++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,17 +121,23 @@ export class InputHandler implements IInputHandler {
|
||||
*/
|
||||
public lineFeed(): void {
|
||||
if (this._terminal.convertEol) {
|
||||
this._terminal.x = 0;
|
||||
this._terminal.buffer.x = 0;
|
||||
}
|
||||
this._terminal.y++;
|
||||
if (this._terminal.y > this._terminal.scrollBottom) {
|
||||
this._terminal.y--;
|
||||
this._terminal.buffer.y++;
|
||||
if (this._terminal.buffer.y > this._terminal.buffer.scrollBottom) {
|
||||
this._terminal.buffer.y--;
|
||||
this._terminal.scroll();
|
||||
}
|
||||
// If the end of the line is hit, prevent this action from wrapping around to the next line.
|
||||
if (this._terminal.x >= this._terminal.cols) {
|
||||
this._terminal.x--;
|
||||
if (this._terminal.buffer.x >= this._terminal.cols) {
|
||||
this._terminal.buffer.x--;
|
||||
}
|
||||
/**
|
||||
* This event is emitted whenever the terminal outputs a LF or NL.
|
||||
*
|
||||
* @event lineFeed
|
||||
*/
|
||||
this._terminal.emit('lineFeed');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,7 +145,7 @@ export class InputHandler implements IInputHandler {
|
||||
* Carriage Return (Ctrl-M).
|
||||
*/
|
||||
public carriageReturn(): void {
|
||||
this._terminal.x = 0;
|
||||
this._terminal.buffer.x = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,8 +153,8 @@ export class InputHandler implements IInputHandler {
|
||||
* Backspace (Ctrl-H).
|
||||
*/
|
||||
public backspace(): void {
|
||||
if (this._terminal.x > 0) {
|
||||
this._terminal.x--;
|
||||
if (this._terminal.buffer.x > 0) {
|
||||
this._terminal.buffer.x--;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +163,7 @@ export class InputHandler implements IInputHandler {
|
||||
* Horizontal Tab (HT) (Ctrl-I).
|
||||
*/
|
||||
public tab(): void {
|
||||
this._terminal.x = this._terminal.nextStop();
|
||||
this._terminal.buffer.x = this._terminal.nextStop();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,13 +194,13 @@ export class InputHandler implements IInputHandler {
|
||||
param = params[0];
|
||||
if (param < 1) param = 1;
|
||||
|
||||
row = this._terminal.y + this._terminal.ybase;
|
||||
j = this._terminal.x;
|
||||
row = this._terminal.buffer.y + this._terminal.buffer.ybase;
|
||||
j = this._terminal.buffer.x;
|
||||
ch = [this._terminal.eraseAttr(), ' ', 1]; // xterm
|
||||
|
||||
while (param-- && j < this._terminal.cols) {
|
||||
this._terminal.lines.get(row).splice(j++, 0, ch);
|
||||
this._terminal.lines.get(row).pop();
|
||||
this._terminal.buffer.lines.get(row).splice(j++, 0, ch);
|
||||
this._terminal.buffer.lines.get(row).pop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,9 +213,9 @@ export class InputHandler implements IInputHandler {
|
||||
if (param < 1) {
|
||||
param = 1;
|
||||
}
|
||||
this._terminal.y -= param;
|
||||
if (this._terminal.y < 0) {
|
||||
this._terminal.y = 0;
|
||||
this._terminal.buffer.y -= param;
|
||||
if (this._terminal.buffer.y < 0) {
|
||||
this._terminal.buffer.y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,13 +228,13 @@ export class InputHandler implements IInputHandler {
|
||||
if (param < 1) {
|
||||
param = 1;
|
||||
}
|
||||
this._terminal.y += param;
|
||||
if (this._terminal.y >= this._terminal.rows) {
|
||||
this._terminal.y = this._terminal.rows - 1;
|
||||
this._terminal.buffer.y += param;
|
||||
if (this._terminal.buffer.y >= this._terminal.rows) {
|
||||
this._terminal.buffer.y = this._terminal.rows - 1;
|
||||
}
|
||||
// If the end of the line is hit, prevent this action from wrapping around to the next line.
|
||||
if (this._terminal.x >= this._terminal.cols) {
|
||||
this._terminal.x--;
|
||||
if (this._terminal.buffer.x >= this._terminal.cols) {
|
||||
this._terminal.buffer.x--;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,9 +247,9 @@ export class InputHandler implements IInputHandler {
|
||||
if (param < 1) {
|
||||
param = 1;
|
||||
}
|
||||
this._terminal.x += param;
|
||||
if (this._terminal.x >= this._terminal.cols) {
|
||||
this._terminal.x = this._terminal.cols - 1;
|
||||
this._terminal.buffer.x += param;
|
||||
if (this._terminal.buffer.x >= this._terminal.cols) {
|
||||
this._terminal.buffer.x = this._terminal.cols - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,12 +263,12 @@ export class InputHandler implements IInputHandler {
|
||||
param = 1;
|
||||
}
|
||||
// If the end of the line is hit, prevent this action from wrapping around to the next line.
|
||||
if (this._terminal.x >= this._terminal.cols) {
|
||||
this._terminal.x--;
|
||||
if (this._terminal.buffer.x >= this._terminal.cols) {
|
||||
this._terminal.buffer.x--;
|
||||
}
|
||||
this._terminal.x -= param;
|
||||
if (this._terminal.x < 0) {
|
||||
this._terminal.x = 0;
|
||||
this._terminal.buffer.x -= param;
|
||||
if (this._terminal.buffer.x < 0) {
|
||||
this._terminal.buffer.x = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,12 +282,12 @@ export class InputHandler implements IInputHandler {
|
||||
if (param < 1) {
|
||||
param = 1;
|
||||
}
|
||||
this._terminal.y += param;
|
||||
if (this._terminal.y >= this._terminal.rows) {
|
||||
this._terminal.y = this._terminal.rows - 1;
|
||||
this._terminal.buffer.y += param;
|
||||
if (this._terminal.buffer.y >= this._terminal.rows) {
|
||||
this._terminal.buffer.y = this._terminal.rows - 1;
|
||||
}
|
||||
this._terminal.x = 0;
|
||||
};
|
||||
this._terminal.buffer.x = 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@@ -289,12 +300,12 @@ export class InputHandler implements IInputHandler {
|
||||
if (param < 1) {
|
||||
param = 1;
|
||||
}
|
||||
this._terminal.y -= param;
|
||||
if (this._terminal.y < 0) {
|
||||
this._terminal.y = 0;
|
||||
this._terminal.buffer.y -= param;
|
||||
if (this._terminal.buffer.y < 0) {
|
||||
this._terminal.buffer.y = 0;
|
||||
}
|
||||
this._terminal.x = 0;
|
||||
};
|
||||
this._terminal.buffer.x = 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@@ -306,7 +317,7 @@ export class InputHandler implements IInputHandler {
|
||||
if (param < 1) {
|
||||
param = 1;
|
||||
}
|
||||
this._terminal.x = param - 1;
|
||||
this._terminal.buffer.x = param - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -336,8 +347,8 @@ export class InputHandler implements IInputHandler {
|
||||
col = this._terminal.cols - 1;
|
||||
}
|
||||
|
||||
this._terminal.x = col;
|
||||
this._terminal.y = row;
|
||||
this._terminal.buffer.x = col;
|
||||
this._terminal.buffer.y = row;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -347,7 +358,7 @@ export class InputHandler implements IInputHandler {
|
||||
public cursorForwardTab(params: number[]): void {
|
||||
let param = params[0] || 1;
|
||||
while (param--) {
|
||||
this._terminal.x = this._terminal.nextStop();
|
||||
this._terminal.buffer.x = this._terminal.nextStop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,15 +378,15 @@ export class InputHandler implements IInputHandler {
|
||||
let j;
|
||||
switch (params[0]) {
|
||||
case 0:
|
||||
this._terminal.eraseRight(this._terminal.x, this._terminal.y);
|
||||
j = this._terminal.y + 1;
|
||||
this._terminal.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
|
||||
j = this._terminal.buffer.y + 1;
|
||||
for (; j < this._terminal.rows; j++) {
|
||||
this._terminal.eraseLine(j);
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
this._terminal.eraseLeft(this._terminal.x, this._terminal.y);
|
||||
j = this._terminal.y;
|
||||
this._terminal.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y);
|
||||
j = this._terminal.buffer.y;
|
||||
while (j--) {
|
||||
this._terminal.eraseLine(j);
|
||||
}
|
||||
@@ -385,7 +396,15 @@ export class InputHandler implements IInputHandler {
|
||||
while (j--) this._terminal.eraseLine(j);
|
||||
break;
|
||||
case 3:
|
||||
; // no saved lines
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -404,13 +423,13 @@ export class InputHandler implements IInputHandler {
|
||||
public eraseInLine(params: number[]): void {
|
||||
switch (params[0]) {
|
||||
case 0:
|
||||
this._terminal.eraseRight(this._terminal.x, this._terminal.y);
|
||||
this._terminal.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
|
||||
break;
|
||||
case 1:
|
||||
this._terminal.eraseLeft(this._terminal.x, this._terminal.y);
|
||||
this._terminal.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y);
|
||||
break;
|
||||
case 2:
|
||||
this._terminal.eraseLine(this._terminal.y);
|
||||
this._terminal.eraseLine(this._terminal.buffer.y);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -426,29 +445,29 @@ export class InputHandler implements IInputHandler {
|
||||
if (param < 1) {
|
||||
param = 1;
|
||||
}
|
||||
row = this._terminal.y + this._terminal.ybase;
|
||||
row = this._terminal.buffer.y + this._terminal.buffer.ybase;
|
||||
|
||||
j = this._terminal.rows - 1 - this._terminal.scrollBottom;
|
||||
j = this._terminal.rows - 1 + this._terminal.ybase - j + 1;
|
||||
j = this._terminal.rows - 1 - this._terminal.buffer.scrollBottom;
|
||||
j = this._terminal.rows - 1 + this._terminal.buffer.ybase - j + 1;
|
||||
|
||||
while (param--) {
|
||||
if (this._terminal.lines.length === this._terminal.lines.maxLength) {
|
||||
if (this._terminal.buffer.lines.length === this._terminal.buffer.lines.maxLength) {
|
||||
// Trim the start of lines to make room for the new line
|
||||
this._terminal.lines.trimStart(1);
|
||||
this._terminal.ybase--;
|
||||
this._terminal.ydisp--;
|
||||
this._terminal.buffer.lines.trimStart(1);
|
||||
this._terminal.buffer.ybase--;
|
||||
this._terminal.buffer.ydisp--;
|
||||
row--;
|
||||
j--;
|
||||
}
|
||||
// test: echo -e '\e[44m\e[1L\e[0m'
|
||||
// blankLine(true) - xterm/linux behavior
|
||||
this._terminal.lines.splice(row, 0, this._terminal.blankLine(true));
|
||||
this._terminal.lines.splice(j, 1);
|
||||
this._terminal.buffer.lines.splice(row, 0, this._terminal.blankLine(true));
|
||||
this._terminal.buffer.lines.splice(j, 1);
|
||||
}
|
||||
|
||||
// this.maxRange();
|
||||
this._terminal.updateRange(this._terminal.y);
|
||||
this._terminal.updateRange(this._terminal.scrollBottom);
|
||||
this._terminal.updateRange(this._terminal.buffer.y);
|
||||
this._terminal.updateRange(this._terminal.buffer.scrollBottom);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -462,27 +481,27 @@ export class InputHandler implements IInputHandler {
|
||||
if (param < 1) {
|
||||
param = 1;
|
||||
}
|
||||
row = this._terminal.y + this._terminal.ybase;
|
||||
row = this._terminal.buffer.y + this._terminal.buffer.ybase;
|
||||
|
||||
j = this._terminal.rows - 1 - this._terminal.scrollBottom;
|
||||
j = this._terminal.rows - 1 + this._terminal.ybase - j;
|
||||
j = this._terminal.rows - 1 - this._terminal.buffer.scrollBottom;
|
||||
j = this._terminal.rows - 1 + this._terminal.buffer.ybase - j;
|
||||
|
||||
while (param--) {
|
||||
if (this._terminal.lines.length === this._terminal.lines.maxLength) {
|
||||
if (this._terminal.buffer.lines.length === this._terminal.buffer.lines.maxLength) {
|
||||
// Trim the start of lines to make room for the new line
|
||||
this._terminal.lines.trimStart(1);
|
||||
this._terminal.ybase -= 1;
|
||||
this._terminal.ydisp -= 1;
|
||||
this._terminal.buffer.lines.trimStart(1);
|
||||
this._terminal.buffer.ybase -= 1;
|
||||
this._terminal.buffer.ydisp -= 1;
|
||||
}
|
||||
// test: echo -e '\e[44m\e[1M\e[0m'
|
||||
// blankLine(true) - xterm/linux behavior
|
||||
this._terminal.lines.splice(j + 1, 0, this._terminal.blankLine(true));
|
||||
this._terminal.lines.splice(row, 1);
|
||||
this._terminal.buffer.lines.splice(j + 1, 0, this._terminal.blankLine(true));
|
||||
this._terminal.buffer.lines.splice(row, 1);
|
||||
}
|
||||
|
||||
// this.maxRange();
|
||||
this._terminal.updateRange(this._terminal.y);
|
||||
this._terminal.updateRange(this._terminal.scrollBottom);
|
||||
this._terminal.updateRange(this._terminal.buffer.y);
|
||||
this._terminal.updateRange(this._terminal.buffer.scrollBottom);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -497,12 +516,12 @@ export class InputHandler implements IInputHandler {
|
||||
param = 1;
|
||||
}
|
||||
|
||||
row = this._terminal.y + this._terminal.ybase;
|
||||
row = this._terminal.buffer.y + this._terminal.buffer.ybase;
|
||||
ch = [this._terminal.eraseAttr(), ' ', 1]; // xterm
|
||||
|
||||
while (param--) {
|
||||
this._terminal.lines.get(row).splice(this._terminal.x, 1);
|
||||
this._terminal.lines.get(row).push(ch);
|
||||
this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 1);
|
||||
this._terminal.buffer.lines.get(row).push(ch);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,12 +531,12 @@ export class InputHandler implements IInputHandler {
|
||||
public scrollUp(params: number[]): void {
|
||||
let param = params[0] || 1;
|
||||
while (param--) {
|
||||
this._terminal.lines.splice(this._terminal.ybase + this._terminal.scrollTop, 1);
|
||||
this._terminal.lines.splice(this._terminal.ybase + this._terminal.scrollBottom, 0, this._terminal.blankLine());
|
||||
this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollTop, 1);
|
||||
this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollBottom, 0, this._terminal.blankLine());
|
||||
}
|
||||
// this.maxRange();
|
||||
this._terminal.updateRange(this._terminal.scrollTop);
|
||||
this._terminal.updateRange(this._terminal.scrollBottom);
|
||||
this._terminal.updateRange(this._terminal.buffer.scrollTop);
|
||||
this._terminal.updateRange(this._terminal.buffer.scrollBottom);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -526,12 +545,12 @@ export class InputHandler implements IInputHandler {
|
||||
public scrollDown(params: number[]): void {
|
||||
let param = params[0] || 1;
|
||||
while (param--) {
|
||||
this._terminal.lines.splice(this._terminal.ybase + this._terminal.scrollBottom, 1);
|
||||
this._terminal.lines.splice(this._terminal.ybase + this._terminal.scrollTop, 0, this._terminal.blankLine());
|
||||
this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollBottom, 1);
|
||||
this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollTop, 0, this._terminal.blankLine());
|
||||
}
|
||||
// this.maxRange();
|
||||
this._terminal.updateRange(this._terminal.scrollTop);
|
||||
this._terminal.updateRange(this._terminal.scrollBottom);
|
||||
this._terminal.updateRange(this._terminal.buffer.scrollTop);
|
||||
this._terminal.updateRange(this._terminal.buffer.scrollBottom);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -546,12 +565,12 @@ export class InputHandler implements IInputHandler {
|
||||
param = 1;
|
||||
}
|
||||
|
||||
row = this._terminal.y + this._terminal.ybase;
|
||||
j = this._terminal.x;
|
||||
row = this._terminal.buffer.y + this._terminal.buffer.ybase;
|
||||
j = this._terminal.buffer.x;
|
||||
ch = [this._terminal.eraseAttr(), ' ', 1]; // xterm
|
||||
|
||||
while (param-- && j < this._terminal.cols) {
|
||||
this._terminal.lines.get(row)[j++] = ch;
|
||||
this._terminal.buffer.lines.get(row)[j++] = ch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -561,7 +580,7 @@ export class InputHandler implements IInputHandler {
|
||||
public cursorBackwardTab(params: number[]): void {
|
||||
let param = params[0] || 1;
|
||||
while (param--) {
|
||||
this._terminal.x = this._terminal.prevStop();
|
||||
this._terminal.buffer.x = this._terminal.prevStop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -574,9 +593,9 @@ export class InputHandler implements IInputHandler {
|
||||
if (param < 1) {
|
||||
param = 1;
|
||||
}
|
||||
this._terminal.x = param - 1;
|
||||
if (this._terminal.x >= this._terminal.cols) {
|
||||
this._terminal.x = this._terminal.cols - 1;
|
||||
this._terminal.buffer.x = param - 1;
|
||||
if (this._terminal.buffer.x >= this._terminal.cols) {
|
||||
this._terminal.buffer.x = this._terminal.cols - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,9 +609,9 @@ export class InputHandler implements IInputHandler {
|
||||
if (param < 1) {
|
||||
param = 1;
|
||||
}
|
||||
this._terminal.x += param;
|
||||
if (this._terminal.x >= this._terminal.cols) {
|
||||
this._terminal.x = this._terminal.cols - 1;
|
||||
this._terminal.buffer.x += param;
|
||||
if (this._terminal.buffer.x >= this._terminal.cols) {
|
||||
this._terminal.buffer.x = this._terminal.cols - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -601,11 +620,11 @@ export class InputHandler implements IInputHandler {
|
||||
*/
|
||||
public repeatPrecedingCharacter(params: number[]): void {
|
||||
let param = params[0] || 1
|
||||
, line = this._terminal.lines.get(this._terminal.ybase + this._terminal.y)
|
||||
, ch = line[this._terminal.x - 1] || [this._terminal.defAttr, ' ', 1];
|
||||
, line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + this._terminal.buffer.y)
|
||||
, ch = line[this._terminal.buffer.x - 1] || [this._terminal.defAttr, ' ', 1];
|
||||
|
||||
while (param--) {
|
||||
line[this._terminal.x++] = ch;
|
||||
line[this._terminal.buffer.x++] = ch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -684,9 +703,9 @@ export class InputHandler implements IInputHandler {
|
||||
if (param < 1) {
|
||||
param = 1;
|
||||
}
|
||||
this._terminal.y = param - 1;
|
||||
if (this._terminal.y >= this._terminal.rows) {
|
||||
this._terminal.y = this._terminal.rows - 1;
|
||||
this._terminal.buffer.y = param - 1;
|
||||
if (this._terminal.buffer.y >= this._terminal.rows) {
|
||||
this._terminal.buffer.y = this._terminal.rows - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -700,13 +719,13 @@ export class InputHandler implements IInputHandler {
|
||||
if (param < 1) {
|
||||
param = 1;
|
||||
}
|
||||
this._terminal.y += param;
|
||||
if (this._terminal.y >= this._terminal.rows) {
|
||||
this._terminal.y = this._terminal.rows - 1;
|
||||
this._terminal.buffer.y += param;
|
||||
if (this._terminal.buffer.y >= this._terminal.rows) {
|
||||
this._terminal.buffer.y = this._terminal.rows - 1;
|
||||
}
|
||||
// If the end of the line is hit, prevent this action from wrapping around to the next line.
|
||||
if (this._terminal.x >= this._terminal.cols) {
|
||||
this._terminal.x--;
|
||||
if (this._terminal.buffer.x >= this._terminal.cols) {
|
||||
this._terminal.buffer.x--;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -719,14 +738,14 @@ export class InputHandler implements IInputHandler {
|
||||
if (params[0] < 1) params[0] = 1;
|
||||
if (params[1] < 1) params[1] = 1;
|
||||
|
||||
this._terminal.y = params[0] - 1;
|
||||
if (this._terminal.y >= this._terminal.rows) {
|
||||
this._terminal.y = this._terminal.rows - 1;
|
||||
this._terminal.buffer.y = params[0] - 1;
|
||||
if (this._terminal.buffer.y >= this._terminal.rows) {
|
||||
this._terminal.buffer.y = this._terminal.rows - 1;
|
||||
}
|
||||
|
||||
this._terminal.x = params[1] - 1;
|
||||
if (this._terminal.x >= this._terminal.cols) {
|
||||
this._terminal.x = this._terminal.cols - 1;
|
||||
this._terminal.buffer.x = params[1] - 1;
|
||||
if (this._terminal.buffer.x >= this._terminal.cols) {
|
||||
this._terminal.buffer.x = this._terminal.cols - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -741,9 +760,9 @@ export class InputHandler implements IInputHandler {
|
||||
public tabClear(params: number[]): void {
|
||||
let param = params[0];
|
||||
if (param <= 0) {
|
||||
delete this._terminal.tabs[this._terminal.x];
|
||||
delete this._terminal.buffer.tabs[this._terminal.buffer.x];
|
||||
} else if (param === 3) {
|
||||
this._terminal.tabs = {};
|
||||
this._terminal.buffer.tabs = {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -897,7 +916,8 @@ export class InputHandler implements IInputHandler {
|
||||
this._terminal.vt200Mouse = params[0] === 1000;
|
||||
this._terminal.normalMouse = params[0] > 1000;
|
||||
this._terminal.mouseEvents = true;
|
||||
this._terminal.element.style.cursor = 'default';
|
||||
this._terminal.element.classList.add('enable-mouse-events');
|
||||
this._terminal.selectionManager.disable();
|
||||
this._terminal.log('Binding to mouse events.');
|
||||
break;
|
||||
case 1004: // send focusin/focusout events
|
||||
@@ -928,30 +948,14 @@ export class InputHandler implements IInputHandler {
|
||||
this._terminal.cursorHidden = false;
|
||||
break;
|
||||
case 1049: // alt screen buffer cursor
|
||||
// this._terminal.saveCursor();
|
||||
; // FALL-THROUGH
|
||||
// TODO: Not sure if we need to save/restore after switching the buffer
|
||||
// this.saveCursor(params);
|
||||
// FALL-THROUGH
|
||||
case 47: // alt screen buffer
|
||||
case 1047: // alt screen buffer
|
||||
if (!this._terminal.normal) {
|
||||
let normal = {
|
||||
lines: this._terminal.lines,
|
||||
ybase: this._terminal.ybase,
|
||||
ydisp: this._terminal.ydisp,
|
||||
x: this._terminal.x,
|
||||
y: this._terminal.y,
|
||||
scrollTop: this._terminal.scrollTop,
|
||||
scrollBottom: this._terminal.scrollBottom,
|
||||
tabs: this._terminal.tabs
|
||||
// XXX save charset(s) here?
|
||||
// charset: this._terminal.charset,
|
||||
// glevel: this._terminal.glevel,
|
||||
// charsets: this._terminal.charsets
|
||||
};
|
||||
this._terminal.reset();
|
||||
this._terminal.viewport.syncScrollArea();
|
||||
this._terminal.normal = normal;
|
||||
this._terminal.showCursor();
|
||||
}
|
||||
this._terminal.buffers.activateAltBuffer();
|
||||
this._terminal.viewport.syncScrollArea();
|
||||
this._terminal.showCursor();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1090,7 +1094,8 @@ export class InputHandler implements IInputHandler {
|
||||
this._terminal.vt200Mouse = false;
|
||||
this._terminal.normalMouse = false;
|
||||
this._terminal.mouseEvents = false;
|
||||
this._terminal.element.style.cursor = '';
|
||||
this._terminal.element.classList.remove('enable-mouse-events');
|
||||
this._terminal.selectionManager.enable();
|
||||
break;
|
||||
case 1004: // send focusin/focusout events
|
||||
this._terminal.sendFocus = false;
|
||||
@@ -1108,27 +1113,19 @@ export class InputHandler implements IInputHandler {
|
||||
this._terminal.cursorHidden = true;
|
||||
break;
|
||||
case 1049: // alt screen buffer cursor
|
||||
; // FALL-THROUGH
|
||||
// FALL-THROUGH
|
||||
case 47: // normal screen buffer
|
||||
case 1047: // normal screen buffer - clearing it first
|
||||
if (this._terminal.normal) {
|
||||
this._terminal.lines = this._terminal.normal.lines;
|
||||
this._terminal.ybase = this._terminal.normal.ybase;
|
||||
this._terminal.ydisp = this._terminal.normal.ydisp;
|
||||
this._terminal.x = this._terminal.normal.x;
|
||||
this._terminal.y = this._terminal.normal.y;
|
||||
this._terminal.scrollTop = this._terminal.normal.scrollTop;
|
||||
this._terminal.scrollBottom = this._terminal.normal.scrollBottom;
|
||||
this._terminal.tabs = this._terminal.normal.tabs;
|
||||
this._terminal.normal = null;
|
||||
// if (params === 1049) {
|
||||
// this.x = this.savedX;
|
||||
// this.y = this.savedY;
|
||||
// }
|
||||
this._terminal.refresh(0, this._terminal.rows - 1);
|
||||
this._terminal.viewport.syncScrollArea();
|
||||
this._terminal.showCursor();
|
||||
}
|
||||
// Ensure the selection manager has the correct buffer
|
||||
this._terminal.buffers.activateNormalBuffer();
|
||||
// TODO: Not sure if we need to save/restore after switching the buffer
|
||||
// if (params[0] === 1049) {
|
||||
// this.restoreCursor(params);
|
||||
// }
|
||||
this._terminal.selectionManager.setBuffer(this._terminal.buffer.lines);
|
||||
this._terminal.refresh(0, this._terminal.rows - 1);
|
||||
this._terminal.viewport.syncScrollArea();
|
||||
this._terminal.showCursor();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1348,9 +1345,9 @@ export class InputHandler implements IInputHandler {
|
||||
case 6:
|
||||
// cursor position
|
||||
this._terminal.send(C0.ESC + '['
|
||||
+ (this._terminal.y + 1)
|
||||
+ (this._terminal.buffer.y + 1)
|
||||
+ ';'
|
||||
+ (this._terminal.x + 1)
|
||||
+ (this._terminal.buffer.x + 1)
|
||||
+ 'R');
|
||||
break;
|
||||
}
|
||||
@@ -1361,9 +1358,9 @@ export class InputHandler implements IInputHandler {
|
||||
case 6:
|
||||
// cursor position
|
||||
this._terminal.send(C0.ESC + '[?'
|
||||
+ (this._terminal.y + 1)
|
||||
+ (this._terminal.buffer.y + 1)
|
||||
+ ';'
|
||||
+ (this._terminal.x + 1)
|
||||
+ (this._terminal.buffer.x + 1)
|
||||
+ 'R');
|
||||
break;
|
||||
case 15:
|
||||
@@ -1394,14 +1391,14 @@ export class InputHandler implements IInputHandler {
|
||||
this._terminal.cursorHidden = false;
|
||||
this._terminal.insertMode = false;
|
||||
this._terminal.originMode = false;
|
||||
this._terminal.wraparoundMode = false; // autowrap
|
||||
this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false
|
||||
this._terminal.applicationKeypad = false; // ?
|
||||
this._terminal.viewport.syncScrollArea();
|
||||
this._terminal.applicationCursor = false;
|
||||
this._terminal.scrollTop = 0;
|
||||
this._terminal.scrollBottom = this._terminal.rows - 1;
|
||||
this._terminal.buffer.scrollTop = 0;
|
||||
this._terminal.buffer.scrollBottom = this._terminal.rows - 1;
|
||||
this._terminal.curAttr = this._terminal.defAttr;
|
||||
this._terminal.x = this._terminal.y = 0; // ?
|
||||
this._terminal.buffer.x = this._terminal.buffer.y = 0; // ?
|
||||
this._terminal.charset = null;
|
||||
this._terminal.glevel = 0; // ??
|
||||
this._terminal.charsets = [null]; // ??
|
||||
@@ -1445,10 +1442,10 @@ export class InputHandler implements IInputHandler {
|
||||
*/
|
||||
public setScrollRegion(params: number[]): void {
|
||||
if (this._terminal.prefix) return;
|
||||
this._terminal.scrollTop = (params[0] || 1) - 1;
|
||||
this._terminal.scrollBottom = (params[1] && params[1] <= this._terminal.rows ? params[1] : this._terminal.rows) - 1;
|
||||
this._terminal.x = 0;
|
||||
this._terminal.y = 0;
|
||||
this._terminal.buffer.scrollTop = (params[0] || 1) - 1;
|
||||
this._terminal.buffer.scrollBottom = (params[1] && params[1] <= this._terminal.rows ? params[1] : this._terminal.rows) - 1;
|
||||
this._terminal.buffer.x = 0;
|
||||
this._terminal.buffer.y = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1457,8 +1454,8 @@ export class InputHandler implements IInputHandler {
|
||||
* Save cursor (ANSI.SYS).
|
||||
*/
|
||||
public saveCursor(params: number[]): void {
|
||||
this._terminal.savedX = this._terminal.x;
|
||||
this._terminal.savedY = this._terminal.y;
|
||||
this._terminal.buffer.savedX = this._terminal.buffer.x;
|
||||
this._terminal.buffer.savedY = this._terminal.buffer.y;
|
||||
}
|
||||
|
||||
|
||||
@@ -1467,100 +1464,102 @@ export class InputHandler implements IInputHandler {
|
||||
* Restore cursor (ANSI.SYS).
|
||||
*/
|
||||
public restoreCursor(params: number[]): void {
|
||||
this._terminal.x = this._terminal.savedX || 0;
|
||||
this._terminal.y = this._terminal.savedY || 0;
|
||||
this._terminal.buffer.x = this._terminal.buffer.savedX || 0;
|
||||
this._terminal.buffer.y = this._terminal.buffer.savedY || 0;
|
||||
}
|
||||
}
|
||||
|
||||
const wcwidth = (function(opts) {
|
||||
// extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c
|
||||
// combining characters
|
||||
const COMBINING = [
|
||||
[0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489],
|
||||
[0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2],
|
||||
[0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603],
|
||||
[0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670],
|
||||
[0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED],
|
||||
[0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A],
|
||||
[0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902],
|
||||
[0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D],
|
||||
[0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981],
|
||||
[0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD],
|
||||
[0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C],
|
||||
[0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D],
|
||||
[0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC],
|
||||
[0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD],
|
||||
[0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C],
|
||||
[0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D],
|
||||
[0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0],
|
||||
[0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48],
|
||||
[0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC],
|
||||
[0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD],
|
||||
[0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D],
|
||||
[0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6],
|
||||
[0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E],
|
||||
[0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC],
|
||||
[0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35],
|
||||
[0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E],
|
||||
[0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97],
|
||||
[0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030],
|
||||
[0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039],
|
||||
[0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F],
|
||||
[0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753],
|
||||
[0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD],
|
||||
[0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD],
|
||||
[0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922],
|
||||
[0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B],
|
||||
[0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34],
|
||||
[0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42],
|
||||
[0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF],
|
||||
[0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063],
|
||||
[0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],
|
||||
[0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],
|
||||
[0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],
|
||||
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB],
|
||||
[0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],
|
||||
[0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169],
|
||||
[0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD],
|
||||
[0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F],
|
||||
[0xE0100, 0xE01EF]
|
||||
];
|
||||
// binary search
|
||||
function bisearch(ucs) {
|
||||
let min = 0;
|
||||
let max = COMBINING.length - 1;
|
||||
let mid;
|
||||
if (ucs < COMBINING[0][0] || ucs > COMBINING[max][1])
|
||||
export const wcwidth = (function(opts) {
|
||||
// extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c
|
||||
// combining characters
|
||||
const COMBINING_BMP = [
|
||||
[0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489],
|
||||
[0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2],
|
||||
[0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603],
|
||||
[0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670],
|
||||
[0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED],
|
||||
[0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A],
|
||||
[0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902],
|
||||
[0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D],
|
||||
[0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981],
|
||||
[0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD],
|
||||
[0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C],
|
||||
[0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D],
|
||||
[0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC],
|
||||
[0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD],
|
||||
[0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C],
|
||||
[0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D],
|
||||
[0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0],
|
||||
[0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48],
|
||||
[0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC],
|
||||
[0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD],
|
||||
[0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D],
|
||||
[0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6],
|
||||
[0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E],
|
||||
[0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC],
|
||||
[0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35],
|
||||
[0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E],
|
||||
[0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97],
|
||||
[0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030],
|
||||
[0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039],
|
||||
[0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F],
|
||||
[0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753],
|
||||
[0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD],
|
||||
[0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD],
|
||||
[0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922],
|
||||
[0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B],
|
||||
[0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34],
|
||||
[0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42],
|
||||
[0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF],
|
||||
[0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063],
|
||||
[0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],
|
||||
[0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],
|
||||
[0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],
|
||||
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB],
|
||||
];
|
||||
const COMBINING_HIGH = [
|
||||
[0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],
|
||||
[0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169],
|
||||
[0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD],
|
||||
[0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F],
|
||||
[0xE0100, 0xE01EF]
|
||||
];
|
||||
// binary search
|
||||
function bisearch(ucs, data) {
|
||||
let min = 0;
|
||||
let max = data.length - 1;
|
||||
let mid;
|
||||
if (ucs < data[0][0] || ucs > data[max][1])
|
||||
return false;
|
||||
while (max >= min) {
|
||||
mid = (min + max) >> 1;
|
||||
if (ucs > data[mid][1])
|
||||
min = mid + 1;
|
||||
else if (ucs < data[mid][0])
|
||||
max = mid - 1;
|
||||
else
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
while (max >= min) {
|
||||
mid = Math.floor((min + max) / 2);
|
||||
if (ucs > COMBINING[mid][1])
|
||||
min = mid + 1;
|
||||
else if (ucs < COMBINING[mid][0])
|
||||
max = mid - 1;
|
||||
else
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function wcwidth(ucs) {
|
||||
// test for 8-bit control characters
|
||||
if (ucs === 0)
|
||||
return opts.nul;
|
||||
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0))
|
||||
return opts.control;
|
||||
// binary search in table of non-spacing characters
|
||||
if (bisearch(ucs))
|
||||
return 0;
|
||||
// if we arrive here, ucs is not a combining or C0/C1 control character
|
||||
if (isWide(ucs)) {
|
||||
return 2;
|
||||
function wcwidthBMP(ucs) {
|
||||
// test for 8-bit control characters
|
||||
if (ucs === 0)
|
||||
return opts.nul;
|
||||
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0))
|
||||
return opts.control;
|
||||
// binary search in table of non-spacing characters
|
||||
if (bisearch(ucs, COMBINING_BMP))
|
||||
return 0;
|
||||
// if we arrive here, ucs is not a combining or C0/C1 control character
|
||||
if (isWideBMP(ucs)) {
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
function isWide(ucs) {
|
||||
return (
|
||||
ucs >= 0x1100 && (
|
||||
function isWideBMP(ucs) {
|
||||
return (
|
||||
ucs >= 0x1100 && (
|
||||
ucs <= 0x115f || // Hangul Jamo init. consonants
|
||||
ucs === 0x2329 ||
|
||||
ucs === 0x232a ||
|
||||
@@ -1570,9 +1569,59 @@ const wcwidth = (function(opts) {
|
||||
(ucs >= 0xfe10 && ucs <= 0xfe19) || // Vertical forms
|
||||
(ucs >= 0xfe30 && ucs <= 0xfe6f) || // CJK Compat Forms
|
||||
(ucs >= 0xff00 && ucs <= 0xff60) || // Fullwidth Forms
|
||||
(ucs >= 0xffe0 && ucs <= 0xffe6) ||
|
||||
(ucs >= 0x20000 && ucs <= 0x2fffd) ||
|
||||
(ucs >= 0x30000 && ucs <= 0x3fffd)));
|
||||
}
|
||||
return wcwidth;
|
||||
(ucs >= 0xffe0 && ucs <= 0xffe6)));
|
||||
}
|
||||
function wcwidthHigh(ucs) {
|
||||
if (bisearch(ucs, COMBINING_HIGH))
|
||||
return 0;
|
||||
if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) {
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
const control = opts.control | 0;
|
||||
let table = null;
|
||||
function init_table() {
|
||||
// lookup table for BMP
|
||||
const CODEPOINTS = 65536; // BMP holds 65536 codepoints
|
||||
const BITWIDTH = 2; // a codepoint can have a width of 0, 1 or 2
|
||||
const ITEMSIZE = 32; // using uint32_t
|
||||
const CONTAINERSIZE = CODEPOINTS * BITWIDTH / ITEMSIZE;
|
||||
const CODEPOINTS_PER_ITEM = ITEMSIZE / BITWIDTH;
|
||||
table = (typeof Uint32Array === 'undefined')
|
||||
? new Array(CONTAINERSIZE)
|
||||
: new Uint32Array(CONTAINERSIZE);
|
||||
for (let i = 0; i < CONTAINERSIZE; ++i) {
|
||||
let num = 0;
|
||||
let pos = CODEPOINTS_PER_ITEM;
|
||||
while (pos--)
|
||||
num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos);
|
||||
table[i] = num;
|
||||
}
|
||||
return table;
|
||||
}
|
||||
// get width from lookup table
|
||||
// position in container : num / CODEPOINTS_PER_ITEM
|
||||
// ==> n = table[Math.floor(num / 16)]
|
||||
// ==> n = table[num >> 4]
|
||||
// 16 codepoints per number: FFEEDDCCBBAA99887766554433221100
|
||||
// position in number : (num % CODEPOINTS_PER_ITEM) * BITWIDTH
|
||||
// ==> m = (n % 16) * 2
|
||||
// ==> m = (num & 15) << 1
|
||||
// right shift to position m
|
||||
// ==> n = n >> m e.g. m=12 000000000000FFEEDDCCBBAA99887766
|
||||
// we are only interested in 2 LSBs, cut off higher bits
|
||||
// ==> n = n & 3 e.g. 000000000000000000000000000000XX
|
||||
return function (num) {
|
||||
num = num | 0; // get asm.js like optimization under V8
|
||||
if (num < 32)
|
||||
return control | 0;
|
||||
if (num < 127)
|
||||
return 1;
|
||||
let t = table || init_table();
|
||||
if (num < 65536)
|
||||
return t[num >> 4] >> ((num & 15) << 1) & 3;
|
||||
// do a full search for high codepoints
|
||||
return wcwidthHigh(num);
|
||||
};
|
||||
})({nul: 0, control: 0}); // configurable options
|
||||
|
||||
+77
-7
@@ -2,6 +2,9 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { LinkMatcherOptions } from './Interfaces';
|
||||
import { LinkMatcherHandler, LinkMatcherValidationCallback } from './Types';
|
||||
|
||||
export interface IBrowser {
|
||||
isNode: boolean;
|
||||
userAgent: string;
|
||||
@@ -17,10 +20,10 @@ export interface IBrowser {
|
||||
export interface ITerminal {
|
||||
element: HTMLElement;
|
||||
rowContainer: HTMLElement;
|
||||
selectionContainer: HTMLElement;
|
||||
selectionManager: ISelectionManager;
|
||||
charMeasure: ICharMeasure;
|
||||
textarea: HTMLTextAreaElement;
|
||||
ybase: number;
|
||||
ydisp: number;
|
||||
lines: ICircularList<string>;
|
||||
rows: number;
|
||||
cols: number;
|
||||
browser: IBrowser;
|
||||
@@ -28,9 +31,10 @@ export interface ITerminal {
|
||||
children: HTMLElement[];
|
||||
cursorHidden: boolean;
|
||||
cursorState: number;
|
||||
x: number;
|
||||
y: number;
|
||||
defAttr: number;
|
||||
scrollback: number;
|
||||
buffers: IBufferSet;
|
||||
buffer: IBuffer;
|
||||
|
||||
/**
|
||||
* Emit the 'data' event and populate the given data.
|
||||
@@ -42,13 +46,55 @@ export interface ITerminal {
|
||||
cancel(ev: Event, force?: boolean);
|
||||
log(text: string): void;
|
||||
emit(event: string, data: any);
|
||||
reset(): void;
|
||||
showCursor(): void;
|
||||
blankLine(cur?: boolean, isWrapped?: boolean, cols?: number);
|
||||
}
|
||||
|
||||
interface ICircularList<T> {
|
||||
export interface IBuffer {
|
||||
lines: ICircularList<[number, string, number][]>;
|
||||
ydisp: number;
|
||||
ybase: number;
|
||||
y: number;
|
||||
x: number;
|
||||
tabs: any;
|
||||
}
|
||||
|
||||
export interface IBufferSet {
|
||||
alt: IBuffer;
|
||||
normal: IBuffer;
|
||||
active: IBuffer;
|
||||
|
||||
activateNormalBuffer(): void;
|
||||
activateAltBuffer(): void;
|
||||
}
|
||||
|
||||
export interface ISelectionManager {
|
||||
selectionText: string;
|
||||
selectionStart: [number, number];
|
||||
selectionEnd: [number, number];
|
||||
|
||||
setSelection(row: number, col: number, length: number);
|
||||
}
|
||||
|
||||
export interface ICharMeasure {
|
||||
width: number;
|
||||
height: number;
|
||||
measure(): void;
|
||||
}
|
||||
|
||||
export interface ILinkifier {
|
||||
linkifyRow(rowIndex: number): void;
|
||||
attachHypertextLinkHandler(handler: LinkMatcherHandler): void;
|
||||
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: LinkMatcherOptions): number;
|
||||
deregisterLinkMatcher(matcherId: number): boolean;
|
||||
}
|
||||
|
||||
export interface ICircularList<T> extends IEventEmitter {
|
||||
length: number;
|
||||
maxLength: number;
|
||||
forEach: (callbackfn: (value: T, index: number) => void) => void;
|
||||
|
||||
forEach(callbackfn: (value: T, index: number, array: T[]) => void): void;
|
||||
get(index: number): T;
|
||||
set(index: number, value: T): void;
|
||||
push(value: T): void;
|
||||
@@ -58,6 +104,30 @@ interface ICircularList<T> {
|
||||
shiftElements(start: number, count: number, offset: number): void;
|
||||
}
|
||||
|
||||
export interface IEventEmitter {
|
||||
on(type, listener): void;
|
||||
off(type, listener): void;
|
||||
}
|
||||
|
||||
export interface LinkMatcherOptions {
|
||||
/**
|
||||
* The index of the link from the regex.match(text) call. This defaults to 0
|
||||
* (for regular expressions without capture groups).
|
||||
*/
|
||||
matchIndex?: number;
|
||||
/**
|
||||
* A callback that validates an individual link, returning true if valid and
|
||||
* false if invalid.
|
||||
*/
|
||||
validationCallback?: LinkMatcherValidationCallback;
|
||||
/**
|
||||
* The priority of the link matcher, this defines the order in which the link
|
||||
* matcher is evaluated relative to others, from highest to lowest. The
|
||||
* default value is 0.
|
||||
*/
|
||||
priority?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles actions generated by the parser.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* @license MIT
|
||||
*/
|
||||
import jsdom = require('jsdom');
|
||||
import { assert } from 'chai';
|
||||
import { ITerminal, ILinkifier } from './Interfaces';
|
||||
import { Linkifier } from './Linkifier';
|
||||
import { LinkMatcher } from './Types';
|
||||
|
||||
class TestLinkifier extends Linkifier {
|
||||
constructor() {
|
||||
Linkifier.TIME_BEFORE_LINKIFY = 0;
|
||||
super();
|
||||
}
|
||||
|
||||
public get linkMatchers(): LinkMatcher[] { return this._linkMatchers; }
|
||||
}
|
||||
|
||||
describe('Linkifier', () => {
|
||||
let dom: jsdom.JSDOM;
|
||||
let window: Window;
|
||||
let document: Document;
|
||||
|
||||
let container: HTMLElement;
|
||||
let rows: HTMLElement[];
|
||||
let linkifier: TestLinkifier;
|
||||
|
||||
beforeEach(() => {
|
||||
dom = new jsdom.JSDOM('');
|
||||
window = dom.window;
|
||||
document = window.document;
|
||||
linkifier = new TestLinkifier();
|
||||
});
|
||||
|
||||
function addRow(html: string) {
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = html;
|
||||
container.appendChild(element);
|
||||
rows.push(element);
|
||||
}
|
||||
|
||||
describe('before attachToDom', () => {
|
||||
it('should allow link matcher registration', done => {
|
||||
assert.doesNotThrow(() => {
|
||||
const linkMatcherId = linkifier.registerLinkMatcher(/foo/, () => {});
|
||||
assert.isTrue(linkifier.deregisterLinkMatcher(linkMatcherId));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('after attachToDom', () => {
|
||||
beforeEach(() => {
|
||||
rows = [];
|
||||
linkifier.attachToDom(document, rows);
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
function clickElement(element: Node) {
|
||||
const event = document.createEvent('MouseEvent');
|
||||
event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
|
||||
element.dispatchEvent(event);
|
||||
}
|
||||
|
||||
function assertLinkifiesEntireRow(uri: string, done: MochaDone) {
|
||||
addRow(uri);
|
||||
linkifier.linkifyRow(0);
|
||||
setTimeout(() => {
|
||||
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
|
||||
assert.equal((<HTMLElement>rows[0].firstChild).textContent, uri);
|
||||
done();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
describe('http links', () => {
|
||||
function assertLinkifiesEntireRow(uri: string, done: MochaDone) {
|
||||
addRow(uri);
|
||||
linkifier.linkifyRow(0);
|
||||
setTimeout(() => {
|
||||
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
|
||||
assert.equal((<HTMLElement>rows[0].firstChild).textContent, uri);
|
||||
done();
|
||||
}, 0);
|
||||
}
|
||||
it('should allow ~ character in URI path', done => assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done));
|
||||
});
|
||||
|
||||
describe('link matcher', () => {
|
||||
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, expectedHtml: string, done: MochaDone) {
|
||||
addRow(rowText);
|
||||
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
|
||||
linkifier.linkifyRow(0);
|
||||
// Allow linkify to happen
|
||||
setTimeout(() => {
|
||||
assert.equal(rows[0].innerHTML, expectedHtml);
|
||||
done();
|
||||
}, 0);
|
||||
}
|
||||
it('should match a single link', done => {
|
||||
assertLinkifiesRow('foo', /foo/, '<a>foo</a>', done);
|
||||
});
|
||||
it('should match a single link at the start of a text node', done => {
|
||||
assertLinkifiesRow('foo bar', /foo/, '<a>foo</a> bar', done);
|
||||
});
|
||||
it('should match a single link in the middle of a text node', done => {
|
||||
assertLinkifiesRow('foo bar baz', /bar/, 'foo <a>bar</a> baz', done);
|
||||
});
|
||||
it('should match a single link at the end of a text node', done => {
|
||||
assertLinkifiesRow('foo bar', /bar/, 'foo <a>bar</a>', done);
|
||||
});
|
||||
it('should match a link after a link at the start of a text node', done => {
|
||||
assertLinkifiesRow('foo bar', /foo|bar/, '<a>foo</a> <a>bar</a>', done);
|
||||
});
|
||||
it('should match a link after a link in the middle of a text node', done => {
|
||||
assertLinkifiesRow('foo bar baz', /bar|baz/, 'foo <a>bar</a> <a>baz</a>', done);
|
||||
});
|
||||
it('should match a link immediately after a link at the end of a text node', done => {
|
||||
assertLinkifiesRow('<span>foo bar</span>baz', /bar|baz/, '<span>foo <a>bar</a></span><a>baz</a>', done);
|
||||
});
|
||||
it('should not duplicate text after a unicode character (wrapped in a span)', done => {
|
||||
// This is a regression test for an issue that came about when using
|
||||
// an oh-my-zsh theme that added the large blue diamond unicode
|
||||
// character (U+1F537) which caused the path to be duplicated. See #642.
|
||||
assertLinkifiesRow('echo \'<span class="xterm-normal-char">🔷</span>foo\'', /foo/, 'echo \'<span class="xterm-normal-char">🔷</span><a>foo</a>\'', done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validationCallback', () => {
|
||||
it('should enable link if true', done => {
|
||||
addRow('test');
|
||||
linkifier.registerLinkMatcher(/test/, () => done(), {
|
||||
validationCallback: (url, element, cb) => {
|
||||
cb(true);
|
||||
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
|
||||
setTimeout(() => clickElement(rows[0].firstChild), 0);
|
||||
}
|
||||
});
|
||||
linkifier.linkifyRow(0);
|
||||
});
|
||||
|
||||
it('should disable link if false', done => {
|
||||
addRow('test');
|
||||
linkifier.registerLinkMatcher(/test/, () => assert.fail(), {
|
||||
validationCallback: (url, element, cb) => {
|
||||
cb(false);
|
||||
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
|
||||
setTimeout(() => clickElement(rows[0].firstChild), 0);
|
||||
}
|
||||
});
|
||||
linkifier.linkifyRow(0);
|
||||
// Allow time for the click to be performed
|
||||
setTimeout(() => done(), 10);
|
||||
});
|
||||
|
||||
it('should trigger for multiple link matches on one row', done => {
|
||||
addRow('test test');
|
||||
let count = 0;
|
||||
linkifier.registerLinkMatcher(/test/, () => assert.fail(), {
|
||||
validationCallback: (url, element, cb) => {
|
||||
count += 1;
|
||||
if (count === 2) {
|
||||
done();
|
||||
}
|
||||
cb(false);
|
||||
}
|
||||
});
|
||||
linkifier.linkifyRow(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('priority', () => {
|
||||
it('should order the list from highest priority to lowest #1', () => {
|
||||
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 1 });
|
||||
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: -1 });
|
||||
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, 0, bId]);
|
||||
});
|
||||
|
||||
it('should order the list from highest priority to lowest #2', () => {
|
||||
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: -1 });
|
||||
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 1 });
|
||||
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, 0, aId]);
|
||||
});
|
||||
|
||||
it('should order items of equal priority in the order they are added', () => {
|
||||
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 0 });
|
||||
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 0 });
|
||||
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [0, aId, bId]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,363 @@
|
||||
/**
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { LinkMatcherOptions } from './Interfaces';
|
||||
import { LinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback } from './Types';
|
||||
|
||||
const INVALID_LINK_CLASS = 'xterm-invalid-link';
|
||||
|
||||
const protocolClause = '(https?:\\/\\/)';
|
||||
const domainCharacterSet = '[\\da-z\\.-]+';
|
||||
const negatedDomainCharacterSet = '[^\\da-z\\.-]+';
|
||||
const domainBodyClause = '(' + domainCharacterSet + ')';
|
||||
const tldClause = '([a-z\\.]{2,6})';
|
||||
const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
|
||||
const localHostClause = '(localhost)';
|
||||
const portClause = '(:\\d{1,5})';
|
||||
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
|
||||
const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*';
|
||||
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
|
||||
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
|
||||
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
|
||||
const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
|
||||
const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;
|
||||
const start = '(?:^|' + negatedDomainCharacterSet + ')(';
|
||||
const end = ')($|' + negatedPathCharacterSet + ')';
|
||||
const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
|
||||
|
||||
/**
|
||||
* The ID of the built in http(s) link matcher.
|
||||
*/
|
||||
const HYPERTEXT_LINK_MATCHER_ID = 0;
|
||||
|
||||
/**
|
||||
* The Linkifier applies links to rows shortly after they have been refreshed.
|
||||
*/
|
||||
export class Linkifier {
|
||||
/**
|
||||
* The time to wait after a row is changed before it is linkified. This prevents
|
||||
* the costly operation of searching every row multiple times, potentially a
|
||||
* huge amount of times.
|
||||
*/
|
||||
protected static TIME_BEFORE_LINKIFY = 200;
|
||||
|
||||
protected _linkMatchers: LinkMatcher[];
|
||||
|
||||
private _document: Document;
|
||||
private _rows: HTMLElement[];
|
||||
private _rowTimeoutIds: number[];
|
||||
private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID;
|
||||
|
||||
constructor() {
|
||||
this._rowTimeoutIds = [];
|
||||
this._linkMatchers = [];
|
||||
this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the linkifier to the DOM, enabling linkification.
|
||||
* @param document The document object.
|
||||
* @param rows The array of rows to apply links to.
|
||||
*/
|
||||
public attachToDom(document: Document, rows: HTMLElement[]) {
|
||||
this._document = document;
|
||||
this._rows = rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a row for linkification.
|
||||
* @param {number} rowIndex The index of the row to linkify.
|
||||
*/
|
||||
public linkifyRow(rowIndex: number): void {
|
||||
// Don't attempt linkify if not yet attached to DOM
|
||||
if (!this._document) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeoutId = this._rowTimeoutIds[rowIndex];
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
this._rowTimeoutIds[rowIndex] = setTimeout(this._linkifyRow.bind(this, rowIndex), Linkifier.TIME_BEFORE_LINKIFY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches a handler for hypertext links, overriding default <a> behavior
|
||||
* for standard http(s) links.
|
||||
* @param {LinkHandler} handler The handler to use, this can be cleared with
|
||||
* null.
|
||||
*/
|
||||
public setHypertextLinkHandler(handler: LinkMatcherHandler): void {
|
||||
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].handler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches a validation callback for hypertext links.
|
||||
* @param {LinkMatcherValidationCallback} callback The callback to use, this
|
||||
* can be cleared with null.
|
||||
*/
|
||||
public setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void {
|
||||
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].validationCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a link matcher, allowing custom link patterns to be matched and
|
||||
* handled.
|
||||
* @param {RegExp} regex The regular expression to search for, specifically
|
||||
* this searches the textContent of the rows. You will want to use \s to match
|
||||
* a space ' ' character for example.
|
||||
* @param {LinkHandler} handler The callback when the link is called.
|
||||
* @param {LinkMatcherOptions} [options] Options for the link matcher.
|
||||
* @return {number} The ID of the new matcher, this can be used to deregister.
|
||||
*/
|
||||
public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options: LinkMatcherOptions = {}): number {
|
||||
if (this._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) {
|
||||
throw new Error('handler must be defined');
|
||||
}
|
||||
const matcher: LinkMatcher = {
|
||||
id: this._nextLinkMatcherId++,
|
||||
regex,
|
||||
handler,
|
||||
matchIndex: options.matchIndex,
|
||||
validationCallback: options.validationCallback,
|
||||
priority: options.priority || 0
|
||||
};
|
||||
this._addLinkMatcherToList(matcher);
|
||||
return matcher.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a link matcher to the list in the correct position based on the
|
||||
* priority of each link matcher. New link matchers of equal priority are
|
||||
* considered after older link matchers.
|
||||
* @param matcher The link matcher to be added.
|
||||
*/
|
||||
private _addLinkMatcherToList(matcher: LinkMatcher): void {
|
||||
if (this._linkMatchers.length === 0) {
|
||||
this._linkMatchers.push(matcher);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = this._linkMatchers.length - 1; i >= 0; i--) {
|
||||
if (matcher.priority <= this._linkMatchers[i].priority) {
|
||||
this._linkMatchers.splice(i + 1, 0, matcher);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._linkMatchers.splice(0, 0, matcher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deregisters a link matcher if it has been registered.
|
||||
* @param {number} matcherId The link matcher's ID (returned after register)
|
||||
* @return {boolean} Whether a link matcher was found and deregistered.
|
||||
*/
|
||||
public deregisterLinkMatcher(matcherId: number): boolean {
|
||||
// ID 0 is the hypertext link matcher which cannot be deregistered
|
||||
for (let i = 1; i < this._linkMatchers.length; i++) {
|
||||
if (this._linkMatchers[i].id === matcherId) {
|
||||
this._linkMatchers.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Linkifies a row.
|
||||
* @param {number} rowIndex The index of the row to linkify.
|
||||
*/
|
||||
private _linkifyRow(rowIndex: number): void {
|
||||
const row = this._rows[rowIndex];
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
const text = row.textContent;
|
||||
for (let i = 0; i < this._linkMatchers.length; i++) {
|
||||
const matcher = this._linkMatchers[i];
|
||||
const linkElements = this._doLinkifyRow(row, matcher);
|
||||
if (linkElements.length > 0) {
|
||||
// Fire validation callback
|
||||
if (matcher.validationCallback) {
|
||||
for (let j = 0; j < linkElements.length; j++) {
|
||||
const element = linkElements[j];
|
||||
matcher.validationCallback(element.textContent, element, isValid => {
|
||||
if (!isValid) {
|
||||
element.classList.add(INVALID_LINK_CLASS);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// Only allow a single LinkMatcher to trigger on any given row.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Linkifies a row given a specific handler.
|
||||
* @param {HTMLElement} row The row to linkify.
|
||||
* @param {LinkMatcher} matcher The link matcher for this line.
|
||||
* @return The link element(s) that were added.
|
||||
*/
|
||||
private _doLinkifyRow(row: HTMLElement, matcher: LinkMatcher): HTMLElement[] {
|
||||
// Iterate over nodes as we want to consider text nodes
|
||||
let result = [];
|
||||
const isHttpLinkMatcher = matcher.id === HYPERTEXT_LINK_MATCHER_ID;
|
||||
const nodes = row.childNodes;
|
||||
|
||||
// Find the first match
|
||||
let match = row.textContent.match(matcher.regex);
|
||||
if (!match || match.length === 0) {
|
||||
return result;
|
||||
}
|
||||
let uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
|
||||
// Set the next searches start index
|
||||
let rowStartIndex = match.index + uri.length;
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
const searchIndex = node.textContent.indexOf(uri);
|
||||
if (searchIndex >= 0) {
|
||||
const linkElement = this._createAnchorElement(uri, matcher.handler, isHttpLinkMatcher);
|
||||
if (node.textContent.length === uri.length) {
|
||||
// Matches entire string
|
||||
if (node.nodeType === 3 /*Node.TEXT_NODE*/) {
|
||||
this._replaceNode(node, linkElement);
|
||||
} else {
|
||||
const element = (<HTMLElement>node);
|
||||
if (element.nodeName === 'A') {
|
||||
// This row has already been linkified
|
||||
return result;
|
||||
}
|
||||
element.innerHTML = '';
|
||||
element.appendChild(linkElement);
|
||||
}
|
||||
} else if (node.childNodes.length > 1) {
|
||||
// Matches part of string in an element with multiple child nodes
|
||||
for (let j = 0; j < node.childNodes.length; j++) {
|
||||
const childNode = node.childNodes[j];
|
||||
const childSearchIndex = childNode.textContent.indexOf(uri);
|
||||
if (childSearchIndex !== -1) {
|
||||
// Match found in currentNode
|
||||
this._replaceNodeSubstringWithNode(childNode, linkElement, uri, childSearchIndex);
|
||||
// Don't need to count nodesAdded by replacing the node as this
|
||||
// is a child node, not a top-level node.
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Matches part of string in a single text node
|
||||
const nodesAdded = this._replaceNodeSubstringWithNode(node, linkElement, uri, searchIndex);
|
||||
// No need to consider the new nodes
|
||||
i += nodesAdded;
|
||||
}
|
||||
result.push(linkElement);
|
||||
|
||||
// Find the next match
|
||||
match = row.textContent.substring(rowStartIndex).match(matcher.regex);
|
||||
if (!match || match.length === 0) {
|
||||
return result;
|
||||
}
|
||||
uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
|
||||
rowStartIndex += match.index + uri.length;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a link anchor element.
|
||||
* @param {string} uri The uri of the link.
|
||||
* @return {HTMLAnchorElement} The link.
|
||||
*/
|
||||
private _createAnchorElement(uri: string, handler: LinkMatcherHandler, isHypertextLinkHandler: boolean): HTMLAnchorElement {
|
||||
const element = this._document.createElement('a');
|
||||
element.textContent = uri;
|
||||
element.draggable = false;
|
||||
if (isHypertextLinkHandler) {
|
||||
element.href = uri;
|
||||
// Force link on another tab so work is not lost
|
||||
element.target = '_blank';
|
||||
element.addEventListener('click', (event: MouseEvent) => {
|
||||
if (handler) {
|
||||
return handler(event, uri);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
element.addEventListener('click', (event: MouseEvent) => {
|
||||
// Don't execute the handler if the link is flagged as invalid
|
||||
if (element.classList.contains(INVALID_LINK_CLASS)) {
|
||||
return;
|
||||
}
|
||||
return handler(event, uri);
|
||||
});
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a node with 1 or more other nodes.
|
||||
* @param {Node} oldNode The node to replace.
|
||||
* @param {Node[]} newNodes The new nodes to insert in order.
|
||||
*/
|
||||
private _replaceNode(oldNode: Node, ...newNodes: Node[]): void {
|
||||
const parent = oldNode.parentNode;
|
||||
for (let i = 0; i < newNodes.length; i++) {
|
||||
parent.insertBefore(newNodes[i], oldNode);
|
||||
}
|
||||
parent.removeChild(oldNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a substring within a node with a new node.
|
||||
* @param {Node} targetNode The target node; either a text node or a <span>
|
||||
* containing a single text node.
|
||||
* @param {Node} newNode The new node to insert.
|
||||
* @param {string} substring The substring to replace.
|
||||
* @param {number} substringIndex The index of the substring within the string.
|
||||
* @return The number of nodes to skip when searching for the next uri.
|
||||
*/
|
||||
private _replaceNodeSubstringWithNode(targetNode: Node, newNode: Node, substring: string, substringIndex: number): number {
|
||||
// If the targetNode is a non-text node with a single child, make the child
|
||||
// the new targetNode.
|
||||
if (targetNode.childNodes.length === 1) {
|
||||
targetNode = targetNode.childNodes[0];
|
||||
}
|
||||
|
||||
// The targetNode will be either a text node or a <span>. The text node
|
||||
// (targetNode or its only-child) needs to be replaced with newNode plus new
|
||||
// text nodes potentially on either side.
|
||||
if (targetNode.nodeType !== 3/*Node.TEXT_NODE*/) {
|
||||
throw new Error('targetNode must be a text node or only contain a single text node');
|
||||
}
|
||||
|
||||
const fullText = targetNode.textContent;
|
||||
|
||||
if (substringIndex === 0) {
|
||||
// Replace with <newNode><textnode>
|
||||
const rightText = fullText.substring(substring.length);
|
||||
const rightTextNode = this._document.createTextNode(rightText);
|
||||
this._replaceNode(targetNode, newNode, rightTextNode);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (substringIndex === targetNode.textContent.length - substring.length) {
|
||||
// Replace with <textnode><newNode>
|
||||
const leftText = fullText.substring(0, substringIndex);
|
||||
const leftTextNode = this._document.createTextNode(leftText);
|
||||
this._replaceNode(targetNode, leftTextNode, newNode);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Replace with <textnode><newNode><textnode>
|
||||
const leftText = fullText.substring(0, substringIndex);
|
||||
const leftTextNode = this._document.createTextNode(leftText);
|
||||
const rightText = fullText.substring(substringIndex + substring.length);
|
||||
const rightTextNode = this._document.createTextNode(rightText);
|
||||
this._replaceNode(targetNode, leftTextNode, newNode, rightTextNode);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
+18
-9
@@ -52,7 +52,7 @@ escapedStateHandler['c'] = (parser, terminal) => {
|
||||
};
|
||||
escapedStateHandler['E'] = (parser, terminal) => {
|
||||
// ESC E Next Line ( NEL is 0x85).
|
||||
terminal.x = 0;
|
||||
terminal.buffer.x = 0;
|
||||
terminal.index();
|
||||
parser.setState(ParserState.NORMAL);
|
||||
};
|
||||
@@ -181,9 +181,13 @@ export class Parser {
|
||||
*
|
||||
* @param data The data to parse.
|
||||
*/
|
||||
public parse(data: string) {
|
||||
public parse(data: string): ParserState {
|
||||
let l = data.length, j, cs, ch, code, low;
|
||||
|
||||
if (this._terminal.debug) {
|
||||
this._terminal.log('data: ' + data);
|
||||
}
|
||||
|
||||
this._position = 0;
|
||||
// apply leftover surrogate high from last write
|
||||
if (this._terminal.surrogate_high) {
|
||||
@@ -458,6 +462,9 @@ export class Parser {
|
||||
|
||||
case ParserState.CSI:
|
||||
if (ch in csiStateHandler) {
|
||||
if (this._terminal.debug) {
|
||||
this._terminal.log(`CSI ${this._terminal.prefix ? this._terminal.prefix : ''} ${this._terminal.params ? this._terminal.params.join(';') : ''} ${this._terminal.postfix ? this._terminal.postfix : ''} ${ch}`);
|
||||
}
|
||||
csiStateHandler[ch](this._inputHandler, this._terminal.params, this._terminal.prefix, this._terminal.postfix, this);
|
||||
} else {
|
||||
this._terminal.error('Unknown CSI code: %s.', ch);
|
||||
@@ -471,6 +478,8 @@ export class Parser {
|
||||
case ParserState.DCS:
|
||||
if (ch === C0.ESC || ch === C0.BEL) {
|
||||
if (ch === C0.ESC) this._position++;
|
||||
let pt;
|
||||
let valid: boolean;
|
||||
|
||||
switch (this._terminal.prefix) {
|
||||
// User-Defined Keys (DECUDK).
|
||||
@@ -480,8 +489,8 @@ export class Parser {
|
||||
// Request Status String (DECRQSS).
|
||||
// test: echo -e '\eP$q"p\e\\'
|
||||
case '$q':
|
||||
let pt = this._terminal.currentParam
|
||||
, valid = false;
|
||||
pt = this._terminal.currentParam;
|
||||
valid = false;
|
||||
|
||||
switch (pt) {
|
||||
// DECSCA
|
||||
@@ -497,9 +506,9 @@ export class Parser {
|
||||
// DECSTBM
|
||||
case 'r':
|
||||
pt = ''
|
||||
+ (this._terminal.scrollTop + 1)
|
||||
+ (this._terminal.buffer.scrollTop + 1)
|
||||
+ ';'
|
||||
+ (this._terminal.scrollBottom + 1)
|
||||
+ (this._terminal.buffer.scrollBottom + 1)
|
||||
+ 'r';
|
||||
break;
|
||||
|
||||
@@ -526,9 +535,8 @@ export class Parser {
|
||||
// This can cause a small glitch in vim.
|
||||
// test: echo -ne '\eP+q6b64\e\\'
|
||||
case '+q':
|
||||
// TODO: Don't declare pt twice
|
||||
/*let*/ pt = this._terminal.currentParam
|
||||
, valid = false;
|
||||
pt = this._terminal.currentParam;
|
||||
valid = false;
|
||||
|
||||
this._terminal.send(C0.ESC + 'P' + +valid + '+r' + pt + C0.ESC + '\\');
|
||||
break;
|
||||
@@ -563,6 +571,7 @@ export class Parser {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return this._state;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+231
-135
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import { ITerminal } from './Interfaces';
|
||||
import { DomElementObjectPool } from './utils/DomElementObjectPool';
|
||||
|
||||
/**
|
||||
* The maximum number of refresh frames to skip when the write buffer is non-
|
||||
@@ -30,12 +31,15 @@ export class Renderer {
|
||||
private _refreshFramesSkipped = 0;
|
||||
private _refreshAnimationFrame = null;
|
||||
|
||||
private _spanElementObjectPool = new DomElementObjectPool('span');
|
||||
|
||||
constructor(private _terminal: ITerminal) {
|
||||
// Figure out whether boldness affects
|
||||
// the character width of monospace fonts.
|
||||
if (brokenBold === null) {
|
||||
brokenBold = checkBoldBroken((<any>this._terminal).document);
|
||||
brokenBold = checkBoldBroken((<any>this._terminal).element);
|
||||
}
|
||||
this._spanElementObjectPool = new DomElementObjectPool('span');
|
||||
|
||||
// TODO: Pull more DOM interactions into Renderer.constructor, element for
|
||||
// example should be owned by Renderer (and also exposed by Terminal due to
|
||||
@@ -117,9 +121,8 @@ export class Renderer {
|
||||
* @param {number} end The row to end at (between fromRow and terminal's height terminal - 1)
|
||||
*/
|
||||
private _refresh(start: number, end: number): void {
|
||||
let x, y, i, line, out, ch, ch_width, width, data, attr, bg, fg, flags, row, parent, focused = document.activeElement;
|
||||
|
||||
// If this is a big refresh, remove the terminal rows from the DOM for faster calculations
|
||||
let parent;
|
||||
if (end - start >= this._terminal.rows / 2) {
|
||||
parent = this._terminal.element.parentNode;
|
||||
if (parent) {
|
||||
@@ -127,8 +130,8 @@ export class Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
width = this._terminal.cols;
|
||||
y = start;
|
||||
let width = this._terminal.cols;
|
||||
let y = start;
|
||||
|
||||
if (end >= this._terminal.rows) {
|
||||
this._terminal.log('`end` is too large. Most likely a bad CSR.');
|
||||
@@ -136,148 +139,180 @@ export class Renderer {
|
||||
}
|
||||
|
||||
for (; y <= end; y++) {
|
||||
row = y + this._terminal.ydisp;
|
||||
let row = y + this._terminal.buffer.ydisp;
|
||||
|
||||
line = this._terminal.lines.get(row);
|
||||
if (!line || !this._terminal.children[y]) {
|
||||
// Continue if the line is not available, this means a resize is currently in progress
|
||||
continue;
|
||||
}
|
||||
out = '';
|
||||
let line = this._terminal.buffer.lines.get(row);
|
||||
|
||||
if (this._terminal.y === y - (this._terminal.ybase - this._terminal.ydisp)
|
||||
&& this._terminal.cursorState
|
||||
&& !this._terminal.cursorHidden) {
|
||||
x = this._terminal.x;
|
||||
let x;
|
||||
if (this._terminal.buffer.y === y - (this._terminal.buffer.ybase - this._terminal.buffer.ydisp) &&
|
||||
this._terminal.cursorState &&
|
||||
!this._terminal.cursorHidden) {
|
||||
x = this._terminal.buffer.x;
|
||||
} else {
|
||||
x = -1;
|
||||
}
|
||||
|
||||
attr = this._terminal.defAttr;
|
||||
i = 0;
|
||||
let attr = this._terminal.defAttr;
|
||||
|
||||
for (; i < width; i++) {
|
||||
if (!line[i]) {
|
||||
// Continue if the character is not available, this means a resize is currently in progress
|
||||
const documentFragment = document.createDocumentFragment();
|
||||
let innerHTML = '';
|
||||
let currentElement;
|
||||
|
||||
// Return the row's spans to the pool
|
||||
while (this._terminal.children[y].children.length) {
|
||||
const child = this._terminal.children[y].children[0];
|
||||
this._terminal.children[y].removeChild(child);
|
||||
this._spanElementObjectPool.release(<HTMLElement>child);
|
||||
}
|
||||
|
||||
for (let i = 0; i < width; i++) {
|
||||
// TODO: Could data be a more specific type?
|
||||
let data: any = line[i][0];
|
||||
const ch = line[i][1];
|
||||
const ch_width: any = line[i][2];
|
||||
const isCursor: boolean = i === x;
|
||||
if (!ch_width) {
|
||||
continue;
|
||||
}
|
||||
data = line[i][0];
|
||||
ch = line[i][1];
|
||||
ch_width = line[i][2];
|
||||
if (!ch_width)
|
||||
continue;
|
||||
|
||||
if (i === x) data = -1;
|
||||
|
||||
if (data !== attr) {
|
||||
if (attr !== this._terminal.defAttr) {
|
||||
out += '</span>';
|
||||
}
|
||||
if (data !== this._terminal.defAttr) {
|
||||
if (data === -1) {
|
||||
out += '<span class="reverse-video terminal-cursor">';
|
||||
} else {
|
||||
let classNames = [];
|
||||
|
||||
bg = data & 0x1ff;
|
||||
fg = (data >> 9) & 0x1ff;
|
||||
flags = data >> 18;
|
||||
|
||||
if (flags & FLAGS.BOLD) {
|
||||
if (!brokenBold) {
|
||||
classNames.push('xterm-bold');
|
||||
}
|
||||
// See: XTerm*boldColors
|
||||
if (fg < 8) fg += 8;
|
||||
}
|
||||
|
||||
if (flags & FLAGS.UNDERLINE) {
|
||||
classNames.push('xterm-underline');
|
||||
}
|
||||
|
||||
if (flags & FLAGS.BLINK) {
|
||||
classNames.push('xterm-blink');
|
||||
}
|
||||
|
||||
// If inverse flag is on, then swap the foreground and background variables.
|
||||
if (flags & FLAGS.INVERSE) {
|
||||
/* One-line variable swap in JavaScript: http://stackoverflow.com/a/16201730 */
|
||||
bg = [fg, fg = bg][0];
|
||||
// Should inverse just be before the
|
||||
// above boldColors effect instead?
|
||||
if ((flags & 1) && fg < 8) fg += 8;
|
||||
}
|
||||
|
||||
if (flags & FLAGS.INVISIBLE) {
|
||||
classNames.push('xterm-hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Weird situation: Invert flag used black foreground and white background results
|
||||
* in invalid background color, positioned at the 256 index of the 256 terminal
|
||||
* color map. Pin the colors manually in such a case.
|
||||
*
|
||||
* Source: https://github.com/sourcelair/xterm.js/issues/57
|
||||
*/
|
||||
if (flags & FLAGS.INVERSE) {
|
||||
if (bg === 257) {
|
||||
bg = 15;
|
||||
}
|
||||
if (fg === 256) {
|
||||
fg = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (bg < 256) {
|
||||
classNames.push('xterm-bg-color-' + bg);
|
||||
}
|
||||
|
||||
if (fg < 256) {
|
||||
classNames.push('xterm-color-' + fg);
|
||||
}
|
||||
|
||||
out += '<span';
|
||||
if (classNames.length) {
|
||||
out += ' class="' + classNames.join(' ') + '"';
|
||||
}
|
||||
out += '>';
|
||||
if (data !== attr || isCursor) {
|
||||
if (attr !== this._terminal.defAttr && !isCursor) {
|
||||
if (innerHTML) {
|
||||
currentElement.innerHTML = innerHTML;
|
||||
innerHTML = '';
|
||||
}
|
||||
documentFragment.appendChild(currentElement);
|
||||
currentElement = null;
|
||||
}
|
||||
if (data !== this._terminal.defAttr || isCursor) {
|
||||
if (innerHTML && !currentElement) {
|
||||
currentElement = this._spanElementObjectPool.acquire();
|
||||
}
|
||||
if (currentElement) {
|
||||
if (innerHTML) {
|
||||
currentElement.innerHTML = innerHTML;
|
||||
innerHTML = '';
|
||||
}
|
||||
documentFragment.appendChild(currentElement);
|
||||
}
|
||||
currentElement = this._spanElementObjectPool.acquire();
|
||||
|
||||
let bg = data & 0x1ff;
|
||||
let fg = (data >> 9) & 0x1ff;
|
||||
let flags = data >> 18;
|
||||
|
||||
if (isCursor) {
|
||||
currentElement.classList.add('reverse-video');
|
||||
currentElement.classList.add('terminal-cursor');
|
||||
}
|
||||
|
||||
if (flags & FLAGS.BOLD) {
|
||||
if (!brokenBold) {
|
||||
currentElement.classList.add('xterm-bold');
|
||||
}
|
||||
// See: XTerm*boldColors
|
||||
if (fg < 8) {
|
||||
fg += 8;
|
||||
}
|
||||
}
|
||||
|
||||
if (flags & FLAGS.UNDERLINE) {
|
||||
currentElement.classList.add('xterm-underline');
|
||||
}
|
||||
|
||||
if (flags & FLAGS.BLINK) {
|
||||
currentElement.classList.add('xterm-blink');
|
||||
}
|
||||
|
||||
// If inverse flag is on, then swap the foreground and background variables.
|
||||
if (flags & FLAGS.INVERSE) {
|
||||
let temp = bg;
|
||||
bg = fg;
|
||||
fg = temp;
|
||||
// Should inverse just be before the above boldColors effect instead?
|
||||
if ((flags & 1) && fg < 8) {
|
||||
fg += 8;
|
||||
}
|
||||
}
|
||||
|
||||
if (flags & FLAGS.INVISIBLE && !isCursor) {
|
||||
currentElement.classList.add('xterm-hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Weird situation: Invert flag used black foreground and white background results
|
||||
* in invalid background color, positioned at the 256 index of the 256 terminal
|
||||
* color map. Pin the colors manually in such a case.
|
||||
*
|
||||
* Source: https://github.com/sourcelair/xterm.js/issues/57
|
||||
*/
|
||||
if (flags & FLAGS.INVERSE) {
|
||||
if (bg === 257) {
|
||||
bg = 15;
|
||||
}
|
||||
if (fg === 256) {
|
||||
fg = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (bg < 256) {
|
||||
currentElement.classList.add(`xterm-bg-color-${bg}`);
|
||||
}
|
||||
|
||||
if (fg < 256) {
|
||||
currentElement.classList.add(`xterm-color-${fg}`);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (ch_width === 2) {
|
||||
out += '<span class="xterm-wide-char">';
|
||||
}
|
||||
switch (ch) {
|
||||
case '&':
|
||||
out += '&';
|
||||
break;
|
||||
case '<':
|
||||
out += '<';
|
||||
break;
|
||||
case '>':
|
||||
out += '>';
|
||||
break;
|
||||
default:
|
||||
if (ch <= ' ') {
|
||||
out += ' ';
|
||||
} else {
|
||||
out += ch;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (ch_width === 2) {
|
||||
out += '</span>';
|
||||
// Wrap wide characters so they're sized correctly. It's more difficult to release these
|
||||
// from the object pool so just create new ones via innerHTML.
|
||||
innerHTML += `<span class="xterm-wide-char">${ch}</span>`;
|
||||
} else if (ch.charCodeAt(0) > 255) {
|
||||
// Wrap any non-wide unicode character as some fonts size them badly
|
||||
innerHTML += `<span class="xterm-normal-char">${ch}</span>`;
|
||||
} else {
|
||||
switch (ch) {
|
||||
case '&':
|
||||
innerHTML += '&';
|
||||
break;
|
||||
case '<':
|
||||
innerHTML += '<';
|
||||
break;
|
||||
case '>':
|
||||
innerHTML += '>';
|
||||
break;
|
||||
default:
|
||||
if (ch <= ' ') {
|
||||
innerHTML += ' ';
|
||||
} else {
|
||||
innerHTML += ch;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
attr = data;
|
||||
// The cursor needs its own element, therefore we set attr to -1
|
||||
// which will cause the next character to be rendered in a new element
|
||||
attr = isCursor ? -1 : data;
|
||||
|
||||
}
|
||||
|
||||
if (attr !== this._terminal.defAttr) {
|
||||
out += '</span>';
|
||||
if (innerHTML && !currentElement) {
|
||||
currentElement = this._spanElementObjectPool.acquire();
|
||||
}
|
||||
if (currentElement) {
|
||||
if (innerHTML) {
|
||||
currentElement.innerHTML = innerHTML;
|
||||
innerHTML = '';
|
||||
}
|
||||
documentFragment.appendChild(currentElement);
|
||||
currentElement = null;
|
||||
}
|
||||
|
||||
this._terminal.children[y].innerHTML = out;
|
||||
this._terminal.children[y].appendChild(documentFragment);
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
@@ -286,19 +321,80 @@ export class Renderer {
|
||||
|
||||
this._terminal.emit('refresh', {element: this._terminal.element, start: start, end: end});
|
||||
};
|
||||
|
||||
/**
|
||||
* Refreshes the selection in the DOM.
|
||||
* @param start The selection start.
|
||||
* @param end The selection end.
|
||||
*/
|
||||
public refreshSelection(start: [number, number], end: [number, number]) {
|
||||
// Remove all selections
|
||||
while (this._terminal.selectionContainer.children.length) {
|
||||
this._terminal.selectionContainer.removeChild(this._terminal.selectionContainer.children[0]);
|
||||
}
|
||||
|
||||
// Selection does not exist
|
||||
if (!start || !end) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Translate from buffer position to viewport position
|
||||
const viewportStartRow = start[1] - this._terminal.buffer.ydisp;
|
||||
const viewportEndRow = end[1] - this._terminal.buffer.ydisp;
|
||||
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
|
||||
const viewportCappedEndRow = Math.min(viewportEndRow, this._terminal.rows - 1);
|
||||
|
||||
// No need to draw the selection
|
||||
if (viewportCappedStartRow >= this._terminal.rows || viewportCappedEndRow < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the selections
|
||||
const documentFragment = document.createDocumentFragment();
|
||||
// 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
|
||||
const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1;
|
||||
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._terminal.cols, middleRowsCount));
|
||||
// Draw final row
|
||||
if (viewportCappedStartRow !== viewportCappedEndRow) {
|
||||
// Only draw viewportEndRow if it's not the same as viewporttartRow
|
||||
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
|
||||
documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol));
|
||||
}
|
||||
this._terminal.selectionContainer.appendChild(documentFragment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a selection element at the specified position.
|
||||
* @param row The row of the selection.
|
||||
* @param colStart The start column.
|
||||
* @param colEnd The end columns.
|
||||
*/
|
||||
private _createSelectionElement(row: number, colStart: number, colEnd: number, rowCount: number = 1): HTMLElement {
|
||||
const element = document.createElement('div');
|
||||
element.style.height = `${rowCount * this._terminal.charMeasure.height}px`;
|
||||
element.style.top = `${row * this._terminal.charMeasure.height}px`;
|
||||
element.style.left = `${colStart * this._terminal.charMeasure.width}px`;
|
||||
element.style.width = `${this._terminal.charMeasure.width * (colEnd - colStart)}px`;
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// if bold is broken, we can't
|
||||
// use it in the terminal.
|
||||
function checkBoldBroken(document) {
|
||||
const body = document.getElementsByTagName('body')[0];
|
||||
// If bold is broken, we can't use it in the terminal.
|
||||
function checkBoldBroken(terminal) {
|
||||
const document = terminal.ownerDocument;
|
||||
const el = document.createElement('span');
|
||||
el.innerHTML = 'hello world';
|
||||
body.appendChild(el);
|
||||
const w1 = el.scrollWidth;
|
||||
terminal.appendChild(el);
|
||||
const w1 = el.offsetWidth;
|
||||
const h1 = el.offsetHeight;
|
||||
el.style.fontWeight = 'bold';
|
||||
const w2 = el.scrollWidth;
|
||||
body.removeChild(el);
|
||||
return w1 !== w2;
|
||||
const w2 = el.offsetWidth;
|
||||
const h2 = el.offsetHeight;
|
||||
terminal.removeChild(el);
|
||||
return w1 !== w2 || h1 !== h2;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* @license MIT
|
||||
*/
|
||||
import jsdom = require('jsdom');
|
||||
import { assert } from 'chai';
|
||||
import { ITerminal, ICircularList } from './Interfaces';
|
||||
import { CharMeasure } from './utils/CharMeasure';
|
||||
import { CircularList } from './utils/CircularList';
|
||||
import { SelectionManager } from './SelectionManager';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
import { BufferSet } from './BufferSet';
|
||||
import { MockTerminal } from './utils/TestUtils';
|
||||
|
||||
class TestSelectionManager extends SelectionManager {
|
||||
constructor(
|
||||
terminal: ITerminal,
|
||||
buffer: ICircularList<[number, string, number][]>,
|
||||
rowContainer: HTMLElement,
|
||||
charMeasure: CharMeasure
|
||||
) {
|
||||
super(terminal, buffer, rowContainer, charMeasure);
|
||||
}
|
||||
|
||||
public get model(): SelectionModel { return this._model; }
|
||||
|
||||
public selectLineAt(line: number): void { this._selectLineAt(line); }
|
||||
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords); }
|
||||
|
||||
// Disable DOM interaction
|
||||
public enable(): void {}
|
||||
public disable(): void {}
|
||||
public refresh(): void {}
|
||||
}
|
||||
|
||||
describe('SelectionManager', () => {
|
||||
let dom: jsdom.JSDOM;
|
||||
let window: Window;
|
||||
let document: Document;
|
||||
|
||||
let terminal: ITerminal;
|
||||
let bufferLines: ICircularList<[number, string, number][]>;
|
||||
let rowContainer: HTMLElement;
|
||||
let selectionManager: TestSelectionManager;
|
||||
|
||||
beforeEach(() => {
|
||||
dom = new jsdom.JSDOM('');
|
||||
window = dom.window;
|
||||
document = window.document;
|
||||
rowContainer = document.createElement('div');
|
||||
terminal = new MockTerminal();
|
||||
terminal.cols = 80;
|
||||
terminal.rows = 2;
|
||||
terminal.scrollback = 100;
|
||||
terminal.buffers = new BufferSet(terminal);
|
||||
terminal.buffer = terminal.buffers.active;
|
||||
bufferLines = terminal.buffer.lines;
|
||||
selectionManager = new TestSelectionManager(terminal, bufferLines, rowContainer, null);
|
||||
});
|
||||
|
||||
function stringToRow(text: string): [number, string, number][] {
|
||||
let result: [number, string, number][] = [];
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result.push([0, text.charAt(i), 1]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
describe('_selectWordAt', () => {
|
||||
it('should expand selection for normal width chars', () => {
|
||||
bufferLines.set(0, stringToRow('foo bar'));
|
||||
selectionManager.selectWordAt([0, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'foo');
|
||||
selectionManager.selectWordAt([1, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'foo');
|
||||
selectionManager.selectWordAt([2, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'foo');
|
||||
selectionManager.selectWordAt([3, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
selectionManager.selectWordAt([4, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'bar');
|
||||
selectionManager.selectWordAt([5, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'bar');
|
||||
selectionManager.selectWordAt([6, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'bar');
|
||||
});
|
||||
it('should expand selection for whitespace', () => {
|
||||
bufferLines.set(0, stringToRow('a b'));
|
||||
selectionManager.selectWordAt([0, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'a');
|
||||
selectionManager.selectWordAt([1, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
selectionManager.selectWordAt([2, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
selectionManager.selectWordAt([3, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
selectionManager.selectWordAt([4, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'b');
|
||||
});
|
||||
it('should expand selection for wide characters', () => {
|
||||
// Wide characters use a special format
|
||||
bufferLines.set(0, [
|
||||
[null, '中', 2],
|
||||
[null, '', 0],
|
||||
[null, '文', 2],
|
||||
[null, '', 0],
|
||||
[null, ' ', 1],
|
||||
[null, 'a', 1],
|
||||
[null, '中', 2],
|
||||
[null, '', 0],
|
||||
[null, '文', 2],
|
||||
[null, '', 0],
|
||||
[null, 'b', 1],
|
||||
[null, ' ', 1],
|
||||
[null, 'f', 1],
|
||||
[null, 'o', 1],
|
||||
[null, 'o', 1]
|
||||
]);
|
||||
// Ensure wide characters take up 2 columns
|
||||
selectionManager.selectWordAt([0, 0]);
|
||||
assert.equal(selectionManager.selectionText, '中文');
|
||||
selectionManager.selectWordAt([1, 0]);
|
||||
assert.equal(selectionManager.selectionText, '中文');
|
||||
selectionManager.selectWordAt([2, 0]);
|
||||
assert.equal(selectionManager.selectionText, '中文');
|
||||
selectionManager.selectWordAt([3, 0]);
|
||||
assert.equal(selectionManager.selectionText, '中文');
|
||||
selectionManager.selectWordAt([4, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
// Ensure wide characters work when wrapped in normal width characters
|
||||
selectionManager.selectWordAt([5, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'a中文b');
|
||||
selectionManager.selectWordAt([6, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'a中文b');
|
||||
selectionManager.selectWordAt([7, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'a中文b');
|
||||
selectionManager.selectWordAt([8, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'a中文b');
|
||||
selectionManager.selectWordAt([9, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'a中文b');
|
||||
selectionManager.selectWordAt([10, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'a中文b');
|
||||
selectionManager.selectWordAt([11, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
// Ensure normal width characters work fine in a line containing wide characters
|
||||
selectionManager.selectWordAt([12, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'foo');
|
||||
selectionManager.selectWordAt([13, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'foo');
|
||||
selectionManager.selectWordAt([14, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'foo');
|
||||
});
|
||||
it('should select up to non-path characters that are commonly adjacent to paths', () => {
|
||||
bufferLines.set(0, stringToRow('(cd)[ef]{gh}\'ij"'));
|
||||
selectionManager.selectWordAt([0, 0]);
|
||||
assert.equal(selectionManager.selectionText, '(cd');
|
||||
selectionManager.selectWordAt([1, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'cd');
|
||||
selectionManager.selectWordAt([2, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'cd');
|
||||
selectionManager.selectWordAt([3, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'cd)');
|
||||
selectionManager.selectWordAt([4, 0]);
|
||||
assert.equal(selectionManager.selectionText, '[ef');
|
||||
selectionManager.selectWordAt([5, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ef');
|
||||
selectionManager.selectWordAt([6, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ef');
|
||||
selectionManager.selectWordAt([7, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ef]');
|
||||
selectionManager.selectWordAt([8, 0]);
|
||||
assert.equal(selectionManager.selectionText, '{gh');
|
||||
selectionManager.selectWordAt([9, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'gh');
|
||||
selectionManager.selectWordAt([10, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'gh');
|
||||
selectionManager.selectWordAt([11, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'gh}');
|
||||
selectionManager.selectWordAt([12, 0]);
|
||||
assert.equal(selectionManager.selectionText, '\'ij');
|
||||
selectionManager.selectWordAt([13, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ij');
|
||||
selectionManager.selectWordAt([14, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ij');
|
||||
selectionManager.selectWordAt([15, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ij"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('_selectLineAt', () => {
|
||||
it('should select the entire line', () => {
|
||||
bufferLines.set(0, stringToRow('foo bar'));
|
||||
selectionManager.selectLineAt(0);
|
||||
assert.equal(selectionManager.selectionText, 'foo bar', 'The selected text is correct');
|
||||
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]);
|
||||
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 0], 'The actual selection spans the entire column');
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectAll', () => {
|
||||
it('should select the entire buffer, beyond the viewport', () => {
|
||||
bufferLines.length = 5;
|
||||
bufferLines.set(0, stringToRow('1'));
|
||||
bufferLines.set(1, stringToRow('2'));
|
||||
bufferLines.set(2, stringToRow('3'));
|
||||
bufferLines.set(3, stringToRow('4'));
|
||||
bufferLines.set(4, stringToRow('5'));
|
||||
selectionManager.selectAll();
|
||||
terminal.buffer.ybase = bufferLines.length - terminal.rows;
|
||||
assert.equal(selectionManager.selectionText, '1\n2\n3\n4\n5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasSelection', () => {
|
||||
it('should return whether there is a selection', () => {
|
||||
selectionManager.model.selectionStart = [0, 0];
|
||||
selectionManager.model.selectionStartLength = 0;
|
||||
assert.equal(selectionManager.hasSelection, false);
|
||||
selectionManager.model.selectionEnd = [0, 0];
|
||||
assert.equal(selectionManager.hasSelection, false);
|
||||
selectionManager.model.selectionEnd = [1, 0];
|
||||
assert.equal(selectionManager.hasSelection, true);
|
||||
selectionManager.model.selectionEnd = [0, 1];
|
||||
assert.equal(selectionManager.hasSelection, true);
|
||||
selectionManager.model.selectionEnd = [1, 1];
|
||||
assert.equal(selectionManager.hasSelection, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,671 @@
|
||||
/**
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import * as Mouse from './utils/Mouse';
|
||||
import * as Browser from './utils/Browser';
|
||||
import { CharMeasure } from './utils/CharMeasure';
|
||||
import { CircularList } from './utils/CircularList';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import { ITerminal, ICircularList } from './Interfaces';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
import { translateBufferLineToString } from './utils/BufferLine';
|
||||
|
||||
/**
|
||||
* The number of pixels the mouse needs to be above or below the viewport in
|
||||
* order to scroll at the maximum speed.
|
||||
*/
|
||||
const DRAG_SCROLL_MAX_THRESHOLD = 50;
|
||||
|
||||
/**
|
||||
* The maximum scrolling speed
|
||||
*/
|
||||
const DRAG_SCROLL_MAX_SPEED = 15;
|
||||
|
||||
/**
|
||||
* The number of milliseconds between drag scroll updates.
|
||||
*/
|
||||
const DRAG_SCROLL_INTERVAL = 50;
|
||||
|
||||
/**
|
||||
* A string containing all characters that are considered word separated by the
|
||||
* double click to select work logic.
|
||||
*/
|
||||
const WORD_SEPARATORS = ' ()[]{}\'"';
|
||||
|
||||
// TODO: Move these constants elsewhere, they belong in a buffer or buffer
|
||||
// data/line class.
|
||||
const LINE_DATA_CHAR_INDEX = 1;
|
||||
const LINE_DATA_WIDTH_INDEX = 2;
|
||||
|
||||
const NON_BREAKING_SPACE_CHAR = String.fromCharCode(160);
|
||||
const ALL_NON_BREAKING_SPACE_REGEX = new RegExp(NON_BREAKING_SPACE_CHAR, 'g');
|
||||
|
||||
/**
|
||||
* Represents a position of a word on a line.
|
||||
*/
|
||||
interface IWordPosition {
|
||||
start: number;
|
||||
length: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A selection mode, this drives how the selection behaves on mouse move.
|
||||
*/
|
||||
enum SelectionMode {
|
||||
NORMAL,
|
||||
WORD,
|
||||
LINE
|
||||
}
|
||||
|
||||
/**
|
||||
* A class that manages the selection of the terminal. With help from
|
||||
* SelectionModel, SelectionManager handles with all logic associated with
|
||||
* dealing with the selection, including handling mouse interaction, wide
|
||||
* characters and fetching the actual text within the selection. Rendering is
|
||||
* not handled by the SelectionManager but a 'refresh' event is fired when the
|
||||
* selection is ready to be redrawn.
|
||||
*/
|
||||
export class SelectionManager extends EventEmitter {
|
||||
protected _model: SelectionModel;
|
||||
|
||||
/**
|
||||
* The amount to scroll every drag scroll update (depends on how far the mouse
|
||||
* drag is above or below the terminal).
|
||||
*/
|
||||
private _dragScrollAmount: number;
|
||||
|
||||
/**
|
||||
* The current selection mode.
|
||||
*/
|
||||
private _activeSelectionMode: SelectionMode;
|
||||
|
||||
/**
|
||||
* A setInterval timer that is active while the mouse is down whose callback
|
||||
* scrolls the viewport when necessary.
|
||||
*/
|
||||
private _dragScrollIntervalTimer: NodeJS.Timer;
|
||||
|
||||
/**
|
||||
* The animation frame ID used for refreshing the selection.
|
||||
*/
|
||||
private _refreshAnimationFrame: number;
|
||||
|
||||
/**
|
||||
* Whether selection is enabled.
|
||||
*/
|
||||
private _enabled = true;
|
||||
|
||||
private _mouseMoveListener: EventListener;
|
||||
private _mouseUpListener: EventListener;
|
||||
|
||||
constructor(
|
||||
private _terminal: ITerminal,
|
||||
private _buffer: ICircularList<[number, string, number][]>,
|
||||
private _rowContainer: HTMLElement,
|
||||
private _charMeasure: CharMeasure
|
||||
) {
|
||||
super();
|
||||
this._initListeners();
|
||||
this.enable();
|
||||
|
||||
this._model = new SelectionModel(_terminal);
|
||||
this._activeSelectionMode = SelectionMode.NORMAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes listener variables.
|
||||
*/
|
||||
private _initListeners() {
|
||||
this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event);
|
||||
this._mouseUpListener = event => this._onMouseUp(<MouseEvent>event);
|
||||
|
||||
this._rowContainer.addEventListener('mousedown', event => this._onMouseDown(<MouseEvent>event));
|
||||
|
||||
// Only adjust the selection on trim, shiftElements is rarely used (only in
|
||||
// reverseIndex) and delete in a splice is only ever used when the same
|
||||
// number of elements was just added. Given this is could actually be
|
||||
// beneficial to leave the selection as is for these cases.
|
||||
this._buffer.on('trim', (amount: number) => this._onTrim(amount));
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the selection manager. This is useful for when terminal mouse
|
||||
* are enabled.
|
||||
*/
|
||||
public disable() {
|
||||
this.clearSelection();
|
||||
this._enabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the selection manager.
|
||||
*/
|
||||
public enable() {
|
||||
this._enabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the active buffer, this should be called when the alt buffer is
|
||||
* switched in or out.
|
||||
* @param buffer The active buffer.
|
||||
*/
|
||||
public setBuffer(buffer: ICircularList<[number, string, number][]>): void {
|
||||
this._buffer = buffer;
|
||||
this.clearSelection();
|
||||
}
|
||||
|
||||
public get selectionStart(): [number, number] { return this._model.finalSelectionStart; }
|
||||
public get selectionEnd(): [number, number] { return this._model.finalSelectionEnd; }
|
||||
|
||||
/**
|
||||
* Gets whether there is an active text selection.
|
||||
*/
|
||||
public get hasSelection(): boolean {
|
||||
const start = this._model.finalSelectionStart;
|
||||
const end = this._model.finalSelectionEnd;
|
||||
if (!start || !end) {
|
||||
return false;
|
||||
}
|
||||
return start[0] !== end[0] || start[1] !== end[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the text currently selected.
|
||||
*/
|
||||
public get selectionText(): string {
|
||||
const start = this._model.finalSelectionStart;
|
||||
const end = this._model.finalSelectionEnd;
|
||||
if (!start || !end) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Get first row
|
||||
const startRowEndCol = start[1] === end[1] ? end[0] : null;
|
||||
let result: string[] = [];
|
||||
result.push(translateBufferLineToString(this._buffer.get(start[1]), true, start[0], startRowEndCol));
|
||||
|
||||
// Get middle rows
|
||||
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
|
||||
const bufferLine = this._buffer.get(i);
|
||||
const lineText = translateBufferLineToString(bufferLine, true);
|
||||
if ((<any>bufferLine).isWrapped) {
|
||||
result[result.length - 1] += lineText;
|
||||
} else {
|
||||
result.push(lineText);
|
||||
}
|
||||
}
|
||||
|
||||
// Get final row
|
||||
if (start[1] !== end[1]) {
|
||||
const bufferLine = this._buffer.get(end[1]);
|
||||
const lineText = translateBufferLineToString(bufferLine, true, 0, end[0]);
|
||||
if ((<any>bufferLine).isWrapped) {
|
||||
result[result.length - 1] += lineText;
|
||||
} else {
|
||||
result.push(lineText);
|
||||
}
|
||||
}
|
||||
|
||||
// Format string by replacing non-breaking space chars with regular spaces
|
||||
// and joining the array into a multi-line string.
|
||||
const formattedResult = result.map(line => {
|
||||
return line.replace(ALL_NON_BREAKING_SPACE_REGEX, ' ');
|
||||
}).join(Browser.isMSWindows ? '\r\n' : '\n');
|
||||
|
||||
return formattedResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the current terminal selection.
|
||||
*/
|
||||
public clearSelection(): void {
|
||||
this._model.clearSelection();
|
||||
this._removeMouseDownListeners();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a refresh, redrawing the selection on the next opportunity.
|
||||
* @param isNewSelection Whether the selection should be registered as a new
|
||||
* selection on Linux.
|
||||
*/
|
||||
public refresh(isNewSelection?: boolean): void {
|
||||
// Queue the refresh for the renderer
|
||||
if (!this._refreshAnimationFrame) {
|
||||
this._refreshAnimationFrame = window.requestAnimationFrame(() => this._refresh());
|
||||
}
|
||||
|
||||
// If the platform is Linux and the refresh call comes from a mouse event,
|
||||
// we need to update the selection for middle click to paste selection.
|
||||
if (Browser.isLinux && isNewSelection) {
|
||||
const selectionText = this.selectionText;
|
||||
if (selectionText.length) {
|
||||
this.emit('newselection', this.selectionText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires the refresh event, causing consumers to pick it up and redraw the
|
||||
* selection state.
|
||||
*/
|
||||
private _refresh(): void {
|
||||
this._refreshAnimationFrame = null;
|
||||
this.emit('refresh', { start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd });
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects all text within the terminal.
|
||||
*/
|
||||
public selectAll(): void {
|
||||
this._model.isSelectAllActive = true;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the buffer being trimmed, adjust the selection position.
|
||||
* @param amount The amount the buffer is being trimmed.
|
||||
*/
|
||||
private _onTrim(amount: number) {
|
||||
const needsRefresh = this._model.onTrim(amount);
|
||||
if (needsRefresh) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the 0-based [x, y] buffer coordinates of the current mouse event.
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _getMouseBufferCoords(event: MouseEvent): [number, number] {
|
||||
const coords = Mouse.getCoords(event, this._rowContainer, this._charMeasure, this._terminal.cols, this._terminal.rows, true);
|
||||
if (!coords) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert to 0-based
|
||||
coords[0]--;
|
||||
coords[1]--;
|
||||
// Convert viewport coords to buffer coords
|
||||
coords[1] += this._terminal.buffer.ydisp;
|
||||
return coords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the amount the viewport should be scrolled based on how far out of the
|
||||
* terminal the mouse is.
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _getMouseEventScrollAmount(event: MouseEvent): number {
|
||||
let offset = Mouse.getCoordsRelativeToElement(event, this._rowContainer)[1];
|
||||
const terminalHeight = this._terminal.rows * this._charMeasure.height;
|
||||
if (offset >= 0 && offset <= terminalHeight) {
|
||||
return 0;
|
||||
}
|
||||
if (offset > terminalHeight) {
|
||||
offset -= terminalHeight;
|
||||
}
|
||||
|
||||
offset = Math.min(Math.max(offset, -DRAG_SCROLL_MAX_THRESHOLD), DRAG_SCROLL_MAX_THRESHOLD);
|
||||
offset /= DRAG_SCROLL_MAX_THRESHOLD;
|
||||
return (offset / Math.abs(offset)) + Math.round(offset * (DRAG_SCROLL_MAX_SPEED - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles te mousedown event, setting up for a new selection.
|
||||
* @param event The mousedown event.
|
||||
*/
|
||||
private _onMouseDown(event: MouseEvent) {
|
||||
// If we have selection, we want the context menu on right click even if the
|
||||
// terminal is in mouse mode.
|
||||
if (event.button === 2 && this.hasSelection) {
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Only action the primary button
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow selection when using a specific modifier key, even when disabled
|
||||
if (!this._enabled) {
|
||||
const shouldForceSelection = Browser.isMac && event.altKey;
|
||||
|
||||
if (!shouldForceSelection) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't send the mouse down event to the current process, we want to select
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
// Tell the browser not to start a regular selection
|
||||
event.preventDefault();
|
||||
|
||||
// Reset drag scroll state
|
||||
this._dragScrollAmount = 0;
|
||||
|
||||
if (this._enabled && event.shiftKey) {
|
||||
this._onIncrementalClick(event);
|
||||
} else {
|
||||
if (event.detail === 1) {
|
||||
this._onSingleClick(event);
|
||||
} else if (event.detail === 2) {
|
||||
this._onDoubleClick(event);
|
||||
} else if (event.detail === 3) {
|
||||
this._onTripleClick(event);
|
||||
}
|
||||
}
|
||||
|
||||
this._addMouseDownListeners();
|
||||
this.refresh(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds listeners when mousedown is triggered.
|
||||
*/
|
||||
private _addMouseDownListeners(): void {
|
||||
// Listen on the document so that dragging outside of viewport works
|
||||
this._rowContainer.ownerDocument.addEventListener('mousemove', this._mouseMoveListener);
|
||||
this._rowContainer.ownerDocument.addEventListener('mouseup', this._mouseUpListener);
|
||||
this._dragScrollIntervalTimer = setInterval(() => this._dragScroll(), DRAG_SCROLL_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listeners that are registered when mousedown is triggered.
|
||||
*/
|
||||
private _removeMouseDownListeners(): void {
|
||||
this._rowContainer.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
|
||||
this._rowContainer.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
|
||||
clearInterval(this._dragScrollIntervalTimer);
|
||||
this._dragScrollIntervalTimer = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an incremental click, setting the selection end position to the mouse
|
||||
* position.
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _onIncrementalClick(event: MouseEvent): void {
|
||||
if (this._model.selectionStart) {
|
||||
this._model.selectionEnd = this._getMouseBufferCoords(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a single click, resetting relevant state and setting the selection
|
||||
* start position.
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _onSingleClick(event: MouseEvent): void {
|
||||
this._model.selectionStartLength = 0;
|
||||
this._model.isSelectAllActive = false;
|
||||
this._activeSelectionMode = SelectionMode.NORMAL;
|
||||
|
||||
// Initialize the new selection
|
||||
this._model.selectionStart = this._getMouseBufferCoords(event);
|
||||
if (!this._model.selectionStart) {
|
||||
return;
|
||||
}
|
||||
this._model.selectionEnd = null;
|
||||
|
||||
// Ensure the line exists
|
||||
const line = this._buffer.get(this._model.selectionStart[1]);
|
||||
if (!line) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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]];
|
||||
if (char[LINE_DATA_WIDTH_INDEX] === 0) {
|
||||
this._model.selectionStart[0]++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a double click, selecting the current work.
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _onDoubleClick(event: MouseEvent): void {
|
||||
const coords = this._getMouseBufferCoords(event);
|
||||
if (coords) {
|
||||
this._activeSelectionMode = SelectionMode.WORD;
|
||||
this._selectWordAt(coords);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a triple click, selecting the current line and activating line
|
||||
* select mode.
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _onTripleClick(event: MouseEvent): void {
|
||||
const coords = this._getMouseBufferCoords(event);
|
||||
if (coords) {
|
||||
this._activeSelectionMode = SelectionMode.LINE;
|
||||
this._selectLineAt(coords[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the mousemove event when the mouse button is down, recording the
|
||||
* end of the selection and refreshing the selection.
|
||||
* @param event The mousemove event.
|
||||
*/
|
||||
private _onMouseMove(event: MouseEvent) {
|
||||
// Record the previous position so we know whether to redraw the selection
|
||||
// at the end.
|
||||
const previousSelectionEnd = this._model.selectionEnd ? [this._model.selectionEnd[0], this._model.selectionEnd[1]] : null;
|
||||
|
||||
// Set the initial selection end based on the mouse coordinates
|
||||
this._model.selectionEnd = this._getMouseBufferCoords(event);
|
||||
if (!this._model.selectionEnd) {
|
||||
this.refresh(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Select the entire line if line select mode is active.
|
||||
if (this._activeSelectionMode === SelectionMode.LINE) {
|
||||
if (this._model.selectionEnd[1] < this._model.selectionStart[1]) {
|
||||
this._model.selectionEnd[0] = 0;
|
||||
} else {
|
||||
this._model.selectionEnd[0] = this._terminal.cols;
|
||||
}
|
||||
} else if (this._activeSelectionMode === SelectionMode.WORD) {
|
||||
this._selectToWordAt(this._model.selectionEnd);
|
||||
}
|
||||
|
||||
// Determine the amount of scrolling that will happen.
|
||||
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 - 1;
|
||||
} 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.length) {
|
||||
const char = this._buffer.get(this._model.selectionEnd[1])[this._model.selectionEnd[0]];
|
||||
if (char && char[2] === 0) {
|
||||
this._model.selectionEnd[0]++;
|
||||
}
|
||||
}
|
||||
|
||||
// Only draw here if the selection changes.
|
||||
if (!previousSelectionEnd ||
|
||||
previousSelectionEnd[0] !== this._model.selectionEnd[0] ||
|
||||
previousSelectionEnd[1] !== this._model.selectionEnd[1]) {
|
||||
this.refresh(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The callback that occurs every DRAG_SCROLL_INTERVAL ms that does the
|
||||
* scrolling of the viewport.
|
||||
*/
|
||||
private _dragScroll() {
|
||||
if (this._dragScrollAmount) {
|
||||
this._terminal.scrollDisp(this._dragScrollAmount, false);
|
||||
// Re-evaluate selection
|
||||
if (this._dragScrollAmount > 0) {
|
||||
this._model.selectionEnd = [this._terminal.cols - 1, this._terminal.buffer.ydisp + this._terminal.rows];
|
||||
} else {
|
||||
this._model.selectionEnd = [0, this._terminal.buffer.ydisp];
|
||||
}
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the mouseup event, removing the mousedown listeners.
|
||||
* @param event The mouseup event.
|
||||
*/
|
||||
private _onMouseUp(event: MouseEvent) {
|
||||
this._removeMouseDownListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a viewport column to the character index on the buffer line, the
|
||||
* latter takes into account wide characters.
|
||||
* @param coords The coordinates to find the 2 index for.
|
||||
*/
|
||||
private _convertViewportColToCharacterIndex(bufferLine: any, coords: [number, number]): number {
|
||||
let charIndex = coords[0];
|
||||
for (let i = 0; coords[0] >= i; i++) {
|
||||
const char = bufferLine[i];
|
||||
if (char[LINE_DATA_WIDTH_INDEX] === 0) {
|
||||
charIndex--;
|
||||
}
|
||||
}
|
||||
return charIndex;
|
||||
}
|
||||
|
||||
public setSelection(col: number, row: number, length: number): void {
|
||||
this._model.clearSelection();
|
||||
this._removeMouseDownListeners();
|
||||
this._model.selectionStart = [col, row];
|
||||
this._model.selectionStartLength = length;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets positional information for the word at the coordinated specified.
|
||||
* @param coords The coordinates to get the word at.
|
||||
*/
|
||||
private _getWordAt(coords: [number, number]): IWordPosition {
|
||||
const bufferLine = this._buffer.get(coords[1]);
|
||||
if (!bufferLine) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const line = translateBufferLineToString(bufferLine, false);
|
||||
|
||||
// Get actual index, taking into consideration wide characters
|
||||
let endIndex = this._convertViewportColToCharacterIndex(bufferLine, coords);
|
||||
let startIndex = endIndex;
|
||||
|
||||
// Record offset to be used later
|
||||
const charOffset = coords[0] - startIndex;
|
||||
let leftWideCharCount = 0;
|
||||
let rightWideCharCount = 0;
|
||||
|
||||
if (line.charAt(startIndex) === ' ') {
|
||||
// Expand until non-whitespace is hit
|
||||
while (startIndex > 0 && line.charAt(startIndex - 1) === ' ') {
|
||||
startIndex--;
|
||||
}
|
||||
while (endIndex < line.length && line.charAt(endIndex + 1) === ' ') {
|
||||
endIndex++;
|
||||
}
|
||||
} else {
|
||||
// Expand until whitespace is hit. This algorithm works by scanning left
|
||||
// and right from the starting position, keeping both the index format
|
||||
// (line) and the column format (bufferLine) in sync. When a wide
|
||||
// character is hit, it is recorded and the column index is adjusted.
|
||||
let startCol = coords[0];
|
||||
let endCol = coords[0];
|
||||
// Consider the initial position, skip it and increment the wide char
|
||||
// variable
|
||||
if (bufferLine[startCol][LINE_DATA_WIDTH_INDEX] === 0) {
|
||||
leftWideCharCount++;
|
||||
startCol--;
|
||||
}
|
||||
if (bufferLine[endCol][LINE_DATA_WIDTH_INDEX] === 2) {
|
||||
rightWideCharCount++;
|
||||
endCol++;
|
||||
}
|
||||
// Expand the string in both directions until a space is hit
|
||||
while (startIndex > 0 && !this._isCharWordSeparator(line.charAt(startIndex - 1))) {
|
||||
if (bufferLine[startCol - 1][LINE_DATA_WIDTH_INDEX] === 0) {
|
||||
// If the next character is a wide char, record it and skip the column
|
||||
leftWideCharCount++;
|
||||
startCol--;
|
||||
}
|
||||
startIndex--;
|
||||
startCol--;
|
||||
}
|
||||
while (endIndex + 1 < line.length && !this._isCharWordSeparator(line.charAt(endIndex + 1))) {
|
||||
if (bufferLine[endCol + 1][LINE_DATA_WIDTH_INDEX] === 2) {
|
||||
// If the next character is a wide char, record it and skip the column
|
||||
rightWideCharCount++;
|
||||
endCol++;
|
||||
}
|
||||
endIndex++;
|
||||
endCol++;
|
||||
}
|
||||
}
|
||||
|
||||
const start = startIndex + charOffset - leftWideCharCount;
|
||||
const length = Math.min(endIndex - startIndex + leftWideCharCount + rightWideCharCount + 1/*include endIndex char*/, this._terminal.cols);
|
||||
return { start, length };
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the word at the coordinates specified.
|
||||
* @param coords The coordinates to get the word at.
|
||||
*/
|
||||
protected _selectWordAt(coords: [number, number]): void {
|
||||
const wordPosition = this._getWordAt(coords);
|
||||
if (wordPosition) {
|
||||
this._model.selectionStart = [wordPosition.start, coords[1]];
|
||||
this._model.selectionStartLength = wordPosition.length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the selection end to the word at the coordinated specified.
|
||||
* @param coords The coordinates to get the word at.
|
||||
*/
|
||||
private _selectToWordAt(coords: [number, number]): void {
|
||||
const wordPosition = this._getWordAt(coords);
|
||||
if (wordPosition) {
|
||||
this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), coords[1]];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the character is considered a word separator by the select
|
||||
* word logic.
|
||||
* @param char The character to check.
|
||||
*/
|
||||
private _isCharWordSeparator(char: string): boolean {
|
||||
return WORD_SEPARATORS.indexOf(char) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the line specified.
|
||||
* @param line The line index.
|
||||
*/
|
||||
protected _selectLineAt(line: number): void {
|
||||
this._model.selectionStart = [0, line];
|
||||
this._model.selectionStartLength = this._terminal.cols;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* @license MIT
|
||||
*/
|
||||
import { assert } from 'chai';
|
||||
import { ITerminal } from './Interfaces';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
import {BufferSet} from './BufferSet';
|
||||
import { MockTerminal } from './utils/TestUtils';
|
||||
|
||||
class TestSelectionModel extends SelectionModel {
|
||||
constructor(
|
||||
terminal: ITerminal
|
||||
) {
|
||||
super(terminal);
|
||||
}
|
||||
}
|
||||
|
||||
describe('SelectionManager', () => {
|
||||
let window: Window;
|
||||
let document: Document;
|
||||
|
||||
let terminal: ITerminal;
|
||||
let model: TestSelectionModel;
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = new MockTerminal();
|
||||
terminal.cols = 80;
|
||||
terminal.rows = 2;
|
||||
terminal.scrollback = 10;
|
||||
terminal.buffers = new BufferSet(terminal);
|
||||
terminal.buffer = terminal.buffers.active;
|
||||
|
||||
model = new TestSelectionModel(terminal);
|
||||
});
|
||||
|
||||
describe('clearSelection', () => {
|
||||
it('should clear the final selection', () => {
|
||||
model.selectionStart = [0, 0];
|
||||
model.selectionEnd = [10, 2];
|
||||
assert.deepEqual(model.finalSelectionStart, [0, 0]);
|
||||
assert.deepEqual(model.finalSelectionEnd, [10, 2]);
|
||||
model.clearSelection();
|
||||
assert.deepEqual(model.finalSelectionStart, null);
|
||||
assert.deepEqual(model.finalSelectionEnd, null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('areSelectionValuesReversed', () => {
|
||||
it('should return true when the selection end is before selection start', () => {
|
||||
model.selectionStart = [1, 0];
|
||||
model.selectionEnd = [0, 0];
|
||||
assert.equal(model.areSelectionValuesReversed(), true);
|
||||
model.selectionStart = [10, 2];
|
||||
model.selectionEnd = [0, 0];
|
||||
assert.equal(model.areSelectionValuesReversed(), true);
|
||||
});
|
||||
it('should return false when the selection end is after selection start', () => {
|
||||
model.selectionStart = [0, 0];
|
||||
model.selectionEnd = [1, 0];
|
||||
assert.equal(model.areSelectionValuesReversed(), false);
|
||||
model.selectionStart = [0, 0];
|
||||
model.selectionEnd = [10, 2];
|
||||
assert.equal(model.areSelectionValuesReversed(), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onTrim', () => {
|
||||
it('should trim a portion of the selection when a part of it is trimmed', () => {
|
||||
model.selectionStart = [0, 0];
|
||||
model.selectionEnd = [10, 2];
|
||||
model.onTrim(1);
|
||||
assert.deepEqual(model.finalSelectionStart, [0, 0]);
|
||||
assert.deepEqual(model.finalSelectionEnd, [10, 1]);
|
||||
model.onTrim(1);
|
||||
assert.deepEqual(model.finalSelectionStart, [0, 0]);
|
||||
assert.deepEqual(model.finalSelectionEnd, [10, 0]);
|
||||
});
|
||||
it('should clear selection when it is trimmed in its entirety', () => {
|
||||
model.selectionStart = [0, 0];
|
||||
model.selectionEnd = [10, 0];
|
||||
model.onTrim(1);
|
||||
assert.deepEqual(model.finalSelectionStart, null);
|
||||
assert.deepEqual(model.finalSelectionEnd, null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('finalSelectionStart', () => {
|
||||
it('should return the start of the buffer if select all is active', () => {
|
||||
model.isSelectAllActive = true;
|
||||
assert.deepEqual(model.finalSelectionStart, [0, 0]);
|
||||
});
|
||||
it('should return selection start if there is no selection end', () => {
|
||||
model.selectionStart = [2, 2];
|
||||
assert.deepEqual(model.finalSelectionStart, [2, 2]);
|
||||
});
|
||||
it('should return selection end if values are reversed', () => {
|
||||
model.selectionStart = [2, 2];
|
||||
model.selectionEnd = [3, 2];
|
||||
assert.deepEqual(model.finalSelectionStart, [2, 2]);
|
||||
model.selectionEnd = [1, 2];
|
||||
assert.deepEqual(model.finalSelectionStart, [1, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('finalSelectionEnd', () => {
|
||||
it('should return the end of the buffer if select all is active', () => {
|
||||
model.isSelectAllActive = true;
|
||||
assert.deepEqual(model.finalSelectionEnd, [80, 1]);
|
||||
});
|
||||
it('should return null if there is no selection start', () => {
|
||||
assert.equal(model.finalSelectionEnd, null);
|
||||
model.selectionEnd = [1, 2];
|
||||
assert.equal(model.finalSelectionEnd, null);
|
||||
});
|
||||
it('should return selection start + length if there is no selection end', () => {
|
||||
model.selectionStart = [2, 2];
|
||||
model.selectionStartLength = 2;
|
||||
assert.deepEqual(model.finalSelectionEnd, [4, 2]);
|
||||
});
|
||||
it('should return selection start + length if values are reversed', () => {
|
||||
model.selectionStart = [2, 2];
|
||||
model.selectionStartLength = 2;
|
||||
model.selectionEnd = [2, 1];
|
||||
assert.deepEqual(model.finalSelectionEnd, [4, 2]);
|
||||
});
|
||||
it('should return selection start + length if selection end is inside the start selection', () => {
|
||||
model.selectionStart = [2, 2];
|
||||
model.selectionStartLength = 2;
|
||||
model.selectionEnd = [3, 2];
|
||||
assert.deepEqual(model.finalSelectionEnd, [4, 2]);
|
||||
});
|
||||
it('should return selection end if selection end is after selection start + length', () => {
|
||||
model.selectionStart = [2, 2];
|
||||
model.selectionStartLength = 2;
|
||||
model.selectionEnd = [5, 2];
|
||||
assert.deepEqual(model.finalSelectionEnd, [5, 2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal } from './Interfaces';
|
||||
|
||||
/**
|
||||
* Represents a selection within the buffer. This model only cares about column
|
||||
* and row coordinates, not wide characters.
|
||||
*/
|
||||
export class SelectionModel {
|
||||
/**
|
||||
* Whether select all is currently active.
|
||||
*/
|
||||
public isSelectAllActive: boolean;
|
||||
|
||||
/**
|
||||
* The [x, y] position the selection starts at.
|
||||
*/
|
||||
public selectionStart: [number, number];
|
||||
|
||||
/**
|
||||
* The minimal length of the selection from the start position. When double
|
||||
* clicking on a word, the word will be selected which makes the selection
|
||||
* start at the start of the word and makes this variable the length.
|
||||
*/
|
||||
public selectionStartLength: number;
|
||||
|
||||
/**
|
||||
* The [x, y] position the selection ends at.
|
||||
*/
|
||||
public selectionEnd: [number, number];
|
||||
|
||||
constructor(
|
||||
private _terminal: ITerminal
|
||||
) {
|
||||
this.clearSelection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the current selection.
|
||||
*/
|
||||
public clearSelection(): void {
|
||||
this.selectionStart = null;
|
||||
this.selectionEnd = null;
|
||||
this.isSelectAllActive = false;
|
||||
this.selectionStartLength = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The final selection start, taking into consideration select all.
|
||||
*/
|
||||
public get finalSelectionStart(): [number, number] {
|
||||
if (this.isSelectAllActive) {
|
||||
return [0, 0];
|
||||
}
|
||||
|
||||
if (!this.selectionEnd || !this.selectionStart) {
|
||||
return this.selectionStart;
|
||||
}
|
||||
|
||||
return this.areSelectionValuesReversed() ? this.selectionEnd : this.selectionStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* The final selection end, taking into consideration select all, double click
|
||||
* word selection and triple click line selection.
|
||||
*/
|
||||
public get finalSelectionEnd(): [number, number] {
|
||||
if (this.isSelectAllActive) {
|
||||
return [this._terminal.cols, this._terminal.buffer.ybase + this._terminal.rows - 1];
|
||||
}
|
||||
|
||||
if (!this.selectionStart) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Use the selection start if the end doesn't exist or they're reversed
|
||||
if (!this.selectionEnd || this.areSelectionValuesReversed()) {
|
||||
return [this.selectionStart[0] + this.selectionStartLength, this.selectionStart[1]];
|
||||
}
|
||||
|
||||
// Ensure the the word/line is selected after a double/triple click
|
||||
if (this.selectionStartLength) {
|
||||
// Select the larger of the two when start and end are on the same line
|
||||
if (this.selectionEnd[1] === this.selectionStart[1]) {
|
||||
return [Math.max(this.selectionStart[0] + this.selectionStartLength, this.selectionEnd[0]), this.selectionEnd[1]];
|
||||
}
|
||||
}
|
||||
return this.selectionEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the selection start and end are reversed.
|
||||
*/
|
||||
public areSelectionValuesReversed(): boolean {
|
||||
const start = this.selectionStart;
|
||||
const end = this.selectionEnd;
|
||||
return start[1] > end[1] || (start[1] === end[1] && start[0] > end[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the buffer being trimmed, adjust the selection position.
|
||||
* @param amount The amount the buffer is being trimmed.
|
||||
* @return Whether a refresh is necessary.
|
||||
*/
|
||||
public onTrim(amount: number): boolean {
|
||||
// Adjust the selection position based on the trimmed amount.
|
||||
if (this.selectionStart) {
|
||||
this.selectionStart[1] -= amount;
|
||||
}
|
||||
if (this.selectionEnd) {
|
||||
this.selectionEnd[1] -= amount;
|
||||
}
|
||||
|
||||
// The selection has moved off the buffer, clear it.
|
||||
if (this.selectionEnd && this.selectionEnd[1] < 0) {
|
||||
this.clearSelection();
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the selection start is trimmed, ensure the start column is 0.
|
||||
if (this.selectionStart && this.selectionStart[1] < 0) {
|
||||
this.selectionStart[1] = 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export type LinkMatcher = {
|
||||
id: number,
|
||||
regex: RegExp,
|
||||
handler: LinkMatcherHandler,
|
||||
matchIndex?: number,
|
||||
validationCallback?: LinkMatcherValidationCallback,
|
||||
priority?: number
|
||||
};
|
||||
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void;
|
||||
export type LinkMatcherValidationCallback = (uri: string, element: HTMLElement, callback: (isValid: boolean) => void) => void;
|
||||
+40
-24
@@ -1,5 +1,6 @@
|
||||
import { assert } from 'chai';
|
||||
import { Viewport } from './Viewport';
|
||||
import {BufferSet} from './BufferSet';
|
||||
|
||||
describe('Viewport', () => {
|
||||
let terminal;
|
||||
@@ -12,7 +13,6 @@ describe('Viewport', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = {
|
||||
lines: [],
|
||||
rows: 0,
|
||||
ydisp: 0,
|
||||
on: () => {},
|
||||
@@ -20,8 +20,16 @@ describe('Viewport', () => {
|
||||
style: {
|
||||
lineHeight: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
selectionContainer: {
|
||||
style: {
|
||||
height: 0
|
||||
}
|
||||
},
|
||||
scrollback: 10
|
||||
};
|
||||
terminal.buffers = new BufferSet(terminal);
|
||||
terminal.buffer = terminal.buffers.active;
|
||||
viewportElement = {
|
||||
addEventListener: () => {},
|
||||
style: {
|
||||
@@ -41,38 +49,46 @@ describe('Viewport', () => {
|
||||
});
|
||||
|
||||
describe('refresh', () => {
|
||||
it('should set the line-height of the terminal', () => {
|
||||
assert.equal(viewportElement.style.lineHeight, CHARACTER_HEIGHT + 'px');
|
||||
assert.equal(terminal.rowContainer.style.lineHeight, CHARACTER_HEIGHT + 'px');
|
||||
charMeasure.height = 1;
|
||||
viewport.refresh();
|
||||
assert.equal(viewportElement.style.lineHeight, '1px');
|
||||
assert.equal(terminal.rowContainer.style.lineHeight, '1px');
|
||||
it('should set the line-height of the terminal', done => {
|
||||
// Allow CharMeasure to be initialized
|
||||
setTimeout(() => {
|
||||
assert.equal(viewportElement.style.lineHeight, CHARACTER_HEIGHT + 'px');
|
||||
assert.equal(terminal.rowContainer.style.lineHeight, CHARACTER_HEIGHT + 'px');
|
||||
charMeasure.height = 1;
|
||||
viewport.refresh();
|
||||
assert.equal(viewportElement.style.lineHeight, '1px');
|
||||
assert.equal(terminal.rowContainer.style.lineHeight, '1px');
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
it('should set the height of the viewport when the line-height changed', () => {
|
||||
terminal.lines.push('');
|
||||
terminal.lines.push('');
|
||||
terminal.buffer.lines.push('');
|
||||
terminal.buffer.lines.push('');
|
||||
terminal.rows = 1;
|
||||
viewport.refresh();
|
||||
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
||||
charMeasure.height = 20;
|
||||
charMeasure.height = 2 * CHARACTER_HEIGHT;
|
||||
viewport.refresh();
|
||||
assert.equal(viewportElement.style.height, 20 + 'px');
|
||||
assert.equal(viewportElement.style.height, 2 * CHARACTER_HEIGHT + 'px');
|
||||
});
|
||||
});
|
||||
|
||||
describe('syncScrollArea', () => {
|
||||
it('should sync the scroll area', () => {
|
||||
terminal.lines.push('');
|
||||
terminal.rows = 1;
|
||||
assert.equal(scrollAreaElement.style.height, 0 * CHARACTER_HEIGHT + 'px');
|
||||
viewport.syncScrollArea();
|
||||
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
||||
assert.equal(scrollAreaElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
||||
terminal.lines.push('');
|
||||
viewport.syncScrollArea();
|
||||
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
||||
assert.equal(scrollAreaElement.style.height, 2 * CHARACTER_HEIGHT + 'px');
|
||||
it('should sync the scroll area', done => {
|
||||
// Allow CharMeasure to be initialized
|
||||
setTimeout(() => {
|
||||
terminal.buffer.lines.push('');
|
||||
terminal.rows = 1;
|
||||
assert.equal(scrollAreaElement.style.height, 0 * CHARACTER_HEIGHT + 'px');
|
||||
viewport.syncScrollArea();
|
||||
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
||||
assert.equal(scrollAreaElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
||||
terminal.buffer.lines.push('');
|
||||
viewport.syncScrollArea();
|
||||
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
||||
assert.equal(scrollAreaElement.style.height, 2 * CHARACTER_HEIGHT + 'px');
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+31
-8
@@ -13,13 +13,14 @@ export class Viewport {
|
||||
private currentRowHeight: number;
|
||||
private lastRecordedBufferLength: number;
|
||||
private lastRecordedViewportHeight: number;
|
||||
private lastTouchY: number;
|
||||
|
||||
/**
|
||||
* Creates a new Viewport.
|
||||
* @param terminal The terminal this viewport belongs to.
|
||||
* @param viewportElement The DOM element acting as the viewport.
|
||||
* @param scrollArea The DOM element acting as the scroll area.
|
||||
* @param charMeasureElement A DOM element used to measure the character size of. the terminal.
|
||||
* @param charMeasure A DOM element used to measure the character size of. the terminal.
|
||||
*/
|
||||
constructor(
|
||||
private terminal: ITerminal,
|
||||
@@ -35,14 +36,13 @@ export class Viewport {
|
||||
this.terminal.on('resize', this.syncScrollArea.bind(this));
|
||||
this.viewportElement.addEventListener('scroll', this.onScroll.bind(this));
|
||||
|
||||
this.syncScrollArea();
|
||||
// Perform this async to ensure the CharMeasure is ready.
|
||||
setTimeout(() => this.syncScrollArea(), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes row height, setting line-height, viewport height and scroll area height if
|
||||
* necessary.
|
||||
* @param charSize A character size measurement bounding rect object, if it doesn't exist it will
|
||||
* be created.
|
||||
*/
|
||||
private refresh(): void {
|
||||
if (this.charMeasure.height > 0) {
|
||||
@@ -56,6 +56,7 @@ export class Viewport {
|
||||
if (rowHeightChanged || viewportHeightChanged) {
|
||||
this.lastRecordedViewportHeight = this.terminal.rows;
|
||||
this.viewportElement.style.height = this.charMeasure.height * this.terminal.rows + 'px';
|
||||
this.terminal.selectionContainer.style.height = this.viewportElement.style.height;
|
||||
}
|
||||
this.scrollArea.style.height = (this.charMeasure.height * this.lastRecordedBufferLength) + 'px';
|
||||
}
|
||||
@@ -65,9 +66,9 @@ export class Viewport {
|
||||
* Updates dimensions and synchronizes the scroll area if necessary.
|
||||
*/
|
||||
public syncScrollArea(): void {
|
||||
if (this.lastRecordedBufferLength !== this.terminal.lines.length) {
|
||||
if (this.lastRecordedBufferLength !== this.terminal.buffer.lines.length) {
|
||||
// If buffer height changed
|
||||
this.lastRecordedBufferLength = this.terminal.lines.length;
|
||||
this.lastRecordedBufferLength = this.terminal.buffer.lines.length;
|
||||
this.refresh();
|
||||
} else if (this.lastRecordedViewportHeight !== this.terminal.rows) {
|
||||
// If viewport height changed
|
||||
@@ -80,7 +81,7 @@ export class Viewport {
|
||||
}
|
||||
|
||||
// Sync scrollTop
|
||||
const scrollTop = this.terminal.ydisp * this.currentRowHeight;
|
||||
const scrollTop = this.terminal.buffer.ydisp * this.currentRowHeight;
|
||||
if (this.viewportElement.scrollTop !== scrollTop) {
|
||||
this.viewportElement.scrollTop = scrollTop;
|
||||
}
|
||||
@@ -93,7 +94,7 @@ export class Viewport {
|
||||
*/
|
||||
private onScroll(ev: Event) {
|
||||
const newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight);
|
||||
const diff = newRow - this.terminal.ydisp;
|
||||
const diff = newRow - this.terminal.buffer.ydisp;
|
||||
this.terminal.scrollDisp(diff, true);
|
||||
}
|
||||
|
||||
@@ -119,4 +120,26 @@ export class Viewport {
|
||||
// Prevent the page from scrolling when the terminal scrolls
|
||||
ev.preventDefault();
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the touchstart event, recording the touch occurred.
|
||||
* @param ev The touch event.
|
||||
*/
|
||||
public onTouchStart(ev: TouchEvent) {
|
||||
this.lastTouchY = ev.touches[0].pageY;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the touchmove event, scrolling the viewport if the position shifted.
|
||||
* @param ev The touch event.
|
||||
*/
|
||||
public onTouchMove(ev: TouchEvent) {
|
||||
let deltaY = this.lastTouchY - ev.touches[0].pageY;
|
||||
this.lastTouchY = ev.touches[0].pageY;
|
||||
if (deltaY === 0) {
|
||||
return;
|
||||
}
|
||||
this.viewportElement.scrollTop += deltaY;
|
||||
ev.preventDefault();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
var exports = {};
|
||||
|
||||
exports.proposeGeometry = function (term) {
|
||||
if (!term.element.parentElement) {
|
||||
return null;
|
||||
}
|
||||
var parentElementStyle = window.getComputedStyle(term.element.parentElement),
|
||||
parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')),
|
||||
parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17),
|
||||
@@ -53,7 +56,7 @@
|
||||
subjectRow.innerHTML = 'W'; // Common character for measuring width, although on monospace
|
||||
characterWidth = subjectRow.getBoundingClientRect().width;
|
||||
subjectRow.style.display = ''; // Revert style before calculating height, since they differ.
|
||||
characterHeight = parseInt(subjectRow.offsetHeight);
|
||||
characterHeight = subjectRow.getBoundingClientRect().height;
|
||||
subjectRow.innerHTML = contentBuffer;
|
||||
|
||||
rows = parseInt(availableHeight / characterHeight);
|
||||
@@ -66,7 +69,9 @@
|
||||
exports.fit = function (term) {
|
||||
var geometry = exports.proposeGeometry(term);
|
||||
|
||||
term.resize(geometry.cols, geometry.rows);
|
||||
if (geometry) {
|
||||
term.resize(geometry.cols, geometry.rows);
|
||||
}
|
||||
};
|
||||
|
||||
Xterm.prototype.proposeGeometry = function () {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../src/xterm.css" />
|
||||
<link rel="stylesheet" href="../../demo/style.css" />
|
||||
<script src="../../src/xterm.js"></script>
|
||||
<script src="linkify.js"></script>
|
||||
<style>
|
||||
body {
|
||||
color: #111;
|
||||
}
|
||||
|
||||
#terminal-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#terminal-container a {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="terminal-container"></div>
|
||||
<script>
|
||||
var term = new Terminal(),
|
||||
container = document.getElementById('terminal-container');
|
||||
|
||||
term.open(container);
|
||||
term.writeln('Hello, welcome to xterm.js');
|
||||
term.writeln('');
|
||||
term.writeln('Check it out at github.com/sourcelair/xterm.js');
|
||||
term.linkify();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,207 +0,0 @@
|
||||
/**
|
||||
* Methods for turning URL subscrings in the terminal's content into links (`a` DOM elements).
|
||||
* @module xterm/addons/linkify/linkify
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
(function (linkify) {
|
||||
if (typeof exports === 'object' && typeof module === 'object') {
|
||||
/*
|
||||
* CommonJS environment
|
||||
*/
|
||||
module.exports = linkify(require('../../xterm'));
|
||||
} else if (typeof define == 'function') {
|
||||
/*
|
||||
* Require.js is available
|
||||
*/
|
||||
define(['../../xterm'], linkify);
|
||||
} else {
|
||||
/*
|
||||
* Plain browser environment
|
||||
*/
|
||||
linkify(window.Terminal);
|
||||
}
|
||||
})(function (Xterm) {
|
||||
'use strict';
|
||||
|
||||
var exports = {},
|
||||
protocolClause = '(https?:\\/\\/)',
|
||||
domainCharacterSet = '[\\da-z\\.-]+',
|
||||
negatedDomainCharacterSet = '[^\\da-z\\.-]+',
|
||||
domainBodyClause = '(' + domainCharacterSet + ')',
|
||||
tldClause = '([a-z\\.]{2,6})',
|
||||
ipClause = '((\\d{1,3}\\.){3}\\d{1,3})',
|
||||
portClause = '(:\\d{1,5})',
|
||||
hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + ')' + portClause + '?',
|
||||
pathClause = '(\\/[\\/\\w\\.-]*)*',
|
||||
negatedPathCharacterSet = '[^\\/\\w\\.-]+',
|
||||
bodyClause = hostClause + pathClause,
|
||||
start = '(?:^|' + negatedDomainCharacterSet + ')(',
|
||||
end = ')($|' + negatedPathCharacterSet + ')',
|
||||
lenientUrlClause = start + protocolClause + '?' + bodyClause + end,
|
||||
strictUrlClause = start + protocolClause + bodyClause + end,
|
||||
lenientUrlRegex = new RegExp(lenientUrlClause),
|
||||
strictUrlRegex = new RegExp(strictUrlClause);
|
||||
|
||||
/**
|
||||
* Converts all valid URLs found in the given terminal line into
|
||||
* hyperlinks. The terminal line can be either the HTML element itself
|
||||
* or the index of the termina line in the children of the terminal
|
||||
* rows container.
|
||||
*
|
||||
* @param {Xterm} terminal - The terminal that owns the given line.
|
||||
* @param {number|HTMLDivElement} line - The terminal line that should get
|
||||
* "linkified".
|
||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
||||
* false, the regex requires a protocol clause. Defaults to true.
|
||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
||||
* Default doesn't set target attribute
|
||||
* @emits linkify
|
||||
* @emits linkify:line
|
||||
*/
|
||||
exports.linkifyTerminalLine = function (terminal, line, lenient, target) {
|
||||
if (typeof line == 'number') {
|
||||
line = terminal.rowContainer.children[line];
|
||||
} else if (! (line instanceof HTMLDivElement)) {
|
||||
var message = 'The "line" argument should be either a number';
|
||||
message += ' or an HTMLDivElement';
|
||||
|
||||
throw new TypeError(message);
|
||||
}
|
||||
|
||||
if (typeof target === 'undefined') {
|
||||
target = '';
|
||||
} else {
|
||||
target = 'target="' + target + '"';
|
||||
}
|
||||
|
||||
var buffer = document.createElement('span'),
|
||||
nodes = line.childNodes;
|
||||
|
||||
for (var j=0; j<nodes.length; j++) {
|
||||
var node = nodes[j],
|
||||
match;
|
||||
|
||||
/**
|
||||
* Since we cannot access the TextNode's HTML representation
|
||||
* from the instance itself, we assign its data as textContent
|
||||
* to a dummy buffer span, in order to retrieve the TextNode's
|
||||
* HTML representation from the buffer's innerHTML.
|
||||
*/
|
||||
buffer.textContent = node.data;
|
||||
|
||||
var nodeHTML = buffer.innerHTML;
|
||||
|
||||
/**
|
||||
* Apply function only on TextNodes
|
||||
*/
|
||||
if (node.nodeType != node.TEXT_NODE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var url = exports.findLinkMatch(node.data, lenient);
|
||||
|
||||
if (!url) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var startsWithProtocol = new RegExp('^' + protocolClause),
|
||||
urlHasProtocol = url.match(startsWithProtocol),
|
||||
href = (urlHasProtocol) ? url : 'http://' + url,
|
||||
link = '<a href="' + href + '" ' + target + '>' + url + '</a>',
|
||||
newHTML = nodeHTML.replace(url, link);
|
||||
|
||||
line.innerHTML = line.innerHTML.replace(nodeHTML, newHTML);
|
||||
}
|
||||
|
||||
/**
|
||||
* This event gets emitted when conversion of all URL susbtrings
|
||||
* to HTML anchor elements (links) has finished, for a specific
|
||||
* line of the current Xterm instance.
|
||||
*
|
||||
* @event linkify:line
|
||||
*/
|
||||
terminal.emit('linkify:line', line);
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds a link within a block of text.
|
||||
*
|
||||
* @param {string} text - The text to search .
|
||||
* @param {boolean} lenient - Whether to use the lenient search.
|
||||
* @return {string} A URL.
|
||||
*/
|
||||
exports.findLinkMatch = function (text, lenient) {
|
||||
var match = text.match(lenient ? lenientUrlRegex : strictUrlRegex);
|
||||
if (!match || match.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts all valid URLs found in the terminal view into hyperlinks.
|
||||
*
|
||||
* @param {Xterm} terminal - The terminal that should get "linkified".
|
||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
||||
* false, the regex requires a protocol clause. Defaults to true.
|
||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
||||
* Default doesn't set target attribute
|
||||
* @emits linkify
|
||||
* @emits linkify:line
|
||||
*/
|
||||
exports.linkify = function (terminal, lenient, target) {
|
||||
var rows = terminal.rowContainer.children;
|
||||
|
||||
lenient = (typeof lenient == "boolean") ? lenient : true;
|
||||
for (var i=0; i<rows.length; i++) {
|
||||
var line = rows[i];
|
||||
|
||||
exports.linkifyTerminalLine(terminal, line, lenient, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* This event gets emitted when conversion of all URL substrings to
|
||||
* HTML anchor elements (links) has finished for the current Xterm
|
||||
* instance's view.
|
||||
*
|
||||
* @event linkify
|
||||
*/
|
||||
terminal.emit('linkify');
|
||||
};
|
||||
|
||||
/**
|
||||
* Extend Xterm prototype.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Converts all valid URLs found in the current terminal linte into
|
||||
* hyperlinks.
|
||||
*
|
||||
* @memberof Xterm
|
||||
* @param {number|HTMLDivElement} line - The terminal line that should get
|
||||
* "linkified".
|
||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
||||
* false, the regex requires a protocol clause. Defaults to true.
|
||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
||||
* Default doesn't set target attribute
|
||||
*/
|
||||
Xterm.prototype.linkifyTerminalLine = function (line, lenient, target) {
|
||||
return exports.linkifyTerminalLine(this, line, lenient, target);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts all valid URLs found in the current terminal into hyperlinks.
|
||||
*
|
||||
* @memberof Xterm
|
||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
||||
* false, the regex requires a protocol clause. Defaults to true.
|
||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
||||
* Default doesn't set target attribute
|
||||
*/
|
||||
Xterm.prototype.linkify = function (lenient, target) {
|
||||
return exports.linkify(this, lenient, target);
|
||||
};
|
||||
|
||||
return exports;
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "xterm.linkify",
|
||||
"main": "linkify.js",
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
// import { ITerminal } from '../../Interfaces';
|
||||
// import { translateBufferLineToString } from '../../utils/BufferLine';
|
||||
|
||||
interface ISearchResult {
|
||||
term: string;
|
||||
col: number;
|
||||
row: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A class that knows how to search the terminal and how to display the results.
|
||||
*/
|
||||
export class SearchHelper {
|
||||
constructor(private _terminal: any, private _translateBufferLineToString: any) {
|
||||
// TODO: Search for multiple instances on 1 line
|
||||
// TODO: Don't use the actual selection, instead use a "find selection" so multiple instances can be highlighted
|
||||
// TODO: Highlight other instances in the viewport
|
||||
// TODO: Support regex, case sensitivity, etc.
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the next instance of the term, then scroll to and select it. If it
|
||||
* doesn't exist, do nothing.
|
||||
* @param term Tne search term.
|
||||
* @return Whether a result was found.
|
||||
*/
|
||||
public findNext(term: string): boolean {
|
||||
if (!term || term.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let result: ISearchResult;
|
||||
|
||||
let startRow = this._terminal.buffer.ydisp;
|
||||
if (this._terminal.selectionManager.selectionEnd) {
|
||||
// Start from the selection end if there is a selection
|
||||
startRow = this._terminal.selectionManager.selectionEnd[1];
|
||||
}
|
||||
|
||||
// Search from ydisp + 1 to end
|
||||
for (let y = startRow + 1; y < this._terminal.buffer.ybase + this._terminal.rows; y++) {
|
||||
result = this._findInLine(term, y);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Search from the top to the current ydisp
|
||||
if (!result) {
|
||||
for (let y = 0; y < startRow; y++) {
|
||||
result = this._findInLine(term, y);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set selection and scroll if a result was found
|
||||
return this._selectResult(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the previous instance of the term, then scroll to and select it. If it
|
||||
* doesn't exist, do nothing.
|
||||
* @param term Tne search term.
|
||||
* @return Whether a result was found.
|
||||
*/
|
||||
public findPrevious(term: string): boolean {
|
||||
if (!term || term.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let result: ISearchResult;
|
||||
|
||||
let startRow = this._terminal.buffer.ydisp;
|
||||
if (this._terminal.selectionManager.selectionStart) {
|
||||
// Start from the selection end if there is a selection
|
||||
startRow = this._terminal.selectionManager.selectionStart[1];
|
||||
}
|
||||
|
||||
// Search from ydisp + 1 to end
|
||||
for (let y = startRow - 1; y >= 0; y--) {
|
||||
result = this._findInLine(term, y);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Search from the top to the current ydisp
|
||||
if (!result) {
|
||||
for (let y = this._terminal.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
|
||||
result = this._findInLine(term, y);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set selection and scroll if a result was found
|
||||
return this._selectResult(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches a line for a search term.
|
||||
* @param term Tne search term.
|
||||
* @param y The line to search.
|
||||
* @return The search result if it was found.
|
||||
*/
|
||||
private _findInLine(term: string, y: number): ISearchResult {
|
||||
const bufferLine = this._terminal.buffer.lines.get(y);
|
||||
const lowerStringLine = this._translateBufferLineToString(bufferLine, true).toLowerCase();
|
||||
const lowerTerm = term.toLowerCase();
|
||||
const searchIndex = lowerStringLine.indexOf(lowerTerm);
|
||||
if (searchIndex >= 0) {
|
||||
return {
|
||||
term,
|
||||
col: searchIndex,
|
||||
row: y
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects and scrolls to a result.
|
||||
* @param result The result to select.
|
||||
* @return Whethera result was selected.
|
||||
*/
|
||||
private _selectResult(result: ISearchResult): boolean {
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
this._terminal.selectionManager.setSelection(result.col, result.row, result.term.length);
|
||||
this._terminal.scrollDisp(result.row - this._terminal.buffer.ydisp, false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { SearchHelper } from './SearchHelper';
|
||||
|
||||
declare var exports: any;
|
||||
declare var module: any;
|
||||
declare var define: any;
|
||||
declare var require: any;
|
||||
declare var window: any;
|
||||
|
||||
(function (addon) {
|
||||
if ('Terminal' in window) {
|
||||
/**
|
||||
* Plain browser environment
|
||||
*/
|
||||
addon(window.Terminal);
|
||||
} else if (typeof exports === 'object' && typeof module === 'object') {
|
||||
/**
|
||||
* CommonJS environment
|
||||
*/
|
||||
module.exports = addon(require('../../xterm'));
|
||||
} else if (typeof define == 'function') {
|
||||
/**
|
||||
* Require.js is available
|
||||
*/
|
||||
define(['../../xterm'], addon);
|
||||
}
|
||||
})((Terminal: any) => {
|
||||
/**
|
||||
* Find the next instance of the term, then scroll to and select it. If it
|
||||
* doesn't exist, do nothing.
|
||||
* @param term Tne search term.
|
||||
* @return Whether a result was found.
|
||||
*/
|
||||
Terminal.prototype.findNext = function(term: string): boolean {
|
||||
if (!this._searchHelper) {
|
||||
this.searchHelper = new SearchHelper(this, Terminal.translateBufferLineToString);
|
||||
}
|
||||
return (<SearchHelper>this.searchHelper).findNext(term);
|
||||
};
|
||||
|
||||
/**
|
||||
* Find the previous instance of the term, then scroll to and select it. If it
|
||||
* doesn't exist, do nothing.
|
||||
* @param term Tne search term.
|
||||
* @return Whether a result was found.
|
||||
*/
|
||||
Terminal.prototype.findPrevious = function(term: string): boolean {
|
||||
if (!this._searchHelper) {
|
||||
this.searchHelper = new SearchHelper(this, Terminal.translateBufferLineToString);
|
||||
}
|
||||
return (<SearchHelper>this.searchHelper).findPrevious(term);
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"rootDir": ".",
|
||||
"outDir": "../../../lib/addons/search/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,13 @@ import { assert } from 'chai';
|
||||
import * as Terminal from '../xterm';
|
||||
import * as Clipboard from './Clipboard';
|
||||
|
||||
describe('evaluatePastedTextProcessing', function () {
|
||||
it('should replace carriage return + line feed with line feed on windows', function () {
|
||||
const pastedText = 'foo\r\nbar\r\n',
|
||||
processedText = Clipboard.prepareTextForTerminal(pastedText, false),
|
||||
windowsProcessedText = Clipboard.prepareTextForTerminal(pastedText, true);
|
||||
|
||||
describe('evaluateCopiedTextProcessing', function () {
|
||||
it('should strip trailing whitespaces and replace nbsps with spaces', function () {
|
||||
let nonBreakingSpace = String.fromCharCode(160),
|
||||
copiedText = 'echo' + nonBreakingSpace + 'hello' + nonBreakingSpace,
|
||||
processedText = Clipboard.prepareTextForClipboard(copiedText);
|
||||
|
||||
// No trailing spaces
|
||||
assert.equal(processedText.match(/\s+$/), null);
|
||||
|
||||
// No non-breaking space
|
||||
assert.equal(processedText.indexOf(nonBreakingSpace), -1);
|
||||
assert.equal(processedText, 'foo\r\nbar\r\n');
|
||||
assert.equal(windowsProcessedText, 'foo\rbar\r');
|
||||
});
|
||||
});
|
||||
|
||||
+51
-89
@@ -5,7 +5,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal } from '../Interfaces';
|
||||
import { ITerminal, ISelectionManager } from '../Interfaces';
|
||||
|
||||
interface IWindow extends Window {
|
||||
clipboardData?: {
|
||||
@@ -17,44 +17,29 @@ interface IWindow extends Window {
|
||||
declare var window: IWindow;
|
||||
|
||||
/**
|
||||
* Prepares text copied from terminal selection, to be saved in the clipboard by:
|
||||
* 1. stripping all trailing white spaces
|
||||
* 2. converting all non-breaking spaces to regular spaces
|
||||
* @param {string} text The copied text that needs processing for storing in clipboard
|
||||
* @returns {string}
|
||||
* Prepares text to be pasted into the terminal by normalizing the line endings
|
||||
* @param text The pasted text that needs processing before inserting into the terminal
|
||||
*/
|
||||
export function prepareTextForClipboard(text: string): string {
|
||||
let space = String.fromCharCode(32),
|
||||
nonBreakingSpace = String.fromCharCode(160),
|
||||
allNonBreakingSpaces = new RegExp(nonBreakingSpace, 'g'),
|
||||
processedText = text.split('\n').map(function (line) {
|
||||
// Strip all trailing white spaces and convert all non-breaking spaces
|
||||
// to regular spaces.
|
||||
let processedLine = line.replace(/\s+$/g, '').replace(allNonBreakingSpaces, space);
|
||||
|
||||
return processedLine;
|
||||
}).join('\n');
|
||||
|
||||
return processedText;
|
||||
export function prepareTextForTerminal(text: string, isMSWindows: boolean): string {
|
||||
if (isMSWindows) {
|
||||
return text.replace(/\r?\n/g, '\r');
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds copy functionality to the given terminal.
|
||||
* @param {ClipboardEvent} ev The original copy event to be handled
|
||||
*/
|
||||
export function copyHandler(ev: ClipboardEvent, term: ITerminal) {
|
||||
// We cast `window` to `any` type, because TypeScript has not declared the `clipboardData`
|
||||
// property that we use below for Internet Explorer.
|
||||
let copiedText = window.getSelection().toString(),
|
||||
text = prepareTextForClipboard(copiedText);
|
||||
|
||||
export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManager: ISelectionManager) {
|
||||
if (term.browser.isMSIE) {
|
||||
window.clipboardData.setData('Text', text);
|
||||
window.clipboardData.setData('Text', selectionManager.selectionText);
|
||||
} else {
|
||||
ev.clipboardData.setData('text/plain', text);
|
||||
ev.clipboardData.setData('text/plain', selectionManager.selectionText);
|
||||
}
|
||||
|
||||
ev.preventDefault(); // Prevent or the original text will be copied.
|
||||
// Prevent or the original text will be copied.
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,8 +53,11 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal) {
|
||||
let text: string;
|
||||
|
||||
let dispatchPaste = function(text) {
|
||||
text = prepareTextForTerminal(text, term.browser.isMSWindows);
|
||||
term.handler(text);
|
||||
term.textarea.value = '';
|
||||
term.emit('paste', text);
|
||||
|
||||
return term.cancel(ev);
|
||||
};
|
||||
|
||||
@@ -87,68 +75,42 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind to right-click event and allow right-click copy and paste.
|
||||
*
|
||||
* **Logic**
|
||||
* If text is selected and right-click happens on selected text, then
|
||||
* do nothing to allow seamless copying.
|
||||
* If no text is selected or right-click is outside of the selection
|
||||
* area, then bring the terminal's input below the cursor, in order to
|
||||
* trigger the event on the textarea and allow-right click paste, without
|
||||
* caring about disappearing selection.
|
||||
* @param {MouseEvent} ev The original right click event to be handled
|
||||
* @param {Terminal} term The terminal on which to apply the handled paste event
|
||||
* Moves the textarea under the mouse cursor and focuses it.
|
||||
* @param ev The original right click event to be handled.
|
||||
* @param textarea The terminal's textarea.
|
||||
*/
|
||||
export function rightClickHandler(ev: MouseEvent, term: ITerminal) {
|
||||
let s = document.getSelection(),
|
||||
selectedText = prepareTextForClipboard(s.toString()),
|
||||
clickIsOnSelection = false,
|
||||
x = ev.clientX,
|
||||
y = ev.clientY;
|
||||
|
||||
if (s.rangeCount) {
|
||||
let r = s.getRangeAt(0),
|
||||
cr = r.getClientRects();
|
||||
|
||||
for (let i = 0; i < cr.length; i++) {
|
||||
let rect = cr[i];
|
||||
|
||||
clickIsOnSelection = (
|
||||
(x > rect.left) && (x < rect.right) &&
|
||||
(y > rect.top) && (y < rect.bottom)
|
||||
);
|
||||
|
||||
if (clickIsOnSelection) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If we clicked on selection and selection is not a single space,
|
||||
// then mark the right click as copy-only. We check for the single
|
||||
// space selection, as this can happen when clicking on an
|
||||
// and there is not much pointing in copying a single space.
|
||||
if (selectedText.match(/^\s$/) || !selectedText.length) {
|
||||
clickIsOnSelection = false;
|
||||
}
|
||||
}
|
||||
|
||||
export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextAreaElement) {
|
||||
// Bring textarea at the cursor position
|
||||
if (!clickIsOnSelection) {
|
||||
term.textarea.style.position = 'fixed';
|
||||
term.textarea.style.width = '20px';
|
||||
term.textarea.style.height = '20px';
|
||||
term.textarea.style.left = (x - 10) + 'px';
|
||||
term.textarea.style.top = (y - 10) + 'px';
|
||||
term.textarea.style.zIndex = '1000';
|
||||
term.textarea.focus();
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.width = '20px';
|
||||
textarea.style.height = '20px';
|
||||
textarea.style.left = (ev.clientX - 10) + 'px';
|
||||
textarea.style.top = (ev.clientY - 10) + 'px';
|
||||
textarea.style.zIndex = '1000';
|
||||
|
||||
// Reset the terminal textarea's styling
|
||||
setTimeout(function () {
|
||||
term.textarea.style.position = null;
|
||||
term.textarea.style.width = null;
|
||||
term.textarea.style.height = null;
|
||||
term.textarea.style.left = null;
|
||||
term.textarea.style.top = null;
|
||||
term.textarea.style.zIndex = null;
|
||||
}, 4);
|
||||
}
|
||||
textarea.focus();
|
||||
|
||||
// Reset the terminal textarea's styling
|
||||
setTimeout(function () {
|
||||
textarea.style.position = null;
|
||||
textarea.style.width = null;
|
||||
textarea.style.height = null;
|
||||
textarea.style.left = null;
|
||||
textarea.style.top = null;
|
||||
textarea.style.zIndex = null;
|
||||
}, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind to right-click event and allow right-click copy and paste.
|
||||
* @param ev The original right click event to be handled.
|
||||
* @param textarea The terminal's textarea.
|
||||
* @param selectionManager The terminal's selection manager.
|
||||
*/
|
||||
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager) {
|
||||
moveTextAreaUnderMouseCursor(ev, textarea);
|
||||
|
||||
// Get textarea ready to copy from the context menu
|
||||
textarea.value = selectionManager.selectionText;
|
||||
textarea.select();
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
var assert = require('chai').assert;
|
||||
var Terminal = require('../../xterm');
|
||||
var linkify = require('../../addons/linkify/linkify');
|
||||
|
||||
describe('linkify addon', function () {
|
||||
var xterm;
|
||||
|
||||
describe('API', function () {
|
||||
it('should define Terminal.prototype.linkify', function () {
|
||||
assert.isDefined(Terminal.prototype.linkify);
|
||||
});
|
||||
it('should define Terminal.prototype.linkifyTerminalLine', function () {
|
||||
assert.isDefined(Terminal.prototype.linkifyTerminalLine);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findUrlMatchOnLine', function () {
|
||||
describe('strict regex', function () {
|
||||
it('should match when the entire text is a match', function () {
|
||||
assert.equal(linkify.findLinkMatch('http://github.com', false), 'http://github.com');
|
||||
assert.equal(linkify.findLinkMatch('http://127.0.0.1', false), 'http://127.0.0.1');
|
||||
});
|
||||
it('should match simple domains', function () {
|
||||
assert.equal(linkify.findLinkMatch('foo http://github.com bar', false), 'http://github.com');
|
||||
assert.equal(linkify.findLinkMatch('foo http://www.github.com bar', false), 'http://www.github.com');
|
||||
assert.equal(linkify.findLinkMatch('foo https://github.com bar', false), 'https://github.com');
|
||||
assert.equal(linkify.findLinkMatch('foo https://www.github.com bar', false), 'https://www.github.com');
|
||||
});
|
||||
it('should match web addresses with alpha paths', function () {
|
||||
assert.equal(linkify.findLinkMatch('foo http://github.com/a/b/c bar', false), 'http://github.com/a/b/c');
|
||||
assert.equal(linkify.findLinkMatch('foo http://www.github.com/a/b/c bar', false), 'http://www.github.com/a/b/c');
|
||||
});
|
||||
it('should not include whitespace surrounding a match', function () {
|
||||
assert.equal(linkify.findLinkMatch(' http://github.com', false), 'http://github.com');
|
||||
assert.equal(linkify.findLinkMatch('http://github.com ', false), 'http://github.com');
|
||||
assert.equal(linkify.findLinkMatch(' http://github.com ', false), 'http://github.com');
|
||||
});
|
||||
it('should match IP addresses', function () {
|
||||
assert.equal(linkify.findLinkMatch('foo http://127.0.0.1 bar', false), 'http://127.0.0.1');
|
||||
assert.equal(linkify.findLinkMatch('foo https://127.0.0.1 bar', false), 'https://127.0.0.1');
|
||||
});
|
||||
it('should match ports on both domains and IP addresses', function () {
|
||||
assert.equal(linkify.findLinkMatch('foo http://127.0.0.1:8080 bar', false), 'http://127.0.0.1:8080');
|
||||
assert.equal(linkify.findLinkMatch('foo http://www.github.com:8080 bar', false), 'http://www.github.com:8080');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,6 @@ var glob = require('glob');
|
||||
var fs = require('fs');
|
||||
var os = require('os');
|
||||
var pty = require('node-pty');
|
||||
var sleep = require('sleep');
|
||||
var Terminal = require('../xterm');
|
||||
|
||||
if (os.platform() === 'win32') {
|
||||
@@ -25,17 +24,18 @@ var primitive_pty = pty.native.open(COLS, ROWS);
|
||||
// we just pipe the data from slave to master as a child program would do
|
||||
// pty.js opens pipe fds with O_NONBLOCK
|
||||
// just wait 10ms instead of setting fds to blocking mode
|
||||
function pty_write_read(s) {
|
||||
function ptyWriteRead(s, cb) {
|
||||
fs.writeSync(primitive_pty.slave, s);
|
||||
sleep.usleep(10000);
|
||||
var b = Buffer(64000);
|
||||
var bytes = fs.readSync(primitive_pty.master, b, 0, 64000);
|
||||
return b.toString('utf8', 0, bytes);
|
||||
setTimeout(() => {
|
||||
var b = Buffer(64000);
|
||||
var bytes = fs.readSync(primitive_pty.master, b, 0, 64000);
|
||||
cb(b.toString('utf8', 0, bytes));
|
||||
});
|
||||
}
|
||||
|
||||
// make sure raw pty is at x=0 and has no pending data
|
||||
function pty_reset() {
|
||||
pty_write_read('\r\n');
|
||||
function ptyReset(cb) {
|
||||
ptyWriteRead('\r\n', cb);
|
||||
}
|
||||
|
||||
/* debug helpers */
|
||||
@@ -62,10 +62,10 @@ function formatError(in_, out_, expected) {
|
||||
function terminalToString(term) {
|
||||
var result = '';
|
||||
var line_s = '';
|
||||
for (var line = term.ybase; line < term.ybase + term.rows; line++) {
|
||||
for (var line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
|
||||
line_s = '';
|
||||
for (var cell=0; cell<term.cols; ++cell) {
|
||||
line_s += term.lines.get(line)[cell][1];
|
||||
line_s += term.buffer.lines.get(line)[cell][1];
|
||||
}
|
||||
// rtrim empty cells as xterm does
|
||||
line_s = line_s.replace(/\s+$/, '');
|
||||
@@ -97,35 +97,42 @@ describe('xterm output comparison', function() {
|
||||
51, 52, 54, 55, 56, 57, 58, 59, 60, 61,
|
||||
63, 68
|
||||
];
|
||||
if (os.platform() === 'darwin') {
|
||||
// These are failing on macOS only
|
||||
skip.push(3, 7, 11, 67);
|
||||
}
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
if (skip.indexOf(i) >= 0) {
|
||||
continue;
|
||||
}
|
||||
(function(filename) {
|
||||
it(filename.split('/').slice(-1)[0], function () {
|
||||
pty_reset();
|
||||
var in_file = fs.readFileSync(filename, 'utf8');
|
||||
var from_pty = pty_write_read(in_file);
|
||||
// uncomment this to get log from terminal
|
||||
//console.log = function(){};
|
||||
it(filename.split('/').slice(-1)[0], done => {
|
||||
ptyReset(() => {
|
||||
var in_file = fs.readFileSync(filename, 'utf8');
|
||||
ptyWriteRead(in_file, from_pty => {
|
||||
// uncomment this to get log from terminal
|
||||
//console.log = function(){};
|
||||
|
||||
// Perform a synchronous .write(data)
|
||||
xterm.writeBuffer.push(from_pty);
|
||||
xterm.innerWrite();
|
||||
// Perform a synchronous .write(data)
|
||||
xterm.writeBuffer.push(from_pty);
|
||||
xterm.innerWrite();
|
||||
|
||||
var from_emulator = terminalToString(xterm);
|
||||
console.log = CONSOLE_LOG;
|
||||
var expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8');
|
||||
// Some of the tests have whitespace on the right of lines, we trim all the linex
|
||||
// from xterm.js so ignore this for now at least.
|
||||
var expectedRightTrimmed = expected.split('\n').map(function (l) {
|
||||
return l.replace(/\s+$/, '');
|
||||
}).join('\n');
|
||||
if (from_emulator != expectedRightTrimmed) {
|
||||
// uncomment to get noisy output
|
||||
throw new Error(formatError(in_file, from_emulator, expected));
|
||||
// throw new Error('mismatch');
|
||||
}
|
||||
var from_emulator = terminalToString(xterm);
|
||||
console.log = CONSOLE_LOG;
|
||||
var expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8');
|
||||
// Some of the tests have whitespace on the right of lines, we trim all the linex
|
||||
// from xterm.js so ignore this for now at least.
|
||||
var expectedRightTrimmed = expected.split('\n').map(function (l) {
|
||||
return l.replace(/\s+$/, '');
|
||||
}).join('\n');
|
||||
if (from_emulator != expectedRightTrimmed) {
|
||||
// uncomment to get noisy output
|
||||
throw new Error(formatError(in_file, from_emulator, expected));
|
||||
// throw new Error('mismatch');
|
||||
}
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
})(files[i]);
|
||||
}
|
||||
|
||||
+156
-134
@@ -21,7 +21,8 @@ describe('xterm.js', function() {
|
||||
};
|
||||
xterm.element = {
|
||||
classList: {
|
||||
toggle: function(){}
|
||||
toggle: function(){},
|
||||
remove: function(){}
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -54,19 +55,24 @@ describe('xterm.js', function() {
|
||||
it('should throw when setting a non-existant option', function() {
|
||||
assert.throws(xterm.setOption.bind(xterm, 'fake', true));
|
||||
});
|
||||
it('should not allow scrollback less than number of rows', function() {
|
||||
let setOptionCall = xterm.setOption.bind(xterm, 'scrollback', xterm.rows - 1);
|
||||
|
||||
assert.equal(setOptionCall(), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', function() {
|
||||
it('should clear a buffer equal to rows', function() {
|
||||
var promptLine = xterm.lines.get(xterm.ybase + xterm.y);
|
||||
var promptLine = xterm.buffer.lines.get(xterm.buffer.ybase + xterm.buffer.y);
|
||||
xterm.clear();
|
||||
assert.equal(xterm.y, 0);
|
||||
assert.equal(xterm.ybase, 0);
|
||||
assert.equal(xterm.ydisp, 0);
|
||||
assert.equal(xterm.lines.length, xterm.rows);
|
||||
assert.deepEqual(xterm.lines.get(0), promptLine);
|
||||
assert.equal(xterm.buffer.y, 0);
|
||||
assert.equal(xterm.buffer.ybase, 0);
|
||||
assert.equal(xterm.buffer.ydisp, 0);
|
||||
assert.equal(xterm.buffer.lines.length, xterm.rows);
|
||||
assert.deepEqual(xterm.buffer.lines.get(0), promptLine);
|
||||
for (var i = 1; i < xterm.rows; i++) {
|
||||
assert.deepEqual(xterm.lines.get(i), xterm.blankLine());
|
||||
assert.deepEqual(xterm.buffer.lines.get(i), xterm.blankLine());
|
||||
}
|
||||
});
|
||||
it('should clear a buffer larger than rows', function() {
|
||||
@@ -75,28 +81,28 @@ describe('xterm.js', function() {
|
||||
xterm.write('test\n');
|
||||
}
|
||||
|
||||
var promptLine = xterm.lines.get(xterm.ybase + xterm.y);
|
||||
var promptLine = xterm.buffer.lines.get(xterm.buffer.ybase + xterm.buffer.y);
|
||||
xterm.clear();
|
||||
assert.equal(xterm.y, 0);
|
||||
assert.equal(xterm.ybase, 0);
|
||||
assert.equal(xterm.ydisp, 0);
|
||||
assert.equal(xterm.lines.length, xterm.rows);
|
||||
assert.deepEqual(xterm.lines.get(0), promptLine);
|
||||
assert.equal(xterm.buffer.y, 0);
|
||||
assert.equal(xterm.buffer.ybase, 0);
|
||||
assert.equal(xterm.buffer.ydisp, 0);
|
||||
assert.equal(xterm.buffer.lines.length, xterm.rows);
|
||||
assert.deepEqual(xterm.buffer.lines.get(0), promptLine);
|
||||
for (var i = 1; i < xterm.rows; i++) {
|
||||
assert.deepEqual(xterm.lines.get(i), xterm.blankLine());
|
||||
assert.deepEqual(xterm.buffer.lines.get(i), xterm.blankLine());
|
||||
}
|
||||
});
|
||||
it('should not break the prompt when cleared twice', function() {
|
||||
var promptLine = xterm.lines.get(xterm.ybase + xterm.y);
|
||||
var promptLine = xterm.buffer.lines.get(xterm.buffer.ybase + xterm.buffer.y);
|
||||
xterm.clear();
|
||||
xterm.clear();
|
||||
assert.equal(xterm.y, 0);
|
||||
assert.equal(xterm.ybase, 0);
|
||||
assert.equal(xterm.ydisp, 0);
|
||||
assert.equal(xterm.lines.length, xterm.rows);
|
||||
assert.deepEqual(xterm.lines.get(0), promptLine);
|
||||
assert.equal(xterm.buffer.y, 0);
|
||||
assert.equal(xterm.buffer.ybase, 0);
|
||||
assert.equal(xterm.buffer.ydisp, 0);
|
||||
assert.equal(xterm.buffer.lines.length, xterm.rows);
|
||||
assert.deepEqual(xterm.buffer.lines.get(0), promptLine);
|
||||
for (var i = 1; i < xterm.rows; i++) {
|
||||
assert.deepEqual(xterm.lines.get(i), xterm.blankLine());
|
||||
assert.deepEqual(xterm.buffer.lines.get(i), xterm.blankLine());
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -111,29 +117,29 @@ describe('xterm.js', function() {
|
||||
startYDisp = xterm.rows + 1;
|
||||
});
|
||||
it('should scroll a single line', function() {
|
||||
assert.equal(xterm.ydisp, startYDisp);
|
||||
assert.equal(xterm.buffer.ydisp, startYDisp);
|
||||
xterm.scrollDisp(-1);
|
||||
assert.equal(xterm.ydisp, startYDisp - 1);
|
||||
assert.equal(xterm.buffer.ydisp, startYDisp - 1);
|
||||
xterm.scrollDisp(1);
|
||||
assert.equal(xterm.ydisp, startYDisp);
|
||||
assert.equal(xterm.buffer.ydisp, startYDisp);
|
||||
});
|
||||
it('should scroll multiple lines', function() {
|
||||
assert.equal(xterm.ydisp, startYDisp);
|
||||
assert.equal(xterm.buffer.ydisp, startYDisp);
|
||||
xterm.scrollDisp(-5);
|
||||
assert.equal(xterm.ydisp, startYDisp - 5);
|
||||
assert.equal(xterm.buffer.ydisp, startYDisp - 5);
|
||||
xterm.scrollDisp(5);
|
||||
assert.equal(xterm.ydisp, startYDisp);
|
||||
assert.equal(xterm.buffer.ydisp, startYDisp);
|
||||
});
|
||||
it('should not scroll beyond the bounds of the buffer', function() {
|
||||
assert.equal(xterm.ydisp, startYDisp);
|
||||
assert.equal(xterm.buffer.ydisp, startYDisp);
|
||||
xterm.scrollDisp(1);
|
||||
assert.equal(xterm.ydisp, startYDisp);
|
||||
assert.equal(xterm.buffer.ydisp, startYDisp);
|
||||
for (var i = 0; i < startYDisp; i++) {
|
||||
xterm.scrollDisp(-1);
|
||||
}
|
||||
assert.equal(xterm.ydisp, 0);
|
||||
assert.equal(xterm.buffer.ydisp, 0);
|
||||
xterm.scrollDisp(-1);
|
||||
assert.equal(xterm.ydisp, 0);
|
||||
assert.equal(xterm.buffer.ydisp, 0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -146,18 +152,18 @@ describe('xterm.js', function() {
|
||||
startYDisp = (xterm.rows * 2) + 1;
|
||||
});
|
||||
it('should scroll a single page', function() {
|
||||
assert.equal(xterm.ydisp, startYDisp);
|
||||
assert.equal(xterm.buffer.ydisp, startYDisp);
|
||||
xterm.scrollPages(-1);
|
||||
assert.equal(xterm.ydisp, startYDisp - (xterm.rows - 1));
|
||||
assert.equal(xterm.buffer.ydisp, startYDisp - (xterm.rows - 1));
|
||||
xterm.scrollPages(1);
|
||||
assert.equal(xterm.ydisp, startYDisp);
|
||||
assert.equal(xterm.buffer.ydisp, startYDisp);
|
||||
});
|
||||
it('should scroll a multiple pages', function() {
|
||||
assert.equal(xterm.ydisp, startYDisp);
|
||||
assert.equal(xterm.buffer.ydisp, startYDisp);
|
||||
xterm.scrollPages(-2);
|
||||
assert.equal(xterm.ydisp, startYDisp - (xterm.rows - 1) * 2);
|
||||
assert.equal(xterm.buffer.ydisp, startYDisp - (xterm.rows - 1) * 2);
|
||||
xterm.scrollPages(2);
|
||||
assert.equal(xterm.ydisp, startYDisp);
|
||||
assert.equal(xterm.buffer.ydisp, startYDisp);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -168,9 +174,9 @@ describe('xterm.js', function() {
|
||||
}
|
||||
});
|
||||
it('should scroll to the top', function() {
|
||||
assert.notEqual(xterm.ydisp, 0);
|
||||
assert.notEqual(xterm.buffer.ydisp, 0);
|
||||
xterm.scrollToTop();
|
||||
assert.equal(xterm.ydisp, 0);
|
||||
assert.equal(xterm.buffer.ydisp, 0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -185,13 +191,13 @@ describe('xterm.js', function() {
|
||||
it('should scroll to the bottom', function() {
|
||||
xterm.scrollDisp(-1);
|
||||
xterm.scrollToBottom();
|
||||
assert.equal(xterm.ydisp, startYDisp);
|
||||
assert.equal(xterm.buffer.ydisp, startYDisp);
|
||||
xterm.scrollPages(-1);
|
||||
xterm.scrollToBottom();
|
||||
assert.equal(xterm.ydisp, startYDisp);
|
||||
assert.equal(xterm.buffer.ydisp, startYDisp);
|
||||
xterm.scrollToTop();
|
||||
xterm.scrollToBottom();
|
||||
assert.equal(xterm.ydisp, startYDisp);
|
||||
assert.equal(xterm.buffer.ydisp, startYDisp);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -208,13 +214,13 @@ describe('xterm.js', function() {
|
||||
stopPropagation: function(){}
|
||||
};
|
||||
|
||||
xterm.ydisp = 0;
|
||||
xterm.ybase = 40;
|
||||
assert.notEqual(xterm.ydisp, xterm.ybase);
|
||||
xterm.buffer.ydisp = 0;
|
||||
xterm.buffer.ybase = 40;
|
||||
assert.notEqual(xterm.buffer.ydisp, xterm.buffer.ybase);
|
||||
xterm.keyDown(event);
|
||||
|
||||
// Ensure that now the terminal is scrolled to bottom
|
||||
assert.equal(xterm.ydisp, xterm.ybase);
|
||||
assert.equal(xterm.buffer.ydisp, xterm.buffer.ybase);
|
||||
});
|
||||
|
||||
it('should not scroll down, when a custom keydown handler prevents the event', function () {
|
||||
@@ -227,11 +233,11 @@ describe('xterm.js', function() {
|
||||
return false;
|
||||
});
|
||||
|
||||
assert.equal(xterm.ydisp, startYDisp);
|
||||
assert.equal(xterm.buffer.ydisp, startYDisp);
|
||||
xterm.scrollDisp(-1);
|
||||
assert.equal(xterm.ydisp, startYDisp - 1);
|
||||
assert.equal(xterm.buffer.ydisp, startYDisp - 1);
|
||||
xterm.keyDown({ keyCode: 0 });
|
||||
assert.equal(xterm.ydisp, startYDisp - 1);
|
||||
assert.equal(xterm.buffer.ydisp, startYDisp - 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -370,12 +376,17 @@ describe('xterm.js', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('attachCustomEventHandler', function () {
|
||||
describe('attachCustomKeyEventHandler', function () {
|
||||
var evKeyDown = {
|
||||
preventDefault: function() {},
|
||||
stopPropagation: function() {},
|
||||
type: 'keydown'
|
||||
}
|
||||
var evKeyPress = {
|
||||
preventDefault: function() {},
|
||||
stopPropagation: function() {},
|
||||
type: 'keypress'
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
xterm.handler = function() {};
|
||||
@@ -386,29 +397,39 @@ describe('xterm.js', function() {
|
||||
bind: function() {
|
||||
return function () { return true; }
|
||||
}
|
||||
},
|
||||
keypress: {
|
||||
bind: function() {
|
||||
return function () { return true; }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should process the keydown event based on what the handler returns', function () {
|
||||
it('should process the keydown/keypress event based on what the handler returns', function () {
|
||||
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), true);
|
||||
xterm.attachCustomKeydownHandler(function (ev) {
|
||||
assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), true);
|
||||
xterm.attachCustomKeyEventHandler(function (ev) {
|
||||
return ev.keyCode === 77;
|
||||
});
|
||||
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), true);
|
||||
xterm.attachCustomKeydownHandler(function (ev) {
|
||||
assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), true);
|
||||
xterm.attachCustomKeyEventHandler(function (ev) {
|
||||
return ev.keyCode !== 77;
|
||||
});
|
||||
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false);
|
||||
assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), false);
|
||||
});
|
||||
|
||||
it('should alive after reset(ESC c Full Reset (RIS))', function () {
|
||||
xterm.attachCustomKeydownHandler(function (ev) {
|
||||
xterm.attachCustomKeyEventHandler(function (ev) {
|
||||
return ev.keyCode !== 77;
|
||||
});
|
||||
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false);
|
||||
assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), false);
|
||||
xterm.reset();
|
||||
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false);
|
||||
assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -550,51 +571,52 @@ describe('xterm.js', function() {
|
||||
|
||||
describe('unicode - surrogates', function() {
|
||||
it('2 characters per cell', function () {
|
||||
this.timeout(10000); // This is needed because istanbul patches code and slows it down
|
||||
var high = String.fromCharCode(0xD800);
|
||||
for (var i=0xDC00; i<=0xDCFF; ++i) {
|
||||
xterm.write(high + String.fromCharCode(i));
|
||||
var tchar = xterm.lines.get(0)[0];
|
||||
var tchar = xterm.buffer.lines.get(0)[0];
|
||||
expect(tchar[1]).eql(high + String.fromCharCode(i));
|
||||
expect(tchar[1].length).eql(2);
|
||||
expect(tchar[2]).eql(1);
|
||||
expect(xterm.lines.get(0)[1][1]).eql(' ');
|
||||
expect(xterm.buffer.lines.get(0)[1][1]).eql(' ');
|
||||
xterm.reset();
|
||||
}
|
||||
});
|
||||
it('2 characters at last cell', function() {
|
||||
var high = String.fromCharCode(0xD800);
|
||||
for (var i=0xDC00; i<=0xDCFF; ++i) {
|
||||
xterm.x = xterm.cols - 1;
|
||||
xterm.buffer.x = xterm.cols - 1;
|
||||
xterm.write(high + String.fromCharCode(i));
|
||||
expect(xterm.lines.get(0)[xterm.x-1][1]).eql(high + String.fromCharCode(i));
|
||||
expect(xterm.lines.get(0)[xterm.x-1][1].length).eql(2);
|
||||
expect(xterm.lines.get(1)[0][1]).eql(' ');
|
||||
expect(xterm.buffer.lines.get(0)[xterm.buffer.x-1][1]).eql(high + String.fromCharCode(i));
|
||||
expect(xterm.buffer.lines.get(0)[xterm.buffer.x-1][1].length).eql(2);
|
||||
expect(xterm.buffer.lines.get(1)[0][1]).eql(' ');
|
||||
xterm.reset();
|
||||
}
|
||||
});
|
||||
it('2 characters per cell over line end with autowrap', function() {
|
||||
var high = String.fromCharCode(0xD800);
|
||||
for (var i=0xDC00; i<=0xDCFF; ++i) {
|
||||
xterm.x = xterm.cols - 1;
|
||||
xterm.buffer.x = xterm.cols - 1;
|
||||
xterm.wraparoundMode = true;
|
||||
xterm.write('a' + high + String.fromCharCode(i));
|
||||
expect(xterm.lines.get(0)[xterm.cols-1][1]).eql('a');
|
||||
expect(xterm.lines.get(1)[0][1]).eql(high + String.fromCharCode(i));
|
||||
expect(xterm.lines.get(1)[0][1].length).eql(2);
|
||||
expect(xterm.lines.get(1)[1][1]).eql(' ');
|
||||
expect(xterm.buffer.lines.get(0)[xterm.cols-1][1]).eql('a');
|
||||
expect(xterm.buffer.lines.get(1)[0][1]).eql(high + String.fromCharCode(i));
|
||||
expect(xterm.buffer.lines.get(1)[0][1].length).eql(2);
|
||||
expect(xterm.buffer.lines.get(1)[1][1]).eql(' ');
|
||||
xterm.reset();
|
||||
}
|
||||
});
|
||||
it('2 characters per cell over line end without autowrap', function() {
|
||||
var high = String.fromCharCode(0xD800);
|
||||
for (var i=0xDC00; i<=0xDCFF; ++i) {
|
||||
xterm.x = xterm.cols - 1;
|
||||
xterm.buffer.x = xterm.cols - 1;
|
||||
xterm.wraparoundMode = false;
|
||||
xterm.write('a' + high + String.fromCharCode(i));
|
||||
// auto wraparound mode should cut off the rest of the line
|
||||
expect(xterm.lines.get(0)[xterm.cols-1][1]).eql('a');
|
||||
expect(xterm.lines.get(0)[xterm.cols-1][1].length).eql(1);
|
||||
expect(xterm.lines.get(1)[1][1]).eql(' ');
|
||||
expect(xterm.buffer.lines.get(0)[xterm.cols-1][1]).eql('a');
|
||||
expect(xterm.buffer.lines.get(0)[xterm.cols-1][1].length).eql(1);
|
||||
expect(xterm.buffer.lines.get(1)[1][1]).eql(' ');
|
||||
xterm.reset();
|
||||
}
|
||||
});
|
||||
@@ -603,11 +625,11 @@ describe('xterm.js', function() {
|
||||
for (var i=0xDC00; i<=0xDCFF; ++i) {
|
||||
xterm.write(high);
|
||||
xterm.write(String.fromCharCode(i));
|
||||
var tchar = xterm.lines.get(0)[0];
|
||||
var tchar = xterm.buffer.lines.get(0)[0];
|
||||
expect(tchar[1]).eql(high + String.fromCharCode(i));
|
||||
expect(tchar[1].length).eql(2);
|
||||
expect(tchar[2]).eql(1);
|
||||
expect(xterm.lines.get(0)[1][1]).eql(' ');
|
||||
expect(xterm.buffer.lines.get(0)[1][1]).eql(' ');
|
||||
xterm.reset();
|
||||
}
|
||||
});
|
||||
@@ -616,30 +638,30 @@ describe('xterm.js', function() {
|
||||
describe('unicode - combining characters', function() {
|
||||
it('café', function () {
|
||||
xterm.write('cafe\u0301');
|
||||
expect(xterm.lines.get(0)[3][1]).eql('e\u0301');
|
||||
expect(xterm.lines.get(0)[3][1].length).eql(2);
|
||||
expect(xterm.lines.get(0)[3][2]).eql(1);
|
||||
expect(xterm.buffer.lines.get(0)[3][1]).eql('e\u0301');
|
||||
expect(xterm.buffer.lines.get(0)[3][1].length).eql(2);
|
||||
expect(xterm.buffer.lines.get(0)[3][2]).eql(1);
|
||||
});
|
||||
it('café - end of line', function() {
|
||||
xterm.x = xterm.cols - 1 - 3;
|
||||
xterm.buffer.x = xterm.cols - 1 - 3;
|
||||
xterm.write('cafe\u0301');
|
||||
expect(xterm.lines.get(0)[xterm.cols-1][1]).eql('e\u0301');
|
||||
expect(xterm.lines.get(0)[xterm.cols-1][1].length).eql(2);
|
||||
expect(xterm.lines.get(0)[xterm.cols-1][2]).eql(1);
|
||||
expect(xterm.lines.get(0)[1][1]).eql(' ');
|
||||
expect(xterm.lines.get(0)[1][1].length).eql(1);
|
||||
expect(xterm.lines.get(0)[1][2]).eql(1);
|
||||
expect(xterm.buffer.lines.get(0)[xterm.cols-1][1]).eql('e\u0301');
|
||||
expect(xterm.buffer.lines.get(0)[xterm.cols-1][1].length).eql(2);
|
||||
expect(xterm.buffer.lines.get(0)[xterm.cols-1][2]).eql(1);
|
||||
expect(xterm.buffer.lines.get(0)[1][1]).eql(' ');
|
||||
expect(xterm.buffer.lines.get(0)[1][1].length).eql(1);
|
||||
expect(xterm.buffer.lines.get(0)[1][2]).eql(1);
|
||||
});
|
||||
it('multiple combined é', function() {
|
||||
xterm.wraparoundMode = true;
|
||||
xterm.write(Array(100).join('e\u0301'));
|
||||
for (var i=0; i<xterm.cols; ++i) {
|
||||
var tchar = xterm.lines.get(0)[i];
|
||||
var tchar = xterm.buffer.lines.get(0)[i];
|
||||
expect(tchar[1]).eql('e\u0301');
|
||||
expect(tchar[1].length).eql(2);
|
||||
expect(tchar[2]).eql(1);
|
||||
}
|
||||
tchar = xterm.lines.get(1)[0];
|
||||
tchar = xterm.buffer.lines.get(1)[0];
|
||||
expect(tchar[1]).eql('e\u0301');
|
||||
expect(tchar[1].length).eql(2);
|
||||
expect(tchar[2]).eql(1);
|
||||
@@ -648,12 +670,12 @@ describe('xterm.js', function() {
|
||||
xterm.wraparoundMode = true;
|
||||
xterm.write(Array(100).join('\uD800\uDC00\u0301'));
|
||||
for (var i=0; i<xterm.cols; ++i) {
|
||||
var tchar = xterm.lines.get(0)[i];
|
||||
var tchar = xterm.buffer.lines.get(0)[i];
|
||||
expect(tchar[1]).eql('\uD800\uDC00\u0301');
|
||||
expect(tchar[1].length).eql(3);
|
||||
expect(tchar[2]).eql(1);
|
||||
}
|
||||
tchar = xterm.lines.get(1)[0];
|
||||
tchar = xterm.buffer.lines.get(1)[0];
|
||||
expect(tchar[1]).eql('\uD800\uDC00\u0301');
|
||||
expect(tchar[1].length).eql(3);
|
||||
expect(tchar[2]).eql(1);
|
||||
@@ -662,21 +684,21 @@ describe('xterm.js', function() {
|
||||
|
||||
describe('unicode - fullwidth characters', function() {
|
||||
it('cursor movement even', function() {
|
||||
expect(xterm.x).eql(0);
|
||||
expect(xterm.buffer.x).eql(0);
|
||||
xterm.write('¥');
|
||||
expect(xterm.x).eql(2);
|
||||
expect(xterm.buffer.x).eql(2);
|
||||
});
|
||||
it('cursor movement odd', function() {
|
||||
xterm.x = 1;
|
||||
expect(xterm.x).eql(1);
|
||||
xterm.buffer.x = 1;
|
||||
expect(xterm.buffer.x).eql(1);
|
||||
xterm.write('¥');
|
||||
expect(xterm.x).eql(3);
|
||||
expect(xterm.buffer.x).eql(3);
|
||||
});
|
||||
it('line of ¥ even', function() {
|
||||
xterm.wraparoundMode = true;
|
||||
xterm.write(Array(50).join('¥'));
|
||||
for (var i=0; i<xterm.cols; ++i) {
|
||||
var tchar = xterm.lines.get(0)[i];
|
||||
var tchar = xterm.buffer.lines.get(0)[i];
|
||||
if (i % 2) {
|
||||
expect(tchar[1]).eql('');
|
||||
expect(tchar[1].length).eql(0);
|
||||
@@ -687,17 +709,17 @@ describe('xterm.js', function() {
|
||||
expect(tchar[2]).eql(2);
|
||||
}
|
||||
}
|
||||
tchar = xterm.lines.get(1)[0];
|
||||
tchar = xterm.buffer.lines.get(1)[0];
|
||||
expect(tchar[1]).eql('¥');
|
||||
expect(tchar[1].length).eql(1);
|
||||
expect(tchar[2]).eql(2);
|
||||
});
|
||||
it('line of ¥ odd', function() {
|
||||
xterm.wraparoundMode = true;
|
||||
xterm.x = 1;
|
||||
xterm.buffer.x = 1;
|
||||
xterm.write(Array(50).join('¥'));
|
||||
for (var i=1; i<xterm.cols-1; ++i) {
|
||||
var tchar = xterm.lines.get(0)[i];
|
||||
var tchar = xterm.buffer.lines.get(0)[i];
|
||||
if (!(i % 2)) {
|
||||
expect(tchar[1]).eql('');
|
||||
expect(tchar[1].length).eql(0);
|
||||
@@ -708,21 +730,21 @@ describe('xterm.js', function() {
|
||||
expect(tchar[2]).eql(2);
|
||||
}
|
||||
}
|
||||
tchar = xterm.lines.get(0)[xterm.cols-1];
|
||||
tchar = xterm.buffer.lines.get(0)[xterm.cols-1];
|
||||
expect(tchar[1]).eql(' ');
|
||||
expect(tchar[1].length).eql(1);
|
||||
expect(tchar[2]).eql(1);
|
||||
tchar = xterm.lines.get(1)[0];
|
||||
tchar = xterm.buffer.lines.get(1)[0];
|
||||
expect(tchar[1]).eql('¥');
|
||||
expect(tchar[1].length).eql(1);
|
||||
expect(tchar[2]).eql(2);
|
||||
});
|
||||
it('line of ¥ with combining odd', function() {
|
||||
xterm.wraparoundMode = true;
|
||||
xterm.x = 1;
|
||||
xterm.buffer.x = 1;
|
||||
xterm.write(Array(50).join('¥\u0301'));
|
||||
for (var i=1; i<xterm.cols-1; ++i) {
|
||||
var tchar = xterm.lines.get(0)[i];
|
||||
var tchar = xterm.buffer.lines.get(0)[i];
|
||||
if (!(i % 2)) {
|
||||
expect(tchar[1]).eql('');
|
||||
expect(tchar[1].length).eql(0);
|
||||
@@ -733,11 +755,11 @@ describe('xterm.js', function() {
|
||||
expect(tchar[2]).eql(2);
|
||||
}
|
||||
}
|
||||
tchar = xterm.lines.get(0)[xterm.cols-1];
|
||||
tchar = xterm.buffer.lines.get(0)[xterm.cols-1];
|
||||
expect(tchar[1]).eql(' ');
|
||||
expect(tchar[1].length).eql(1);
|
||||
expect(tchar[2]).eql(1);
|
||||
tchar = xterm.lines.get(1)[0];
|
||||
tchar = xterm.buffer.lines.get(1)[0];
|
||||
expect(tchar[1]).eql('¥\u0301');
|
||||
expect(tchar[1].length).eql(2);
|
||||
expect(tchar[2]).eql(2);
|
||||
@@ -746,7 +768,7 @@ describe('xterm.js', function() {
|
||||
xterm.wraparoundMode = true;
|
||||
xterm.write(Array(50).join('¥\u0301'));
|
||||
for (var i=0; i<xterm.cols; ++i) {
|
||||
var tchar = xterm.lines.get(0)[i];
|
||||
var tchar = xterm.buffer.lines.get(0)[i];
|
||||
if (i % 2) {
|
||||
expect(tchar[1]).eql('');
|
||||
expect(tchar[1].length).eql(0);
|
||||
@@ -757,17 +779,17 @@ describe('xterm.js', function() {
|
||||
expect(tchar[2]).eql(2);
|
||||
}
|
||||
}
|
||||
tchar = xterm.lines.get(1)[0];
|
||||
tchar = xterm.buffer.lines.get(1)[0];
|
||||
expect(tchar[1]).eql('¥\u0301');
|
||||
expect(tchar[1].length).eql(2);
|
||||
expect(tchar[2]).eql(2);
|
||||
});
|
||||
it('line of surrogate fullwidth with combining odd', function() {
|
||||
xterm.wraparoundMode = true;
|
||||
xterm.x = 1;
|
||||
xterm.buffer.x = 1;
|
||||
xterm.write(Array(50).join('\ud843\ude6d\u0301'));
|
||||
for (var i=1; i<xterm.cols-1; ++i) {
|
||||
var tchar = xterm.lines.get(0)[i];
|
||||
var tchar = xterm.buffer.lines.get(0)[i];
|
||||
if (!(i % 2)) {
|
||||
expect(tchar[1]).eql('');
|
||||
expect(tchar[1].length).eql(0);
|
||||
@@ -778,11 +800,11 @@ describe('xterm.js', function() {
|
||||
expect(tchar[2]).eql(2);
|
||||
}
|
||||
}
|
||||
tchar = xterm.lines.get(0)[xterm.cols-1];
|
||||
tchar = xterm.buffer.lines.get(0)[xterm.cols-1];
|
||||
expect(tchar[1]).eql(' ');
|
||||
expect(tchar[1].length).eql(1);
|
||||
expect(tchar[2]).eql(1);
|
||||
tchar = xterm.lines.get(1)[0];
|
||||
tchar = xterm.buffer.lines.get(1)[0];
|
||||
expect(tchar[1]).eql('\ud843\ude6d\u0301');
|
||||
expect(tchar[1].length).eql(3);
|
||||
expect(tchar[2]).eql(2);
|
||||
@@ -791,7 +813,7 @@ describe('xterm.js', function() {
|
||||
xterm.wraparoundMode = true;
|
||||
xterm.write(Array(50).join('\ud843\ude6d\u0301'));
|
||||
for (var i=0; i<xterm.cols; ++i) {
|
||||
var tchar = xterm.lines.get(0)[i];
|
||||
var tchar = xterm.buffer.lines.get(0)[i];
|
||||
if (i % 2) {
|
||||
expect(tchar[1]).eql('');
|
||||
expect(tchar[1].length).eql(0);
|
||||
@@ -802,7 +824,7 @@ describe('xterm.js', function() {
|
||||
expect(tchar[2]).eql(2);
|
||||
}
|
||||
}
|
||||
tchar = xterm.lines.get(1)[0];
|
||||
tchar = xterm.buffer.lines.get(1)[0];
|
||||
expect(tchar[1]).eql('\ud843\ude6d\u0301');
|
||||
expect(tchar[1].length).eql(3);
|
||||
expect(tchar[2]).eql(2);
|
||||
@@ -812,44 +834,44 @@ describe('xterm.js', function() {
|
||||
describe('insert mode', function() {
|
||||
it('halfwidth - all', function () {
|
||||
xterm.write(Array(9).join('0123456789').slice(-80));
|
||||
xterm.x = 10;
|
||||
xterm.y = 0;
|
||||
xterm.buffer.x = 10;
|
||||
xterm.buffer.y = 0;
|
||||
xterm.insertMode = true;
|
||||
xterm.write('abcde');
|
||||
expect(xterm.lines.get(0).length).eql(xterm.cols);
|
||||
expect(xterm.lines.get(0)[10][1]).eql('a');
|
||||
expect(xterm.lines.get(0)[14][1]).eql('e');
|
||||
expect(xterm.lines.get(0)[15][1]).eql('0');
|
||||
expect(xterm.lines.get(0)[79][1]).eql('4');
|
||||
expect(xterm.buffer.lines.get(0).length).eql(xterm.cols);
|
||||
expect(xterm.buffer.lines.get(0)[10][1]).eql('a');
|
||||
expect(xterm.buffer.lines.get(0)[14][1]).eql('e');
|
||||
expect(xterm.buffer.lines.get(0)[15][1]).eql('0');
|
||||
expect(xterm.buffer.lines.get(0)[79][1]).eql('4');
|
||||
});
|
||||
it('fullwidth - insert', function() {
|
||||
xterm.write(Array(9).join('0123456789').slice(-80));
|
||||
xterm.x = 10;
|
||||
xterm.y = 0;
|
||||
xterm.buffer.x = 10;
|
||||
xterm.buffer.y = 0;
|
||||
xterm.insertMode = true;
|
||||
xterm.write('¥¥¥');
|
||||
expect(xterm.lines.get(0).length).eql(xterm.cols);
|
||||
expect(xterm.lines.get(0)[10][1]).eql('¥');
|
||||
expect(xterm.lines.get(0)[11][1]).eql('');
|
||||
expect(xterm.lines.get(0)[14][1]).eql('¥');
|
||||
expect(xterm.lines.get(0)[15][1]).eql('');
|
||||
expect(xterm.lines.get(0)[79][1]).eql('3');
|
||||
expect(xterm.buffer.lines.get(0).length).eql(xterm.cols);
|
||||
expect(xterm.buffer.lines.get(0)[10][1]).eql('¥');
|
||||
expect(xterm.buffer.lines.get(0)[11][1]).eql('');
|
||||
expect(xterm.buffer.lines.get(0)[14][1]).eql('¥');
|
||||
expect(xterm.buffer.lines.get(0)[15][1]).eql('');
|
||||
expect(xterm.buffer.lines.get(0)[79][1]).eql('3');
|
||||
});
|
||||
it('fullwidth - right border', function() {
|
||||
xterm.write(Array(41).join('¥'));
|
||||
xterm.x = 10;
|
||||
xterm.y = 0;
|
||||
xterm.buffer.x = 10;
|
||||
xterm.buffer.y = 0;
|
||||
xterm.insertMode = true;
|
||||
xterm.write('a');
|
||||
expect(xterm.lines.get(0).length).eql(xterm.cols);
|
||||
expect(xterm.lines.get(0)[10][1]).eql('a');
|
||||
expect(xterm.lines.get(0)[11][1]).eql('¥');
|
||||
expect(xterm.lines.get(0)[79][1]).eql(' '); // fullwidth char got replaced
|
||||
expect(xterm.buffer.lines.get(0).length).eql(xterm.cols);
|
||||
expect(xterm.buffer.lines.get(0)[10][1]).eql('a');
|
||||
expect(xterm.buffer.lines.get(0)[11][1]).eql('¥');
|
||||
expect(xterm.buffer.lines.get(0)[79][1]).eql(' '); // fullwidth char got replaced
|
||||
xterm.write('b');
|
||||
expect(xterm.lines.get(0).length).eql(xterm.cols);
|
||||
expect(xterm.lines.get(0)[11][1]).eql('b');
|
||||
expect(xterm.lines.get(0)[12][1]).eql('¥');
|
||||
expect(xterm.lines.get(0)[79][1]).eql(''); // empty cell after fullwidth
|
||||
expect(xterm.buffer.lines.get(0).length).eql(xterm.cols);
|
||||
expect(xterm.buffer.lines.get(0)[11][1]).eql('b');
|
||||
expect(xterm.buffer.lines.get(0)[12][1]).eql('¥');
|
||||
expect(xterm.buffer.lines.get(0)[79][1]).eql(''); // empty cell after fullwidth
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,3 +20,4 @@ export const isMac = contains(['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], pla
|
||||
export const isIpad = platform === 'iPad';
|
||||
export const isIphone = platform === 'iPhone';
|
||||
export const isMSWindows = contains(['Windows', 'Win16', 'Win32', 'WinCE'], platform);
|
||||
export const isLinux = platform.indexOf('Linux') >= 0;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
// TODO: This module should be merged into a buffer or buffer line class
|
||||
|
||||
const LINE_DATA_CHAR_INDEX = 1;
|
||||
const LINE_DATA_WIDTH_INDEX = 2;
|
||||
|
||||
/**
|
||||
* Translates a buffer line to a string, with optional start and end columns.
|
||||
* Wide characters will count as two columns in the resulting string. This
|
||||
* function is useful for getting the actual text underneath the raw selection
|
||||
* position.
|
||||
* @param line The line being translated.
|
||||
* @param trimRight Whether to trim whitespace to the right.
|
||||
* @param startCol The column to start at.
|
||||
* @param endCol The column to end at.
|
||||
*/
|
||||
export function translateBufferLineToString(line: any, trimRight: boolean, startCol: number = 0, endCol: number = null): string {
|
||||
// Get full line
|
||||
let lineString = '';
|
||||
let widthAdjustedStartCol = startCol;
|
||||
let widthAdjustedEndCol = endCol;
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const char = line[i];
|
||||
lineString += char[LINE_DATA_CHAR_INDEX];
|
||||
// Adjust start and end cols for wide characters if they affect their
|
||||
// column indexes
|
||||
if (char[LINE_DATA_WIDTH_INDEX] === 0) {
|
||||
if (startCol >= i) {
|
||||
widthAdjustedStartCol--;
|
||||
}
|
||||
if (endCol >= i) {
|
||||
widthAdjustedEndCol--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the final end col by trimming whitespace on the right of the
|
||||
// line if needed.
|
||||
let finalEndCol = widthAdjustedEndCol || line.length;
|
||||
if (trimRight) {
|
||||
const rightWhitespaceIndex = lineString.search(/\s+$/);
|
||||
if (rightWhitespaceIndex !== -1) {
|
||||
finalEndCol = Math.min(finalEndCol, rightWhitespaceIndex);
|
||||
}
|
||||
// Return the empty string if only trimmed whitespace is selected
|
||||
if (finalEndCol <= widthAdjustedStartCol) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return lineString.substring(widthAdjustedStartCol, finalEndCol);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* @license MIT
|
||||
*/
|
||||
import jsdom = require('jsdom');
|
||||
import { assert } from 'chai';
|
||||
import { ICharMeasure, ITerminal } from '../Interfaces';
|
||||
import { CharMeasure } from './CharMeasure';
|
||||
|
||||
describe('CharMeasure', () => {
|
||||
let dom: jsdom.JSDOM;
|
||||
let window: Window;
|
||||
let document: Document;
|
||||
let container: HTMLElement;
|
||||
let charMeasure: ICharMeasure;
|
||||
|
||||
beforeEach(() => {
|
||||
dom = new jsdom.JSDOM('');
|
||||
window = dom.window;
|
||||
document = window.document;
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
charMeasure = new CharMeasure(document, container);
|
||||
});
|
||||
|
||||
describe('measure', () => {
|
||||
it('should set _measureElement on first call', () => {
|
||||
charMeasure.measure();
|
||||
assert.isDefined((<any>charMeasure)._measureElement, 'CharMeasure.measure should have created _measureElement');
|
||||
});
|
||||
|
||||
it('should be performed async on first call', done => {
|
||||
assert.equal(charMeasure.width, null);
|
||||
charMeasure.measure();
|
||||
// Mock getBoundingClientRect since jsdom doesn't have a layout engine
|
||||
(<any>charMeasure)._measureElement.getBoundingClientRect = () => {
|
||||
return { width: 1, height: 1 };
|
||||
};
|
||||
assert.equal(charMeasure.width, null);
|
||||
setTimeout(() => {
|
||||
assert.equal(charMeasure.width, 1);
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('should be performed sync on successive calls', done => {
|
||||
charMeasure.measure();
|
||||
// Mock getBoundingClientRect since jsdom doesn't have a layout engine
|
||||
(<any>charMeasure)._measureElement.getBoundingClientRect = () => {
|
||||
return { width: 1, height: 1 };
|
||||
};
|
||||
setTimeout(() => {
|
||||
const firstWidth = charMeasure.width;
|
||||
// Mock getBoundingClientRect since jsdom doesn't have a layout engine
|
||||
(<any>charMeasure)._measureElement.getBoundingClientRect = () => {
|
||||
return { width: 2, height: 2 };
|
||||
};
|
||||
charMeasure.measure();
|
||||
assert.equal(charMeasure.width, firstWidth * 2);
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('should NOT do a measure when the parent is hidden', done => {
|
||||
charMeasure.measure();
|
||||
setTimeout(() => {
|
||||
const firstWidth = charMeasure.width;
|
||||
container.style.display = 'none';
|
||||
container.style.fontSize = '2em';
|
||||
charMeasure.measure();
|
||||
assert.equal(charMeasure.width, firstWidth);
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,13 +9,15 @@ import { EventEmitter } from '../EventEmitter.js';
|
||||
* Utility class that measures the size of a character.
|
||||
*/
|
||||
export class CharMeasure extends EventEmitter {
|
||||
private _document: Document;
|
||||
private _parentElement: HTMLElement;
|
||||
private _measureElement: HTMLElement;
|
||||
private _width: number;
|
||||
private _height: number;
|
||||
|
||||
constructor(parentElement: HTMLElement) {
|
||||
constructor(document: Document, parentElement: HTMLElement) {
|
||||
super();
|
||||
this._document = document;
|
||||
this._parentElement = parentElement;
|
||||
}
|
||||
|
||||
@@ -29,11 +31,12 @@ export class CharMeasure extends EventEmitter {
|
||||
|
||||
public measure(): void {
|
||||
if (!this._measureElement) {
|
||||
this._measureElement = document.createElement('span');
|
||||
this._measureElement = this._document.createElement('span');
|
||||
this._measureElement.style.position = 'absolute';
|
||||
this._measureElement.style.top = '0';
|
||||
this._measureElement.style.left = '-9999em';
|
||||
this._measureElement.textContent = 'W';
|
||||
this._measureElement.setAttribute('aria-hidden', 'true');
|
||||
this._parentElement.appendChild(this._measureElement);
|
||||
// Perform _doMeasure async if the element was just attached as sometimes
|
||||
// getBoundingClientRect does not return accurate values without this.
|
||||
|
||||
@@ -4,12 +4,16 @@
|
||||
* @module xterm/utils/CircularList
|
||||
* @license MIT
|
||||
*/
|
||||
export class CircularList<T> {
|
||||
import { EventEmitter } from '../EventEmitter';
|
||||
import { ICircularList } from '../Interfaces';
|
||||
|
||||
export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
||||
private _array: T[];
|
||||
private _startIndex: number;
|
||||
private _length: number;
|
||||
|
||||
constructor(maxLength: number) {
|
||||
super();
|
||||
this._array = new Array<T>(maxLength);
|
||||
this._startIndex = 0;
|
||||
this._length = 0;
|
||||
@@ -43,8 +47,14 @@ export class CircularList<T> {
|
||||
this._length = newLength;
|
||||
}
|
||||
|
||||
public get forEach(): (callbackfn: (value: T, index: number, array: T[]) => void) => void {
|
||||
return this._array.forEach;
|
||||
public get forEach(): (callbackfn: (value: T, index: number) => void) => void {
|
||||
return (callbackfn: (value: T, index: number) => void) => {
|
||||
let i = 0;
|
||||
let length = this.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
callbackfn(this.get(i), i);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,6 +93,7 @@ export class CircularList<T> {
|
||||
if (this._startIndex === this.maxLength) {
|
||||
this._startIndex = 0;
|
||||
}
|
||||
this.emit('trim', 1);
|
||||
} else {
|
||||
this._length++;
|
||||
}
|
||||
@@ -106,13 +117,16 @@ export class CircularList<T> {
|
||||
* @param items The items to insert.
|
||||
*/
|
||||
public splice(start: number, deleteCount: number, ...items: T[]): void {
|
||||
// Delete items
|
||||
if (deleteCount) {
|
||||
for (let i = start; i < this._length - deleteCount; i++) {
|
||||
this._array[this._getCyclicIndex(i)] = this._array[this._getCyclicIndex(i + deleteCount)];
|
||||
}
|
||||
this._length -= deleteCount;
|
||||
}
|
||||
|
||||
if (items && items.length) {
|
||||
// Add items
|
||||
for (let i = this._length - 1; i >= start; i--) {
|
||||
this._array[this._getCyclicIndex(i + items.length)] = this._array[this._getCyclicIndex(i)];
|
||||
}
|
||||
@@ -120,9 +134,12 @@ export class CircularList<T> {
|
||||
this._array[this._getCyclicIndex(start + i)] = items[i];
|
||||
}
|
||||
|
||||
// Adjust length as needed
|
||||
if (this._length + items.length > this.maxLength) {
|
||||
this._startIndex += (this._length + items.length) - this.maxLength;
|
||||
const countToTrim = (this._length + items.length) - this.maxLength;
|
||||
this._startIndex += countToTrim;
|
||||
this._length = this.maxLength;
|
||||
this.emit('trim', countToTrim);
|
||||
} else {
|
||||
this._length += items.length;
|
||||
}
|
||||
@@ -139,6 +156,7 @@ export class CircularList<T> {
|
||||
}
|
||||
this._startIndex += count;
|
||||
this._length -= count;
|
||||
this.emit('trim', count);
|
||||
}
|
||||
|
||||
public shiftElements(start: number, count: number, offset: number): void {
|
||||
@@ -162,6 +180,7 @@ export class CircularList<T> {
|
||||
while (this._length > this.maxLength) {
|
||||
this._length--;
|
||||
this._startIndex++;
|
||||
this.emit('trim', 1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { assert } from 'chai';
|
||||
import { DomElementObjectPool } from './DomElementObjectPool';
|
||||
|
||||
class MockDocument {
|
||||
private _attr: {[key: string]: string} = {};
|
||||
constructor() {}
|
||||
public getAttribute(key: string): string { return this._attr[key]; };
|
||||
public setAttribute(key: string, value: string): void { this._attr[key] = value; }
|
||||
}
|
||||
|
||||
describe('DomElementObjectPool', () => {
|
||||
let pool: DomElementObjectPool;
|
||||
|
||||
beforeEach(() => {
|
||||
pool = new DomElementObjectPool('span');
|
||||
(<any>global).document = {
|
||||
createElement: () => new MockDocument()
|
||||
};
|
||||
});
|
||||
|
||||
it('should acquire distinct elements', () => {
|
||||
const element1 = pool.acquire();
|
||||
const element2 = pool.acquire();
|
||||
assert.notEqual(element1, element2);
|
||||
});
|
||||
|
||||
it('should acquire released elements', () => {
|
||||
const element = pool.acquire();
|
||||
pool.release(element);
|
||||
assert.equal(pool.acquire(), element);
|
||||
});
|
||||
|
||||
it('should handle a series of acquisitions and releases', () => {
|
||||
const element1 = pool.acquire();
|
||||
const element2 = pool.acquire();
|
||||
pool.release(element1);
|
||||
assert.equal(pool.acquire(), element1);
|
||||
pool.release(element1);
|
||||
pool.release(element2);
|
||||
assert.equal(pool.acquire(), element2);
|
||||
assert.equal(pool.acquire(), element1);
|
||||
});
|
||||
|
||||
it('should throw when releasing an element that was not acquired', () => {
|
||||
assert.throws(() => pool.release(document.createElement('span')));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* @module xterm/utils/DomElementObjectPool
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* An object pool that manages acquisition and releasing of DOM elements for
|
||||
* when reuse is desirable.
|
||||
*/
|
||||
export class DomElementObjectPool {
|
||||
private static readonly OBJECT_ID_ATTRIBUTE = 'data-obj-id';
|
||||
|
||||
private static _objectCount = 0;
|
||||
|
||||
private _type: string;
|
||||
private _pool: HTMLElement[];
|
||||
private _inUse: {[key: string]: HTMLElement};
|
||||
|
||||
/**
|
||||
* @param type The DOM element type (div, span, etc.).
|
||||
*/
|
||||
constructor(private type: string) {
|
||||
this._type = type;
|
||||
this._pool = [];
|
||||
this._inUse = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire an element from the pool, creating it if the pool is empty.
|
||||
*/
|
||||
public acquire(): HTMLElement {
|
||||
let element: HTMLElement;
|
||||
if (this._pool.length === 0) {
|
||||
element = this._createNew();
|
||||
} else {
|
||||
element = this._pool.pop();
|
||||
}
|
||||
this._inUse[element.getAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE)] = element;
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release an element back into the pool. It's up to the caller of this
|
||||
* function to ensure that all external references to the element have been
|
||||
* removed.
|
||||
* @param element The element being released.
|
||||
*/
|
||||
public release(element: HTMLElement): void {
|
||||
if (!this._inUse[element.getAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE)]) {
|
||||
throw new Error('Could not release an element not yet acquired');
|
||||
}
|
||||
delete this._inUse[element.getAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE)];
|
||||
this._cleanElement(element);
|
||||
this._pool.push(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new element for the pool.
|
||||
*/
|
||||
private _createNew(): HTMLElement {
|
||||
const element = document.createElement(this._type);
|
||||
const id = DomElementObjectPool._objectCount++;
|
||||
element.setAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE, id.toString(10));
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets an element back to a "clean state".
|
||||
* @param element The element to be cleaned.
|
||||
*/
|
||||
private _cleanElement(element: HTMLElement): void {
|
||||
element.className = '';
|
||||
element.innerHTML = '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { CharMeasure } from './CharMeasure';
|
||||
|
||||
export function getCoordsRelativeToElement(event: MouseEvent, element: HTMLElement): [number, number] {
|
||||
// Ignore browsers that don't support MouseEvent.pageX
|
||||
if (event.pageX == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let x = event.pageX;
|
||||
let y = event.pageY;
|
||||
|
||||
// Converts the coordinates from being relative to the document to being
|
||||
// relative to the terminal.
|
||||
while (element && element !== self.document.documentElement) {
|
||||
x -= element.offsetLeft;
|
||||
y -= element.offsetTop;
|
||||
element = 'offsetParent' in element ? <HTMLElement>element.offsetParent : <HTMLElement>element.parentElement;
|
||||
}
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets coordinates within the terminal for a particular mouse event. The result
|
||||
* is returned as an array in the form [x, y] instead of an object as it's a
|
||||
* little faster and this function is used in some low level code.
|
||||
* @param event The mouse event.
|
||||
* @param rowContainer The terminal's row container.
|
||||
* @param charMeasure The char measure object used to determine character sizes.
|
||||
* @param colCount The number of columns in the terminal.
|
||||
* @param rowCount The number of rows n the terminal.
|
||||
* @param isSelection Whether the request is for the selection or not. This will
|
||||
* apply an offset to the x value such that the left half of the cell will
|
||||
* select that cell and the right half will select the next cell.
|
||||
*/
|
||||
export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number] {
|
||||
// Coordinates cannot be measured if charMeasure has not been initialized
|
||||
if (!charMeasure.width || !charMeasure.height) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const coords = getCoordsRelativeToElement(event, rowContainer);
|
||||
if (!coords) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert to cols/rows.
|
||||
coords[0] = Math.ceil((coords[0] + (isSelection ? charMeasure.width / 2 : 0)) / charMeasure.width);
|
||||
coords[1] = Math.ceil(coords[1] / charMeasure.height);
|
||||
|
||||
// Ensure coordinates are within the terminal viewport.
|
||||
coords[0] = Math.min(Math.max(coords[0], 1), colCount + 1);
|
||||
coords[1] = Math.min(Math.max(coords[1], 1), rowCount + 1);
|
||||
|
||||
return coords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets coordinates within the terminal for a particular mouse event, wrapping
|
||||
* them to the bounds of the terminal and adding 32 to both the x and y values
|
||||
* as expected by xterm.
|
||||
* @param event The mouse event.
|
||||
* @param rowContainer The terminal's row container.
|
||||
* @param charMeasure The char measure object used to determine character sizes.
|
||||
* @param colCount The number of columns in the terminal.
|
||||
* @param rowCount The number of rows in the terminal.
|
||||
*/
|
||||
export function getRawByteCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, colCount: number, rowCount: number): { x: number, y: number } {
|
||||
const coords = getCoords(event, rowContainer, charMeasure, colCount, rowCount);
|
||||
let x = coords[0];
|
||||
let y = coords[1];
|
||||
|
||||
// xterm sends raw bytes and starts at 32 (SP) for each.
|
||||
x += 32;
|
||||
y += 32;
|
||||
|
||||
return { x, y };
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager } from '../Interfaces';
|
||||
|
||||
export class MockTerminal implements ITerminal {
|
||||
public element: HTMLElement;
|
||||
public rowContainer: HTMLElement;
|
||||
public selectionContainer: HTMLElement;
|
||||
public selectionManager: ISelectionManager;
|
||||
public charMeasure: ICharMeasure;
|
||||
public textarea: HTMLTextAreaElement;
|
||||
public rows: number;
|
||||
public cols: number;
|
||||
public browser: IBrowser;
|
||||
public writeBuffer: string[];
|
||||
public children: HTMLElement[];
|
||||
public cursorHidden: boolean;
|
||||
public cursorState: number;
|
||||
public defAttr: number;
|
||||
public scrollback: number;
|
||||
public buffers: IBufferSet;
|
||||
public buffer: IBuffer;
|
||||
|
||||
handler(data: string) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
on(event: string, callback: () => void) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
scrollDisp(disp: number, suppressScrollEvent: boolean) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
cancel(ev: Event, force?: boolean) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
log(text: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
emit(event: string, data: any) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
reset(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
showCursor(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
blankLine(cur?: boolean, isWrapped?: boolean, cols?: number) {
|
||||
const line = [];
|
||||
cols = cols || this.cols;
|
||||
for (let i = 0; i < cols; i++) {
|
||||
line.push([0, ' ', 1]);
|
||||
}
|
||||
return line;
|
||||
}
|
||||
}
|
||||
+56
-37
@@ -41,6 +41,9 @@
|
||||
font-family: courier-new, courier, monospace;
|
||||
font-feature-settings: "liga" 0;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.terminal.focus,
|
||||
@@ -71,63 +74,55 @@
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.terminal:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar) .terminal-cursor {
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
.terminal a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.terminal a:hover {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.terminal a.xterm-invalid-link:hover {
|
||||
cursor: text;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.terminal .terminal-cursor {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.terminal:not(.focus) .terminal-cursor {
|
||||
outline: 1px solid #fff;
|
||||
outline-offset: -1px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.terminal:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus.xterm-cursor-blink .terminal-cursor {
|
||||
animation: xterm-cursor-blink 1.2s infinite step-end;
|
||||
.terminal.xterm-cursor-style-block.focus:not(.xterm-cursor-blink-on) .terminal-cursor {
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
@keyframes xterm-cursor-blink {
|
||||
0% {
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
}
|
||||
50% {
|
||||
background-color: transparent;
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
|
||||
.terminal.xterm-cursor-style-bar .terminal-cursor,
|
||||
.terminal.xterm-cursor-style-underline .terminal-cursor {
|
||||
position: relative;
|
||||
}
|
||||
.terminal.xterm-cursor-style-bar .terminal-cursor::before,
|
||||
.terminal.xterm-cursor-style-underline .terminal-cursor::before {
|
||||
content: "";
|
||||
display: block;
|
||||
.terminal.focus.xterm-cursor-style-bar:not(.xterm-cursor-blink-on) .terminal-cursor::before,
|
||||
.terminal.focus.xterm-cursor-style-underline:not(.xterm-cursor-blink-on) .terminal-cursor::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-color: #fff;
|
||||
}
|
||||
.terminal.xterm-cursor-style-bar .terminal-cursor::before {
|
||||
|
||||
.terminal.focus.xterm-cursor-style-bar:not(.xterm-cursor-blink-on) .terminal-cursor::before {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
}
|
||||
.terminal.xterm-cursor-style-underline .terminal-cursor::before {
|
||||
|
||||
.terminal.focus.xterm-cursor-style-underline:not(.xterm-cursor-blink-on) .terminal-cursor::before {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
}
|
||||
.terminal.xterm-cursor-style-bar.focus.xterm-cursor-blink .terminal-cursor::before,
|
||||
.terminal.xterm-cursor-style-underline.focus.xterm-cursor-blink .terminal-cursor::before {
|
||||
animation: xterm-cursor-non-bar-blink 1.2s infinite step-end;
|
||||
}
|
||||
@keyframes xterm-cursor-non-bar-blink {
|
||||
0% { background-color: #fff; }
|
||||
50% { background-color: transparent; }
|
||||
}
|
||||
|
||||
.terminal .composition-view {
|
||||
background: #000;
|
||||
@@ -148,7 +143,8 @@
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.terminal .xterm-wide-char {
|
||||
.terminal .xterm-wide-char,
|
||||
.terminal .xterm-normal-char {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@@ -174,6 +170,25 @@
|
||||
left: -9999em;
|
||||
}
|
||||
|
||||
.terminal.enable-mouse-events {
|
||||
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.terminal .xterm-selection {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.terminal .xterm-selection div {
|
||||
position: absolute;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/*
|
||||
* Determine default colors for xterm.js
|
||||
*/
|
||||
@@ -189,6 +204,10 @@
|
||||
text-decoration: blink;
|
||||
}
|
||||
|
||||
.terminal .xterm-blink.xterm-underline {
|
||||
text-decoration: blink underline;
|
||||
}
|
||||
|
||||
.terminal .xterm-hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
+439
-299
File diff suppressed because it is too large
Load Diff
+1
-8
@@ -12,13 +12,6 @@
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"src/addons/**/*",
|
||||
"build",
|
||||
"demo",
|
||||
"dist",
|
||||
"out",
|
||||
"test",
|
||||
"node_modules",
|
||||
"docs"
|
||||
"src/addons/**/*"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user