Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a61642b81 | ||
|
|
0a0ebb68b5 | ||
|
|
e77a0ec886 | ||
|
|
1bf2ccc572 | ||
|
|
5f680c2185 | ||
|
|
38450082ac | ||
|
|
1801403ce2 | ||
|
|
4c7d89e67d | ||
|
|
5d3a9e864d | ||
|
|
911d92200a | ||
|
|
1a788f6363 | ||
|
|
a354f36e6f | ||
|
|
343e1c8a86 | ||
|
|
783722c2f3 | ||
|
|
44e4b56ced | ||
|
|
52044e9aad | ||
|
|
4cdf308c2f | ||
|
|
d24a27d93e | ||
|
|
55beec8b96 | ||
|
|
81dce7a4ba | ||
|
|
ddf3f08a6c | ||
|
|
8c6140c2e3 | ||
|
|
1639c0a031 | ||
|
|
fb950fcbe5 | ||
|
|
1bb465d6b0 | ||
|
|
efb939d171 | ||
|
|
fe99422ca4 | ||
|
|
d28819de41 | ||
|
|
f8e9f1813a | ||
|
|
48b8e31794 | ||
|
|
58b657bc3b | ||
|
|
bc9c9dd6fd | ||
|
|
6b16732192 | ||
|
|
5aaa8dafc5 | ||
|
|
7a652c4169 | ||
|
|
1980f675f9 | ||
|
|
98c40e3f8e | ||
|
|
6ced3014cf | ||
|
|
3e8aecaea3 | ||
|
|
e3c008ba38 | ||
|
|
d2083c5e8c | ||
|
|
0c0a34d888 | ||
|
|
cd975d22cc | ||
|
|
b5f53b341d | ||
|
|
221db55680 | ||
|
|
5cc1114594 | ||
|
|
6ad553483d | ||
|
|
49872f9377 | ||
|
|
1e17ba7a80 | ||
|
|
1c8b08d55c | ||
|
|
ca083f257c | ||
|
|
8b53b8c255 | ||
|
|
ba078ca1f9 | ||
|
|
4ededf5b06 | ||
|
|
384cd9db30 | ||
|
|
55b420b016 | ||
|
|
ddd43e0f09 | ||
|
|
41fdb96b68 | ||
|
|
e5fb255b79 | ||
|
|
7f14bfc7d9 | ||
|
|
8f97a5a786 | ||
|
|
5353808437 | ||
|
|
38f8bc5f3c | ||
|
|
48c21dc3ae | ||
|
|
bcb71e854f | ||
|
|
e3c361327c | ||
|
|
51ef27894e | ||
|
|
7edc11bd04 | ||
|
|
ebe55e5b5f | ||
|
|
3d541dd323 | ||
|
|
53f05de3e7 | ||
|
|
d1a87facfd | ||
|
|
18a2751471 | ||
|
|
59e1bddb6e | ||
|
|
d6ad4a4fc3 | ||
|
|
f9bd0b8fed | ||
|
|
ec26820ea6 | ||
|
|
d7fdf17eab | ||
|
|
a5f2ea336e | ||
|
|
de5df69333 | ||
|
|
f2b90d690f | ||
|
|
eec53261e0 | ||
|
|
eb96187801 | ||
|
|
c502272373 | ||
|
|
87bc48f5bc | ||
|
|
d7fb0141d2 | ||
|
|
2fa125904a | ||
|
|
de03391c2d | ||
|
|
a38e97f1ec | ||
|
|
5d98304249 | ||
|
|
82a6144fd1 | ||
|
|
995080c40c | ||
|
|
5ad12dff8d | ||
|
|
a458bae36d | ||
|
|
900200b871 | ||
|
|
77d5d52e71 | ||
|
|
1a50cfabe0 | ||
|
|
3abbaa9ba9 | ||
|
|
5a9f2635d3 | ||
|
|
d24c473172 | ||
|
|
da93a35671 | ||
|
|
7d4b61413e | ||
|
|
13e33a8da4 | ||
|
|
ede3d10528 | ||
|
|
64ea96cc13 | ||
|
|
47e3dc7bcd | ||
|
|
748d0cc74f | ||
|
|
343ea9bcd0 | ||
|
|
cd01157e55 | ||
|
|
4cc4b2cd42 | ||
|
|
29c1264b42 | ||
|
|
d84b71d4c6 | ||
|
|
f18528d2bb | ||
|
|
ddde738326 | ||
|
|
24919e36f0 | ||
|
|
f5223d30b8 | ||
|
|
7b4423819e | ||
|
|
372c2970e6 | ||
|
|
bc4158c9b8 | ||
|
|
8164c638e5 | ||
|
|
8b9fa324f4 | ||
|
|
0976b15709 | ||
|
|
181fb1cfe5 | ||
|
|
09aed4d339 | ||
|
|
aeb4fa0fe9 | ||
|
|
0ae5aedcf9 | ||
|
|
3d41f028e4 | ||
|
|
324ea2ecb7 | ||
|
|
999f255893 | ||
|
|
931ee9e89e | ||
|
|
c0b4edaf31 | ||
|
|
abdce874db | ||
|
|
edba006045 | ||
|
|
6b4df216d3 | ||
|
|
f502f55ae1 | ||
|
|
afe1d7ed6f | ||
|
|
1628795557 | ||
|
|
e9643f5936 | ||
|
|
ba61509fdd | ||
|
|
a67e7286d3 | ||
|
|
b85f5aacd6 | ||
|
|
de574a27fc | ||
|
|
7c87f5b47a | ||
|
|
0a26cee08b | ||
|
|
40716b8ca5 | ||
|
|
d29388394c | ||
|
|
714d4b1cd6 | ||
|
|
9f1db12b1b | ||
|
|
56c7c5c4ed | ||
|
|
13abd2aa96 | ||
|
|
10598462d0 | ||
|
|
0c6877f46f | ||
|
|
0c4ee0db65 | ||
|
|
5d09d3b217 | ||
|
|
63eb9a4555 | ||
|
|
b7c0332626 | ||
|
|
95979c4a7d | ||
|
|
5019e14753 | ||
|
|
85e0625078 | ||
|
|
873e3745ec | ||
|
|
9b19187307 | ||
|
|
218745ab85 | ||
|
|
1687d06f1c | ||
|
|
261209cab6 | ||
|
|
bb55cd2881 | ||
|
|
7b38283db0 | ||
|
|
908e84a1f8 | ||
|
|
656912c934 | ||
|
|
7fa32af051 | ||
|
|
fa92e6074d | ||
|
|
239f669041 | ||
|
|
214555085a | ||
|
|
da33543ffb | ||
|
|
e643286cd5 | ||
|
|
38f0dd9be2 | ||
|
|
6d801311cd | ||
|
|
3bd37115d1 | ||
|
|
0785a1bf24 | ||
|
|
413a2b22fa | ||
|
|
e98b6315e4 | ||
|
|
d9a33d080b | ||
|
|
85b5161380 | ||
|
|
2b916a24aa | ||
|
|
ccaf0a71c6 | ||
|
|
b3fd901699 | ||
|
|
a32e3958d8 | ||
|
|
dbd999c4a4 | ||
|
|
8158a16629 | ||
|
|
7eace4d479 | ||
|
|
e98a20ba9e | ||
|
|
d072a628ca | ||
|
|
6b26a1bcf8 | ||
|
|
8524c23deb | ||
|
|
7c5853bf00 | ||
|
|
23afd53301 | ||
|
|
1d728f7f87 | ||
|
|
5030dc3d6a | ||
|
|
47fe7b2318 | ||
|
|
51d18e533f | ||
|
|
842f4c2088 | ||
|
|
c8b0ddbc72 | ||
|
|
4319df0eeb | ||
|
|
bd3bd7fda8 | ||
|
|
7f87b46328 | ||
|
|
32ae29be37 | ||
|
|
89af5b1098 | ||
|
|
5aaed8cf0b | ||
|
|
06f3f8f267 | ||
|
|
14d22a2002 | ||
|
|
4337326169 | ||
|
|
28dcd38a53 | ||
|
|
bb2367ab4a | ||
|
|
02795f8803 | ||
|
|
8528e8e83b | ||
|
|
1f690c7703 | ||
|
|
24fb498757 | ||
|
|
7836097e56 | ||
|
|
86f80b344b | ||
|
|
49bcb969b6 | ||
|
|
b55c7e342a | ||
|
|
23ed3327ce | ||
|
|
d57329f7e5 | ||
|
|
f6a384b401 | ||
|
|
79c990d73e | ||
|
|
ecfc2d4b40 | ||
|
|
becab08b6a | ||
|
|
8ad0bd1638 | ||
|
|
544c10009c | ||
|
|
3457fb4872 | ||
|
|
8e17cace6f | ||
|
|
93f7ad86da | ||
|
|
3a0f64545e | ||
|
|
5af5fe319b | ||
|
|
b9d10f1453 | ||
|
|
14bfc0bf0e | ||
|
|
078d88fbbe | ||
|
|
dfe35a93d5 | ||
|
|
7ff27240f0 | ||
|
|
550b19f334 | ||
|
|
74409da947 | ||
|
|
1f60a398b2 | ||
|
|
21e1c5ef58 | ||
|
|
dddde8ec23 | ||
|
|
43a014e8c1 | ||
|
|
b39477d81a | ||
|
|
6ef1bdd7ce | ||
|
|
f9deb2f840 | ||
|
|
23076ba6cc | ||
|
|
fe530d92ec | ||
|
|
efda0e77a9 | ||
|
|
87dd275a51 | ||
|
|
ef3695f727 | ||
|
|
09d1a01126 | ||
|
|
7a70db709c | ||
|
|
7f131aa19a | ||
|
|
4645ef706e | ||
|
|
866ee23df9 | ||
|
|
fefdd8a4e9 | ||
|
|
7a502c6b83 | ||
|
|
90dca7d650 | ||
|
|
e8aa4eba75 | ||
|
|
64d5af38f5 | ||
|
|
4cea6b3f41 | ||
|
|
b9331ec810 | ||
|
|
97e4af3707 | ||
|
|
faa3769ed1 | ||
|
|
1098173908 | ||
|
|
8ff34a6aee | ||
|
|
bec4cb37d4 | ||
|
|
12a999dfe0 | ||
|
|
8225ecd3ba | ||
|
|
31c11c7a65 | ||
|
|
a0aae62cb7 | ||
|
|
e79c21d4b2 | ||
|
|
61e5c10fd3 | ||
|
|
fef30bdba4 | ||
|
|
61c2ca71d8 | ||
|
|
8328fcac52 | ||
|
|
dd503785be | ||
|
|
85683a457d | ||
|
|
15c2432b6f | ||
|
|
66dcd1694c | ||
|
|
8e68c67a0c | ||
|
|
d2913d607d | ||
|
|
01fd148673 | ||
|
|
47ec31361c | ||
|
|
39d1546753 | ||
|
|
b5f208294d | ||
|
|
77622f8919 | ||
|
|
816220ebd2 | ||
|
|
4a661da57b | ||
|
|
f43afde8df | ||
|
|
6c4f8b42dd | ||
|
|
a1468d0f58 | ||
|
|
c8dc679086 | ||
|
|
9bc5090bac | ||
|
|
29e9685839 | ||
|
|
02e2727fb5 | ||
|
|
a05cdbd48e | ||
|
|
8f1dcecb50 | ||
|
|
bb2193676b | ||
|
|
f5b5716582 | ||
|
|
38ccf1d5c7 | ||
|
|
ba4252bc0b | ||
|
|
900d0132cb | ||
|
|
5a6a275a53 | ||
|
|
731432a852 | ||
|
|
c70be89bc2 | ||
|
|
03bc29e0ed | ||
|
|
eb562b5d89 | ||
|
|
d60431f4bf | ||
|
|
de56d04ae5 | ||
|
|
c2145d5125 | ||
|
|
90f64ba6cc | ||
|
|
26f8fce538 | ||
|
|
19399066f1 | ||
|
|
f38d7d86ac | ||
|
|
450ee0c6a5 | ||
|
|
6899f9dc4d | ||
|
|
f92d1f46a6 | ||
|
|
12efc1a455 | ||
|
|
f4593b76df | ||
|
|
9e8e6159cf | ||
|
|
b5d0a46ccb | ||
|
|
fb13f812f7 | ||
|
|
6162ae9ffe | ||
|
|
b6e1925ba8 | ||
|
|
fd3b22e8ab | ||
|
|
a4212e98d2 | ||
|
|
e293d29fd3 | ||
|
|
1a9965ef01 | ||
|
|
05eb12e6dd | ||
|
|
40b8854df4 | ||
|
|
1bab173e99 | ||
|
|
f678f0824b | ||
|
|
8b32d0c8ba | ||
|
|
8b55497adf | ||
|
|
3870507305 | ||
|
|
a0a7abbf49 | ||
|
|
c277fb742d | ||
|
|
7f8592282f | ||
|
|
963ed59459 | ||
|
|
22d889ab07 | ||
|
|
a4f830aa5d | ||
|
|
6f5008dba6 | ||
|
|
5777fbc608 | ||
|
|
035a7f710e | ||
|
|
d92e636482 | ||
|
|
b05db37ac1 | ||
|
|
53c432df27 | ||
|
|
8ccbef850e | ||
|
|
19b212d570 | ||
|
|
a9a6920b13 | ||
|
|
9f6d4f52f3 | ||
|
|
e7baccdbcd | ||
|
|
4cc0db4273 | ||
|
|
3e283e9168 | ||
|
|
a39facfe28 | ||
|
|
1c38f859b7 | ||
|
|
cee2411fb4 | ||
|
|
fdc2bc4960 | ||
|
|
b146ffd30f | ||
|
|
45ecc837e4 | ||
|
|
5b8777267a | ||
|
|
8488f1ad63 | ||
|
|
a93f81c270 | ||
|
|
21263a5f60 | ||
|
|
1905d7d8ca | ||
|
|
adaee5e495 | ||
|
|
71649f5e40 | ||
|
|
bd21c6afa5 | ||
|
|
df929f2dd5 | ||
|
|
e7e9d5ca4c | ||
|
|
80e7963dbe | ||
|
|
f98b4ce1ba | ||
|
|
520d5dd9eb | ||
|
|
4ef5f6ae86 | ||
|
|
39f9c3507b | ||
|
|
3e47c96445 | ||
|
|
079bbc8242 | ||
|
|
bfb048110f | ||
|
|
fc09730ffb | ||
|
|
3dc4f1a7f2 | ||
|
|
65b9a6cfc4 | ||
|
|
4b615f48ff | ||
|
|
c8d1266f0a | ||
|
|
7dc3332b05 | ||
|
|
ced06629d4 | ||
|
|
3b279e4a4d | ||
|
|
9e598d9fba | ||
|
|
f025c0cf4e | ||
|
|
8adf9473b7 | ||
|
|
692df151e5 | ||
|
|
4022277cec | ||
|
|
1250ab637a | ||
|
|
a14248d710 | ||
|
|
98a03dd477 | ||
|
|
2f85448e51 | ||
|
|
b4de2787c7 | ||
|
|
e7bbc4151a | ||
|
|
453cb00807 | ||
|
|
da2c59d1d0 | ||
|
|
372b6b633b | ||
|
|
fa4479c797 | ||
|
|
96574425c7 | ||
|
|
60c11e87f6 | ||
|
|
bdfc82d1b7 | ||
|
|
167a6fdbf6 | ||
|
|
89a0cee5a4 | ||
|
|
724bbebc14 | ||
|
|
e797070121 | ||
|
|
cfd7912afa | ||
|
|
1948ec886f | ||
|
|
249e73cd25 | ||
|
|
89112fb1ae | ||
|
|
d46a685925 | ||
|
|
785a8190a6 | ||
|
|
3a499fc35d | ||
|
|
4350c7ffcd | ||
|
|
a5a9c3b368 | ||
|
|
19367a6042 | ||
|
|
2263a1579e | ||
|
|
622a5bb9c3 | ||
|
|
82706e77db | ||
|
|
e75b47ca01 | ||
|
|
1af22b24c0 | ||
|
|
85dd6a9e87 | ||
|
|
7bf12f9123 | ||
|
|
798b805fe0 | ||
|
|
f5b66775db | ||
|
|
ff6fa739c4 | ||
|
|
4799832a33 | ||
|
|
c10a451381 | ||
|
|
78c2f06107 | ||
|
|
1e8901630c | ||
|
|
f4da8af2e9 | ||
|
|
83cb7c3184 | ||
|
|
a8fbc8d7a3 | ||
|
|
c5a48043ae | ||
|
|
9b02e950f8 | ||
|
|
14366e7c40 | ||
|
|
e1c149521a | ||
|
|
f5ccf75781 | ||
|
|
771d5f580a | ||
|
|
336b45a645 | ||
|
|
0f15a78ba2 | ||
|
|
69d6d53631 | ||
|
|
2c76f9bee7 | ||
|
|
0314d95a32 | ||
|
|
d06a2e1ad7 | ||
|
|
134e1ebb5f | ||
|
|
3c5b73df8e | ||
|
|
ef0312473e | ||
|
|
7003c24126 | ||
|
|
34ec80a322 | ||
|
|
ded6750ff5 | ||
|
|
d2e34277e8 | ||
|
|
e8579bd655 | ||
|
|
a4f5cbf0cc | ||
|
|
b6937d05cd | ||
|
|
56b824d379 | ||
|
|
72f7e3fac3 | ||
|
|
d90bbdaa8a | ||
|
|
8df23582d2 | ||
|
|
e5459d7e24 | ||
|
|
b3d9104136 | ||
|
|
912284c24c | ||
|
|
dd86776c6c | ||
|
|
44bc9a1f58 | ||
|
|
7b9d775a97 | ||
|
|
1da492646c | ||
|
|
e7743c4baf | ||
|
|
cd30f54276 | ||
|
|
a923953373 | ||
|
|
c16b45cc1c | ||
|
|
365a07968b | ||
|
|
da5c24a33f | ||
|
|
92a61e6506 | ||
|
|
e08962b8b8 | ||
|
|
f839a0da8b | ||
|
|
1efdc2e4ce | ||
|
|
a4fee6d4b5 | ||
|
|
ed24e952f8 | ||
|
|
5b44deaa30 | ||
|
|
db1fd267a6 | ||
|
|
816f62cebc | ||
|
|
b8e003768d | ||
|
|
7134eaf20c | ||
|
|
b5477f71a8 | ||
|
|
f42e78d832 | ||
|
|
9847dcaacc | ||
|
|
d943aa3e5b | ||
|
|
8098fd8d01 | ||
|
|
4ca26f7e7b | ||
|
|
4da2db8e0b | ||
|
|
8dfed8f979 | ||
|
|
e720643315 | ||
|
|
40e6dba43c | ||
|
|
7d357b941f | ||
|
|
3f525fb960 | ||
|
|
de3a09e05f | ||
|
|
d8573e38a8 | ||
|
|
e44659e8bd | ||
|
|
f160436df8 | ||
|
|
06357b1184 | ||
|
|
d64cbc41d5 | ||
|
|
71bfbe434b | ||
|
|
9ed95206c1 | ||
|
|
14b71b611e | ||
|
|
881bc4f0a8 | ||
|
|
b3b9f3c7cd | ||
|
|
74246497a4 | ||
|
|
bf5fed083b | ||
|
|
c7df6e3c2e | ||
|
|
6a79017484 | ||
|
|
e5d4e96aad | ||
|
|
93bfc18237 | ||
|
|
87675649e1 | ||
|
|
e2a402d80e | ||
|
|
3e204584d9 | ||
|
|
95c9fe11fb | ||
|
|
313cb2fe09 | ||
|
|
8e75cecba3 | ||
|
|
a2721d43fd | ||
|
|
db9bdd46b4 | ||
|
|
591f32d3eb | ||
|
|
fb4b19a393 | ||
|
|
ae958c2b12 | ||
|
|
216849b135 | ||
|
|
cfe306fa32 | ||
|
|
a9b496f06f | ||
|
|
f2991a5636 | ||
|
|
53606820a7 | ||
|
|
ad9818fb1a | ||
|
|
88bd33f2d7 | ||
|
|
5fd7216bf2 | ||
|
|
a9722371e1 | ||
|
|
58c2ba5310 | ||
|
|
d8234affa4 | ||
|
|
571bf9e533 | ||
|
|
723e915895 | ||
|
|
3e5ecbf52b | ||
|
|
01b66b756f | ||
|
|
275ae6b2d3 | ||
|
|
094763ea8b | ||
|
|
3299315822 | ||
|
|
10d2e66137 | ||
|
|
19b9d0eadd | ||
|
|
cae9f44f5c | ||
|
|
8b540c5f3b | ||
|
|
8bf3cd1102 | ||
|
|
b226eb2d64 | ||
|
|
e96b709d49 | ||
|
|
ac6877aba7 | ||
|
|
a13f11adb4 | ||
|
|
494aef1c94 | ||
|
|
10adc87a3e | ||
|
|
4d7d666c71 | ||
|
|
ac7486856c | ||
|
|
8999d3948a | ||
|
|
f3bd1e24b7 | ||
|
|
e9913522ac | ||
|
|
8f7db4b270 | ||
|
|
3745496d73 | ||
|
|
a64d352d48 | ||
|
|
42d403807c | ||
|
|
b2f541d4a6 | ||
|
|
912c33ca67 | ||
|
|
4c1722da2c | ||
|
|
4d70606feb | ||
|
|
16257386b1 | ||
|
|
a63890e627 | ||
|
|
904b491521 | ||
|
|
fea1079b1e | ||
|
|
b39d245050 | ||
|
|
b557ebe0a2 | ||
|
|
bb44dbfc51 | ||
|
|
490bf81b86 | ||
|
|
c3261beaea | ||
|
|
345b8d910b | ||
|
|
922ee7c48d | ||
|
|
ad59db9a8a | ||
|
|
49085bbd5b | ||
|
|
93ac9efe92 | ||
|
|
bc9e6b49e8 | ||
|
|
adedfed2bd | ||
|
|
9473777e43 | ||
|
|
a359647683 | ||
|
|
09a77998ba | ||
|
|
5ccb41b9b9 | ||
|
|
131e11b52b | ||
|
|
7d7726817a | ||
|
|
fc6245122d | ||
|
|
c5a0711f84 | ||
|
|
5a3e0612a7 | ||
|
|
b246318090 | ||
|
|
d807bdac5e | ||
|
|
1695baa5b0 | ||
|
|
9b4801fb43 | ||
|
|
d7854bc154 | ||
|
|
f9ac540031 | ||
|
|
00ce85166e | ||
|
|
6dc813bdff | ||
|
|
ebdd07b8ae | ||
|
|
c5810e6d54 | ||
|
|
c71252cd23 | ||
|
|
6c18b7f2cc | ||
|
|
5e77ad5cb4 | ||
|
|
e1578813a7 | ||
|
|
21811cb37d | ||
|
|
265227edcb | ||
|
|
1d3c1964f9 | ||
|
|
58b7a4a2d7 | ||
|
|
a6b0734fe8 | ||
|
|
55cd34c7a5 | ||
|
|
0e8aeebedb | ||
|
|
1c4d41db86 | ||
|
|
63c281e405 | ||
|
|
f03d0858cc | ||
|
|
8bdeb14bc6 | ||
|
|
ad7fe8f881 | ||
|
|
1dcc1d1baf | ||
|
|
0029bc3b42 | ||
|
|
0b73a8d469 | ||
|
|
3a2b97e0f7 | ||
|
|
9bcc98beb9 | ||
|
|
1fb11707a5 | ||
|
|
84548d5d7b | ||
|
|
6f666f56c1 | ||
|
|
2866977941 | ||
|
|
c087592e2c | ||
|
|
65182176b9 | ||
|
|
c8123d911e | ||
|
|
cacda99707 | ||
|
|
bac4eba35e | ||
|
|
625df61489 | ||
|
|
ccca58bb95 | ||
|
|
9557b9132b | ||
|
|
d6d540e748 |
Vendored
+8
-3
@@ -11,10 +11,14 @@
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "test",
|
||||
"group": "test",
|
||||
"group":{
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "watch",
|
||||
"type": "npm",
|
||||
"script": "watch",
|
||||
"group": "build",
|
||||
@@ -25,9 +29,10 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "start",
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"dependsOn": "npm: watch",
|
||||
"dependsOn": "watch",
|
||||
"group": "build",
|
||||
"isBackground": true,
|
||||
"problemMatcher": [],
|
||||
@@ -37,7 +42,7 @@
|
||||
},
|
||||
{
|
||||
"label": "Start demo",
|
||||
"dependsOn": "npm: start",
|
||||
"dependsOn": "start",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
|
||||
@@ -185,7 +185,14 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
||||
- [**gdbgui**](https://github.com/cs01/gdbgui): Browser-based frontend to gdb (gnu debugger)
|
||||
- [**goormIDE**](https://ide.goorm.io/): Run almost every programming languages with real-time collaboration, live pair programming, and built-in messenger.
|
||||
- [**FleetDeck**](https://fleetdeck.io): Remote desktop & virtual terminal
|
||||
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
|
||||
- [**OpenSumi**](https://github.com/opensumi/core): A framework helps you quickly build Cloud or Desktop IDE products.
|
||||
- [**KubeSail**](https://kubesail.com): The Self-Hosting Company - uses xterm to allow users to exec into kubernetes pods and build github apps
|
||||
- [**WiTTY**](https://github.com/syssecfsu/witty): Web-based interactive terminal emulator that allows users to easily record, share, and replay console sessions.
|
||||
- [**libv86 Terminal Forwarding**](https://github.com/hello-smile6/libv86-terminal-forwarding): Peer-to-peer SSH for the web, using WebRTC via [Bugout](https://github.com/chr15m/bugout) for data transfer and [v86](https://github.com/copy/v86) for web-based virtualization.
|
||||
- [**hack.courses**](https://hack.courses): Interactive Linux and command-line classes using xterm.js to expose a real terminal available for everyone.
|
||||
- [**Render**](https://render.com): Platform-as-a-service for your apps, websites, and databases using xterm.js to provide a command prompt for user containers and for streaming build and runtime logs.
|
||||
- [**CloudTTY**](https://github.com/cloudtty/cloudtty): A Friendly Kubernetes CloudShell (Web Terminal).
|
||||
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D)
|
||||
|
||||
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 on our list. Note: Please add any new contributions to the end of the list only.
|
||||
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
"types": "typings/xterm-addon-attach.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"terminal",
|
||||
"xterm",
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"prepackage": "npm run build",
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
"types": "typings/xterm-addon-fit.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"terminal",
|
||||
"xterm",
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"prepackage": "npm run build",
|
||||
|
||||
@@ -63,6 +63,9 @@ export class FitAddon implements ITerminalAddon {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const scrollbarWidth = this._terminal.options.scrollback === 0 ?
|
||||
0 : core.viewport.scrollBarWidth;
|
||||
|
||||
const parentElementStyle = window.getComputedStyle(this._terminal.element.parentElement);
|
||||
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
||||
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
|
||||
@@ -76,7 +79,7 @@ export class FitAddon implements ITerminalAddon {
|
||||
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
|
||||
const elementPaddingHor = elementPadding.right + elementPadding.left;
|
||||
const availableHeight = parentElementHeight - elementPaddingVer;
|
||||
const availableWidth = parentElementWidth - elementPaddingHor - core.viewport.scrollBarWidth;
|
||||
const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth;
|
||||
const geometry = {
|
||||
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / core._renderService.dimensions.actualCellWidth)),
|
||||
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / core._renderService.dimensions.actualCellHeight))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-ligatures",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.3",
|
||||
"description": "Add support for programming ligatures to xterm.js",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
@@ -25,14 +25,14 @@
|
||||
"keywords": [
|
||||
"font",
|
||||
"ligature",
|
||||
"terminal",
|
||||
"xterm",
|
||||
"xterm.js",
|
||||
"terminal"
|
||||
"xterm.js"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"font-finder": "^1.1.0",
|
||||
"font-ligatures": "^1.4.0"
|
||||
"font-ligatures": "^1.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/sinon": "^5.0.1",
|
||||
|
||||
@@ -78,7 +78,7 @@ describe('xterm-addon-ligatures', () => {
|
||||
});
|
||||
|
||||
it('handles quoted font names', done => {
|
||||
term.setOption('fontFamily', '"Fira Code", monospace');
|
||||
term.options.fontFamily = '"Fira Code", monospace';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
onRefresh.callsFake(() => {
|
||||
assert.deepEqual(term.joiner!(input), [[2, 4], [7, 10]]);
|
||||
@@ -87,7 +87,7 @@ describe('xterm-addon-ligatures', () => {
|
||||
});
|
||||
|
||||
it('falls back to later fonts if earlier ones are not present', done => {
|
||||
term.setOption('fontFamily', 'notinstalled, Fira Code, monospace');
|
||||
term.options.fontFamily = 'notinstalled, Fira Code, monospace';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
onRefresh.callsFake(() => {
|
||||
assert.deepEqual(term.joiner!(input), [[2, 4], [7, 10]]);
|
||||
@@ -98,17 +98,17 @@ describe('xterm-addon-ligatures', () => {
|
||||
it('uses the current font value', done => {
|
||||
// The first three calls are all synchronous so that we don't allow time for
|
||||
// any fonts to load while we're switching things around
|
||||
term.setOption('fontFamily', 'Fira Code');
|
||||
term.options.fontFamily = 'Fira Code';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
term.setOption('fontFamily', 'notinstalled');
|
||||
term.options.fontFamily = 'notinstalled';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
term.setOption('fontFamily', 'Iosevka');
|
||||
term.options.fontFamily = 'Iosevka';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
onRefresh.callsFake(() => {
|
||||
assert.deepEqual(term.joiner!(input), [[2, 4]]);
|
||||
|
||||
// And switch it back to Fira Code for good measure
|
||||
term.setOption('fontFamily', 'Fira Code');
|
||||
term.options.fontFamily = 'Fira Code';
|
||||
|
||||
// At this point, we haven't loaded the new font, so the result reverts
|
||||
// back to empty until that happens
|
||||
@@ -124,7 +124,7 @@ describe('xterm-addon-ligatures', () => {
|
||||
it('allows multiple terminal instances that use different fonts', done => {
|
||||
const onRefresh2 = sinon.stub();
|
||||
const term2 = new MockTerminal(onRefresh2);
|
||||
term2.setOption('fontFamily', 'Iosevka');
|
||||
term2.options.fontFamily = 'Iosevka';
|
||||
ligatureSupport.enableLigatures(term2 as any);
|
||||
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
@@ -140,7 +140,7 @@ describe('xterm-addon-ligatures', () => {
|
||||
});
|
||||
|
||||
it('fails if it finds but cannot load the font', async () => {
|
||||
term.setOption('fontFamily', 'Nonexistant Font, monospace');
|
||||
term.options.fontFamily = 'Nonexistant Font, monospace';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
await delay(500);
|
||||
assert.isTrue(onRefresh.notCalled);
|
||||
@@ -148,7 +148,7 @@ describe('xterm-addon-ligatures', () => {
|
||||
});
|
||||
|
||||
it('returns nothing if the font is not present on the system', async () => {
|
||||
term.setOption('fontFamily', 'notinstalled');
|
||||
term.options.fontFamily = 'notinstalled';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
await delay(500);
|
||||
assert.isTrue(onRefresh.notCalled);
|
||||
@@ -156,7 +156,7 @@ describe('xterm-addon-ligatures', () => {
|
||||
});
|
||||
|
||||
it('returns nothing if no specific font is specified', async () => {
|
||||
term.setOption('fontFamily', 'monospace');
|
||||
term.options.fontFamily = 'monospace';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
await delay(500);
|
||||
assert.isTrue(onRefresh.notCalled);
|
||||
@@ -164,7 +164,7 @@ describe('xterm-addon-ligatures', () => {
|
||||
});
|
||||
|
||||
it('returns nothing if no fonts are provided', async () => {
|
||||
term.setOption('fontFamily', '');
|
||||
term.options.fontFamily = '';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
await delay(500);
|
||||
assert.isTrue(onRefresh.notCalled);
|
||||
@@ -172,7 +172,7 @@ describe('xterm-addon-ligatures', () => {
|
||||
});
|
||||
|
||||
it('fails when given malformed inputs', async () => {
|
||||
term.setOption('fontFamily', {} as any);
|
||||
term.options.fontFamily = {} as any;
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
await delay(500);
|
||||
assert.isTrue(onRefresh.notCalled);
|
||||
@@ -181,7 +181,7 @@ describe('xterm-addon-ligatures', () => {
|
||||
|
||||
it('ensures no empty errors are thrown', async () => {
|
||||
sinon.stub(fontLigatures, 'loadFile').callsFake(async () => { throw undefined; });
|
||||
term.setOption('fontFamily', 'Iosevka');
|
||||
term.options.fontFamily = 'Iosevka';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
await delay(500);
|
||||
assert.isTrue(onRefresh.notCalled);
|
||||
@@ -209,11 +209,11 @@ class MockTerminal {
|
||||
public deregisterCharacterJoiner(id: number): void {
|
||||
this.joiner = undefined;
|
||||
}
|
||||
public setOption(name: string, value: string | number): void {
|
||||
this._options[name] = value;
|
||||
}
|
||||
public getOption(name: string): string | number {
|
||||
return this._options[name];
|
||||
public get options(): { [name: string]: string | number } { return this._options; }
|
||||
public set options(options: { [name: string]: string | number }) {
|
||||
for (const key in this._options) {
|
||||
this._options[key] = options[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export function enableLigatures(term: Terminal): void {
|
||||
|
||||
term.registerCharacterJoiner((text: string): [number, number][] => {
|
||||
// If the font hasn't been loaded yet, load it and return an empty result
|
||||
const termFont = term.getOption('fontFamily');
|
||||
const termFont = term.options.fontFamily;
|
||||
if (
|
||||
termFont &&
|
||||
(loadingState === LoadingState.UNLOADED || currentFontName !== termFont)
|
||||
@@ -48,20 +48,20 @@ export function enableLigatures(term: Terminal): void {
|
||||
.then(f => {
|
||||
// Another request may have come in while we were waiting, so make
|
||||
// sure our font is still vaild.
|
||||
if (currentCallFontName === term.getOption('fontFamily')) {
|
||||
if (currentCallFontName === term.options.fontFamily) {
|
||||
loadingState = LoadingState.LOADED;
|
||||
font = f;
|
||||
|
||||
// Only refresh things if we actually found a font
|
||||
if (f) {
|
||||
term.refresh(0, term.getOption('rows') - 1);
|
||||
term.refresh(0, term.options.rows! - 1);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
// Another request may have come in while we were waiting, so make
|
||||
// sure our font is still vaild.
|
||||
if (currentCallFontName === term.getOption('fontFamily')) {
|
||||
if (currentCallFontName === term.options.fontFamily) {
|
||||
loadingState = LoadingState.FAILED;
|
||||
font = undefined;
|
||||
loadError = e;
|
||||
|
||||
@@ -67,9 +67,9 @@ fd-slicer@~1.1.0:
|
||||
pend "~1.2.0"
|
||||
|
||||
follow-redirects@^1.14.0:
|
||||
version "1.14.3"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.3.tgz#6ada78118d8d24caee595595accdc0ac6abd022e"
|
||||
integrity sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw==
|
||||
version "1.14.8"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc"
|
||||
integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==
|
||||
|
||||
font-finder@^1.0.3:
|
||||
version "1.0.4"
|
||||
@@ -86,10 +86,10 @@ font-finder@^1.1.0:
|
||||
get-system-fonts "^2.0.0"
|
||||
promise-stream-reader "^1.0.1"
|
||||
|
||||
font-ligatures@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.npmjs.org/font-ligatures/-/font-ligatures-1.4.0.tgz#6a7b370d96be1358dddfad67830e82fbfd59e6dc"
|
||||
integrity sha512-n7DFnnEpJ0NrVoLqZIL4tMGVs+CnFwQc92m80LWyrbgAFO4x234+t2/H9o4eOYA1eh6ta9dZAEEsJAwsBdNezA==
|
||||
font-ligatures@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.npmjs.org/font-ligatures/-/font-ligatures-1.4.1.tgz#e8c9df7cf6bee80c0cca4a9a578618c7b1f5d8de"
|
||||
integrity sha512-7W6zlfyhvCqShZ5ReUWqmSd9vBaUudW0Hxis+tqUjtHhsPU+L3Grf8mcZAtCiXHTzorhwdRTId2WeH/88gdFkw==
|
||||
dependencies:
|
||||
font-finder "^1.0.3"
|
||||
lru-cache "^6.0.0"
|
||||
@@ -141,9 +141,9 @@ lru-cache@^6.0.0:
|
||||
yallist "^4.0.0"
|
||||
|
||||
minimist@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
|
||||
mkdirp@0.5.5:
|
||||
version "0.5.5"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-search",
|
||||
"version": "0.8.2",
|
||||
"version": "0.9.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
@@ -9,6 +9,11 @@
|
||||
"types": "typings/xterm-addon-search.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"terminal",
|
||||
"xterm",
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"prepackage": "npm run build",
|
||||
|
||||
@@ -3,13 +3,25 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, IBufferLine, IDisposable, ITerminalAddon, ISelectionPosition } from 'xterm';
|
||||
import { Terminal, IDisposable, ITerminalAddon, ISelectionPosition, IDecoration } from 'xterm';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
|
||||
export interface ISearchOptions {
|
||||
regex?: boolean;
|
||||
wholeWord?: boolean;
|
||||
caseSensitive?: boolean;
|
||||
incremental?: boolean;
|
||||
decorations?: ISearchDecorationOptions;
|
||||
noScroll?: boolean;
|
||||
}
|
||||
|
||||
interface ISearchDecorationOptions {
|
||||
matchBackground?: string;
|
||||
matchBorder?: string;
|
||||
matchOverviewRuler: string;
|
||||
activeMatchBackground?: string;
|
||||
activeMatchBorder?: string;
|
||||
activeMatchColorOverviewRuler: string;
|
||||
}
|
||||
|
||||
export interface ISearchPosition {
|
||||
@@ -40,7 +52,14 @@ const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
|
||||
|
||||
export class SearchAddon implements ITerminalAddon {
|
||||
private _terminal: Terminal | undefined;
|
||||
|
||||
private _cachedSearchTerm: string | undefined;
|
||||
private _selectedDecoration: IDecoration | undefined;
|
||||
private _resultDecorations: Map<number, IDecoration[]> | undefined;
|
||||
private _searchResults: Map<string, ISearchResult> | undefined;
|
||||
private _onDataDisposable: IDisposable | undefined;
|
||||
private _onResizeDisposable: IDisposable | undefined;
|
||||
private _lastSearchOptions: ISearchOptions | undefined;
|
||||
private _highlightTimeout: number | undefined;
|
||||
/**
|
||||
* translateBufferLineToStringWithWrap is a fairly expensive call.
|
||||
* We memoize the calls into an array that has a time based ttl.
|
||||
@@ -51,11 +70,56 @@ export class SearchAddon implements ITerminalAddon {
|
||||
private _cursorMoveListener: IDisposable | undefined;
|
||||
private _resizeListener: IDisposable | undefined;
|
||||
|
||||
private _resultIndex: number | undefined;
|
||||
|
||||
private readonly _onDidChangeResults = new EventEmitter<{ resultIndex: number, resultCount: number } | undefined>();
|
||||
public readonly onDidChangeResults = this._onDidChangeResults.event;
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
this._terminal = terminal;
|
||||
this._onDataDisposable = this._terminal.onWriteParsed(() => this._updateMatches());
|
||||
this._onResizeDisposable = this._terminal.onResize(() => this._updateMatches());
|
||||
}
|
||||
|
||||
public dispose(): void { }
|
||||
private _updateMatches(): void {
|
||||
if (this._highlightTimeout) {
|
||||
window.clearTimeout(this._highlightTimeout);
|
||||
}
|
||||
if (this._cachedSearchTerm && this._lastSearchOptions?.decorations) {
|
||||
this._highlightTimeout = setTimeout(() => {
|
||||
this.findPrevious(this._cachedSearchTerm!, { ...this._lastSearchOptions, incremental: true, noScroll: true });
|
||||
this._resultIndex = this._searchResults ? this._searchResults.size - 1 : -1;
|
||||
this._onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults?.size ?? -1 });
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.clearDecorations();
|
||||
this._onDataDisposable?.dispose();
|
||||
this._onResizeDisposable?.dispose();
|
||||
}
|
||||
|
||||
public clearDecorations(retainCachedSearchTerm?: boolean): void {
|
||||
this._selectedDecoration?.dispose();
|
||||
this._searchResults?.clear();
|
||||
this._resultDecorations?.forEach(decorations => {
|
||||
for (const d of decorations) {
|
||||
d.dispose();
|
||||
}
|
||||
});
|
||||
this._resultDecorations?.clear();
|
||||
this._searchResults = undefined;
|
||||
this._resultDecorations = undefined;
|
||||
if (!retainCachedSearchTerm) {
|
||||
this._cachedSearchTerm = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public clearActiveDecoration(): void {
|
||||
this._selectedDecoration?.dispose();
|
||||
this._selectedDecoration = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the next instance of the term, then scroll to and select it. If it
|
||||
@@ -68,12 +132,107 @@ export class SearchAddon implements ITerminalAddon {
|
||||
if (!this._terminal) {
|
||||
throw new Error('Cannot use addon until it has been loaded');
|
||||
}
|
||||
this._lastSearchOptions = searchOptions;
|
||||
if (searchOptions?.decorations) {
|
||||
if (this._resultIndex !== undefined || this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
|
||||
this._highlightAllMatches(term, searchOptions);
|
||||
}
|
||||
}
|
||||
return this._fireResults(term, this._findNextAndSelect(term, searchOptions), searchOptions);
|
||||
}
|
||||
|
||||
private _highlightAllMatches(term: string, searchOptions: ISearchOptions): void {
|
||||
if (!this._terminal) {
|
||||
throw new Error('Cannot use addon until it has been loaded');
|
||||
}
|
||||
if (!term || term.length === 0) {
|
||||
this._terminal.clearSelection();
|
||||
this.clearDecorations();
|
||||
return;
|
||||
}
|
||||
searchOptions = searchOptions || {};
|
||||
|
||||
// new search, clear out the old decorations
|
||||
this.clearDecorations(true);
|
||||
this._searchResults = new Map<string, ISearchResult>();
|
||||
this._resultDecorations = new Map<number, IDecoration[]>();
|
||||
const resultDecorations = this._resultDecorations;
|
||||
let result = this._find(term, 0, 0, searchOptions);
|
||||
while (result && !this._searchResults.get(`${result.row}-${result.col}`)) {
|
||||
this._searchResults.set(`${result.row}-${result.col}`, result);
|
||||
result = this._find(
|
||||
term,
|
||||
result.col + result.term.length >= this._terminal.cols ? result.row + 1 : result.row,
|
||||
result.col + result.term.length >= this._terminal.cols ? 0 : result.col + 1,
|
||||
searchOptions
|
||||
);
|
||||
if (this._searchResults.size > 1000) {
|
||||
this.clearDecorations();
|
||||
this._resultIndex = undefined;
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._searchResults.forEach(result => {
|
||||
const resultDecoration = this._createResultDecoration(result, searchOptions.decorations!);
|
||||
if (resultDecoration) {
|
||||
const decorationsForLine = resultDecorations.get(resultDecoration.marker.line) || [];
|
||||
decorationsForLine.push(resultDecoration);
|
||||
resultDecorations.set(resultDecoration.marker.line, decorationsForLine);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _find(term: string, startRow: number, startCol: number, searchOptions?: ISearchOptions): ISearchResult | undefined {
|
||||
if (!this._terminal || !term || term.length === 0) {
|
||||
this._terminal?.clearSelection();
|
||||
this.clearDecorations();
|
||||
return undefined;
|
||||
}
|
||||
if (startCol > this._terminal.cols) {
|
||||
throw new Error(`Invalid col: ${startCol} to search in terminal of ${this._terminal.cols} cols`);
|
||||
}
|
||||
|
||||
let result: ISearchResult | undefined = undefined;
|
||||
|
||||
this._initLinesCache();
|
||||
|
||||
const searchPosition: ISearchPosition = {
|
||||
startRow,
|
||||
startCol
|
||||
};
|
||||
|
||||
// Search startRow
|
||||
result = this._findInLine(term, searchPosition, searchOptions);
|
||||
// Search from startRow + 1 to end
|
||||
if (!result) {
|
||||
|
||||
for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {
|
||||
searchPosition.startRow = y;
|
||||
searchPosition.startCol = 0;
|
||||
// If the current line is wrapped line, increase index of column to ignore the previous scan
|
||||
// Otherwise, reset beginning column index to zero with set new unwrapped line index
|
||||
result = this._findInLine(term, searchPosition, searchOptions);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private _findNextAndSelect(term: string, searchOptions?: ISearchOptions): boolean {
|
||||
if (!this._terminal || !term || term.length === 0) {
|
||||
this._terminal?.clearSelection();
|
||||
this.clearDecorations();
|
||||
this._cachedSearchTerm = undefined;
|
||||
this._resultIndex = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._cachedSearchTerm !== term) {
|
||||
this._resultIndex = undefined;
|
||||
this._terminal.clearSelection();
|
||||
}
|
||||
|
||||
let startCol = 0;
|
||||
let startRow = 0;
|
||||
let currentSelection: ISelectionPosition | undefined;
|
||||
@@ -95,7 +254,6 @@ export class SearchAddon implements ITerminalAddon {
|
||||
|
||||
// Search startRow
|
||||
let result = this._findInLine(term, searchPosition, searchOptions);
|
||||
|
||||
// Search from startRow + 1 to end
|
||||
if (!result) {
|
||||
|
||||
@@ -129,10 +287,21 @@ export class SearchAddon implements ITerminalAddon {
|
||||
result = this._findInLine(term, searchPosition, searchOptions);
|
||||
}
|
||||
|
||||
if (this._searchResults) {
|
||||
if (this._searchResults.size === 0) {
|
||||
this._resultIndex = -1;
|
||||
} else if (this._resultIndex === undefined) {
|
||||
this._resultIndex = 0;
|
||||
} else {
|
||||
this._resultIndex++;
|
||||
if (this._resultIndex >= this._searchResults.size) {
|
||||
this._resultIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Set selection and scroll if a result was found
|
||||
return this._selectResult(result);
|
||||
return this._selectResult(result, searchOptions?.decorations, searchOptions?.noScroll);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the previous instance of the term, then scroll to and select it. If it
|
||||
* doesn't exist, do nothing.
|
||||
@@ -144,16 +313,49 @@ export class SearchAddon implements ITerminalAddon {
|
||||
if (!this._terminal) {
|
||||
throw new Error('Cannot use addon until it has been loaded');
|
||||
}
|
||||
this._lastSearchOptions = searchOptions;
|
||||
if (searchOptions?.decorations) {
|
||||
if (this._resultIndex !== undefined || this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
|
||||
this._highlightAllMatches(term, searchOptions);
|
||||
}
|
||||
}
|
||||
return this._fireResults(term, this._findPreviousAndSelect(term, searchOptions), searchOptions);
|
||||
}
|
||||
|
||||
if (!term || term.length === 0) {
|
||||
this._terminal.clearSelection();
|
||||
private _fireResults(term: string, found: boolean, searchOptions?: ISearchOptions): boolean {
|
||||
if (searchOptions?.decorations) {
|
||||
if (this._resultIndex !== undefined && this._searchResults?.size !== undefined) {
|
||||
this._onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults.size });
|
||||
} else {
|
||||
this._onDidChangeResults.fire(undefined);
|
||||
}
|
||||
}
|
||||
this._cachedSearchTerm = term;
|
||||
return found;
|
||||
}
|
||||
|
||||
private _findPreviousAndSelect(term: string, searchOptions?: ISearchOptions): boolean {
|
||||
if (!this._terminal) {
|
||||
throw new Error('Cannot use addon until it has been loaded');
|
||||
}
|
||||
let result: ISearchResult | undefined;
|
||||
if (!this._terminal || !term || term.length === 0) {
|
||||
result = undefined;
|
||||
this._terminal?.clearSelection();
|
||||
this.clearDecorations();
|
||||
this._resultIndex = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
const isReverseSearch = true;
|
||||
if (this._cachedSearchTerm !== term) {
|
||||
this._resultIndex = undefined;
|
||||
this._terminal.clearSelection();
|
||||
}
|
||||
|
||||
let startRow = this._terminal.buffer.active.baseY + this._terminal.rows;
|
||||
let startCol = this._terminal.cols;
|
||||
let result: ISearchResult | undefined;
|
||||
const isReverseSearch = true;
|
||||
|
||||
const incremental = searchOptions ? searchOptions.incremental : false;
|
||||
let currentSelection: ISelectionPosition | undefined;
|
||||
if (this._terminal.hasSelection()) {
|
||||
@@ -207,11 +409,24 @@ export class SearchAddon implements ITerminalAddon {
|
||||
}
|
||||
}
|
||||
|
||||
if (this._searchResults) {
|
||||
if (this._searchResults.size === 0) {
|
||||
this._resultIndex = -1;
|
||||
} else if (this._resultIndex === undefined || this._resultIndex < 0) {
|
||||
this._resultIndex = this._searchResults.size - 1;
|
||||
} else {
|
||||
this._resultIndex--;
|
||||
if (this._resultIndex === -1) {
|
||||
this._resultIndex = this._searchResults.size - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is only one result, return true.
|
||||
if (!result && currentSelection) return true;
|
||||
|
||||
// Set selection and scroll if a result was found
|
||||
return this._selectResult(result);
|
||||
return this._selectResult(result, searchOptions?.decorations, searchOptions?.noScroll);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -393,7 +608,8 @@ export class SearchAddon implements ITerminalAddon {
|
||||
break;
|
||||
}
|
||||
if (cell.getWidth()) {
|
||||
offset += cell.getChars().length;
|
||||
// Treat null characters as whitespace to align with the translateToString API
|
||||
offset += cell.getCode() === 0 ? 1 : cell.getChars().length;
|
||||
}
|
||||
}
|
||||
lineIndex++;
|
||||
@@ -446,21 +662,91 @@ export class SearchAddon implements ITerminalAddon {
|
||||
/**
|
||||
* Selects and scrolls to a result.
|
||||
* @param result The result to select.
|
||||
* @return Whethera result was selected.
|
||||
* @return Whether a result was selected.
|
||||
*/
|
||||
private _selectResult(result: ISearchResult | undefined): boolean {
|
||||
private _selectResult(result: ISearchResult | undefined, options?: ISearchDecorationOptions, noScroll?: boolean): boolean {
|
||||
const terminal = this._terminal!;
|
||||
this.clearActiveDecoration();
|
||||
if (!result) {
|
||||
terminal.clearSelection();
|
||||
return false;
|
||||
}
|
||||
terminal.select(result.col, result.row, result.size);
|
||||
// If it is not in the viewport then we scroll else it just gets selected
|
||||
if (result.row >= (terminal.buffer.active.viewportY + terminal.rows) || result.row < terminal.buffer.active.viewportY) {
|
||||
let scroll = result.row - terminal.buffer.active.viewportY;
|
||||
scroll -= Math.floor(terminal.rows / 2);
|
||||
terminal.scrollLines(scroll);
|
||||
if (options) {
|
||||
const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row);
|
||||
if (marker) {
|
||||
this._selectedDecoration = terminal.registerDecoration({
|
||||
marker,
|
||||
x: result.col,
|
||||
width: result.size,
|
||||
backgroundColor: options.activeMatchBackground,
|
||||
layer: 'top',
|
||||
overviewRulerOptions: {
|
||||
color: options.activeMatchColorOverviewRuler
|
||||
}
|
||||
});
|
||||
this._selectedDecoration?.onRender((e) => this._applyStyles(e, options.activeMatchBorder, true));
|
||||
this._selectedDecoration?.onDispose(() => marker.dispose());
|
||||
}
|
||||
}
|
||||
|
||||
if (!noScroll) {
|
||||
// If it is not in the viewport then we scroll else it just gets selected
|
||||
if (result.row >= (terminal.buffer.active.viewportY + terminal.rows) || result.row < terminal.buffer.active.viewportY) {
|
||||
let scroll = result.row - terminal.buffer.active.viewportY;
|
||||
scroll -= Math.floor(terminal.rows / 2);
|
||||
terminal.scrollLines(scroll);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies styles to the decoration when it is rendered
|
||||
* @param element the decoration's element
|
||||
* @param backgroundColor the background color to apply
|
||||
* @param borderColor the border color to apply
|
||||
* @returns
|
||||
*/
|
||||
private _applyStyles(element: HTMLElement, borderColor: string | undefined, isActiveResult: boolean): void {
|
||||
if (element.clientWidth <= 0) {
|
||||
return;
|
||||
}
|
||||
if (!element.classList.contains('xterm-find-result-decoration')) {
|
||||
element.classList.add('xterm-find-result-decoration');
|
||||
if (borderColor) {
|
||||
element.style.outline = `1px solid ${borderColor}`;
|
||||
}
|
||||
}
|
||||
if (isActiveResult) {
|
||||
element.classList.add('xterm-find-active-result-decoration');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a decoration for the result and applies styles
|
||||
* @param result the search result for which to create the decoration
|
||||
* @param options the options for the decoration
|
||||
* @returns the {@link IDecoration} or undefined if the marker has already been disposed of
|
||||
*/
|
||||
private _createResultDecoration(result: ISearchResult, options: ISearchDecorationOptions): IDecoration | undefined {
|
||||
const terminal = this._terminal!;
|
||||
const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row);
|
||||
if (!marker) {
|
||||
return undefined;
|
||||
}
|
||||
const findResultDecoration = terminal.registerDecoration({
|
||||
marker,
|
||||
x: result.col,
|
||||
width: result.size,
|
||||
backgroundColor: options.matchBackground,
|
||||
overviewRulerOptions: this._resultDecorations?.get(marker.line) ? undefined : {
|
||||
color: options.matchOverviewRuler,
|
||||
position: 'center'
|
||||
}
|
||||
});
|
||||
findResultDecoration?.onRender((e) => this._applyStyles(e, options.matchBorder, false));
|
||||
findResultDecoration?.onDispose(() => marker.dispose());
|
||||
return findResultDecoration;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,20 @@
|
||||
"strict": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha"
|
||||
]
|
||||
],
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../../../typings/xterm.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src/common"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { assert } from 'chai';
|
||||
import { readFile } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
import { openTerminal, writeSync, launchBrowser } from '../../../out-test/api/TestUtils';
|
||||
import { openTerminal, writeSync, launchBrowser, timeout } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright';
|
||||
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
@@ -16,15 +16,13 @@ let page: Page;
|
||||
const width = 800;
|
||||
const height = 600;
|
||||
|
||||
describe('Search Tests', function(): void {
|
||||
before(async function(): Promise<any> {
|
||||
describe('Search Tests', function (): void {
|
||||
before(async function (): Promise<any> {
|
||||
browser = await launchBrowser();
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
await page.goto(APP);
|
||||
await openTerminal(page);
|
||||
await page.evaluate(`window.search = new SearchAddon();`);
|
||||
await page.evaluate(`window.term.loadAddon(window.search);`);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
@@ -32,7 +30,12 @@ describe('Search Tests', function(): void {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await page.evaluate(`window.term.reset()`);
|
||||
await page.evaluate(`
|
||||
window.term.reset()
|
||||
window.search?.dispose();
|
||||
window.search = new SearchAddon();
|
||||
window.term.loadAddon(window.search);
|
||||
`);
|
||||
});
|
||||
|
||||
it('Simple Search', async () => {
|
||||
@@ -120,6 +123,176 @@ describe('Search Tests', function(): void {
|
||||
});
|
||||
});
|
||||
|
||||
describe('onDidChangeResults', async () => {
|
||||
describe('findNext', () => {
|
||||
it('should not fire unless the decorations option is set', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc');
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('a')`), true);
|
||||
assert.strictEqual(await page.evaluate('window.calls.length'), 0);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.strictEqual(await page.evaluate('window.calls.length'), 1);
|
||||
});
|
||||
it('should fire with correct event values', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc bc c');
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 }
|
||||
]);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 }
|
||||
]);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('d', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 0, resultIndex: -1 }
|
||||
]);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 0, resultIndex: -1 },
|
||||
{ resultCount: 3, resultIndex: 0 },
|
||||
{ resultCount: 3, resultIndex: 1 },
|
||||
{ resultCount: 3, resultIndex: 2 }
|
||||
]);
|
||||
});
|
||||
it('should fire with correct event values (incremental)', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc aabc');
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('a', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 0 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('ab', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('abcd', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 0, resultIndex: -1 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('findPrevious', () => {
|
||||
it('should not fire unless the decorations option is set', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc');
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('a')`), true);
|
||||
assert.strictEqual(await page.evaluate('window.calls.length'), 0);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.strictEqual(await page.evaluate('window.calls.length'), 1);
|
||||
});
|
||||
it('should fire with correct event values', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc bc c');
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 }
|
||||
]);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
]);
|
||||
await timeout(2000);
|
||||
assert.strictEqual(await page.evaluate(`debugger; window.search.findPrevious('d', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 0, resultIndex: -1 }
|
||||
]);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 0, resultIndex: -1 },
|
||||
{ resultCount: 3, resultIndex: 2 },
|
||||
{ resultCount: 3, resultIndex: 1 },
|
||||
{ resultCount: 3, resultIndex: 0 }
|
||||
]);
|
||||
});
|
||||
it('should fire with correct event values (incremental)', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc aabc');
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('a', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 2 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('ab', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 2 },
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 2 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 2 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 0 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('abcd', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 2 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 0, resultIndex: -1 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Regression tests', () => {
|
||||
describe('#2444 wrapped line content not being found', () => {
|
||||
let fixture: string;
|
||||
@@ -134,7 +307,7 @@ describe('Search Tests', function(): void {
|
||||
.replace(/\n/g, '\\n\\r');
|
||||
}
|
||||
fixture = fixture
|
||||
.replace(/'/g, '\\\'');
|
||||
.replace(/'/g, `\\'`);
|
||||
});
|
||||
it('should find all occurrences using findNext', async () => {
|
||||
await writeSync(page, fixture);
|
||||
@@ -206,6 +379,21 @@ describe('Search Tests', function(): void {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#3834 lines with null characters before search terms', () => {
|
||||
// This case can be triggered by the prompt when using starship under conpty
|
||||
it('should find all matches on a line containing null characters', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
// Move cursor forward 1 time to create a null character, as opposed to regular whitespace
|
||||
await writeSync(page, '\\x1b[CHi Hi');
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('h', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function makeData(length: number): string {
|
||||
|
||||
+63
-1
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ILinkMatcherOptions, IDisposable, ITerminalAddon } from 'xterm';
|
||||
import { Terminal, ITerminalAddon, IEvent } from 'xterm';
|
||||
|
||||
declare module 'xterm-addon-search' {
|
||||
/**
|
||||
@@ -32,6 +32,47 @@ declare module 'xterm-addon-search' {
|
||||
* `findNext`, not `findPrevious`.
|
||||
*/
|
||||
incremental?: boolean;
|
||||
|
||||
/**
|
||||
* When set, will highlight all instances of the word on search and show
|
||||
* them in the overview ruler if it's enabled.
|
||||
*/
|
||||
decorations?: ISearchDecorationOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for showing decorations when searching.
|
||||
*/
|
||||
interface ISearchDecorationOptions {
|
||||
/**
|
||||
* The background color of a match, this must use #RRGGBB format.
|
||||
*/
|
||||
matchBackground?: string;
|
||||
|
||||
/**
|
||||
* The border color of a match.
|
||||
*/
|
||||
matchBorder?: string;
|
||||
|
||||
/**
|
||||
* The overview ruler color of a match.
|
||||
*/
|
||||
matchOverviewRuler: string;
|
||||
|
||||
/**
|
||||
* The background color for the currently active match, this must use #RRGGBB format.
|
||||
*/
|
||||
activeMatchBackground?: string;
|
||||
|
||||
/**
|
||||
* The border color of the currently active match.
|
||||
*/
|
||||
activeMatchBorder?: string;
|
||||
|
||||
/**
|
||||
* The overview ruler color of the currently active match.
|
||||
*/
|
||||
activeMatchColorOverviewRuler: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,5 +105,26 @@ declare module 'xterm-addon-search' {
|
||||
* @param searchOptions The options for the search.
|
||||
*/
|
||||
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean;
|
||||
|
||||
/**
|
||||
* Clears the decorations and selection
|
||||
*/
|
||||
public clearDecorations(): void;
|
||||
|
||||
/**
|
||||
* Clears the active result decoration, this decoration is applied on top of the selection so
|
||||
* removing it will reveal the selection underneath. This is intended to be called on the search
|
||||
* textarea's `blur` event.
|
||||
*/
|
||||
public clearActiveDecoration(): void;
|
||||
|
||||
/**
|
||||
* When decorations are enabled, fires when
|
||||
* the search results change.
|
||||
* @returns -1 for resultIndex for a resultCount of 0
|
||||
* and @returns undefined when the threshold of 1k results
|
||||
* is exceeded and decorations are disposed of.
|
||||
*/
|
||||
readonly onDidChangeResults: IEvent<{ resultIndex: number, resultCount: number } | undefined>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,13 @@ module.exports = {
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
modules: ['./node_modules'],
|
||||
extensions: [ '.js' ],
|
||||
alias: {
|
||||
common: path.resolve('../../out/common')
|
||||
}
|
||||
},
|
||||
output: {
|
||||
filename: mainFile,
|
||||
path: path.resolve('./lib'),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-serialize",
|
||||
"version": "0.6.1",
|
||||
"version": "0.7.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
@@ -9,6 +9,11 @@
|
||||
"types": "typings/xterm-addon-serialize.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"terminal",
|
||||
"xterm",
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"prepackage": "npm run build",
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import jsdom = require('jsdom');
|
||||
import { assert } from 'chai';
|
||||
import { SerializeAddon } from './SerializeAddon';
|
||||
import { Terminal } from 'browser/public/Terminal';
|
||||
import { ColorManager } from 'browser/ColorManager';
|
||||
import { SelectionModel } from 'browser/selection/SelectionModel';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
|
||||
function sgr(...seq: string[]): string {
|
||||
return `\x1b[${seq.join(';')}m`;
|
||||
}
|
||||
|
||||
function writeP(terminal: Terminal, data: string | Uint8Array): Promise<void> {
|
||||
return new Promise(r => terminal.write(data, r));
|
||||
}
|
||||
|
||||
class TestSelectionService {
|
||||
private _model: SelectionModel;
|
||||
private _hasSelection: boolean = false;
|
||||
|
||||
constructor(
|
||||
bufferService: IBufferService
|
||||
) {
|
||||
this._model = new SelectionModel(bufferService);
|
||||
}
|
||||
|
||||
public get model(): SelectionModel { return this._model; }
|
||||
|
||||
public get hasSelection(): boolean { return this._hasSelection; }
|
||||
|
||||
public get selectionStart(): [number, number] | undefined { return this._model.finalSelectionStart; }
|
||||
public get selectionEnd(): [number, number] | undefined { return this._model.finalSelectionEnd; }
|
||||
|
||||
public setSelection(col: number, row: number, length: number): void {
|
||||
this._model.selectionStart = [col, row];
|
||||
this._model.selectionStartLength = length;
|
||||
this._hasSelection = true;
|
||||
}
|
||||
}
|
||||
|
||||
describe('xterm-addon-serialize', () => {
|
||||
let cm: ColorManager;
|
||||
let dom: jsdom.JSDOM;
|
||||
let document: Document;
|
||||
let window: jsdom.DOMWindow;
|
||||
|
||||
let serializeAddon: SerializeAddon;
|
||||
let terminal: Terminal;
|
||||
let selectionService: any;
|
||||
|
||||
before(() => {
|
||||
serializeAddon = new SerializeAddon();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
dom = new jsdom.JSDOM('');
|
||||
window = dom.window;
|
||||
document = window.document;
|
||||
|
||||
(window as any).HTMLCanvasElement.prototype.getContext = () => ({
|
||||
createLinearGradient(): any {
|
||||
return null;
|
||||
},
|
||||
|
||||
fillRect(): void { },
|
||||
|
||||
getImageData(): any {
|
||||
return { data: [0, 0, 0, 0xFF] };
|
||||
}
|
||||
});
|
||||
|
||||
terminal = new Terminal({ cols: 10, rows: 2 });
|
||||
terminal.loadAddon(serializeAddon);
|
||||
|
||||
selectionService = new TestSelectionService((terminal as any)._core._bufferService);
|
||||
cm = new ColorManager(document, false);
|
||||
(terminal as any)._core._colorManager = cm;
|
||||
(terminal as any)._core._selectionService = selectionService;
|
||||
});
|
||||
|
||||
describe('text', () => {
|
||||
it('restoring cursor styles', async () => {
|
||||
await writeP(terminal, sgr('32') + '> ' + sgr('0'));
|
||||
assert.equal(serializeAddon.serialize(), '\u001b[32m> \u001b[0m');
|
||||
});
|
||||
});
|
||||
|
||||
describe('html', () => {
|
||||
it('empty terminal with selection turned off', () => {
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.notEqual(output, '');
|
||||
assert.equal((output.match(/<div><span> {10}<\/span><\/div>/g) || []).length, 2);
|
||||
});
|
||||
|
||||
it('empty terminal with no selection', () => {
|
||||
const output = serializeAddon.serializeAsHTML({
|
||||
onlySelection: true
|
||||
});
|
||||
assert.equal(output, '');
|
||||
});
|
||||
|
||||
it('basic terminal with selection', async () => {
|
||||
await writeP(terminal, ' terminal ');
|
||||
terminal.select(1, 0, 8);
|
||||
|
||||
const output = serializeAddon.serializeAsHTML({
|
||||
onlySelection: true
|
||||
});
|
||||
assert.equal((output.match(/<div><span>terminal<\/span><\/div>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with bold styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='font-weight: bold;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with italic styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('3') + 'terminal' + sgr('23') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='font-style: italic;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with inverse styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('7') + 'terminal' + sgr('27') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='color: #000000; background-color: #BFBFBF;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with underline styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('4') + 'terminal' + sgr('24') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='text-decoration: underline;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with invisible styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('8') + 'terminal' + sgr('28') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='visibility: hidden;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with dim styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('2') + 'terminal' + sgr('22') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='opacity: 0.5;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with strikethrough styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('9') + 'terminal' + sgr('29') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='text-decoration: line-through;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with combined styling', async () => {
|
||||
await writeP(terminal, sgr('1') + ' ' + sgr('9') + 'termi' + sgr('22') + 'nal' + sgr('29') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='font-weight: bold;'> <\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='font-weight: bold; text-decoration: line-through;'>termi<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='text-decoration: line-through;'>nal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with color styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='color: #00ff00;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with background styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('48;5;46') + 'terminal' + sgr('49') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='background-color: #00ff00;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('empty terminal with default options', async () => {
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/color: #000000; background-color: #ffffff; font-family: courier-new, courier, monospace; font-size: 15px;/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('empty terminal with custom options', async () => {
|
||||
terminal.options.fontFamily = 'verdana';
|
||||
terminal.options.fontSize = 20;
|
||||
terminal.options.theme = {
|
||||
foreground: '#ff00ff',
|
||||
background: '#00ff00'
|
||||
};
|
||||
const output = serializeAddon.serializeAsHTML({
|
||||
includeGlobalBackground: true
|
||||
});
|
||||
assert.equal((output.match(/color: #ff00ff; background-color: #00ff00; font-family: verdana; font-size: 20px;/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('empty terminal with background included', async () => {
|
||||
const output = serializeAddon.serializeAsHTML({
|
||||
includeGlobalBackground: true
|
||||
});
|
||||
assert.equal((output.match(/color: #ffffff; background-color: #000000; font-family: courier-new, courier, monospace; font-size: 15px;/g) || []).length, 1, output);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,8 +5,9 @@
|
||||
* (EXPERIMENTAL) This Addon is still under development
|
||||
*/
|
||||
|
||||
import { Terminal, ITerminalAddon, IBuffer, IBufferCell } from 'xterm';
|
||||
|
||||
import { Terminal, ITerminalAddon, IBuffer, IBufferCell, IBufferRange } from 'xterm';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IAttributeData } from 'common/Types';
|
||||
|
||||
function constrain(value: number, low: number, high: number): number {
|
||||
return Math.max(low, Math.min(value, high));
|
||||
@@ -19,18 +20,25 @@ abstract class BaseSerializeHandler {
|
||||
) {
|
||||
}
|
||||
|
||||
public serialize(startRow: number, endRow: number): string {
|
||||
public serialize(range: IBufferRange): string {
|
||||
// we need two of them to flip between old and new cell
|
||||
const cell1 = this._buffer.getNullCell();
|
||||
const cell2 = this._buffer.getNullCell();
|
||||
let oldCell = cell1;
|
||||
|
||||
const startRow = range.start.x;
|
||||
const endRow = range.end.x;
|
||||
const startColumn = range.start.y;
|
||||
const endColumn = range.end.y;
|
||||
|
||||
this._beforeSerialize(endRow - startRow, startRow, endRow);
|
||||
|
||||
for (let row = startRow; row < endRow; row++) {
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
const line = this._buffer.getLine(row);
|
||||
if (line) {
|
||||
for (let col = 0; col < line.length; col++) {
|
||||
const startLineColumn = row !== range.start.x ? 0 : startColumn;
|
||||
const endLineColumn = row !== range.end.x ? line.length : endColumn;
|
||||
for (let col = startLineColumn; col < endLineColumn; col++) {
|
||||
const c = line.getCell(col, oldCell === cell1 ? cell2 : cell1);
|
||||
if (!c) {
|
||||
console.warn(`Can't get cell at row=${row}, col=${col}`);
|
||||
@@ -40,7 +48,7 @@ abstract class BaseSerializeHandler {
|
||||
oldCell = c;
|
||||
}
|
||||
}
|
||||
this._rowEnd(row, row === endRow - 1);
|
||||
this._rowEnd(row, row === endRow);
|
||||
}
|
||||
|
||||
this._afterSerialize();
|
||||
@@ -55,17 +63,17 @@ abstract class BaseSerializeHandler {
|
||||
protected _serializeString(): string { return ''; }
|
||||
}
|
||||
|
||||
function equalFg(cell1: IBufferCell, cell2: IBufferCell): boolean {
|
||||
function equalFg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
|
||||
return cell1.getFgColorMode() === cell2.getFgColorMode()
|
||||
&& cell1.getFgColor() === cell2.getFgColor();
|
||||
}
|
||||
|
||||
function equalBg(cell1: IBufferCell, cell2: IBufferCell): boolean {
|
||||
function equalBg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
|
||||
return cell1.getBgColorMode() === cell2.getBgColorMode()
|
||||
&& cell1.getBgColor() === cell2.getBgColor();
|
||||
}
|
||||
|
||||
function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean {
|
||||
function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
|
||||
return cell1.isInverse() === cell2.isInverse()
|
||||
&& cell1.isBold() === cell2.isBold()
|
||||
&& cell1.isUnderline() === cell2.isUnderline()
|
||||
@@ -222,7 +230,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
this._nullCellCount = 0;
|
||||
}
|
||||
|
||||
private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): number[] {
|
||||
private _diffStyle(cell: IBufferCell | IAttributeData, oldCell: IBufferCell): number[] {
|
||||
const sgrSeq: number[] = [];
|
||||
const fgChanged = !equalFg(cell, oldCell);
|
||||
const bgChanged = !equalBg(cell, oldCell);
|
||||
@@ -386,6 +394,15 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
moveRight(realCursorCol - this._lastCursorCol);
|
||||
}
|
||||
|
||||
// Restore the cursor's current style, see https://github.com/xtermjs/xterm.js/issues/3677
|
||||
// HACK: Internal API access since it's awkward to expose this in the API and serialize will
|
||||
// likely be the only consumer
|
||||
const curAttrData: IAttributeData = (this._terminal as any)._core._inputHandler._curAttrData;
|
||||
const sgrSeq = this._diffStyle(curAttrData, this._cursorStyle);
|
||||
if (sgrSeq.length > 0) {
|
||||
content += `\u001b[${sgrSeq.join(';')}m`;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
@@ -403,7 +420,35 @@ export class SerializeAddon implements ITerminalAddon {
|
||||
const maxRows = buffer.length;
|
||||
const handler = new StringSerializeHandler(buffer, terminal);
|
||||
const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows);
|
||||
return handler.serialize(maxRows - correctRows, maxRows);
|
||||
return handler.serialize({
|
||||
start: { x: maxRows - correctRows, y: 0 },
|
||||
end: { x: maxRows - 1, y: terminal.cols }
|
||||
});
|
||||
}
|
||||
|
||||
private _serializeBufferAsHTML(terminal: Terminal, options: Partial<IHTMLSerializeOptions>): string {
|
||||
const buffer = terminal.buffer.active;
|
||||
const handler = new HTMLSerializeHandler(buffer, terminal, options);
|
||||
const onlySelection = options.onlySelection ?? false;
|
||||
if (!onlySelection) {
|
||||
const maxRows = buffer.length;
|
||||
const scrollback = options.scrollback;
|
||||
const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows);
|
||||
return handler.serialize({
|
||||
start: { x: maxRows - correctRows, y: 0 },
|
||||
end: { x: maxRows - 1, y: terminal.cols }
|
||||
});
|
||||
}
|
||||
|
||||
const selection = this._terminal?.getSelectionPosition();
|
||||
if (selection !== undefined) {
|
||||
return handler.serialize({
|
||||
start: { x: selection.startRow, y: selection.startColumn },
|
||||
end: { x: selection.endRow, y: selection.endColumn }
|
||||
});
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private _serializeModes(terminal: Terminal): string {
|
||||
@@ -460,6 +505,14 @@ export class SerializeAddon implements ITerminalAddon {
|
||||
return content;
|
||||
}
|
||||
|
||||
public serializeAsHTML(options?: Partial<IHTMLSerializeOptions>): string {
|
||||
if (!this._terminal) {
|
||||
throw new Error('Cannot use addon until it has been loaded');
|
||||
}
|
||||
|
||||
return this._serializeBufferAsHTML(this._terminal, options || {});
|
||||
}
|
||||
|
||||
public dispose(): void { }
|
||||
}
|
||||
|
||||
@@ -469,3 +522,150 @@ interface ISerializeOptions {
|
||||
excludeModes?: boolean;
|
||||
excludeAltBuffer?: boolean;
|
||||
}
|
||||
|
||||
interface IHTMLSerializeOptions {
|
||||
scrollback: number;
|
||||
onlySelection: boolean;
|
||||
includeGlobalBackground: boolean;
|
||||
}
|
||||
|
||||
export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
private _currentRow: string = '';
|
||||
|
||||
private _htmlContent = '';
|
||||
|
||||
private _colors: IColorSet;
|
||||
|
||||
constructor(
|
||||
buffer: IBuffer,
|
||||
private readonly _terminal: Terminal,
|
||||
private readonly _options: Partial<IHTMLSerializeOptions>
|
||||
) {
|
||||
super(buffer);
|
||||
|
||||
// https://github.com/xtermjs/xterm.js/issues/3601
|
||||
this._colors = (_terminal as any)._core._colorManager.colors;
|
||||
}
|
||||
|
||||
private _padStart(target: string, targetLength: number, padString: string): string {
|
||||
targetLength = targetLength >> 0;
|
||||
padString = padString ?? ' ';
|
||||
if (target.length > targetLength) {
|
||||
return target;
|
||||
}
|
||||
|
||||
targetLength -= target.length;
|
||||
if (targetLength > padString.length) {
|
||||
padString += padString.repeat(targetLength / padString.length);
|
||||
}
|
||||
return padString.slice(0, targetLength) + target;
|
||||
}
|
||||
|
||||
protected _beforeSerialize(rows: number, start: number, end: number): void {
|
||||
this._htmlContent += '<html><body><!--StartFragment--><pre>';
|
||||
|
||||
let foreground = '#000000';
|
||||
let background = '#ffffff';
|
||||
if (this._options.includeGlobalBackground ?? false) {
|
||||
foreground = this._terminal.options.theme?.foreground ?? '#ffffff';
|
||||
background = this._terminal.options.theme?.background ?? '#000000';
|
||||
}
|
||||
|
||||
const globalStyleDefinitions = [];
|
||||
globalStyleDefinitions.push('color: ' + foreground + ';');
|
||||
globalStyleDefinitions.push('background-color: ' + background + ';');
|
||||
globalStyleDefinitions.push('font-family: ' + this._terminal.options.fontFamily + ';');
|
||||
globalStyleDefinitions.push('font-size: ' + this._terminal.options.fontSize + 'px;');
|
||||
this._htmlContent += '<div style=\'' + globalStyleDefinitions.join(' ') + '\'>';
|
||||
}
|
||||
|
||||
protected _afterSerialize(): void {
|
||||
this._htmlContent += '</div>';
|
||||
this._htmlContent += '</pre><!--EndFragment--></body></html>';
|
||||
}
|
||||
|
||||
protected _rowEnd(row: number, isLastRow: boolean): void {
|
||||
this._htmlContent += '<div><span>' + this._currentRow + '</span></div>';
|
||||
this._currentRow = '';
|
||||
}
|
||||
|
||||
private _getHexColor(cell: IBufferCell, isFg: boolean): string | undefined {
|
||||
const color = isFg ? cell.getFgColor() : cell.getBgColor();
|
||||
if (isFg ? cell.isFgRGB() : cell.isBgRGB()) {
|
||||
const rgb = [
|
||||
(color >> 16) & 255,
|
||||
(color >> 8) & 255,
|
||||
(color ) & 255
|
||||
];
|
||||
return rgb.map(x => this._padStart(x.toString(16), 2, '0')).join('');
|
||||
}
|
||||
if (isFg ? cell.isFgPalette() : cell.isBgPalette()) {
|
||||
return this._colors.ansi[color].css;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): string[] | undefined {
|
||||
const content: string[] = [];
|
||||
|
||||
const fgChanged = !equalFg(cell, oldCell);
|
||||
const bgChanged = !equalBg(cell, oldCell);
|
||||
const flagsChanged = !equalFlags(cell, oldCell);
|
||||
|
||||
if (fgChanged || bgChanged || flagsChanged) {
|
||||
const fgHexColor = this._getHexColor(cell, true);
|
||||
if (fgHexColor) {
|
||||
content.push('color: ' + fgHexColor + ';');
|
||||
}
|
||||
|
||||
const bgHexColor = this._getHexColor(cell, false);
|
||||
if (bgHexColor) {
|
||||
content.push('background-color: ' + bgHexColor + ';');
|
||||
}
|
||||
|
||||
if (cell.isInverse()) { content.push('color: #000000; background-color: #BFBFBF;'); }
|
||||
if (cell.isBold()) { content.push('font-weight: bold;'); }
|
||||
if (cell.isUnderline()) { content.push('text-decoration: underline;'); }
|
||||
if (cell.isBlink()) { content.push('text-decoration: blink;'); }
|
||||
if (cell.isInvisible()) { content.push('visibility: hidden;'); }
|
||||
if (cell.isItalic()) { content.push('font-style: italic;'); }
|
||||
if (cell.isDim()) { content.push('opacity: 0.5;'); }
|
||||
if (cell.isStrikethrough()) { content.push('text-decoration: line-through;'); }
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void {
|
||||
// a width 0 cell don't need to be count because it is just a placeholder after a CJK character;
|
||||
const isPlaceHolderCell = cell.getWidth() === 0;
|
||||
if (isPlaceHolderCell) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this cell don't have content
|
||||
const isEmptyCell = cell.getChars() === '';
|
||||
|
||||
const styleDefinitions = this._diffStyle(cell, oldCell);
|
||||
|
||||
// handles style change
|
||||
if (styleDefinitions) {
|
||||
this._currentRow += styleDefinitions.length === 0 ?
|
||||
'</span><span>' :
|
||||
'</span><span style=\'' + styleDefinitions.join(' ') + '\'>';
|
||||
}
|
||||
|
||||
// handles actual content
|
||||
if (isEmptyCell) {
|
||||
this._currentRow += ' ';
|
||||
} else {
|
||||
this._currentRow += cell.getChars();
|
||||
}
|
||||
}
|
||||
|
||||
protected _serializeString(): string {
|
||||
return this._htmlContent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
@@ -28,6 +31,9 @@
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src/common"
|
||||
},
|
||||
{
|
||||
"path": "../../../src/browser"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ let page: Page;
|
||||
const width = 800;
|
||||
const height = 600;
|
||||
|
||||
const writeRawSync = (page: any, str: string): Promise<void> => writeSync(page, '\' +' + JSON.stringify(str) + '+ \'');
|
||||
const writeRawSync = (page: any, str: string): Promise<void> => writeSync(page, `' +` + JSON.stringify(str) + `+ '`);
|
||||
|
||||
const testNormalScreenEqual = async (page: any, str: string): Promise<void> => {
|
||||
await writeRawSync(page, str);
|
||||
@@ -83,7 +83,7 @@ describe('SerializeAddon', () => {
|
||||
const buffer3 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||
|
||||
await page.evaluate(`term.reset();`);
|
||||
await writeRawSync(page, '1234567890n12345');
|
||||
await writeRawSync(page, '123456789012345');
|
||||
const buffer4 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||
|
||||
assert.throw(() => {
|
||||
|
||||
@@ -24,14 +24,23 @@ declare module 'xterm-addon-serialize' {
|
||||
* the state. The cursor will also be positioned to the correct cell. When restoring a terminal
|
||||
* it is best to do before `Terminal.open` is called to avoid wasting CPU cycles rendering
|
||||
* incomplete frames.
|
||||
*
|
||||
*
|
||||
* It's recommended that you write the serialized data into a terminal of the same size in which
|
||||
* it originated from and then resize it after if needed.
|
||||
*
|
||||
*
|
||||
* @param options Custom options to allow control over what gets serialized.
|
||||
*/
|
||||
public serialize(options?: ISerializeOptions): string;
|
||||
|
||||
/**
|
||||
* Serializes terminal content as HTML, which can be written to the clipboard using the
|
||||
* `text/html` mimetype. For applications that support it, the pasted text should then retain
|
||||
* its colors/styles.
|
||||
*
|
||||
* @param options Custom options to allow control over what gets serialized.
|
||||
*/
|
||||
public serializeAsHTML(options?: Partial<IHTMLSerializeOptions>): string;
|
||||
|
||||
/**
|
||||
* Disposes the addon.
|
||||
*/
|
||||
@@ -56,4 +65,24 @@ declare module 'xterm-addon-serialize' {
|
||||
*/
|
||||
excludeAltBuffer?: boolean;
|
||||
}
|
||||
|
||||
export interface IHTMLSerializeOptions {
|
||||
/**
|
||||
* The number of rows in the scrollback buffer to serialize, starting from the bottom of the
|
||||
* scrollback buffer. When not specified, all available rows in the scrollback buffer will be
|
||||
* serialized. This setting is ignored if {@link IHTMLSerializeOptions.onlySelection} is true.
|
||||
*/
|
||||
scrollback: number;
|
||||
|
||||
/**
|
||||
* Whether to only serialize the selection. If false, the whole active buffer is serialized in HTML.
|
||||
* False by default.
|
||||
*/
|
||||
onlySelection: boolean;
|
||||
|
||||
/**
|
||||
* Whether to include the global background of the terminal. False by default.
|
||||
*/
|
||||
includeGlobalBackground: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
"types": "typings/xterm-addon-unicode11.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"terminal",
|
||||
"xterm",
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"prepackage": "npm run build",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-web-links",
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
@@ -9,6 +9,11 @@
|
||||
"types": "typings/xterm-addon-web-links.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"terminal",
|
||||
"xterm",
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"prepackage": "npm run build",
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
|
||||
import { ILinkProvider, ILink, Terminal, IViewportRange } from 'xterm';
|
||||
|
||||
interface ILinkProviderOptions {
|
||||
export interface ILinkProviderOptions {
|
||||
hover?(event: MouseEvent, text: string, location: IViewportRange): void;
|
||||
leave?(event: MouseEvent, text: string): void;
|
||||
urlRegex?: RegExp;
|
||||
}
|
||||
|
||||
export class WebLinkProvider implements ILinkProvider {
|
||||
@@ -78,10 +79,17 @@ export class LinkComputer {
|
||||
endY++;
|
||||
}
|
||||
|
||||
let startX = stringIndex + 1;
|
||||
let startY = startLineIndex + 1;
|
||||
while (startX > terminal.cols) {
|
||||
startX -= terminal.cols;
|
||||
startY++;
|
||||
}
|
||||
|
||||
const range = {
|
||||
start: {
|
||||
x: stringIndex + 1,
|
||||
y: startLineIndex + 1
|
||||
x: startX,
|
||||
y: startY
|
||||
},
|
||||
end: {
|
||||
x: endX,
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ILinkMatcherOptions, ITerminalAddon, IDisposable, IViewportRange } from 'xterm';
|
||||
import { WebLinkProvider } from './WebLinkProvider';
|
||||
import { Terminal, ILinkMatcherOptions, ITerminalAddon, IDisposable } from 'xterm';
|
||||
import { ILinkProviderOptions, WebLinkProvider } from './WebLinkProvider';
|
||||
|
||||
const protocolClause = '(https?:\\/\\/)';
|
||||
const domainCharacterSet = '[\\da-z\\.-]+';
|
||||
const negatedDomainCharacterSet = '[^\\da-z\\.-]+';
|
||||
const domainBodyClause = '(' + domainCharacterSet + ')';
|
||||
const tldClause = '([a-z\\.]{2,6})';
|
||||
const tldClause = '([a-z\\.]{2,18})';
|
||||
const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
|
||||
const localHostClause = '(localhost)';
|
||||
const portClause = '(:\\d{1,5})';
|
||||
@@ -40,12 +40,6 @@ function handleLink(event: MouseEvent, uri: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
interface ILinkProviderOptions {
|
||||
hover?(event: MouseEvent, text: string, location: IViewportRange): void;
|
||||
leave?(event: MouseEvent, text: string): void;
|
||||
urlRegex?: RegExp;
|
||||
}
|
||||
|
||||
export class WebLinksAddon implements ITerminalAddon {
|
||||
private _linkMatcherId: number | undefined;
|
||||
private _terminal: Terminal | undefined;
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
"strict": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha",
|
||||
"../../../node_modules/@types/node",
|
||||
"../../../out-test/api/TestUtils"
|
||||
"../../../node_modules/@types/node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
|
||||
@@ -49,5 +49,10 @@ declare module 'xterm-addon-web-links' {
|
||||
* happen even when tooltipCallback hasn't fired for the link yet.
|
||||
*/
|
||||
leave?(event: MouseEvent, text: string): void;
|
||||
|
||||
/**
|
||||
* A callback to use instead of the default one.
|
||||
*/
|
||||
urlRegex?: RegExp;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-webgl",
|
||||
"version": "0.11.4",
|
||||
"version": "0.12.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
@@ -9,6 +9,12 @@
|
||||
"types": "typings/xterm-addon-webgl.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"terminal",
|
||||
"webgl",
|
||||
"xterm",
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"prepackage": "npm run build",
|
||||
|
||||
@@ -6,14 +6,11 @@
|
||||
import { createProgram, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
|
||||
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
|
||||
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types';
|
||||
import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_INDICIES_PER_CELL, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_BG_OFFSET } from './RenderModel';
|
||||
import { fill } from 'common/TypedArrayUtils';
|
||||
import { slice } from './TypedArray';
|
||||
import { NULL_CELL_CODE, WHITESPACE_CELL_CODE, Attributes, FgFlags } from 'common/buffer/Constants';
|
||||
import { NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { Terminal, IBufferLine } from 'xterm';
|
||||
import { IColorSet, IColor } from 'browser/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
|
||||
interface IVertices {
|
||||
attributes: Float32Array;
|
||||
@@ -24,7 +21,6 @@ interface IVertices {
|
||||
* working on the next frame.
|
||||
*/
|
||||
attributesBuffers: Float32Array[];
|
||||
selectionAttributes: Float32Array;
|
||||
count: number;
|
||||
}
|
||||
|
||||
@@ -91,8 +87,7 @@ export class GlyphRenderer {
|
||||
attributesBuffers: [
|
||||
new Float32Array(0),
|
||||
new Float32Array(0)
|
||||
],
|
||||
selectionAttributes: new Float32Array(0)
|
||||
]
|
||||
};
|
||||
|
||||
constructor(
|
||||
@@ -187,6 +182,8 @@ export class GlyphRenderer {
|
||||
if (!this._atlas) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the glyph
|
||||
if (chars && chars.length > 1) {
|
||||
rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg);
|
||||
} else {
|
||||
@@ -214,121 +211,41 @@ export class GlyphRenderer {
|
||||
// a_cellpos only changes on resize
|
||||
}
|
||||
|
||||
public updateSelection(model: IRenderModel): void {
|
||||
public clear(force?: boolean): void {
|
||||
const terminal = this._terminal;
|
||||
const newCount = terminal.cols * terminal.rows * INDICES_PER_CELL;
|
||||
|
||||
this._vertices.selectionAttributes = slice(this._vertices.attributes, 0);
|
||||
// Don't clear if not forced and the array length is correct
|
||||
if (!force && this._vertices.count === newCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bg = (this._colors.selectionOpaque.rgba >>> 8) | Attributes.CM_RGB;
|
||||
|
||||
if (model.selection.columnSelectMode) {
|
||||
const startCol = model.selection.startCol;
|
||||
const width = model.selection.endCol - startCol;
|
||||
const height = model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow + 1;
|
||||
for (let y = model.selection.viewportCappedStartRow; y < model.selection.viewportCappedStartRow + height; y++) {
|
||||
this._updateSelectionRange(startCol, startCol + width, y, model, bg);
|
||||
}
|
||||
} else {
|
||||
// Draw first row
|
||||
const startCol = model.selection.viewportStartRow === model.selection.viewportCappedStartRow ? model.selection.startCol : 0;
|
||||
const startRowEndCol = model.selection.viewportCappedStartRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols;
|
||||
this._updateSelectionRange(startCol, startRowEndCol, model.selection.viewportCappedStartRow, model, bg);
|
||||
|
||||
// Draw middle rows
|
||||
const middleRowsCount = Math.max(model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow - 1, 0);
|
||||
for (let y = model.selection.viewportCappedStartRow + 1; y <= model.selection.viewportCappedStartRow + middleRowsCount; y++) {
|
||||
this._updateSelectionRange(0, startRowEndCol, y, model, bg);
|
||||
}
|
||||
|
||||
// Draw final row
|
||||
if (model.selection.viewportCappedStartRow !== model.selection.viewportCappedEndRow) {
|
||||
// Only draw viewportEndRow if it's not the same as viewportStartRow
|
||||
const endCol = model.selection.viewportEndRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols;
|
||||
this._updateSelectionRange(0, endCol, model.selection.viewportCappedEndRow, model, bg);
|
||||
// Clear vertices
|
||||
this._vertices.count = newCount;
|
||||
this._vertices.attributes = new Float32Array(newCount);
|
||||
for (let i = 0; i < this._vertices.attributesBuffers.length; i++) {
|
||||
this._vertices.attributesBuffers[i] = new Float32Array(newCount);
|
||||
}
|
||||
let i = 0;
|
||||
for (let y = 0; y < terminal.rows; y++) {
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
this._vertices.attributes[i + 8] = x / terminal.cols;
|
||||
this._vertices.attributes[i + 9] = y / terminal.rows;
|
||||
i += INDICES_PER_CELL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _updateSelectionRange(startCol: number, endCol: number, y: number, model: IRenderModel, bg: number): void {
|
||||
const terminal = this._terminal;
|
||||
const row = y + terminal.buffer.active.viewportY;
|
||||
let line: IBufferLine | undefined;
|
||||
for (let x = startCol; x < endCol; x++) {
|
||||
const offset = (y * this._terminal.cols + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
const code = model.cells[offset];
|
||||
let fg = model.cells[offset + RENDER_MODEL_FG_OFFSET];
|
||||
if (fg & FgFlags.INVERSE) {
|
||||
const workCell = new AttributeData();
|
||||
workCell.fg = fg;
|
||||
workCell.bg = model.cells[offset + RENDER_MODEL_BG_OFFSET];
|
||||
// Get attributes from fg (excluding inverse) and resolve inverse by pullibng rgb colors
|
||||
// from bg. This is needed since the inverse fg color should be based on the original bg
|
||||
// color, not on the selection color
|
||||
fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK | FgFlags.INVERSE);
|
||||
switch (workCell.getBgColorMode()) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
const c = this._getColorFromAnsiIndex(workCell.getBgColor()).rgba;
|
||||
fg |= (c >> 8) & Attributes.RED_MASK | (c >> 8) & Attributes.GREEN_MASK | (c >> 8) & Attributes.BLUE_MASK;
|
||||
case Attributes.CM_RGB:
|
||||
const arr = AttributeData.toColorRGB(workCell.getBgColor());
|
||||
fg |= arr[0] << Attributes.RED_SHIFT | arr[1] << Attributes.GREEN_SHIFT | arr[2] << Attributes.BLUE_SHIFT;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
const c2 = this._colors.background.rgba;
|
||||
fg |= (c2 >> 8) & Attributes.RED_MASK | (c2 >> 8) & Attributes.GREEN_MASK | (c2 >> 8) & Attributes.BLUE_MASK;
|
||||
}
|
||||
fg |= Attributes.CM_RGB;
|
||||
}
|
||||
if (code & COMBINED_CHAR_BIT_MASK) {
|
||||
if (!line) {
|
||||
line = terminal.buffer.active.getLine(row);
|
||||
}
|
||||
const chars = line!.getCell(x)!.getChars();
|
||||
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, fg, chars);
|
||||
} else {
|
||||
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _getColorFromAnsiIndex(idx: number): IColor {
|
||||
if (idx >= this._colors.ansi.length) {
|
||||
throw new Error('No color found for idx ' + idx);
|
||||
}
|
||||
return this._colors.ansi[idx];
|
||||
}
|
||||
|
||||
public onResize(): void {
|
||||
const terminal = this._terminal;
|
||||
const gl = this._gl;
|
||||
|
||||
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||
|
||||
// Update vertices
|
||||
const newCount = terminal.cols * terminal.rows * INDICES_PER_CELL;
|
||||
if (this._vertices.count !== newCount) {
|
||||
this._vertices.count = newCount;
|
||||
this._vertices.attributes = new Float32Array(newCount);
|
||||
for (let i = 0; i < this._vertices.attributesBuffers.length; i++) {
|
||||
this._vertices.attributesBuffers[i] = new Float32Array(newCount);
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
for (let y = 0; y < terminal.rows; y++) {
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
this._vertices.attributes[i + 8] = x / terminal.cols;
|
||||
this._vertices.attributes[i + 9] = y / terminal.rows;
|
||||
i += INDICES_PER_CELL;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.clear();
|
||||
}
|
||||
|
||||
public setColors(): void {
|
||||
}
|
||||
|
||||
public render(renderModel: IRenderModel, isSelectionVisible: boolean): void {
|
||||
public render(renderModel: IRenderModel): void {
|
||||
if (!this._atlas) {
|
||||
return;
|
||||
}
|
||||
@@ -352,7 +269,7 @@ export class GlyphRenderer {
|
||||
let bufferLength = 0;
|
||||
for (let y = 0; y < renderModel.lineLengths.length; y++) {
|
||||
const si = y * this._terminal.cols * INDICES_PER_CELL;
|
||||
const sub = (isSelectionVisible ? this._vertices.selectionAttributes : this._vertices.attributes).subarray(si, si + renderModel.lineLengths[y] * INDICES_PER_CELL);
|
||||
const sub = this._vertices.attributes.subarray(si, si + renderModel.lineLengths[y] * INDICES_PER_CELL);
|
||||
activeBuffer.set(sub, bufferLength);
|
||||
bufferLength += sub.length;
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
*/
|
||||
|
||||
import { createProgram, expandFloat32Array, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
|
||||
import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext, ISelectionRenderModel } from './Types';
|
||||
import { fill } from 'common/TypedArrayUtils';
|
||||
import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext } from './Types';
|
||||
import { Attributes, FgFlags } from 'common/buffer/Constants';
|
||||
import { Terminal } from 'xterm';
|
||||
import { IColorSet, IColor } from 'browser/Types';
|
||||
import { IColor } from 'common/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
|
||||
|
||||
@@ -49,7 +49,6 @@ void main() {
|
||||
|
||||
interface IVertices {
|
||||
attributes: Float32Array;
|
||||
selection: Float32Array;
|
||||
count: number;
|
||||
}
|
||||
|
||||
@@ -66,12 +65,10 @@ export class RectangleRenderer {
|
||||
private _attributesBuffer: WebGLBuffer;
|
||||
private _projectionLocation: WebGLUniformLocation;
|
||||
private _bgFloat!: Float32Array;
|
||||
private _selectionFloat!: Float32Array;
|
||||
|
||||
private _vertices: IVertices = {
|
||||
count: 0,
|
||||
attributes: new Float32Array(INITIAL_BUFFER_RECTANGLE_CAPACITY),
|
||||
selection: new Float32Array(3 * INDICES_PER_RECTANGLE)
|
||||
attributes: new Float32Array(INITIAL_BUFFER_RECTANGLE_CAPACITY)
|
||||
};
|
||||
|
||||
constructor(
|
||||
@@ -137,11 +134,6 @@ export class RectangleRenderer {
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, this._vertices.attributes, gl.DYNAMIC_DRAW);
|
||||
gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, this._vertices.count);
|
||||
|
||||
// Bind selection buffer and draw
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, this._vertices.selection, gl.DYNAMIC_DRAW);
|
||||
gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, 3);
|
||||
}
|
||||
|
||||
public onResize(): void {
|
||||
@@ -155,7 +147,6 @@ export class RectangleRenderer {
|
||||
|
||||
private _updateCachedColors(): void {
|
||||
this._bgFloat = this._colorToFloat32Array(this._colors.background);
|
||||
this._selectionFloat = this._colorToFloat32Array(this._colors.selectionOpaque);
|
||||
}
|
||||
|
||||
private _updateViewportRectangle(): void {
|
||||
@@ -171,73 +162,6 @@ export class RectangleRenderer {
|
||||
);
|
||||
}
|
||||
|
||||
public updateSelection(model: ISelectionRenderModel): void {
|
||||
const terminal = this._terminal;
|
||||
|
||||
if (!model.hasSelection) {
|
||||
fill(this._vertices.selection, 0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.columnSelectMode) {
|
||||
const startCol = model.startCol;
|
||||
const width = model.endCol - startCol;
|
||||
const height = model.viewportCappedEndRow - model.viewportCappedStartRow + 1;
|
||||
this._addRectangleFloat(
|
||||
this._vertices.selection,
|
||||
0,
|
||||
startCol * this._dimensions.scaledCellWidth,
|
||||
model.viewportCappedStartRow * this._dimensions.scaledCellHeight,
|
||||
width * this._dimensions.scaledCellWidth,
|
||||
height * this._dimensions.scaledCellHeight,
|
||||
this._selectionFloat
|
||||
);
|
||||
fill(this._vertices.selection, 0, INDICES_PER_RECTANGLE);
|
||||
} else {
|
||||
// Draw first row
|
||||
const startCol = model.viewportStartRow === model.viewportCappedStartRow ? model.startCol : 0;
|
||||
const startRowEndCol = model.viewportCappedStartRow === model.viewportEndRow ? model.endCol : terminal.cols;
|
||||
this._addRectangleFloat(
|
||||
this._vertices.selection,
|
||||
0,
|
||||
startCol * this._dimensions.scaledCellWidth,
|
||||
model.viewportCappedStartRow * this._dimensions.scaledCellHeight,
|
||||
(startRowEndCol - startCol) * this._dimensions.scaledCellWidth,
|
||||
this._dimensions.scaledCellHeight,
|
||||
this._selectionFloat
|
||||
);
|
||||
|
||||
// Draw middle rows
|
||||
const middleRowsCount = Math.max(model.viewportCappedEndRow - model.viewportCappedStartRow - 1, 0);
|
||||
this._addRectangleFloat(
|
||||
this._vertices.selection,
|
||||
INDICES_PER_RECTANGLE,
|
||||
0,
|
||||
(model.viewportCappedStartRow + 1) * this._dimensions.scaledCellHeight,
|
||||
terminal.cols * this._dimensions.scaledCellWidth,
|
||||
middleRowsCount * this._dimensions.scaledCellHeight,
|
||||
this._selectionFloat
|
||||
);
|
||||
|
||||
// Draw final row
|
||||
if (model.viewportCappedStartRow !== model.viewportCappedEndRow) {
|
||||
// Only draw viewportEndRow if it's not the same as viewportStartRow
|
||||
const endCol = model.viewportEndRow === model.viewportCappedEndRow ? model.endCol : terminal.cols;
|
||||
this._addRectangleFloat(
|
||||
this._vertices.selection,
|
||||
INDICES_PER_RECTANGLE * 2,
|
||||
0,
|
||||
model.viewportCappedEndRow * this._dimensions.scaledCellHeight,
|
||||
endCol * this._dimensions.scaledCellWidth,
|
||||
this._dimensions.scaledCellHeight,
|
||||
this._selectionFloat
|
||||
);
|
||||
} else {
|
||||
fill(this._vertices.selection, 0, INDICES_PER_RECTANGLE * 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public updateBackgrounds(model: IRenderModel): void {
|
||||
const terminal = this._terminal;
|
||||
const vertices = this._vertices;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ICharacterJoinerService, IRenderService } from 'browser/services/Servic
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { isSafari } from 'common/Platform';
|
||||
import { IDecorationService } from 'common/services/Services';
|
||||
|
||||
export class WebglAddon implements ITerminalAddon {
|
||||
private _terminal?: Terminal;
|
||||
@@ -30,8 +31,9 @@ export class WebglAddon implements ITerminalAddon {
|
||||
this._terminal = terminal;
|
||||
const renderService: IRenderService = (terminal as any)._core._renderService;
|
||||
const characterJoinerService: ICharacterJoinerService = (terminal as any)._core._characterJoinerService;
|
||||
const decorationService: IDecorationService = (terminal as any)._core._decorationService;
|
||||
const colors: IColorSet = (terminal as any)._core._colorManager.colors;
|
||||
this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, this._preserveDrawingBuffer);
|
||||
this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, decorationService, this._preserveDrawingBuffer);
|
||||
this._renderer.onContextLoss(() => this._onContextLoss.fire());
|
||||
renderService.setRenderer(this._renderer);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { RectangleRenderer } from './RectangleRenderer';
|
||||
import { IWebGL2RenderingContext } from './Types';
|
||||
import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { Attributes, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { Terminal, IEvent } from 'xterm';
|
||||
import { IRenderLayer } from './renderLayer/Types';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
@@ -23,6 +23,7 @@ import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { ICharacterJoinerService } from 'browser/services/Services';
|
||||
import { CharData, ICellData } from 'common/Types';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { IDecorationService } from 'common/services/Services';
|
||||
|
||||
export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private _renderLayers: IRenderLayer[];
|
||||
@@ -31,6 +32,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
private _model: RenderModel = new RenderModel();
|
||||
private _workCell: CellData = new CellData();
|
||||
private _workColors: { fg: number, bg: number } = { fg: 0, bg: 0 };
|
||||
|
||||
private _canvas: HTMLCanvasElement;
|
||||
private _gl: IWebGL2RenderingContext;
|
||||
@@ -52,6 +54,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private _terminal: Terminal,
|
||||
private _colors: IColorSet,
|
||||
private readonly _characterJoinerService: ICharacterJoinerService,
|
||||
private readonly _decorationService: IDecorationService,
|
||||
preserveDrawingBuffer?: boolean
|
||||
) {
|
||||
super();
|
||||
@@ -164,11 +167,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._core.screenElement!.style.height = `${this.dimensions.canvasHeight}px`;
|
||||
|
||||
this._rectangleRenderer.onResize();
|
||||
if (this._model.selection.hasSelection) {
|
||||
// Update selection as dimensions have changed
|
||||
this._rectangleRenderer.updateSelection(this._model.selection);
|
||||
}
|
||||
|
||||
this._glyphRenderer.setDimensions(this.dimensions);
|
||||
this._glyphRenderer.onResize();
|
||||
|
||||
@@ -198,10 +196,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
for (const l of this._renderLayers) {
|
||||
l.onSelectionChanged(this._terminal, start, end, columnSelectMode);
|
||||
}
|
||||
|
||||
this._updateSelectionModel(start, end, columnSelectMode);
|
||||
|
||||
this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 });
|
||||
this._requestRedrawViewport();
|
||||
}
|
||||
|
||||
public onCursorMove(): void {
|
||||
@@ -243,10 +239,12 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._charAtlas?.clearTexture();
|
||||
this._model.clear();
|
||||
this._updateModel(0, this._terminal.rows - 1);
|
||||
this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 });
|
||||
this._requestRedrawViewport();
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._model.clear();
|
||||
this._glyphRenderer.clear(true);
|
||||
for (const l of this._renderLayers) {
|
||||
l.reset(this._terminal);
|
||||
}
|
||||
@@ -287,7 +285,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
// Render
|
||||
this._rectangleRenderer.render();
|
||||
this._glyphRenderer.render(this._model, this._model.selection.hasSelection);
|
||||
this._glyphRenderer.render(this._model);
|
||||
}
|
||||
|
||||
private _updateModel(start: number, end: number): void {
|
||||
@@ -329,14 +327,17 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
let code = cell.getCode();
|
||||
const i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
|
||||
// Load colors/resolve overrides into work colors
|
||||
this._loadColorsForCell(x, row);
|
||||
|
||||
if (code !== NULL_CELL_CODE) {
|
||||
this._model.lineLengths[y] = x + 1;
|
||||
}
|
||||
|
||||
// Nothing has changed, no updates needed
|
||||
if (this._model.cells[i] === code &&
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === cell.bg &&
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === cell.fg) {
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workColors.bg &&
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workColors.fg) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -347,10 +348,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
// Cache the results in the model
|
||||
this._model.cells[i] = code;
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = cell.bg;
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = cell.fg;
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workColors.bg;
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workColors.fg;
|
||||
|
||||
this._glyphRenderer.updateCell(x, y, code, cell.bg, cell.fg, chars);
|
||||
this._glyphRenderer.updateCell(x, y, code, this._workColors.bg, this._workColors.fg, chars);
|
||||
|
||||
if (isJoined) {
|
||||
// Restore work cell
|
||||
@@ -361,17 +362,110 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
const j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
this._glyphRenderer.updateCell(x, y, NULL_CELL_CODE, 0, 0, NULL_CELL_CHAR);
|
||||
this._model.cells[j] = NULL_CELL_CODE;
|
||||
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workCell.bg;
|
||||
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workCell.fg;
|
||||
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workColors.bg;
|
||||
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workColors.fg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this._rectangleRenderer.updateBackgrounds(this._model);
|
||||
if (this._model.selection.hasSelection) {
|
||||
// Model could be updated but the selection is unchanged
|
||||
this._glyphRenderer.updateSelection(this._model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads colors for the cell into the work colors object. This resolves overrides/inverse if
|
||||
* necessary which is why the work cell object is not used.
|
||||
*/
|
||||
private _loadColorsForCell(x: number, y: number): void {
|
||||
this._workColors.bg = this._workCell.bg;
|
||||
this._workColors.fg = this._workCell.fg;
|
||||
|
||||
// Get any foreground/background overrides, this happens on the model to avoid spreading
|
||||
// override logic throughout the different sub-renderers
|
||||
let bgOverride: number | undefined;
|
||||
let fgOverride: number | undefined;
|
||||
|
||||
// Apply decorations on the bottom layer
|
||||
for (const d of this._decorationService.getDecorationsAtCell(x, y, 'bottom')) {
|
||||
if (d.backgroundColorRGB) {
|
||||
bgOverride = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
}
|
||||
if (d.foregroundColorRGB) {
|
||||
fgOverride = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the selection color if needed
|
||||
if (this._isCellSelected(x, y)) {
|
||||
bgOverride = this._colors.selectionOpaque.rgba >> 8 & 0xFFFFFF;
|
||||
if (this._colors.selectionForeground) {
|
||||
fgOverride = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply decorations on the top layer
|
||||
for (const d of this._decorationService.getDecorationsAtCell(x, y, 'top')) {
|
||||
if (d.backgroundColorRGB) {
|
||||
bgOverride = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
}
|
||||
if (d.foregroundColorRGB) {
|
||||
fgOverride = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag
|
||||
// ahead of time in order to use the correct cache key
|
||||
if (bgOverride !== undefined) {
|
||||
// Non-RGB attributes from model + override + force RGB color mode
|
||||
bgOverride = (this._workCell.bg & ~Attributes.RGB_MASK) | bgOverride | Attributes.CM_RGB;
|
||||
}
|
||||
if (fgOverride !== undefined) {
|
||||
// Non-RGB attributes from model + force disable inverse + override + force RGB color mode
|
||||
fgOverride = (this._workCell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | fgOverride | Attributes.CM_RGB;
|
||||
}
|
||||
|
||||
// Handle case where inverse was specified by only one of bgOverride or fgOverride was set,
|
||||
// resolving the other inverse color and setting the inverse flag if needed.
|
||||
if (this._workColors.fg & FgFlags.INVERSE) {
|
||||
if (bgOverride !== undefined && fgOverride === undefined) {
|
||||
// Resolve bg color type (default color has a different meaning in fg vs bg)
|
||||
if ((this._workColors.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
|
||||
fgOverride = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | ((this._colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
|
||||
} else {
|
||||
fgOverride = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this._workColors.bg & (Attributes.RGB_MASK | Attributes.CM_MASK);
|
||||
}
|
||||
}
|
||||
if (bgOverride === undefined && fgOverride !== undefined) {
|
||||
// Resolve bg color type (default color has a different meaning in fg vs bg)
|
||||
if ((this._workColors.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
|
||||
bgOverride = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | ((this._colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
|
||||
} else {
|
||||
bgOverride = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this._workColors.fg & (Attributes.RGB_MASK | Attributes.CM_MASK);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the override if it exists
|
||||
this._workColors.bg = bgOverride ?? this._workColors.bg;
|
||||
this._workColors.fg = fgOverride ?? this._workColors.fg;
|
||||
}
|
||||
|
||||
private _isCellSelected(x: number, y: number): boolean {
|
||||
if (!this._model.selection.hasSelection) {
|
||||
return false;
|
||||
}
|
||||
y -= this._terminal.buffer.active.viewportY;
|
||||
if (this._model.selection.columnSelectMode) {
|
||||
if (this._model.selection.startCol <= this._model.selection.endCol) {
|
||||
return x >= this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow &&
|
||||
x < this._model.selection.endCol && y <= this._model.selection.viewportCappedEndRow;
|
||||
}
|
||||
return x < this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow &&
|
||||
x >= this._model.selection.endCol && y <= this._model.selection.viewportCappedEndRow;
|
||||
}
|
||||
return (y > this._model.selection.viewportStartRow && y < this._model.selection.viewportEndRow) ||
|
||||
(this._model.selection.viewportStartRow === this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol && x < this._model.selection.endCol) ||
|
||||
(this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportEndRow && x < this._model.selection.endCol) ||
|
||||
(this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol);
|
||||
}
|
||||
|
||||
private _updateSelectionModel(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
|
||||
@@ -380,7 +474,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
// Selection does not exist
|
||||
if (!start || !end || (start[0] === end[0] && start[1] === end[1])) {
|
||||
this._model.clearSelection();
|
||||
this._rectangleRenderer.updateSelection(this._model.selection);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -393,7 +486,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
// No need to draw the selection
|
||||
if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) {
|
||||
this._model.clearSelection();
|
||||
this._rectangleRenderer.updateSelection(this._model.selection);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -405,8 +497,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._model.selection.viewportCappedEndRow = viewportCappedEndRow;
|
||||
this._model.selection.startCol = start[0];
|
||||
this._model.selection.endCol = end[0];
|
||||
|
||||
this._rectangleRenderer.updateSelection(this._model.selection);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -438,18 +528,18 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
// will be floored because since lineHeight can never be lower then 1, there
|
||||
// is a guarentee that the scaled line height will always be larger than
|
||||
// scaled char height.
|
||||
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.getOption('lineHeight'));
|
||||
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight!);
|
||||
|
||||
// Calculate the y coordinate within a cell that text should draw from in
|
||||
// order to draw in the center of a cell.
|
||||
this.dimensions.scaledCharTop = this._terminal.getOption('lineHeight') === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
|
||||
this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
|
||||
|
||||
// Calculate the scaled cell width, taking the letterSpacing into account.
|
||||
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.getOption('letterSpacing'));
|
||||
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing!);
|
||||
|
||||
// Calculate the x coordinate with a cell that text should draw from in
|
||||
// order to draw in the center of a cell.
|
||||
this.dimensions.scaledCharLeft = Math.floor(this._terminal.getOption('letterSpacing') / 2);
|
||||
this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing! / 2);
|
||||
|
||||
// Recalculate the canvas dimensions; scaled* define the actual number of
|
||||
// pixel in the canvas
|
||||
@@ -480,6 +570,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this.dimensions.actualCellHeight = this.dimensions.scaledCellHeight / this._devicePixelRatio;
|
||||
this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio;
|
||||
}
|
||||
|
||||
private _requestRedrawViewport(): void {
|
||||
this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 });
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Share impl with core
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import { ICharAtlasConfig } from './Types';
|
||||
import { Attributes } from 'common/buffer/Constants';
|
||||
import { Terminal, FontWeight } from 'xterm';
|
||||
import { IColorSet, IColor } from 'browser/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IColor } from 'common/Types';
|
||||
|
||||
const NULL_COLOR: IColor = {
|
||||
css: '',
|
||||
@@ -22,27 +23,28 @@ export function generateConfig(scaledCellWidth: number, scaledCellHeight: number
|
||||
cursorAccent: NULL_COLOR,
|
||||
selectionTransparent: NULL_COLOR,
|
||||
selectionOpaque: NULL_COLOR,
|
||||
selectionForeground: NULL_COLOR,
|
||||
// For the static char atlas, we only use the first 16 colors, but we need all 256 for the
|
||||
// dynamic character atlas.
|
||||
ansi: colors.ansi.slice(),
|
||||
contrastCache: colors.contrastCache
|
||||
};
|
||||
return {
|
||||
customGlyphs: terminal.getOption('customGlyphs'),
|
||||
customGlyphs: terminal.options.customGlyphs!,
|
||||
devicePixelRatio: window.devicePixelRatio,
|
||||
letterSpacing: terminal.getOption('letterSpacing'),
|
||||
lineHeight: terminal.getOption('lineHeight'),
|
||||
letterSpacing: terminal.options.letterSpacing!,
|
||||
lineHeight: terminal.options.lineHeight!,
|
||||
scaledCellWidth,
|
||||
scaledCellHeight,
|
||||
scaledCharWidth,
|
||||
scaledCharHeight,
|
||||
fontFamily: terminal.getOption('fontFamily'),
|
||||
fontSize: terminal.getOption('fontSize'),
|
||||
fontWeight: terminal.getOption('fontWeight') as FontWeight,
|
||||
fontWeightBold: terminal.getOption('fontWeightBold') as FontWeight,
|
||||
allowTransparency: terminal.getOption('allowTransparency'),
|
||||
drawBoldTextInBrightColors: terminal.getOption('drawBoldTextInBrightColors'),
|
||||
minimumContrastRatio: terminal.getOption('minimumContrastRatio'),
|
||||
fontFamily: terminal.options.fontFamily!,
|
||||
fontSize: terminal.options.fontSize!,
|
||||
fontWeight: terminal.options.fontWeight as FontWeight,
|
||||
fontWeightBold: terminal.options.fontWeightBold as FontWeight,
|
||||
allowTransparency: terminal.options.allowTransparency!,
|
||||
drawBoldTextInBrightColors: terminal.options.drawBoldTextInBrightColors!,
|
||||
minimumContrastRatio: terminal.options.minimumContrastRatio!,
|
||||
colors: clonedColors
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,11 +8,12 @@ import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/atlas/Constants';
|
||||
import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from '../Types';
|
||||
import { DEFAULT_COLOR, Attributes } from 'common/buffer/Constants';
|
||||
import { throwIfFalsy } from '../WebglUtils';
|
||||
import { IColor } from 'browser/Types';
|
||||
import { IColor } from 'common/Types';
|
||||
import { IDisposable } from 'xterm';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { channels, rgba } from 'browser/Color';
|
||||
import { channels, rgba } from 'common/Color';
|
||||
import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
|
||||
import { excludeFromContrastRatioDemands, isPowerlineGlyph } from 'browser/renderer/RendererUtils';
|
||||
|
||||
// For debugging purposes, it can be useful to set this to a really tiny value,
|
||||
// to verify that LRU eviction works.
|
||||
@@ -216,8 +217,8 @@ export class WebglCharAtlas implements IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
private _getForegroundCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string {
|
||||
const minimumContrastCss = this._getMinimumContrastCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold);
|
||||
private _getForegroundCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): string {
|
||||
const minimumContrastCss = this._getMinimumContrastCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, excludeFromContrastRatioDemands);
|
||||
if (minimumContrastCss) {
|
||||
return minimumContrastCss;
|
||||
}
|
||||
@@ -238,7 +239,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
const bg = this._config.colors.background.css;
|
||||
if (bg.length === 9) {
|
||||
// Remove bg alpha channel if present
|
||||
return bg.substr(0, 7);
|
||||
return bg.slice(0, 7);
|
||||
}
|
||||
return bg;
|
||||
}
|
||||
@@ -281,8 +282,8 @@ export class WebglCharAtlas implements IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
private _getMinimumContrastCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string | undefined {
|
||||
if (this._config.minimumContrastRatio === 1) {
|
||||
private _getMinimumContrastCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): string | undefined {
|
||||
if (this._config.minimumContrastRatio === 1 || excludeFromContrastRatioDemands) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -321,10 +322,15 @@ export class WebglCharAtlas implements IDisposable {
|
||||
// Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used
|
||||
// to draw the glyph to the canvas as well as to restrict the bounding box search to ensure
|
||||
// giant ligatures (eg. =====>) don't impact overall performance.
|
||||
const allowedWidth = this._config.scaledCharWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
const allowedWidth = this._config.scaledCellWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
if (this._tmpCanvas.width < allowedWidth) {
|
||||
this._tmpCanvas.width = allowedWidth;
|
||||
}
|
||||
// Include line height when drawing glyphs
|
||||
const allowedHeight = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
if (this._tmpCanvas.height < allowedHeight) {
|
||||
this._tmpCanvas.height = allowedHeight;
|
||||
}
|
||||
this._tmpCtx.save();
|
||||
|
||||
this._workAttributeData.fg = fg;
|
||||
@@ -370,26 +376,16 @@ export class WebglCharAtlas implements IDisposable {
|
||||
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
|
||||
this._tmpCtx.textBaseline = TEXT_BASELINE;
|
||||
|
||||
this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold);
|
||||
const powerLineGlyph = chars.length === 1 && isPowerlineGlyph(chars.charCodeAt(0));
|
||||
this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, excludeFromContrastRatioDemands(chars.charCodeAt(0)));
|
||||
|
||||
// Apply alpha to dim the character
|
||||
if (dim) {
|
||||
this._tmpCtx.globalAlpha = DIM_OPACITY;
|
||||
}
|
||||
|
||||
// Check if the char is a powerline glyph, these will be restricted to a single cell glyph, no
|
||||
// padding on either side that are allowed for other glyphs since they are designed to be pixel
|
||||
// perfect but may render with "bad" anti-aliasing
|
||||
let isPowerlineGlyph = false;
|
||||
if (chars.length === 1) {
|
||||
const code = chars.charCodeAt(0);
|
||||
if (code >= 0xE0A0 && code <= 0xE0D6) {
|
||||
isPowerlineGlyph = true;
|
||||
}
|
||||
}
|
||||
|
||||
// For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129)
|
||||
const padding = isPowerlineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING;
|
||||
const padding = powerLineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING;
|
||||
|
||||
// Draw custom characters if applicable
|
||||
let drawSuccess = false;
|
||||
@@ -459,7 +455,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
return NULL_RASTERIZED_GLYPH;
|
||||
}
|
||||
|
||||
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, isPowerlineGlyph, drawSuccess);
|
||||
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, powerLineGlyph, drawSuccess);
|
||||
const clippedImageData = this._clipImageData(imageData, this._workBoundingBox);
|
||||
|
||||
// Check if there is enough room in the current row and go to next if needed
|
||||
@@ -494,7 +490,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
*/
|
||||
private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, allowedWidth: number, restrictedGlyph: boolean, customGlyph: boolean): IRasterizedGlyph {
|
||||
boundingBox.top = 0;
|
||||
const height = restrictedGlyph ? this._config.scaledCharHeight : this._tmpCanvas.height;
|
||||
const height = restrictedGlyph ? this._config.scaledCellHeight : this._tmpCanvas.height;
|
||||
const width = restrictedGlyph ? this._config.scaledCharWidth : allowedWidth;
|
||||
let found = false;
|
||||
for (let y = 0; y < height; y++) {
|
||||
|
||||
@@ -254,10 +254,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* @param isBold If we should use the bold fontWeight.
|
||||
*/
|
||||
protected _getFont(terminal: Terminal, isBold: boolean, isItalic: boolean): string {
|
||||
const fontWeight = isBold ? terminal.getOption('fontWeightBold') : terminal.getOption('fontWeight');
|
||||
const fontWeight = isBold ? terminal.options.fontWeightBold : terminal.options.fontWeight;
|
||||
const fontStyle = isItalic ? 'italic' : '';
|
||||
|
||||
return `${fontStyle} ${fontWeight} ${terminal.getOption('fontSize') * window.devicePixelRatio}px ${terminal.getOption('fontFamily')}`;
|
||||
return `${fontStyle} ${fontWeight} ${terminal.options.fontSize! * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
public onOptionsChanged(terminal: Terminal): void {
|
||||
if (terminal.getOption('cursorBlink')) {
|
||||
if (terminal.options.cursorBlink) {
|
||||
if (!this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager = new CursorBlinkStateManager(terminal, () => {
|
||||
this._render(terminal, true);
|
||||
@@ -140,7 +140,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
this._clearCursor();
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
const cursorStyle = terminal.getOption('cursorStyle');
|
||||
const cursorStyle = terminal.options.cursorStyle;
|
||||
if (cursorStyle && cursorStyle !== 'block') {
|
||||
this._cursorRenderers[cursorStyle](terminal, cursorX, viewportRelativeCursorY, this._cell);
|
||||
} else {
|
||||
@@ -150,7 +150,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
this._state.x = cursorX;
|
||||
this._state.y = viewportRelativeCursorY;
|
||||
this._state.isFocused = false;
|
||||
this._state.style = cursorStyle;
|
||||
this._state.style = cursorStyle!;
|
||||
this._state.width = this._cell.getWidth();
|
||||
return;
|
||||
}
|
||||
@@ -166,7 +166,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
if (this._state.x === cursorX &&
|
||||
this._state.y === viewportRelativeCursorY &&
|
||||
this._state.isFocused === isTerminalFocused(terminal) &&
|
||||
this._state.style === terminal.getOption('cursorStyle') &&
|
||||
this._state.style === terminal.options.cursorStyle &&
|
||||
this._state.width === this._cell.getWidth()) {
|
||||
return;
|
||||
}
|
||||
@@ -174,13 +174,13 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
this._ctx.save();
|
||||
this._cursorRenderers[terminal.getOption('cursorStyle') || 'block'](terminal, cursorX, viewportRelativeCursorY, this._cell);
|
||||
this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, cursorX, viewportRelativeCursorY, this._cell);
|
||||
this._ctx.restore();
|
||||
|
||||
this._state.x = cursorX;
|
||||
this._state.y = viewportRelativeCursorY;
|
||||
this._state.isFocused = false;
|
||||
this._state.style = terminal.getOption('cursorStyle');
|
||||
this._state.style = terminal.options.cursorStyle!;
|
||||
this._state.width = this._cell.getWidth();
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
private _renderBarCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._fillLeftLineAtCell(x, y, terminal.getOption('cursorWidth'));
|
||||
this._fillLeftLineAtCell(x, y, terminal.options.cursorWidth!);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
"downlevelIteration": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha"
|
||||
]
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { assert } from 'chai';
|
||||
import { Browser, Page } from 'playwright';
|
||||
import { ITheme } from 'xterm';
|
||||
import { getBrowserType, launchBrowser, openTerminal, pollFor, writeSync } from '../../../out-test/api/TestUtils';
|
||||
import { getBrowserType, launchBrowser, openTerminal, pollFor, timeout, writeSync } from '../../../out-test/api/TestUtils';
|
||||
import { ITerminalOptions } from '../../../src/common/Types';
|
||||
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
@@ -49,7 +49,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
cyan: '#131415',
|
||||
white: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
await writeSync(page, `\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█`);
|
||||
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
||||
@@ -73,8 +73,8 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
brightWhite: '#161718'
|
||||
};
|
||||
await page.evaluate(`
|
||||
window.term.setOption('theme', ${JSON.stringify(theme)});
|
||||
window.term.setOption('drawBoldTextInBrightColors', true);
|
||||
window.term.options.theme = ${JSON.stringify(theme)};
|
||||
window.term.options.drawBoldTextInBrightColors = true;
|
||||
`);
|
||||
await writeSync(page, `\\x1b[1;30m█\\x1b[1;31m█\\x1b[1;32m█\\x1b[1;33m█\\x1b[1;34m█\\x1b[1;35m█\\x1b[1;36m█\\x1b[1;37m█`);
|
||||
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
@@ -98,7 +98,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
cyan: '#131415',
|
||||
white: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
await writeSync(page, `\\x1b[40m \\x1b[41m \\x1b[42m \\x1b[43m \\x1b[44m \\x1b[45m \\x1b[46m \\x1b[47m `);
|
||||
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
||||
@@ -121,7 +121,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
cyan: '#131415',
|
||||
white: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
await writeSync(page, `\\x1b[7;30m \\x1b[7;31m \\x1b[7;32m \\x1b[7;33m \\x1b[7;34m \\x1b[7;35m \\x1b[7;36m \\x1b[7;37m `);
|
||||
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
||||
@@ -144,7 +144,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
cyan: '#131415',
|
||||
white: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
await writeSync(page, `\\x1b[7;40m█\\x1b[7;41m█\\x1b[7;42m█\\x1b[7;43m█\\x1b[7;44m█\\x1b[7;45m█\\x1b[7;46m█\\x1b[7;47m█`);
|
||||
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
||||
@@ -167,7 +167,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
cyan: '#131415',
|
||||
white: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
await writeSync(page, `\\x1b[8;30m \\x1b[8;31m \\x1b[8;32m \\x1b[8;33m \\x1b[8;34m \\x1b[8;35m \\x1b[8;36m \\x1b[8;37m `);
|
||||
await pollFor(page, () => getCellColor(1, 1), [0, 0, 0, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 1), [0, 0, 0, 255]);
|
||||
@@ -190,7 +190,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
cyan: '#131415',
|
||||
white: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
await writeSync(page, `\\x1b[8;40m█\\x1b[8;41m█\\x1b[8;42m█\\x1b[8;43m█\\x1b[8;44m█\\x1b[8;45m█\\x1b[8;46m█\\x1b[8;47m█`);
|
||||
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
||||
@@ -213,7 +213,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
brightCyan: '#131415',
|
||||
brightWhite: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
await writeSync(page, `\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`);
|
||||
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
||||
@@ -236,7 +236,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
brightCyan: '#131415',
|
||||
brightWhite: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
await writeSync(page, `\\x1b[100m \\x1b[101m \\x1b[102m \\x1b[103m \\x1b[104m \\x1b[105m \\x1b[106m \\x1b[107m `);
|
||||
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
||||
@@ -260,9 +260,9 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
for (let y = 0; y < 240 / 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
const cssColor = COLORS_16_TO_255[y * 16 + x];
|
||||
const r = parseInt(cssColor.substr(1, 2), 16);
|
||||
const g = parseInt(cssColor.substr(3, 2), 16);
|
||||
const b = parseInt(cssColor.substr(5, 2), 16);
|
||||
const r = parseInt(cssColor.slice(1, 3), 16);
|
||||
const g = parseInt(cssColor.slice(3, 5), 16);
|
||||
const b = parseInt(cssColor.slice(5, 7), 16);
|
||||
await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
|
||||
}
|
||||
}
|
||||
@@ -280,9 +280,9 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
for (let y = 0; y < 240 / 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
const cssColor = COLORS_16_TO_255[y * 16 + x];
|
||||
const r = parseInt(cssColor.substr(1, 2), 16);
|
||||
const g = parseInt(cssColor.substr(3, 2), 16);
|
||||
const b = parseInt(cssColor.substr(5, 2), 16);
|
||||
const r = parseInt(cssColor.slice(1, 3), 16);
|
||||
const g = parseInt(cssColor.slice(3, 5), 16);
|
||||
const b = parseInt(cssColor.slice(5, 7), 16);
|
||||
await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
|
||||
}
|
||||
}
|
||||
@@ -300,9 +300,9 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
for (let y = 0; y < 240 / 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
const cssColor = COLORS_16_TO_255[y * 16 + x];
|
||||
const r = parseInt(cssColor.substr(1, 2), 16);
|
||||
const g = parseInt(cssColor.substr(3, 2), 16);
|
||||
const b = parseInt(cssColor.substr(5, 2), 16);
|
||||
const r = parseInt(cssColor.slice(1, 3), 16);
|
||||
const g = parseInt(cssColor.slice(3, 5), 16);
|
||||
const b = parseInt(cssColor.slice(5, 7), 16);
|
||||
await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
|
||||
}
|
||||
}
|
||||
@@ -320,9 +320,9 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
for (let y = 0; y < 240 / 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
const cssColor = COLORS_16_TO_255[y * 16 + x];
|
||||
const r = parseInt(cssColor.substr(1, 2), 16);
|
||||
const g = parseInt(cssColor.substr(3, 2), 16);
|
||||
const b = parseInt(cssColor.substr(5, 2), 16);
|
||||
const r = parseInt(cssColor.slice(1, 3), 16);
|
||||
const g = parseInt(cssColor.slice(3, 5), 16);
|
||||
const b = parseInt(cssColor.slice(5, 7), 16);
|
||||
await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
|
||||
}
|
||||
}
|
||||
@@ -356,9 +356,9 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
for (let y = 0; y < 240 / 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
const cssColor = COLORS_16_TO_255[y * 16 + x];
|
||||
const r = parseInt(cssColor.substr(1, 2), 16);
|
||||
const g = parseInt(cssColor.substr(3, 2), 16);
|
||||
const b = parseInt(cssColor.substr(5, 2), 16);
|
||||
const r = parseInt(cssColor.slice(1, 3), 16);
|
||||
const g = parseInt(cssColor.slice(3, 5), 16);
|
||||
const b = parseInt(cssColor.slice(5, 7), 16);
|
||||
await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
|
||||
}
|
||||
}
|
||||
@@ -715,12 +715,13 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
brightWhite: '#eeeeec'
|
||||
};
|
||||
await page.evaluate(`
|
||||
window.term.setOption('theme', ${JSON.stringify(theme)});
|
||||
window.term.setOption('minimumContrastRatio', 1);
|
||||
window.term.options.theme = ${JSON.stringify(theme)};
|
||||
window.term.options.minimumContrastRatio = 1;
|
||||
`);
|
||||
// Block characters ignore block elements so a different char is used here
|
||||
await writeSync(page,
|
||||
`\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█\\r\\n` +
|
||||
`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`
|
||||
`\\x1b[30m■\\x1b[31m■\\x1b[32m■\\x1b[33m■\\x1b[34m■\\x1b[35m■\\x1b[36m■\\x1b[37m■\\r\\n` +
|
||||
`\\x1b[90m■\\x1b[91m■\\x1b[92m■\\x1b[93m■\\x1b[94m■\\x1b[95m■\\x1b[96m■\\x1b[97m■`
|
||||
);
|
||||
// Validate before minimumContrastRatio is applied
|
||||
await pollFor(page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]);
|
||||
@@ -742,21 +743,21 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
// Setting and check for minimum contrast values, note that these are note
|
||||
// exact to the contrast ratio, if the increase luminance algorithm
|
||||
// changes then these will probably fail
|
||||
await page.evaluate(`window.term.setOption('minimumContrastRatio', 10);`);
|
||||
await page.evaluate(`window.term.options.minimumContrastRatio = 10;`);
|
||||
await pollFor(page, () => getCellColor(1, 1), [176, 180, 180, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 1), [238, 158, 158, 255]);
|
||||
await pollFor(page, () => getCellColor(3, 1), [197, 223, 171, 255]);
|
||||
await pollFor(page, () => getCellColor(4, 1), [235, 221, 158, 255]);
|
||||
await pollFor(page, () => getCellColor(5, 1), [124, 156, 198, 255]);
|
||||
await pollFor(page, () => getCellColor(6, 1), [183, 165, 187, 255]);
|
||||
await pollFor(page, () => getCellColor(3, 1), [152, 198, 110, 255]);
|
||||
await pollFor(page, () => getCellColor(4, 1), [208, 179, 49, 255]);
|
||||
await pollFor(page, () => getCellColor(5, 1), [161, 183, 215, 255]);
|
||||
await pollFor(page, () => getCellColor(6, 1), [191, 174, 194, 255]);
|
||||
await pollFor(page, () => getCellColor(7, 1), [110, 197, 198, 255]);
|
||||
await pollFor(page, () => getCellColor(8, 1), [211, 215, 207, 255]);
|
||||
await pollFor(page, () => getCellColor(1, 2), [183, 185, 183, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 2), [249, 156, 156, 255]);
|
||||
await pollFor(page, () => getCellColor(3, 2), [138, 226, 52, 255]);
|
||||
await pollFor(page, () => getCellColor(4, 2), [252, 233, 79, 255]);
|
||||
await pollFor(page, () => getCellColor(5, 2), [114, 159, 207, 255]);
|
||||
await pollFor(page, () => getCellColor(6, 2), [190, 152, 185, 255]);
|
||||
await pollFor(page, () => getCellColor(5, 2), [154, 186, 221, 255]);
|
||||
await pollFor(page, () => getCellColor(6, 2), [203, 173, 199, 255]);
|
||||
// Unchanged
|
||||
await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]);
|
||||
await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]);
|
||||
@@ -783,12 +784,13 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
brightWhite: '#eeeeec'
|
||||
};
|
||||
await page.evaluate(`
|
||||
window.term.setOption('theme', ${JSON.stringify(theme)});
|
||||
window.term.setOption('minimumContrastRatio', 1);
|
||||
window.term.options.theme = ${JSON.stringify(theme)};
|
||||
window.term.options.minimumContrastRatio = 1;
|
||||
`);
|
||||
// Block characters ignore block elements so a different char is used here
|
||||
await writeSync(page,
|
||||
`\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█\\r\\n` +
|
||||
`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`
|
||||
`\\x1b[30m■\\x1b[31m■\\x1b[32m■\\x1b[33m■\\x1b[34m■\\x1b[35m■\\x1b[36m■\\x1b[37m■\\r\\n` +
|
||||
`\\x1b[90m■\\x1b[91m■\\x1b[92m■\\x1b[93m■\\x1b[94m■\\x1b[95m■\\x1b[96m■\\x1b[97m■`
|
||||
);
|
||||
// Validate before minimumContrastRatio is applied
|
||||
await pollFor(page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]);
|
||||
@@ -810,21 +812,21 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
// Setting and check for minimum contrast values, note that these are note
|
||||
// exact to the contrast ratio, if the increase luminance algorithm
|
||||
// changes then these will probably fail
|
||||
await page.evaluate(`window.term.setOption('minimumContrastRatio', 10);`);
|
||||
await page.evaluate(`window.term.options.minimumContrastRatio = 10;`);
|
||||
await pollFor(page, () => getCellColor(1, 1), [46, 52, 54, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 1), [132, 0, 0, 255]);
|
||||
await pollFor(page, () => getCellColor(3, 1), [78, 154, 6, 255]);
|
||||
await pollFor(page, () => getCellColor(4, 1), [114, 93, 0, 255]);
|
||||
await pollFor(page, () => getCellColor(5, 1), [19, 40, 68, 255]);
|
||||
await pollFor(page, () => getCellColor(6, 1), [60, 40, 64, 255]);
|
||||
await pollFor(page, () => getCellColor(3, 1), [36, 72, 0, 255]);
|
||||
await pollFor(page, () => getCellColor(4, 1), [72, 59, 0, 255]);
|
||||
await pollFor(page, () => getCellColor(5, 1), [32, 64, 106, 255]);
|
||||
await pollFor(page, () => getCellColor(6, 1), [75, 51, 80, 255]);
|
||||
await pollFor(page, () => getCellColor(7, 1), [0, 71, 72, 255]);
|
||||
await pollFor(page, () => getCellColor(8, 1), [64, 64, 63, 255]);
|
||||
await pollFor(page, () => getCellColor(1, 2), [61, 63, 59, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 2), [125, 19, 19, 255]);
|
||||
await pollFor(page, () => getCellColor(3, 2), [89, 146, 32, 255]);
|
||||
await pollFor(page, () => getCellColor(4, 2), [105, 98, 32, 255]);
|
||||
await pollFor(page, () => getCellColor(5, 2), [36, 52, 70, 255]);
|
||||
await pollFor(page, () => getCellColor(6, 2), [64, 45, 63, 255]);
|
||||
await pollFor(page, () => getCellColor(3, 2), [40, 67, 13, 255]);
|
||||
await pollFor(page, () => getCellColor(4, 2), [67, 63, 19, 255]);
|
||||
await pollFor(page, () => getCellColor(5, 2), [45, 65, 87, 255]);
|
||||
await pollFor(page, () => getCellColor(6, 2), [81, 57, 78, 255]);
|
||||
await pollFor(page, () => getCellColor(7, 2), [13, 67, 67, 255]);
|
||||
await pollFor(page, () => getCellColor(8, 2), [64, 64, 64, 255]);
|
||||
});
|
||||
@@ -843,7 +845,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
background: '#00FF00',
|
||||
selection: '#0000FF'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
await writeSync(page, ` █\\x1b[7m█\\x1b[0m`);
|
||||
await pollFor(page, () => getCellColor(1, 1), [0, 255, 0, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 1), [255, 0, 0, 255]);
|
||||
@@ -867,13 +869,121 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
const theme: ITheme = {
|
||||
background: '#ff000080'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
const data = `\\x1b[7m█\x1b[0m`;
|
||||
await writeSync(page, data);
|
||||
// Inverse background should be opaque
|
||||
await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectionForeground', () => {
|
||||
if (areTestsEnabled) {
|
||||
before(async () => setupBrowser({ rendererType: 'dom' }));
|
||||
after(async () => browser.close());
|
||||
beforeEach(async () => page.evaluate(`window.term.reset()`));
|
||||
}
|
||||
|
||||
itWebgl('transparent background inverse', async () => {
|
||||
const theme: ITheme = {
|
||||
selectionForeground: '#ff0000'
|
||||
};
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
const data = `\\x1b[7m█\x1b[0m`;
|
||||
await writeSync(page, data);
|
||||
await page.evaluate(`window.term.selectAll()`);
|
||||
await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('decoration color overrides', async () => {
|
||||
if (areTestsEnabled) {
|
||||
before(async () => setupBrowser({ rendererType: 'dom' }));
|
||||
after(async () => browser.close());
|
||||
beforeEach(async () => page.evaluate(`window.term.reset()`));
|
||||
}
|
||||
|
||||
itWebgl('foregroundColor', async () => {
|
||||
await page.evaluate(`
|
||||
const marker = window.term.registerMarker(-window.term.buffer.active.cursorY);
|
||||
window.term.registerDecoration({
|
||||
marker,
|
||||
foregroundColor: '#ff0000',
|
||||
backgroundColor: '#0000ff'
|
||||
});
|
||||
`);
|
||||
const data = `█`;
|
||||
await writeSync(page, data);
|
||||
await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]);
|
||||
});
|
||||
itWebgl('foregroundColor should ignore inverse', async () => {
|
||||
await page.evaluate(`
|
||||
const marker = window.term.registerMarker(-window.term.buffer.active.cursorY);
|
||||
window.term.registerDecoration({
|
||||
marker,
|
||||
foregroundColor: '#ff0000',
|
||||
backgroundColor: '#0000ff'
|
||||
});
|
||||
`);
|
||||
const data = `\\x1b[7m█\\x1b[0m`;
|
||||
await writeSync(page, data);
|
||||
await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]);
|
||||
});
|
||||
itWebgl('foregroundColor should ignore inverse (only fg on decoration)', async () => {
|
||||
await page.evaluate(`
|
||||
const marker = window.term.registerMarker(-window.term.buffer.active.cursorY);
|
||||
window.term.registerDecoration({
|
||||
marker,
|
||||
width: 2,
|
||||
foregroundColor: '#ff0000'
|
||||
});
|
||||
`);
|
||||
const data = `\\x1b[7m█ \\x1b[0m`;
|
||||
await writeSync(page, data);
|
||||
await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]); // inverse foreground of '█' should be decoration fg override
|
||||
await pollFor(page, () => getCellColor(2, 1), [255, 255, 255, 255]); // inverse background of ' ' should be default foreground
|
||||
});
|
||||
itWebgl('backgroundColor', async () => {
|
||||
await page.evaluate(`
|
||||
const marker = window.term.registerMarker(-window.term.buffer.active.cursorY);
|
||||
window.term.registerDecoration({
|
||||
marker,
|
||||
foregroundColor: '#ff0000',
|
||||
backgroundColor: '#0000ff'
|
||||
});
|
||||
`);
|
||||
const data = ` `;
|
||||
await writeSync(page, data);
|
||||
await pollFor(page, () => getCellColor(1, 1), [0, 0, 255, 255]);
|
||||
});
|
||||
itWebgl('backgroundColor should ignore inverse', async () => {
|
||||
await page.evaluate(`
|
||||
const marker = window.term.registerMarker(-window.term.buffer.active.cursorY);
|
||||
window.term.registerDecoration({
|
||||
marker,
|
||||
foregroundColor: '#ff0000',
|
||||
backgroundColor: '#0000ff'
|
||||
});
|
||||
`);
|
||||
const data = `\\x1b[7m \\x1b[0m`;
|
||||
await writeSync(page, data);
|
||||
await pollFor(page, () => getCellColor(1, 1), [0, 0, 255, 255]);
|
||||
});
|
||||
itWebgl('backgroundColor should ignore inverse (only bg on decoration)', async () => {
|
||||
const data = `\\x1b[7m█ \\x1b[0m`;
|
||||
await writeSync(page, data);
|
||||
await page.evaluate(`
|
||||
const marker = window.term.registerMarker(-window.term.buffer.active.cursorY);
|
||||
window.term.registerDecoration({
|
||||
marker,
|
||||
width: 2,
|
||||
backgroundColor: '#0000ff'
|
||||
});
|
||||
`);
|
||||
await pollFor(page, () => getCellColor(1, 1), [0, 0, 0, 255]); // inverse foreground of '█' should be default
|
||||
await pollFor(page, () => getCellColor(2, 1), [0, 0, 255, 255]); // inverse background of ' ' should be decoration bg override
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function getCellColor(col: number, row: number): Promise<number[]> {
|
||||
|
||||
+18
-17
@@ -105,22 +105,23 @@ jobs:
|
||||
- script: xvfb-run --auto-servernum -- bash -c "yarn test-api-firefox --headless --forbid-only"
|
||||
displayName: 'Integration tests (Firefox)'
|
||||
|
||||
- job: macOS_IntegrationTests
|
||||
pool:
|
||||
vmImage: 'macOS-10.15'
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '14.x'
|
||||
displayName: 'Install Node.js'
|
||||
- script: yarn --frozen-lockfile
|
||||
displayName: 'Install dependencies and build'
|
||||
- script: yarn test-api-chromium --headless --forbid-only
|
||||
displayName: 'Integration tests (Chromium)'
|
||||
- script: yarn test-api-firefox --headless --forbid-only
|
||||
displayName: 'Integration tests (Firefox)'
|
||||
- script: yarn test-api-webkit --headless --forbid-only
|
||||
displayName: 'Integration tests (Webkit)'
|
||||
# Integration tests are too flaky on macOS https://github.com/xtermjs/xterm.js/issues/3590
|
||||
# - job: macOS_IntegrationTests
|
||||
# pool:
|
||||
# vmImage: 'macOS-10.15'
|
||||
# steps:
|
||||
# - task: NodeTool@0
|
||||
# inputs:
|
||||
# versionSpec: '14.x'
|
||||
# displayName: 'Install Node.js'
|
||||
# - script: yarn --frozen-lockfile
|
||||
# displayName: 'Install dependencies and build'
|
||||
# - script: yarn test-api-chromium --headless --forbid-only
|
||||
# displayName: 'Integration tests (Chromium)'
|
||||
# - script: yarn test-api-firefox --headless --forbid-only
|
||||
# displayName: 'Integration tests (Firefox)'
|
||||
# - script: yarn test-api-webkit --headless --forbid-only
|
||||
# displayName: 'Integration tests (Webkit)'
|
||||
|
||||
- job: Windows_IntegrationTests
|
||||
pool:
|
||||
@@ -143,7 +144,7 @@ jobs:
|
||||
- macOS
|
||||
- Windows
|
||||
- Linux_IntegrationTests
|
||||
- macOS_IntegrationTests
|
||||
# - macOS_IntegrationTests
|
||||
- Windows_IntegrationTests
|
||||
condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['FORCE_RELEASE'], 'true')))
|
||||
pool:
|
||||
|
||||
+3
-3
@@ -104,11 +104,11 @@ function getNextBetaVersion(packageJson) {
|
||||
return `${nextStableVersion}-${tag}.1`;
|
||||
}
|
||||
const latestPublishedVersion = publishedVersions.sort((a, b) => {
|
||||
const aVersion = parseInt(a.substr(a.search(/\d+$/)));
|
||||
const bVersion = parseInt(b.substr(b.search(/\d+$/)));
|
||||
const aVersion = parseInt(a.slice(a.search(/\d+$/)));
|
||||
const bVersion = parseInt(b.slice(b.search(/\d+$/)));
|
||||
return aVersion > bVersion ? -1 : 1;
|
||||
})[0];
|
||||
const latestTagVersion = parseInt(latestPublishedVersion.substr(latestPublishedVersion.search(/\d+$/)), 10);
|
||||
const latestTagVersion = parseInt(latestPublishedVersion.slice(latestPublishedVersion.search(/\d+$/)), 10);
|
||||
return `${nextStableVersion}-${tag}.${latestTagVersion + 1}`;
|
||||
}
|
||||
|
||||
|
||||
+19
-4
@@ -36,6 +36,7 @@
|
||||
*/
|
||||
|
||||
.xterm {
|
||||
cursor: text;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
@@ -124,10 +125,6 @@
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.xterm {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.xterm.enable-mouse-events {
|
||||
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
|
||||
cursor: default;
|
||||
@@ -173,3 +170,21 @@
|
||||
.xterm-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.xterm-screen .xterm-decoration-container .xterm-decoration {
|
||||
z-index: 6;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.xterm-decoration-overview-ruler {
|
||||
z-index: 7;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xterm-decoration-top {
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
+129
-4
@@ -92,8 +92,10 @@ const addons: { [T in AddonType]: IDemoAddon<T>} = {
|
||||
|
||||
const terminalContainer = document.getElementById('terminal-container');
|
||||
const actionElements = {
|
||||
find: <HTMLInputElement>document.querySelector('#find'),
|
||||
findNext: <HTMLInputElement>document.querySelector('#find-next'),
|
||||
findPrevious: <HTMLInputElement>document.querySelector('#find-previous')
|
||||
findPrevious: <HTMLInputElement>document.querySelector('#find-previous'),
|
||||
findResults: document.querySelector('#find-results')
|
||||
};
|
||||
const paddingElement = <HTMLInputElement>document.getElementById('padding');
|
||||
|
||||
@@ -107,7 +109,15 @@ function getSearchOptions(e: KeyboardEvent): ISearchOptions {
|
||||
regex: (document.getElementById('regex') as HTMLInputElement).checked,
|
||||
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
|
||||
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked,
|
||||
incremental: e.key !== `Enter`
|
||||
incremental: e.key !== `Enter`,
|
||||
decorations: (document.getElementById('highlight-all-matches') as HTMLInputElement).checked ? {
|
||||
matchBackground: '#232422',
|
||||
matchBorder: '#555753',
|
||||
matchOverviewRuler: '#555753',
|
||||
activeMatchBackground: '#ef2929',
|
||||
activeMatchBorder: '#ffffff',
|
||||
activeMatchColorOverviewRuler: '#ef2929'
|
||||
} : undefined
|
||||
};
|
||||
}
|
||||
|
||||
@@ -147,8 +157,12 @@ if (document.location.pathname === '/test') {
|
||||
createTerminal();
|
||||
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
|
||||
document.getElementById('serialize').addEventListener('click', serializeButtonHandler);
|
||||
document.getElementById('htmlserialize').addEventListener('click', htmlSerializeButtonHandler);
|
||||
document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler);
|
||||
document.getElementById('load-test').addEventListener('click', loadTest);
|
||||
document.getElementById('powerline-symbol-test').addEventListener('click', powerlineSymbolTest);
|
||||
document.getElementById('add-decoration').addEventListener('click', addDecoration);
|
||||
document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler);
|
||||
}
|
||||
|
||||
function createTerminal(): void {
|
||||
@@ -200,10 +214,15 @@ function createTerminal(): void {
|
||||
addDomListener(actionElements.findNext, 'keyup', (e) => {
|
||||
addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions(e));
|
||||
});
|
||||
|
||||
addDomListener(actionElements.findPrevious, 'keyup', (e) => {
|
||||
addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions(e));
|
||||
});
|
||||
addDomListener(actionElements.findNext, 'blur', (e) => {
|
||||
addons.search.instance.clearActiveDecoration();
|
||||
});
|
||||
addDomListener(actionElements.findPrevious, 'blur', (e) => {
|
||||
addons.search.instance.clearActiveDecoration();
|
||||
});
|
||||
|
||||
// fit is called within a setTimeout, cols and rows need this.
|
||||
setTimeout(() => {
|
||||
@@ -380,9 +399,12 @@ function initAddons(term: TerminalType): void {
|
||||
if (!addon.canChange) {
|
||||
checkbox.disabled = true;
|
||||
}
|
||||
if(name === 'unicode11' && checkbox.checked) {
|
||||
if (name === 'unicode11' && checkbox.checked) {
|
||||
term.unicode.activeVersion = '11';
|
||||
}
|
||||
if (name === 'search' && checkbox.checked) {
|
||||
addon.instance.onDidChangeResults(e => updateFindResults(e));
|
||||
}
|
||||
addDomListener(checkbox, 'change', () => {
|
||||
if (checkbox.checked) {
|
||||
addon.instance = new addon.ctor();
|
||||
@@ -393,6 +415,8 @@ function initAddons(term: TerminalType): void {
|
||||
}, 0);
|
||||
} else if (name === 'unicode11') {
|
||||
term.unicode.activeVersion = '11';
|
||||
} else if (name === 'search') {
|
||||
addon.instance.onDidChangeResults(e => updateFindResults(e));
|
||||
}
|
||||
} else {
|
||||
if (name === 'webgl') {
|
||||
@@ -421,6 +445,16 @@ function initAddons(term: TerminalType): void {
|
||||
container.appendChild(fragment);
|
||||
}
|
||||
|
||||
function updateFindResults(e: { resultIndex: number, resultCount: number } | undefined) {
|
||||
let content: string;
|
||||
if (e === undefined) {
|
||||
content = 'undefined';
|
||||
} else {
|
||||
content = `index: ${e.resultIndex}, count: ${e.resultCount}`;
|
||||
}
|
||||
actionElements.findResults.textContent = content;
|
||||
}
|
||||
|
||||
function addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void {
|
||||
element.addEventListener(type, handler);
|
||||
term._core.register({ dispose: () => element.removeEventListener(type, handler) });
|
||||
@@ -447,6 +481,21 @@ function serializeButtonHandler(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function htmlSerializeButtonHandler(): void {
|
||||
const output = addons.serialize.instance.serializeAsHTML();
|
||||
document.getElementById('htmlserialize-output').innerText = output;
|
||||
|
||||
// Deprecated, but the most supported for now.
|
||||
function listener(e: any) {
|
||||
e.clipboardData.setData("text/html", output);
|
||||
e.preventDefault();
|
||||
}
|
||||
document.addEventListener("copy", listener);
|
||||
document.execCommand("copy");
|
||||
document.removeEventListener("copy", listener);
|
||||
document.getElementById("htmlserialize-output-result").innerText = "Copied to clipboard";
|
||||
}
|
||||
|
||||
|
||||
function writeCustomGlyphHandler() {
|
||||
term.write('\n\r');
|
||||
@@ -525,3 +574,79 @@ function loadTest() {
|
||||
term._core._onData.fire('\x03');
|
||||
});
|
||||
}
|
||||
|
||||
function powerlineSymbolTest() {
|
||||
function s(char: string): string {
|
||||
return `${char} \x1b[7m${char}\x1b[0m `;
|
||||
}
|
||||
term.write('\n\n\r');
|
||||
term.writeln('Standard powerline symbols:');
|
||||
term.writeln(' 0 1 2 3 4 5 6 7 8 9 A B C D E F');
|
||||
term.writeln(`0xA_ ${s('\ue0a0')}${s('\ue0a1')}${s('\ue0a2')}`);
|
||||
term.writeln(`0xB_ ${s('\ue0b0')}${s('\ue0b1')}${s('\ue0b2')}${s('\ue0b3')}`);
|
||||
term.writeln('');
|
||||
term.writeln(
|
||||
`\x1b[7m` +
|
||||
` inverse \ue0b1 \x1b[0;40m\ue0b0` +
|
||||
` 0 \ue0b1 \x1b[30;41m\ue0b0\x1b[39m` +
|
||||
` 1 \ue0b1 \x1b[31;42m\ue0b0\x1b[39m` +
|
||||
` 2 \ue0b1 \x1b[32;43m\ue0b0\x1b[39m` +
|
||||
` 3 \ue0b1 \x1b[33;44m\ue0b0\x1b[39m` +
|
||||
` 4 \ue0b1 \x1b[34;45m\ue0b0\x1b[39m` +
|
||||
` 5 \ue0b1 \x1b[35;46m\ue0b0\x1b[39m` +
|
||||
` 6 \ue0b1 \x1b[36;47m\ue0b0\x1b[39m` +
|
||||
` 7 \ue0b1 \x1b[37;49m\ue0b0\x1b[0m`
|
||||
);
|
||||
term.writeln('');
|
||||
term.writeln(
|
||||
`\x1b[7m` +
|
||||
` inverse \ue0b3 \x1b[0;7;40m\ue0b2\x1b[27m` +
|
||||
` 0 \ue0b3 \x1b[7;30;41m\ue0b2\x1b[27;39m` +
|
||||
` 1 \ue0b3 \x1b[7;31;42m\ue0b2\x1b[27;39m` +
|
||||
` 2 \ue0b3 \x1b[7;32;43m\ue0b2\x1b[27;39m` +
|
||||
` 3 \ue0b3 \x1b[7;33;44m\ue0b2\x1b[27;39m` +
|
||||
` 4 \ue0b3 \x1b[7;34;45m\ue0b2\x1b[27;39m` +
|
||||
` 5 \ue0b3 \x1b[7;35;46m\ue0b2\x1b[27;39m` +
|
||||
` 6 \ue0b3 \x1b[7;36;47m\ue0b2\x1b[27;39m` +
|
||||
` 7 \ue0b3 \x1b[7;37;49m\ue0b2\x1b[0m`
|
||||
);
|
||||
term.writeln('');
|
||||
term.writeln('Powerline extra symbols:');
|
||||
term.writeln(' 0 1 2 3 4 5 6 7 8 9 A B C D E F');
|
||||
term.writeln(`0xA_ ${s('\ue0a3')}`);
|
||||
term.writeln(`0xB_ ${s('\ue0b4')}${s('\ue0b5')}${s('\ue0b6')}${s('\ue0b7')}${s('\ue0b8')}${s('\ue0b9')}${s('\ue0ba')}${s('\ue0bb')}${s('\ue0bc')}${s('\ue0bd')}${s('\ue0be')}${s('\ue0bf')}`);
|
||||
term.writeln(`0xC_ ${s('\ue0c0')}${s('\ue0c1')}${s('\ue0c2')}${s('\ue0c3')}${s('\ue0c4')}${s('\ue0c5')}${s('\ue0c6')}${s('\ue0c7')}${s('\ue0c8')}${s('\ue0c9')}${s('\ue0ca')}${s('\ue0cb')}${s('\ue0cc')}${s('\ue0cd')}${s('\ue0be')}${s('\ue0bf')}`);
|
||||
term.writeln(`0xD_ ${s('\ue0d0')}${s('\ue0d1')}${s('\ue0d2')} ${s('\ue0d4')}`);
|
||||
term.writeln('');
|
||||
term.writeln('Sample of nerd fonts icons:');
|
||||
term.writeln(' nf-linux-apple (\\uF302) \uf302');
|
||||
term.writeln('nf-mdi-github_face (\\uFbd9) \ufbd9');
|
||||
}
|
||||
|
||||
function addDecoration() {
|
||||
term.options['overviewRulerWidth'] = 15;
|
||||
const marker = term.addMarker(1);
|
||||
const decoration = term.registerDecoration({
|
||||
marker,
|
||||
backgroundColor: '#00FF00',
|
||||
foregroundColor: '#00FE00',
|
||||
overviewRulerOptions: { color: '#ef292980', position: 'left' }
|
||||
});
|
||||
decoration.onRender((e: HTMLElement) => {
|
||||
e.style.right = '100%';
|
||||
e.style.backgroundColor = '#ef292980';
|
||||
});
|
||||
}
|
||||
|
||||
function addOverviewRuler() {
|
||||
term.options['overviewRulerWidth'] = 15;
|
||||
term.registerDecoration({marker: term.addMarker(1), overviewRulerOptions: { color: '#ef2929' }});
|
||||
term.registerDecoration({marker: term.addMarker(3), overviewRulerOptions: { color: '#8ae234' }});
|
||||
term.registerDecoration({marker: term.addMarker(5), overviewRulerOptions: { color: '#729fcf' }});
|
||||
term.registerDecoration({marker: term.addMarker(7), overviewRulerOptions: { color: '#ef2929', position: 'left' }});
|
||||
term.registerDecoration({marker: term.addMarker(7), overviewRulerOptions: { color: '#8ae234', position: 'center' }});
|
||||
term.registerDecoration({marker: term.addMarker(7), overviewRulerOptions: { color: '#729fcf', position: 'right' }});
|
||||
term.registerDecoration({marker: term.addMarker(10), overviewRulerOptions: { color: '#8ae234', position: 'center' }});
|
||||
term.registerDecoration({marker: term.addMarker(10), overviewRulerOptions: { color: '#ffffff80', position: 'full' }});
|
||||
}
|
||||
|
||||
|
||||
+22
-4
@@ -28,7 +28,7 @@
|
||||
</div>
|
||||
<div id="options" class="tabContent">
|
||||
<h3>Options</h3>
|
||||
<p>These options can be set in the <code>Terminal</code> constructor or by using the <code>Terminal.setOption</code> function.</p>
|
||||
<p>These options can be set in the <code>Terminal</code> constructor or by using the <code>Terminal.options</code> property.</p>
|
||||
<div id="options-container"></div>
|
||||
</div>
|
||||
<div id="addons" class="tabContent">
|
||||
@@ -40,15 +40,21 @@
|
||||
<div style= "display:flex; flex-direction:column;">
|
||||
<label>Find next <input id="find-next"/></label>
|
||||
<label>Find previous <input id="find-previous"/></label>
|
||||
<div>Results: <span id="find-results"></span></div>
|
||||
<label><input type="checkbox" id="regex"/>Use regex</label>
|
||||
<label><input type="checkbox" id="case-sensitive"/>Case sensitive</label>
|
||||
<label><input type="checkbox" id="whole-word"/>Whole word</label>
|
||||
<label><input type="checkbox" id="highlight-all-matches" checked/>Highlight All Matches</label>
|
||||
</div>
|
||||
<h4>SerializeAddon</h4>
|
||||
<div>
|
||||
<button id="serialize">Serialize the content of terminal</button>
|
||||
<label><input type="checkbox" id="write-to-terminal">Write back to terminal</label>
|
||||
<div><pre id="serialize-output"></pre></div>
|
||||
|
||||
<button id="htmlserialize">Serialize the content of terminal in HTML</button>
|
||||
<span id="htmlserialize-output-result"></span>
|
||||
<div><pre id="htmlserialize-output"></pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="style" class="tabContent">
|
||||
@@ -61,9 +67,21 @@
|
||||
<div id="test" class="tabContent">
|
||||
<h3>Test</h3>
|
||||
<div style="display: inline-block; margin-right: 16px;">
|
||||
<button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button>
|
||||
<button id="custom-glyph" title="Write custom box drawing and block element characters to the terminal">Test custom glyphs</button>
|
||||
<button id="load-test" title="Write several MB of data to simulate a lot of data coming from the process">Load test</button>
|
||||
<dl>
|
||||
<dt>Lifecycle</dt>
|
||||
<dd><button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button></dd>
|
||||
|
||||
<dt>Performance</dt>
|
||||
<dd><button id="load-test" title="Write several MB of data to simulate a lot of data coming from the process">Load test</button></dd>
|
||||
|
||||
<dt>Styles</dt>
|
||||
<dd><button id="custom-glyph" title="Write custom box drawing and block element characters to the terminal">Test custom glyphs</button></dd>
|
||||
<dd><button id="powerline-symbol-test" title="Write powerline symbol characters to the terminal (\ue0a0+)">Powerline symbol test</button></dd>
|
||||
|
||||
<dt>Decorations</dt>
|
||||
<dd><button id="add-decoration" title="Add a decoration to the terminal">Decoration</button></dd>
|
||||
<dd><button id="add-overview-ruler" title="Add an overview ruler to the terminal">Add Overview Ruler</button></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -114,6 +114,9 @@ function startServer() {
|
||||
}
|
||||
const send = USE_BINARY ? bufferUtf8(ws, 5) : buffer(ws, 5);
|
||||
|
||||
// WARNING: This is a naive implementation that will not throttle the flow of data. This means
|
||||
// it could flood the communication channel and make the terminal unresponsive. Learn more about
|
||||
// the problem and how to implement flow control at https://xtermjs.org/docs/guides/flowcontrol/
|
||||
term.on('data', function(data) {
|
||||
try {
|
||||
send(data);
|
||||
|
||||
+19
-4
@@ -1,12 +1,27 @@
|
||||
{
|
||||
"name": "xterm",
|
||||
"description": "Full xterm terminal, in your browser",
|
||||
"version": "4.16.0",
|
||||
"version": "4.19.0",
|
||||
"main": "lib/xterm.js",
|
||||
"style": "css/xterm.css",
|
||||
"types": "typings/xterm.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"cli",
|
||||
"command-line",
|
||||
"console",
|
||||
"pty",
|
||||
"shell",
|
||||
"ssh",
|
||||
"styles",
|
||||
"terminal-emulator",
|
||||
"terminal",
|
||||
"tty",
|
||||
"vt100",
|
||||
"webgl",
|
||||
"xterm"
|
||||
],
|
||||
"scripts": {
|
||||
"prepackage": "npm run build",
|
||||
"package": "webpack",
|
||||
@@ -57,11 +72,11 @@
|
||||
"express-ws": "^5.0.2",
|
||||
"glob": "^7.2.0",
|
||||
"jsdom": "^18.0.1",
|
||||
"mocha": "^9.1.3",
|
||||
"mocha": "^9.2.0",
|
||||
"mustache": "^4.2.0",
|
||||
"node-pty": "^0.10.1",
|
||||
"nyc": "^15.1.0",
|
||||
"playwright": "^1.16.2",
|
||||
"playwright": "^1.22.1",
|
||||
"source-map-loader": "^3.0.0",
|
||||
"source-map-support": "^0.5.20",
|
||||
"ts-loader": "^9.1.2",
|
||||
@@ -70,6 +85,6 @@
|
||||
"webpack": "^5.61.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"ws": "^8.2.3",
|
||||
"xterm-benchmark": "^0.3.0"
|
||||
"xterm-benchmark": "^0.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@ export class AccessibilityManager extends Disposable {
|
||||
) {
|
||||
super();
|
||||
this._accessibilityTreeRoot = document.createElement('div');
|
||||
this._accessibilityTreeRoot.setAttribute('role', 'document');
|
||||
this._accessibilityTreeRoot.classList.add('xterm-accessibility');
|
||||
this._accessibilityTreeRoot.tabIndex = 0;
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IColor, IColorContrastCache } from 'browser/Types';
|
||||
import { IColorContrastCache } from 'browser/Types';
|
||||
import { IColor } from 'common/Types';
|
||||
|
||||
export class ColorContrastCache implements IColorContrastCache {
|
||||
private _color: { [bg: number]: { [fg: number]: IColor | null | undefined } | undefined } = {};
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('ColorManager', () => {
|
||||
describe('constructor', () => {
|
||||
it('should fill all colors with values', () => {
|
||||
for (const key of Object.keys(cm.colors)) {
|
||||
if (key !== 'ansi' && key !== 'contrastCache') {
|
||||
if (key !== 'ansi' && key !== 'contrastCache' && key !== 'selectionForeground') {
|
||||
// A #rrggbb or rgba(...)
|
||||
assert.ok((cm.colors as any)[key].css.length >= 7);
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IColorManager, IColor, IColorSet, IColorContrastCache } from 'browser/Types';
|
||||
import { IColorManager, IColorSet, IColorContrastCache } from 'browser/Types';
|
||||
import { ITheme } from 'common/services/Services';
|
||||
import { channels, color, css } from 'browser/Color';
|
||||
import { channels, color, css } from 'common/Color';
|
||||
import { ColorContrastCache } from 'browser/ColorContrastCache';
|
||||
import { ColorIndex } from 'common/Types';
|
||||
import { ColorIndex, IColor } from 'common/Types';
|
||||
|
||||
|
||||
interface IRestoreColorSet {
|
||||
@@ -104,6 +104,7 @@ export class ColorManager implements IColorManager {
|
||||
cursorAccent: DEFAULT_CURSOR_ACCENT,
|
||||
selectionTransparent: DEFAULT_SELECTION,
|
||||
selectionOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
|
||||
selectionForeground: undefined,
|
||||
ansi: DEFAULT_ANSI_COLORS.slice(),
|
||||
contrastCache: this._contrastCache
|
||||
};
|
||||
@@ -128,6 +129,15 @@ export class ColorManager implements IColorManager {
|
||||
this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT, true);
|
||||
this.colors.selectionTransparent = this._parseColor(theme.selection, DEFAULT_SELECTION, true);
|
||||
this.colors.selectionOpaque = color.blend(this.colors.background, this.colors.selectionTransparent);
|
||||
const nullColor: IColor = {
|
||||
css: '',
|
||||
rgba: 0
|
||||
};
|
||||
this.colors.selectionForeground = theme.selectionForeground ? this._parseColor(theme.selectionForeground, nullColor) : undefined;
|
||||
if (this.colors.selectionForeground === nullColor) {
|
||||
this.colors.selectionForeground = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* If selection color is opaque, blend it with background with 0.3 opacity
|
||||
* Issue #2737
|
||||
@@ -185,7 +195,7 @@ export class ColorManager implements IColorManager {
|
||||
foreground: this.colors.foreground,
|
||||
background: this.colors.background,
|
||||
cursor: this.colors.cursor,
|
||||
ansi: [...this.colors.ansi]
|
||||
ansi: this.colors.ansi.slice()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
private _linkProviders: ILinkProvider[] = [];
|
||||
public get currentLink(): ILinkWithState | undefined { return this._currentLink; }
|
||||
protected _currentLink: ILinkWithState | undefined;
|
||||
private _mouseDownLink: ILinkWithState | undefined;
|
||||
private _lastMouseEvent: MouseEvent | undefined;
|
||||
private _linkCacheDisposables: IDisposable[] = [];
|
||||
private _lastBufferCell: IBufferCellPosition | undefined;
|
||||
@@ -61,7 +62,8 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
this._clearCurrentLink();
|
||||
}));
|
||||
this.register(addDisposableDomListener(this._element, 'mousemove', this._onMouseMove.bind(this)));
|
||||
this.register(addDisposableDomListener(this._element, 'click', this._onClick.bind(this)));
|
||||
this.register(addDisposableDomListener(this._element, 'mousedown', this._handleMouseDown.bind(this)));
|
||||
this.register(addDisposableDomListener(this._element, 'mouseup', this._handleMouseUp.bind(this)));
|
||||
}
|
||||
|
||||
private _onMouseMove(event: MouseEvent): void {
|
||||
@@ -129,7 +131,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
let linkProvided = false;
|
||||
|
||||
// There is no link cached, so ask for one
|
||||
this._linkProviders.forEach((linkProvider, i) => {
|
||||
for (const [i, linkProvider] of this._linkProviders.entries()) {
|
||||
if (useLineCache) {
|
||||
const existingReply = this._activeProviderReplies?.get(i);
|
||||
// If there isn't a reply, the provider hasn't responded yet.
|
||||
@@ -156,7 +158,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _removeIntersectingLinks(y: number, replies: Map<Number, ILinkWithState[] | undefined>): void {
|
||||
@@ -222,18 +224,21 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
return linkProvided;
|
||||
}
|
||||
|
||||
private _onClick(event: MouseEvent): void {
|
||||
private _handleMouseDown(): void {
|
||||
this._mouseDownLink = this._currentLink;
|
||||
}
|
||||
|
||||
private _handleMouseUp(event: MouseEvent): void {
|
||||
if (!this._element || !this._mouseService || !this._currentLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
const position = this._positionFromMouseEvent(event, this._element, this._mouseService);
|
||||
|
||||
if (!position) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._linkAtPosition(this._currentLink.link, position)) {
|
||||
if (this._mouseDownLink === this._currentLink && this._linkAtPosition(this._currentLink.link, position)) {
|
||||
this._currentLink.link.activate(event, this._currentLink.link.text);
|
||||
}
|
||||
}
|
||||
@@ -303,7 +308,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
|
||||
// Add listener for rerendering
|
||||
if (this._renderService) {
|
||||
this._linkCacheDisposables.push(this._renderService.onRenderedBufferChange(e => {
|
||||
this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(e => {
|
||||
// When start is 0 a scroll most likely occurred, make sure links above the fold also get
|
||||
// cleared.
|
||||
const start = e.start === 0 ? 0 : e.start + 1 + this._bufferService.buffer.ydisp;
|
||||
|
||||
@@ -150,7 +150,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
}
|
||||
|
||||
// Restart the tooltip timeout
|
||||
this._tooltipTimeout = window.setTimeout(() => this._onTooltip(e), this._optionsService.options.linkTooltipHoverDuration);
|
||||
this._tooltipTimeout = window.setTimeout(() => this._onTooltip(e), this._optionsService.rawOptions.linkTooltipHoverDuration);
|
||||
}
|
||||
|
||||
private _onTooltip(e: MouseEvent): void {
|
||||
|
||||
@@ -3,16 +3,17 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderDebouncer } from 'browser/Types';
|
||||
import { IRenderDebouncerWithCallback } from 'browser/Types';
|
||||
|
||||
/**
|
||||
* Debounces calls to render terminal rows using animation frames.
|
||||
*/
|
||||
export class RenderDebouncer implements IRenderDebouncer {
|
||||
export class RenderDebouncer implements IRenderDebouncerWithCallback {
|
||||
private _rowStart: number | undefined;
|
||||
private _rowEnd: number | undefined;
|
||||
private _rowCount: number | undefined;
|
||||
private _animationFrame: number | undefined;
|
||||
private _refreshCallbacks: FrameRequestCallback[] = [];
|
||||
|
||||
constructor(
|
||||
private _renderCallback: (start: number, end: number) => void
|
||||
@@ -26,6 +27,14 @@ export class RenderDebouncer implements IRenderDebouncer {
|
||||
}
|
||||
}
|
||||
|
||||
public addRefreshCallback(callback: FrameRequestCallback): number {
|
||||
this._refreshCallbacks.push(callback);
|
||||
if (!this._animationFrame) {
|
||||
this._animationFrame = window.requestAnimationFrame(() => this._innerRefresh());
|
||||
}
|
||||
return this._animationFrame;
|
||||
}
|
||||
|
||||
public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void {
|
||||
this._rowCount = rowCount;
|
||||
// Get the min/max row start/end for the arg values
|
||||
@@ -43,8 +52,11 @@ export class RenderDebouncer implements IRenderDebouncer {
|
||||
}
|
||||
|
||||
private _innerRefresh(): void {
|
||||
this._animationFrame = undefined;
|
||||
|
||||
// Make sure values are set
|
||||
if (this._rowStart === undefined || this._rowEnd === undefined || this._rowCount === undefined) {
|
||||
this._runRefreshCallbacks();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -55,9 +67,16 @@ export class RenderDebouncer implements IRenderDebouncer {
|
||||
// Reset debouncer (this happens before render callback as the render could trigger it again)
|
||||
this._rowStart = undefined;
|
||||
this._rowEnd = undefined;
|
||||
this._animationFrame = undefined;
|
||||
|
||||
// Run render callback
|
||||
this._renderCallback(start, end);
|
||||
this._runRefreshCallbacks();
|
||||
}
|
||||
|
||||
private _runRefreshCallbacks(): void {
|
||||
for (const callback of this._refreshCallbacks) {
|
||||
callback(0);
|
||||
}
|
||||
this._refreshCallbacks = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ describe('Terminal', () => {
|
||||
});
|
||||
term.paste('foo');
|
||||
});
|
||||
it('should sanitize \n chars', done => {
|
||||
it('should sanitize \\n chars', done => {
|
||||
term.onData(e => {
|
||||
assert.equal(e, '\rfoo\rbar\r');
|
||||
done();
|
||||
@@ -1332,8 +1332,8 @@ describe('Terminal', () => {
|
||||
(!(i % 3))
|
||||
? input[i]
|
||||
: (i % 3 === 1)
|
||||
? input.substr(i, 2)
|
||||
: input.substr(i - 1, 2),
|
||||
? input.slice(i, i + 2)
|
||||
: input.slice(i - 1, i + 1),
|
||||
terminal.buffer.lines.get(bufferIndex[0])!.loadCell(bufferIndex[1], new CellData()).getChars());
|
||||
}
|
||||
});
|
||||
|
||||
+64
-16
@@ -26,7 +26,7 @@ import { IRenderer } from 'browser/renderer/Types';
|
||||
import { CompositionHelper } from 'browser/input/CompositionHelper';
|
||||
import { Viewport } from 'browser/Viewport';
|
||||
import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard';
|
||||
import { C0 } from 'common/data/EscapeSequences';
|
||||
import { C0, C1_ESCAPED } from 'common/data/EscapeSequences';
|
||||
import { WindowsOptionsReportType } from '../common/InputHandler';
|
||||
import { Renderer } from 'browser/renderer/Renderer';
|
||||
import { Linkifier } from 'browser/Linkifier';
|
||||
@@ -37,7 +37,7 @@ import * as Strings from 'browser/LocalizableStrings';
|
||||
import { SoundService } from 'browser/services/SoundService';
|
||||
import { MouseZoneManager } from 'browser/MouseZoneManager';
|
||||
import { AccessibilityManager } from './AccessibilityManager';
|
||||
import { ITheme, IMarker, IDisposable, ISelectionPosition, ILinkProvider } from 'xterm';
|
||||
import { ITheme, IMarker, IDisposable, ISelectionPosition, ILinkProvider, IDecorationOptions, IDecoration } from 'xterm';
|
||||
import { DomRenderer } from 'browser/renderer/dom/DomRenderer';
|
||||
import { KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions, ScrollSource, IColorEvent, ColorIndex, ColorRequestType } from 'common/Types';
|
||||
import { evaluateKeyboardEvent } from 'common/input/Keyboard';
|
||||
@@ -52,9 +52,13 @@ import { MouseService } from 'browser/services/MouseService';
|
||||
import { Linkifier2 } from 'browser/Linkifier2';
|
||||
import { CoreBrowserService } from 'browser/services/CoreBrowserService';
|
||||
import { CoreTerminal } from 'common/CoreTerminal';
|
||||
import { color, rgba } from 'browser/Color';
|
||||
import { color, rgba } from 'common/Color';
|
||||
import { CharacterJoinerService } from 'browser/services/CharacterJoinerService';
|
||||
import { toRgbString } from 'common/input/XParseColor';
|
||||
import { BufferDecorationRenderer } from 'browser/decorations/BufferDecorationRenderer';
|
||||
import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer';
|
||||
import { DecorationService } from 'common/services/DecorationService';
|
||||
import { IDecorationService } from 'common/services/Services';
|
||||
|
||||
// Let it work inside Node.js for automated testing purposes.
|
||||
const document: Document = (typeof window !== 'undefined') ? window.document : null as any;
|
||||
@@ -70,6 +74,8 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
private _helperContainer: HTMLElement | undefined;
|
||||
private _compositionView: HTMLElement | undefined;
|
||||
|
||||
private _overviewRulerRenderer: OverviewRulerRenderer | undefined;
|
||||
|
||||
// private _visualBellTimer: number;
|
||||
|
||||
public browser: IBrowser = Browser as any;
|
||||
@@ -77,6 +83,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
private _customKeyEventHandler: CustomKeyEventHandler | undefined;
|
||||
|
||||
// browser services
|
||||
private _decorationService: DecorationService;
|
||||
private _charSizeService: ICharSizeService | undefined;
|
||||
private _mouseService: IMouseService | undefined;
|
||||
private _renderService: IRenderService | undefined;
|
||||
@@ -91,6 +98,12 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
*/
|
||||
private _keyDownHandled: boolean = false;
|
||||
|
||||
/**
|
||||
* Records whether a keydown event has occured since the last keyup event, i.e. whether a key
|
||||
* is currently "pressed".
|
||||
*/
|
||||
private _keyDownSeen: boolean = false;
|
||||
|
||||
/**
|
||||
* Records whether the keypress event has already been handled and triggered a data event, if so
|
||||
* the input event should not trigger a data event but should still print to the textarea so
|
||||
@@ -157,6 +170,8 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
this.linkifier = this._instantiationService.createInstance(Linkifier);
|
||||
this.linkifier2 = this.register(this._instantiationService.createInstance(Linkifier2));
|
||||
this._decorationService = this._instantiationService.createInstance(DecorationService);
|
||||
this._instantiationService.setService(IDecorationService, this._decorationService);
|
||||
|
||||
// Setup InputHandler listeners
|
||||
this.register(this._inputHandler.onRequestBell(() => this.bell()));
|
||||
@@ -209,7 +224,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
const channels = color.toColorRGB(acc === 'ansi'
|
||||
? this._colorManager.colors.ansi[req.index]
|
||||
: this._colorManager.colors[acc]);
|
||||
this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${C0.BEL}`);
|
||||
this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${C1_ESCAPED.ST}`);
|
||||
break;
|
||||
case ColorRequestType.SET:
|
||||
if (acc === 'ansi') this._colorManager.colors.ansi[req.index] = rgba.toColor(...req.color);
|
||||
@@ -298,7 +313,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this.viewport?.syncScrollArea();
|
||||
break;
|
||||
case 'screenReaderMode':
|
||||
if (this.optionsService.options.screenReaderMode) {
|
||||
if (this.optionsService.rawOptions.screenReaderMode) {
|
||||
if (!this._accessibilityManager && this._renderService) {
|
||||
this._accessibilityManager = new AccessibilityManager(this, this._renderService);
|
||||
}
|
||||
@@ -309,7 +324,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
break;
|
||||
case 'tabStopWidth': this.buffers.setupTabStops(); break;
|
||||
case 'theme':
|
||||
this._setTheme(this.optionsService.options.theme);
|
||||
this._setTheme(this.optionsService.rawOptions.theme);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -468,6 +483,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this._viewportElement = document.createElement('div');
|
||||
this._viewportElement.classList.add('xterm-viewport');
|
||||
fragment.appendChild(this._viewportElement);
|
||||
|
||||
this._viewportScrollArea = document.createElement('div');
|
||||
this._viewportScrollArea.classList.add('xterm-scroll-area');
|
||||
this._viewportElement.appendChild(this._viewportScrollArea);
|
||||
@@ -510,7 +526,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
const renderer = this._createRenderer();
|
||||
this._renderService = this.register(this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement));
|
||||
this._instantiationService.setService(IRenderService, this._renderService);
|
||||
this.register(this._renderService.onRenderedBufferChange(e => this._onRender.fire(e)));
|
||||
this.register(this._renderService.onRenderedViewportChange(e => this._onRender.fire(e)));
|
||||
this.onResize(e => this._renderService!.resize(e.cols, e.rows));
|
||||
|
||||
this._compositionView = document.createElement('div');
|
||||
@@ -573,7 +589,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this.register(this.onScroll(() => this._mouseZoneManager!.clearAll()));
|
||||
this.linkifier.attachToDom(this.element, this._mouseZoneManager);
|
||||
this.linkifier2.attachToDom(this.screenElement, this._mouseService, this._renderService);
|
||||
|
||||
this.register(this._instantiationService.createInstance(BufferDecorationRenderer, this.screenElement));
|
||||
// This event listener must be registered aftre MouseZoneManager is created
|
||||
this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.onMouseDown(e)));
|
||||
|
||||
@@ -591,6 +607,14 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this._accessibilityManager = new AccessibilityManager(this, this._renderService);
|
||||
}
|
||||
|
||||
if (this.options.overviewRulerWidth) {
|
||||
this._overviewRulerRenderer = this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement);
|
||||
}
|
||||
this.optionsService.onOptionChange(() => {
|
||||
if (!this._overviewRulerRenderer && this.options.overviewRulerWidth && this._viewportElement && this.screenElement) {
|
||||
this._overviewRulerRenderer = this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement);
|
||||
}
|
||||
});
|
||||
// Measure the character size
|
||||
this._charSizeService.measure();
|
||||
|
||||
@@ -679,10 +703,13 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
|
||||
break;
|
||||
case 'wheel':
|
||||
// only UP/DOWN wheel events are respected
|
||||
if ((ev as WheelEvent).deltaY !== 0) {
|
||||
action = (ev as WheelEvent).deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN;
|
||||
const amount = self.viewport!.getLinesScrolled(ev as WheelEvent);
|
||||
|
||||
if (amount === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
action = (ev as WheelEvent).deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN;
|
||||
but = CoreMouseButton.WHEEL;
|
||||
break;
|
||||
default:
|
||||
@@ -753,7 +780,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this.register(this.coreMouseService.onProtocolChange(events => {
|
||||
// apply global changes on events
|
||||
if (events) {
|
||||
if (this.optionsService.options.logLevel === 'debug') {
|
||||
if (this.optionsService.rawOptions.logLevel === 'debug') {
|
||||
this._logService.debug('Binding to mouse events:', this.coreMouseService.explainEvents(events));
|
||||
}
|
||||
this.element!.classList.add('enable-mouse-events');
|
||||
@@ -998,6 +1025,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
return this.buffer.addMarker(this.buffer.ybase + this.buffer.y + cursorYOffset);
|
||||
}
|
||||
|
||||
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined {
|
||||
return this._decorationService.registerDecoration(decorationOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the terminal has an active selection.
|
||||
*/
|
||||
@@ -1062,19 +1093,23 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
*/
|
||||
protected _keyDown(event: KeyboardEvent): boolean | undefined {
|
||||
this._keyDownHandled = false;
|
||||
this._keyDownSeen = true;
|
||||
|
||||
if (this._customKeyEventHandler && this._customKeyEventHandler(event) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this._compositionHelper!.keydown(event)) {
|
||||
// Ignore composing with Alt key on Mac when macOptionIsMeta is enabled
|
||||
const shouldIgnoreComposition = this.browser.isMac && this.options.macOptionIsMeta && event.altKey;
|
||||
|
||||
if (!shouldIgnoreComposition && !this._compositionHelper!.keydown(event)) {
|
||||
if (this.buffer.ybase !== this.buffer.ydisp) {
|
||||
this._bufferService.scrollToBottom();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.key === 'Dead' || event.key === 'AltGraph') {
|
||||
if (!shouldIgnoreComposition && (event.key === 'Dead' || event.key === 'AltGraph')) {
|
||||
this._unprocessedDeadKey = true;
|
||||
}
|
||||
|
||||
@@ -1105,6 +1140,14 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
return true;
|
||||
}
|
||||
|
||||
// HACK: Process A-Z in the keypress event to fix an issue with macOS IMEs where lower case
|
||||
// letters cannot be input while caps lock is on.
|
||||
if (event.key && !event.ctrlKey && !event.altKey && !event.metaKey && event.key.length === 1) {
|
||||
if (event.key.charCodeAt(0) >= 65 && event.key.charCodeAt(0) <= 90) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._unprocessedDeadKey) {
|
||||
this._unprocessedDeadKey = false;
|
||||
return true;
|
||||
@@ -1125,7 +1168,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
// other listeners. When screen reader mode is enabled, this could cause issues if the event
|
||||
// is handled at a higher level, this is a compromise in order to echo keys to the screen
|
||||
// reader.
|
||||
if (!this.optionsService.options.screenReaderMode) {
|
||||
if (!this.optionsService.rawOptions.screenReaderMode) {
|
||||
return this.cancel(event, true);
|
||||
}
|
||||
|
||||
@@ -1147,6 +1190,8 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
}
|
||||
|
||||
protected _keyUp(ev: KeyboardEvent): void {
|
||||
this._keyDownSeen = false;
|
||||
|
||||
if (this._customKeyEventHandler && this._customKeyEventHandler(ev) === false) {
|
||||
return;
|
||||
}
|
||||
@@ -1220,7 +1265,8 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
protected _inputEvent(ev: InputEvent): boolean {
|
||||
// Only support emoji IMEs when screen reader mode is disabled as the event must bubble up to
|
||||
// support reading out character input which can doubling up input characters
|
||||
if (ev.data && ev.inputType === 'insertText' && !ev.composed && !this.optionsService.options.screenReaderMode) {
|
||||
// Based on these event traces: https://github.com/xtermjs/xterm.js/issues/3679
|
||||
if (ev.data && ev.inputType === 'insertText' && (!ev.composed || !this._keyDownSeen) && !this.optionsService.rawOptions.screenReaderMode) {
|
||||
if (this._keyPressHandled) {
|
||||
return false;
|
||||
}
|
||||
@@ -1293,6 +1339,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
// Don't clear if it's already clear
|
||||
return;
|
||||
}
|
||||
this.buffer.clearAllMarkers();
|
||||
this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)!);
|
||||
this.buffer.lines.length = 1;
|
||||
this.buffer.ydisp = 0;
|
||||
@@ -1325,6 +1372,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this._setup();
|
||||
super.reset();
|
||||
this._selectionService?.reset();
|
||||
this._decorationService.reset();
|
||||
|
||||
// reattach
|
||||
this._customKeyEventHandler = customKeyEventHandler;
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable, IMarker, ISelectionPosition, ILinkProvider } from 'xterm';
|
||||
import { IDisposable, IMarker, ISelectionPosition, ILinkProvider, IDecorationOptions, IDecoration } from 'xterm';
|
||||
import { IEvent, EventEmitter } from 'common/EventEmitter';
|
||||
import { ICharacterJoinerService, ICharSizeService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
import { IColorSet, ILinkMatcherOptions, ITerminal, ILinkifier, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler } from 'browser/Types';
|
||||
import { IColorSet, ILinkMatcherOptions, ITerminal, ILinkifier, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler, IRenderDebouncer } from 'browser/Types';
|
||||
import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types';
|
||||
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types';
|
||||
import { Buffer } from 'common/buffer/Buffer';
|
||||
@@ -16,6 +16,7 @@ import { Terminal } from 'browser/Terminal';
|
||||
import { IUnicodeService, IOptionsService, ICoreService, ICoreMouseService } from 'common/services/Services';
|
||||
import { IFunctionIdentifier, IParams } from 'common/parser/Types';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
|
||||
|
||||
export class TestTerminal extends Terminal {
|
||||
public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; }
|
||||
@@ -30,6 +31,7 @@ export class MockTerminal implements ITerminal {
|
||||
public onBlur!: IEvent<void>;
|
||||
public onFocus!: IEvent<void>;
|
||||
public onA11yChar!: IEvent<string>;
|
||||
public onWriteParsed!: IEvent<void>;
|
||||
public onA11yTab!: IEvent<number>;
|
||||
public onCursorMove!: IEvent<void>;
|
||||
public onLineFeed!: IEvent<void>;
|
||||
@@ -102,6 +104,9 @@ export class MockTerminal implements ITerminal {
|
||||
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public hasSelection(): boolean {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
@@ -254,6 +259,12 @@ export class MockBuffer implements IBuffer {
|
||||
public getWhitespaceCell(attr?: IAttributeData): ICellData {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public clearMarkers(y: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public clearAllMarkers(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
export class MockRenderer implements IRenderer {
|
||||
@@ -280,6 +291,9 @@ export class MockRenderer implements IRenderer {
|
||||
public setColors(colors: IColorSet): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public onResize(cols: number, rows: number): void { }
|
||||
public onCharSizeChanged(): void { }
|
||||
public onBlur(): void { }
|
||||
@@ -358,7 +372,8 @@ export class MockMouseService implements IMouseService {
|
||||
export class MockRenderService implements IRenderService {
|
||||
public serviceBrand: undefined;
|
||||
public onDimensionsChange: IEvent<IRenderDimensions> = new EventEmitter<IRenderDimensions>().event;
|
||||
public onRenderedBufferChange: IEvent<{ start: number, end: number }, void> = new EventEmitter<{ start: number, end: number }>().event;
|
||||
public onRenderedViewportChange: IEvent<{ start: number, end: number }, void> = new EventEmitter<{ start: number, end: number }>().event;
|
||||
public onRender: IEvent<{ start: number, end: number }, void> = new EventEmitter<{ start: number, end: number }>().event;
|
||||
public onRefreshRequest: IEvent<{ start: number, end: number}, void> = new EventEmitter<{ start: number, end: number }>().event;
|
||||
public dimensions: IRenderDimensions = {
|
||||
scaledCharWidth: 0,
|
||||
@@ -377,15 +392,15 @@ export class MockRenderService implements IRenderService {
|
||||
public refreshRows(start: number, end: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public addRefreshCallback(callback: FrameRequestCallback): number {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public clearTextureAtlas(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public resize(cols: number, rows: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public changeOptions(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public setRenderer(renderer: IRenderer): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
@@ -419,6 +434,9 @@ export class MockRenderService implements IRenderService {
|
||||
public dispose(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
export class MockCharacterJoinerService implements ICharacterJoinerService {
|
||||
@@ -433,3 +451,54 @@ export class MockCharacterJoinerService implements ICharacterJoinerService {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export class MockSelectionService implements ISelectionService {
|
||||
public serviceBrand: undefined;
|
||||
public selectionText: string = '';
|
||||
public hasSelection: boolean = false;
|
||||
public selectionStart: [number, number] | undefined;
|
||||
public selectionEnd: [number, number] | undefined;
|
||||
public onLinuxMouseSelection = new EventEmitter<string>().event;
|
||||
public onRequestRedraw = new EventEmitter<ISelectionRedrawRequestEvent>().event;
|
||||
public onRequestScrollLines = new EventEmitter<ISelectionRequestScrollLinesEvent>().event;
|
||||
public onSelectionChange = new EventEmitter<void>().event;
|
||||
public disable(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public enable(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public reset(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public setSelection(row: number, col: number, length: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public selectAll(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public selectLines(start: number, end: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public clearSelection(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public rightClickSelect(event: MouseEvent): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public shouldColumnSelect(event: MouseEvent | KeyboardEvent): boolean {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public shouldForceSelection(event: MouseEvent): boolean {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public refresh(isLinuxMouseSelection?: boolean): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public onMouseDown(event: MouseEvent): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public isCellInSelection(x: number, y: number): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+10
-8
@@ -3,11 +3,11 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable, IMarker, ISelectionPosition } from 'xterm';
|
||||
import { IDecorationOptions, IDecoration, IDisposable, IMarker, ISelectionPosition } from 'xterm';
|
||||
import { IEvent } from 'common/EventEmitter';
|
||||
import { ICoreTerminal, CharData, ITerminalOptions } from 'common/Types';
|
||||
import { ICoreTerminal, CharData, ITerminalOptions, IColor } from 'common/Types';
|
||||
import { IMouseService, IRenderService } from './services/Services';
|
||||
import { IBuffer, IBufferSet } from 'common/buffer/Types';
|
||||
import { IBuffer } from 'common/buffer/Types';
|
||||
import { IFunctionIdentifier, IParams } from 'common/parser/Types';
|
||||
|
||||
export interface ITerminal extends IPublicTerminal, ICoreTerminal {
|
||||
@@ -44,6 +44,7 @@ export interface IPublicTerminal extends IDisposable {
|
||||
onSelectionChange: IEvent<void>;
|
||||
onRender: IEvent<{ start: number, end: number }>;
|
||||
onResize: IEvent<{ cols: number, rows: number }>;
|
||||
onWriteParsed: IEvent<void>;
|
||||
onTitleChange: IEvent<string>;
|
||||
onBell: IEvent<void>;
|
||||
blur(): void;
|
||||
@@ -61,6 +62,7 @@ export interface IPublicTerminal extends IDisposable {
|
||||
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
|
||||
deregisterCharacterJoiner(joinerId: number): void;
|
||||
addMarker(cursorYOffset: number): IMarker | undefined;
|
||||
registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined;
|
||||
hasSelection(): boolean;
|
||||
getSelection(): string;
|
||||
getSelectionPosition(): ISelectionPosition | undefined;
|
||||
@@ -111,11 +113,6 @@ export interface IColorManager {
|
||||
onOptionsChange(key: string): void;
|
||||
}
|
||||
|
||||
export interface IColor {
|
||||
css: string;
|
||||
rgba: number; // 32-bit int with rgba in each byte
|
||||
}
|
||||
|
||||
export interface IColorSet {
|
||||
foreground: IColor;
|
||||
background: IColor;
|
||||
@@ -124,6 +121,7 @@ export interface IColorSet {
|
||||
selectionTransparent: IColor;
|
||||
/** The selection blended on top of background. */
|
||||
selectionOpaque: IColor;
|
||||
selectionForeground: IColor | undefined;
|
||||
ansi: IColor[];
|
||||
contrastCache: IColorContrastCache;
|
||||
}
|
||||
@@ -313,3 +311,7 @@ export interface ICharacterJoiner {
|
||||
export interface IRenderDebouncer extends IDisposable {
|
||||
refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void;
|
||||
}
|
||||
|
||||
export interface IRenderDebouncerWithCallback extends IRenderDebouncer {
|
||||
addRefreshCallback(callback: FrameRequestCallback): number;
|
||||
}
|
||||
|
||||
+3
-21
@@ -26,7 +26,6 @@ export class Viewport extends Disposable implements IViewport {
|
||||
private _lastRecordedBufferHeight: number = 0;
|
||||
private _lastTouchY: number = 0;
|
||||
private _lastScrollTop: number = 0;
|
||||
private _lastHadScrollBar: boolean = false;
|
||||
private _activeBuffer: IBuffer;
|
||||
private _renderDimensions: IRenderDimensions;
|
||||
|
||||
@@ -54,7 +53,6 @@ export class Viewport extends Disposable implements IViewport {
|
||||
// Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case,
|
||||
// therefore we account for a standard amount to make it visible
|
||||
this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
|
||||
this._lastHadScrollBar = true;
|
||||
this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this)));
|
||||
|
||||
// Track properties used in performance critical code manually to avoid using slow getters
|
||||
@@ -109,17 +107,6 @@ export class Viewport extends Disposable implements IViewport {
|
||||
this._viewportElement.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
// Update scroll bar width
|
||||
if (this._optionsService.options.scrollback === 0) {
|
||||
this.scrollBarWidth = 0;
|
||||
} else {
|
||||
this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
|
||||
}
|
||||
this._lastHadScrollBar = this.scrollBarWidth > 0;
|
||||
|
||||
const elementStyle = window.getComputedStyle(this._element);
|
||||
const elementPadding = parseInt(elementStyle.paddingLeft) + parseInt(elementStyle.paddingRight);
|
||||
this._viewportElement.style.width = (this._renderService.dimensions.actualCellWidth * (this._bufferService.cols) + this.scrollBarWidth + (this._lastHadScrollBar ? elementPadding : 0)).toString() + 'px';
|
||||
this._refreshAnimationFrame = null;
|
||||
}
|
||||
|
||||
@@ -151,11 +138,6 @@ export class Viewport extends Disposable implements IViewport {
|
||||
this._refresh(immediate);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the scroll bar visibility changed
|
||||
if (this._lastHadScrollBar !== (this._optionsService.options.scrollback > 0)) {
|
||||
this._refresh(immediate);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,15 +241,15 @@ export class Viewport extends Disposable implements IViewport {
|
||||
}
|
||||
|
||||
private _applyScrollModifier(amount: number, ev: WheelEvent): number {
|
||||
const modifier = this._optionsService.options.fastScrollModifier;
|
||||
const modifier = this._optionsService.rawOptions.fastScrollModifier;
|
||||
// Multiply the scroll speed when the modifier is down
|
||||
if ((modifier === 'alt' && ev.altKey) ||
|
||||
(modifier === 'ctrl' && ev.ctrlKey) ||
|
||||
(modifier === 'shift' && ev.shiftKey)) {
|
||||
return amount * this._optionsService.options.fastScrollSensitivity * this._optionsService.options.scrollSensitivity;
|
||||
return amount * this._optionsService.rawOptions.fastScrollSensitivity * this._optionsService.rawOptions.scrollSensitivity;
|
||||
}
|
||||
|
||||
return amount * this._optionsService.options.scrollSensitivity;
|
||||
return amount * this._optionsService.rawOptions.scrollSensitivity;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { IRenderService } from 'browser/services/Services';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { IBufferService, IDecorationService, IInternalDecoration } from 'common/services/Services';
|
||||
|
||||
export class BufferDecorationRenderer extends Disposable {
|
||||
private readonly _container: HTMLElement;
|
||||
private readonly _decorationElements: Map<IInternalDecoration, HTMLElement> = new Map();
|
||||
|
||||
private _animationFrame: number | undefined;
|
||||
private _altBufferIsActive: boolean = false;
|
||||
private _dimensionsChanged: boolean = false;
|
||||
|
||||
constructor(
|
||||
private readonly _screenElement: HTMLElement,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@IDecorationService private readonly _decorationService: IDecorationService,
|
||||
@IRenderService private readonly _renderService: IRenderService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._container = document.createElement('div');
|
||||
this._container.classList.add('xterm-decoration-container');
|
||||
this._screenElement.appendChild(this._container);
|
||||
|
||||
this.register(this._renderService.onRenderedViewportChange(() => this._queueRefresh()));
|
||||
this.register(this._renderService.onDimensionsChange(() => {
|
||||
this._dimensionsChanged = true;
|
||||
this._queueRefresh();
|
||||
}));
|
||||
this.register(addDisposableDomListener(window, 'resize', () => this._queueRefresh()));
|
||||
this.register(this._bufferService.buffers.onBufferActivate(() => {
|
||||
this._altBufferIsActive = this._bufferService.buffer === this._bufferService.buffers.alt;
|
||||
}));
|
||||
this.register(this._decorationService.onDecorationRegistered(() => this._queueRefresh()));
|
||||
this.register(this._decorationService.onDecorationRemoved(decoration => this._removeDecoration(decoration)));
|
||||
}
|
||||
|
||||
public override dispose(): void {
|
||||
this._container.remove();
|
||||
this._decorationElements.clear();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private _queueRefresh(): void {
|
||||
if (this._animationFrame !== undefined) {
|
||||
return;
|
||||
}
|
||||
this._animationFrame = this._renderService.addRefreshCallback(() => {
|
||||
this.refreshDecorations();
|
||||
this._animationFrame = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
public refreshDecorations(): void {
|
||||
for (const decoration of this._decorationService.decorations) {
|
||||
this._renderDecoration(decoration);
|
||||
}
|
||||
this._dimensionsChanged = false;
|
||||
}
|
||||
|
||||
private _renderDecoration(decoration: IInternalDecoration): void {
|
||||
this._refreshStyle(decoration);
|
||||
if (this._dimensionsChanged) {
|
||||
this._refreshXPosition(decoration);
|
||||
}
|
||||
}
|
||||
|
||||
private _createElement(decoration: IInternalDecoration): HTMLElement {
|
||||
const element = document.createElement('div');
|
||||
element.classList.add('xterm-decoration');
|
||||
element.style.width = `${Math.round((decoration.options.width || 1) * this._renderService.dimensions.actualCellWidth)}px`;
|
||||
element.style.height = `${(decoration.options.height || 1) * this._renderService.dimensions.actualCellHeight}px`;
|
||||
element.style.top = `${(decoration.marker.line - this._bufferService.buffers.active.ydisp) * this._renderService.dimensions.actualCellHeight}px`;
|
||||
element.style.lineHeight = `${this._renderService.dimensions.actualCellHeight}px`;
|
||||
|
||||
const x = decoration.options.x ?? 0;
|
||||
if (x && x > this._bufferService.cols) {
|
||||
// exceeded the container width, so hide
|
||||
element.style.display = 'none';
|
||||
}
|
||||
this._refreshXPosition(decoration, element);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
private _refreshStyle(decoration: IInternalDecoration): void {
|
||||
const line = decoration.marker.line - this._bufferService.buffers.active.ydisp;
|
||||
if (line < 0 || line >= this._bufferService.rows) {
|
||||
// outside of viewport
|
||||
if (decoration.element) {
|
||||
decoration.element.style.display = 'none';
|
||||
decoration.onRenderEmitter.fire(decoration.element);
|
||||
}
|
||||
} else {
|
||||
let element = this._decorationElements.get(decoration);
|
||||
if (!element) {
|
||||
decoration.onDispose(() => this._removeDecoration(decoration));
|
||||
element = this._createElement(decoration);
|
||||
decoration.element = element;
|
||||
this._decorationElements.set(decoration, element);
|
||||
this._container.appendChild(element);
|
||||
}
|
||||
element.style.top = `${line * this._renderService.dimensions.actualCellHeight}px`;
|
||||
element.style.display = this._altBufferIsActive ? 'none' : 'block';
|
||||
decoration.onRenderEmitter.fire(element);
|
||||
}
|
||||
}
|
||||
|
||||
private _refreshXPosition(decoration: IInternalDecoration, element: HTMLElement | undefined = decoration.element): void {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
const x = decoration.options.x ?? 0;
|
||||
if ((decoration.options.anchor || 'left') === 'right') {
|
||||
element.style.right = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : '';
|
||||
} else {
|
||||
element.style.left = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : '';
|
||||
}
|
||||
}
|
||||
|
||||
private _removeDecoration(decoration: IInternalDecoration): void {
|
||||
this._decorationElements.get(decoration)?.remove();
|
||||
this._decorationElements.delete(decoration);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { ColorZoneStore } from 'browser/decorations/ColorZoneStore';
|
||||
|
||||
const optionsRedFull = {
|
||||
overviewRulerOptions: {
|
||||
color: 'red',
|
||||
position: 'full' as 'full'
|
||||
}
|
||||
};
|
||||
|
||||
describe('ColorZoneStore', () => {
|
||||
let store: ColorZoneStore;
|
||||
|
||||
beforeEach(() => {
|
||||
store = new ColorZoneStore();
|
||||
store.setPadding({
|
||||
full: 1,
|
||||
left: 1,
|
||||
center: 1,
|
||||
right: 1
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge adjacent zones', () => {
|
||||
store.addDecoration({
|
||||
marker: { line: 0 },
|
||||
options: optionsRedFull
|
||||
});
|
||||
store.addDecoration({
|
||||
marker: { line: 1 },
|
||||
options: optionsRedFull
|
||||
});
|
||||
assert.deepStrictEqual(store.zones, [
|
||||
{
|
||||
color: 'red',
|
||||
position: 'full',
|
||||
startBufferLine: 0,
|
||||
endBufferLine: 1
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not merge non-adjacent zones', () => {
|
||||
store.addDecoration({
|
||||
marker: { line: 0 },
|
||||
options: optionsRedFull
|
||||
});
|
||||
store.addDecoration({
|
||||
marker: { line: 2 },
|
||||
options: optionsRedFull
|
||||
});
|
||||
assert.deepStrictEqual(store.zones, [
|
||||
{
|
||||
color: 'red',
|
||||
position: 'full',
|
||||
startBufferLine: 0,
|
||||
endBufferLine: 0
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
position: 'full',
|
||||
startBufferLine: 2,
|
||||
endBufferLine: 2
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should reuse zone objects', () => {
|
||||
const obj = {
|
||||
marker: { line: 0 },
|
||||
options: optionsRedFull
|
||||
};
|
||||
store.addDecoration(obj);
|
||||
const zone = store.zones[0];
|
||||
store.clear();
|
||||
store.addDecoration({
|
||||
marker: { line: 1 },
|
||||
options: optionsRedFull
|
||||
});
|
||||
// The object reference should be the same
|
||||
assert.equal(zone, store.zones[0]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IInternalDecoration } from 'common/services/Services';
|
||||
|
||||
export interface IColorZoneStore {
|
||||
readonly zones: IColorZone[];
|
||||
clear(): void;
|
||||
addDecoration(decoration: IInternalDecoration): void;
|
||||
/**
|
||||
* Sets the amount of padding in lines that will be added between zones, if new lines intersect
|
||||
* the padding they will be merged into the same zone.
|
||||
*/
|
||||
setPadding(padding: { [position: string]: number }): void;
|
||||
}
|
||||
|
||||
export interface IColorZone {
|
||||
/** Color in a format supported by canvas' fillStyle. */
|
||||
color: string;
|
||||
position: 'full' | 'left' | 'center' | 'right' | undefined;
|
||||
startBufferLine: number;
|
||||
endBufferLine: number;
|
||||
}
|
||||
|
||||
interface IMinimalDecorationForColorZone {
|
||||
marker: Pick<IInternalDecoration['marker'], 'line'>;
|
||||
options: Pick<IInternalDecoration['options'], 'overviewRulerOptions'>;
|
||||
}
|
||||
|
||||
export class ColorZoneStore implements IColorZoneStore {
|
||||
private _zones: IColorZone[] = [];
|
||||
|
||||
// The zone pool is used to keep zone objects from being freed between clearing the color zone
|
||||
// store and fetching the zones. This helps reduce GC pressure since the color zones are
|
||||
// accumulated on potentially every scroll event.
|
||||
private _zonePool: IColorZone[] = [];
|
||||
private _zonePoolIndex = 0;
|
||||
|
||||
private _linePadding: { [position: string]: number } = {
|
||||
full: 0,
|
||||
left: 0,
|
||||
center: 0,
|
||||
right: 0
|
||||
};
|
||||
|
||||
public get zones(): IColorZone[] {
|
||||
// Trim the zone pool to free unused memory
|
||||
this._zonePool.length = Math.min(this._zonePool.length, this._zones.length);
|
||||
return this._zones;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._zones.length = 0;
|
||||
this._zonePoolIndex = 0;
|
||||
}
|
||||
|
||||
public addDecoration(decoration: IMinimalDecorationForColorZone): void {
|
||||
if (!decoration.options.overviewRulerOptions) {
|
||||
return;
|
||||
}
|
||||
for (const z of this._zones) {
|
||||
if (z.color === decoration.options.overviewRulerOptions.color &&
|
||||
z.position === decoration.options.overviewRulerOptions.position) {
|
||||
if (this._lineIntersectsZone(z, decoration.marker.line)) {
|
||||
return;
|
||||
}
|
||||
if (this._lineAdjacentToZone(z, decoration.marker.line, decoration.options.overviewRulerOptions.position)) {
|
||||
this._addLineToZone(z, decoration.marker.line);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Create using zone pool if possible
|
||||
if (this._zonePoolIndex < this._zonePool.length) {
|
||||
this._zonePool[this._zonePoolIndex].color = decoration.options.overviewRulerOptions.color;
|
||||
this._zonePool[this._zonePoolIndex].position = decoration.options.overviewRulerOptions.position;
|
||||
this._zonePool[this._zonePoolIndex].startBufferLine = decoration.marker.line;
|
||||
this._zonePool[this._zonePoolIndex].endBufferLine = decoration.marker.line;
|
||||
this._zones.push(this._zonePool[this._zonePoolIndex++]);
|
||||
return;
|
||||
}
|
||||
// Create
|
||||
this._zones.push({
|
||||
color: decoration.options.overviewRulerOptions.color,
|
||||
position: decoration.options.overviewRulerOptions.position,
|
||||
startBufferLine: decoration.marker.line,
|
||||
endBufferLine: decoration.marker.line
|
||||
});
|
||||
this._zonePool.push(this._zones[this._zones.length - 1]);
|
||||
this._zonePoolIndex++;
|
||||
}
|
||||
|
||||
public setPadding(padding: { [position: string]: number }): void {
|
||||
this._linePadding = padding;
|
||||
}
|
||||
|
||||
private _lineIntersectsZone(zone: IColorZone, line: number): boolean {
|
||||
return (
|
||||
line >= zone.startBufferLine &&
|
||||
line <= zone.endBufferLine
|
||||
);
|
||||
}
|
||||
|
||||
private _lineAdjacentToZone(zone: IColorZone, line: number, position: IColorZone['position']): boolean {
|
||||
return (
|
||||
(line >= zone.startBufferLine - this._linePadding[position || 'full']) &&
|
||||
(line <= zone.endBufferLine + this._linePadding[position || 'full'])
|
||||
);
|
||||
}
|
||||
|
||||
private _addLineToZone(zone: IColorZone, line: number): void {
|
||||
zone.startBufferLine = Math.min(zone.startBufferLine, line);
|
||||
zone.endBufferLine = Math.max(zone.endBufferLine, line);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ColorZoneStore, IColorZone, IColorZoneStore } from 'browser/decorations/ColorZoneStore';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { IRenderService } from 'browser/services/Services';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { IBufferService, IDecorationService, IInternalDecoration, IOptionsService } from 'common/services/Services';
|
||||
|
||||
// Helper objects to avoid excessive calculation and garbage collection during rendering. These are
|
||||
// static values for each render and can be accessed using the decoration position as the key.
|
||||
const drawHeight = {
|
||||
full: 0,
|
||||
left: 0,
|
||||
center: 0,
|
||||
right: 0
|
||||
};
|
||||
const drawWidth = {
|
||||
full: 0,
|
||||
left: 0,
|
||||
center: 0,
|
||||
right: 0
|
||||
};
|
||||
const drawX = {
|
||||
full: 0,
|
||||
left: 0,
|
||||
center: 0,
|
||||
right: 0
|
||||
};
|
||||
|
||||
export class OverviewRulerRenderer extends Disposable {
|
||||
private readonly _canvas: HTMLCanvasElement;
|
||||
private readonly _ctx: CanvasRenderingContext2D;
|
||||
private readonly _colorZoneStore: IColorZoneStore = new ColorZoneStore();
|
||||
private get _width(): number {
|
||||
return this._optionsService.options.overviewRulerWidth || 0;
|
||||
}
|
||||
private _animationFrame: number | undefined;
|
||||
|
||||
private _shouldUpdateDimensions: boolean | undefined = true;
|
||||
private _shouldUpdateAnchor: boolean | undefined = true;
|
||||
private _lastKnownBufferLength: number = 0;
|
||||
|
||||
private _containerHeight: number | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly _viewportElement: HTMLElement,
|
||||
private readonly _screenElement: HTMLElement,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@IDecorationService private readonly _decorationService: IDecorationService,
|
||||
@IRenderService private readonly _renderService: IRenderService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService
|
||||
) {
|
||||
super();
|
||||
this._canvas = document.createElement('canvas');
|
||||
this._canvas.classList.add('xterm-decoration-overview-ruler');
|
||||
this._refreshCanvasDimensions();
|
||||
this._viewportElement.parentElement?.insertBefore(this._canvas, this._viewportElement);
|
||||
const ctx = this._canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('Ctx cannot be null');
|
||||
} else {
|
||||
this._ctx = ctx;
|
||||
}
|
||||
this._registerDecorationListeners();
|
||||
this._registerBufferChangeListeners();
|
||||
this._registerDimensionChangeListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* On decoration add or remove, redraw
|
||||
*/
|
||||
private _registerDecorationListeners(): void {
|
||||
this.register(this._decorationService.onDecorationRegistered(() => this._queueRefresh(undefined, true)));
|
||||
this.register(this._decorationService.onDecorationRemoved(() => this._queueRefresh(undefined, true)));
|
||||
}
|
||||
|
||||
/**
|
||||
* On buffer change, redraw
|
||||
* and hide the canvas if the alt buffer is active
|
||||
*/
|
||||
private _registerBufferChangeListeners(): void {
|
||||
this.register(this._renderService.onRenderedViewportChange(() => this._queueRefresh()));
|
||||
this.register(this._bufferService.buffers.onBufferActivate(() => {
|
||||
this._canvas!.style.display = this._bufferService.buffer === this._bufferService.buffers.alt ? 'none' : 'block';
|
||||
}));
|
||||
this.register(this._bufferService.onScroll(() => {
|
||||
if (this._lastKnownBufferLength !== this._bufferService.buffers.normal.lines.length) {
|
||||
this._refreshDrawHeightConstants();
|
||||
this._refreshColorZonePadding();
|
||||
}
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* On dimension change, update canvas dimensions
|
||||
* and then redraw
|
||||
*/
|
||||
private _registerDimensionChangeListeners(): void {
|
||||
// container height changed
|
||||
this.register(this._renderService.onRender((): void => {
|
||||
if (!this._containerHeight || this._containerHeight !== this._screenElement.clientHeight) {
|
||||
this._queueRefresh(true);
|
||||
this._containerHeight = this._screenElement.clientHeight;
|
||||
}
|
||||
}));
|
||||
// overview ruler width changed
|
||||
this.register(this._optionsService.onOptionChange(o => {
|
||||
if (o === 'overviewRulerWidth') {
|
||||
this._queueRefresh(true);
|
||||
}
|
||||
}));
|
||||
// device pixel ratio changed
|
||||
this.register(addDisposableDomListener(window, 'resize', () => {
|
||||
this._queueRefresh(true);
|
||||
}));
|
||||
// set the canvas dimensions
|
||||
this._queueRefresh(true);
|
||||
}
|
||||
|
||||
public override dispose(): void {
|
||||
this._canvas?.remove();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private _refreshDrawConstants(): void {
|
||||
// width
|
||||
const outerWidth = Math.floor(this._canvas.width / 3);
|
||||
const innerWidth = Math.ceil(this._canvas.width / 3);
|
||||
drawWidth.full = this._canvas.width;
|
||||
drawWidth.left = outerWidth;
|
||||
drawWidth.center = innerWidth;
|
||||
drawWidth.right = outerWidth;
|
||||
// height
|
||||
this._refreshDrawHeightConstants();
|
||||
// x
|
||||
drawX.full = 0;
|
||||
drawX.left = 0;
|
||||
drawX.center = drawWidth.left;
|
||||
drawX.right = drawWidth.left + drawWidth.center;
|
||||
}
|
||||
|
||||
private _refreshDrawHeightConstants(): void {
|
||||
drawHeight.full = Math.round(2 * window.devicePixelRatio);
|
||||
// Calculate actual pixels per line
|
||||
const pixelsPerLine = this._canvas.height / this._bufferService.buffer.lines.length;
|
||||
// Clamp actual pixels within a range
|
||||
const nonFullHeight = Math.round(Math.max(Math.min(pixelsPerLine, 12), 6) * window.devicePixelRatio);
|
||||
drawHeight.left = nonFullHeight;
|
||||
drawHeight.center = nonFullHeight;
|
||||
drawHeight.right = nonFullHeight;
|
||||
}
|
||||
|
||||
private _refreshColorZonePadding(): void {
|
||||
this._colorZoneStore.setPadding({
|
||||
full: Math.floor(this._bufferService.buffers.active.lines.length / (this._canvas.height - 1) * drawHeight.full),
|
||||
left: Math.floor(this._bufferService.buffers.active.lines.length / (this._canvas.height - 1) * drawHeight.left),
|
||||
center: Math.floor(this._bufferService.buffers.active.lines.length / (this._canvas.height - 1) * drawHeight.center),
|
||||
right: Math.floor(this._bufferService.buffers.active.lines.length / (this._canvas.height - 1) * drawHeight.right)
|
||||
});
|
||||
this._lastKnownBufferLength = this._bufferService.buffers.normal.lines.length;
|
||||
}
|
||||
|
||||
private _refreshCanvasDimensions(): void {
|
||||
this._canvas.style.width = `${this._width}px`;
|
||||
this._canvas.width = Math.round(this._width * window.devicePixelRatio);
|
||||
this._canvas.style.height = `${this._screenElement.clientHeight}px`;
|
||||
this._canvas.height = Math.round(this._screenElement.clientHeight * window.devicePixelRatio);
|
||||
this._refreshDrawConstants();
|
||||
this._refreshColorZonePadding();
|
||||
}
|
||||
|
||||
private _refreshDecorations(): void {
|
||||
if (this._shouldUpdateDimensions) {
|
||||
this._refreshCanvasDimensions();
|
||||
}
|
||||
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
this._colorZoneStore.clear();
|
||||
for (const decoration of this._decorationService.decorations) {
|
||||
this._colorZoneStore.addDecoration(decoration);
|
||||
}
|
||||
this._ctx.lineWidth = 1;
|
||||
const zones = this._colorZoneStore.zones;
|
||||
for (const zone of zones) {
|
||||
if (zone.position !== 'full') {
|
||||
this._renderColorZone(zone);
|
||||
}
|
||||
}
|
||||
for (const zone of zones) {
|
||||
if (zone.position === 'full') {
|
||||
this._renderColorZone(zone);
|
||||
}
|
||||
}
|
||||
this._shouldUpdateDimensions = false;
|
||||
this._shouldUpdateAnchor = false;
|
||||
}
|
||||
|
||||
private _renderColorZone(zone: IColorZone): void {
|
||||
// TODO: Is _decorationElements needed?
|
||||
|
||||
this._ctx.fillStyle = zone.color;
|
||||
this._ctx.fillRect(
|
||||
/* x */ drawX[zone.position || 'full'],
|
||||
/* y */ Math.round(
|
||||
(this._canvas.height - 1) * // -1 to ensure at least 2px are allowed for decoration on last line
|
||||
(zone.startBufferLine / this._bufferService.buffers.active.lines.length) - drawHeight[zone.position || 'full'] / 2
|
||||
),
|
||||
/* w */ drawWidth[zone.position || 'full'],
|
||||
/* h */ Math.round(
|
||||
(this._canvas.height - 1) * // -1 to ensure at least 2px are allowed for decoration on last line
|
||||
((zone.endBufferLine - zone.startBufferLine) / this._bufferService.buffers.active.lines.length) + drawHeight[zone.position || 'full']
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _queueRefresh(updateCanvasDimensions?: boolean, updateAnchor?: boolean): void {
|
||||
this._shouldUpdateDimensions = updateCanvasDimensions || this._shouldUpdateDimensions;
|
||||
this._shouldUpdateAnchor = updateAnchor || this._shouldUpdateAnchor;
|
||||
if (this._animationFrame !== undefined) {
|
||||
return;
|
||||
}
|
||||
this._animationFrame = window.requestAnimationFrame(() => {
|
||||
this._refreshDecorations();
|
||||
this._animationFrame = undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -217,8 +217,8 @@ export class CompositionHelper {
|
||||
this._compositionView.style.top = cursorTop + 'px';
|
||||
this._compositionView.style.height = cellHeight + 'px';
|
||||
this._compositionView.style.lineHeight = cellHeight + 'px';
|
||||
this._compositionView.style.fontFamily = this._optionsService.options.fontFamily;
|
||||
this._compositionView.style.fontSize = this._optionsService.options.fontSize + 'px';
|
||||
this._compositionView.style.fontFamily = this._optionsService.rawOptions.fontFamily;
|
||||
this._compositionView.style.fontSize = this._optionsService.rawOptions.fontSize + 'px';
|
||||
// Sync the textarea to the exact position of the composition view so the IME knows where the
|
||||
// text is.
|
||||
const compositionViewBounds = this._compositionView.getBoundingClientRect();
|
||||
|
||||
@@ -11,30 +11,38 @@ const CHAR_WIDTH = 10;
|
||||
const CHAR_HEIGHT = 20;
|
||||
|
||||
describe('Mouse getCoords', () => {
|
||||
let windowOverride: Pick<Window, 'getComputedStyle'>;
|
||||
let document: Document;
|
||||
|
||||
beforeEach(() => {
|
||||
windowOverride = {
|
||||
getComputedStyle(): any {
|
||||
return {
|
||||
getPropertyValue: () => '0px'
|
||||
} as Pick<CSSStyleDeclaration, 'getPropertyValue'>;
|
||||
}
|
||||
};
|
||||
document = new jsdom.JSDOM('').window.document;
|
||||
});
|
||||
|
||||
it('should return the cell that was clicked', () => {
|
||||
let coords: [number, number] | undefined;
|
||||
coords = getCoords({ clientX: CHAR_WIDTH / 2, clientY: CHAR_HEIGHT / 2 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
coords = getCoords(windowOverride, { clientX: CHAR_WIDTH / 2, clientY: CHAR_HEIGHT / 2 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
assert.deepEqual(coords, [1, 1]);
|
||||
coords = getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
coords = getCoords(windowOverride, { clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
assert.deepEqual(coords, [1, 1]);
|
||||
coords = getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT + 1 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
coords = getCoords(windowOverride, { clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT + 1 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
assert.deepEqual(coords, [1, 2]);
|
||||
coords = getCoords({ clientX: CHAR_WIDTH + 1, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
coords = getCoords(windowOverride, { clientX: CHAR_WIDTH + 1, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
assert.deepEqual(coords, [2, 1]);
|
||||
});
|
||||
|
||||
it('should ensure the coordinates are returned within the terminal bounds', () => {
|
||||
let coords: [number, number] | undefined;
|
||||
coords = getCoords({ clientX: -1, clientY: -1 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
coords = getCoords(windowOverride, { clientX: -1, clientY: -1 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
assert.deepEqual(coords, [1, 1]);
|
||||
// Event are double the cols/rows
|
||||
coords = getCoords({ clientX: CHAR_WIDTH * 20, clientY: CHAR_HEIGHT * 20 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
coords = getCoords(windowOverride, { clientX: CHAR_WIDTH * 20, clientY: CHAR_HEIGHT * 20 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
assert.deepEqual(coords, [10, 10], 'coordinates should never come back as larger than the terminal');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,9 +3,15 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export function getCoordsRelativeToElement(event: {clientX: number, clientY: number}, element: HTMLElement): [number, number] {
|
||||
export function getCoordsRelativeToElement(window: Pick<Window, 'getComputedStyle'>, event: {clientX: number, clientY: number}, element: HTMLElement): [number, number] {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return [event.clientX - rect.left, event.clientY - rect.top];
|
||||
const elementStyle = window.getComputedStyle(element);
|
||||
const leftPadding = parseInt(elementStyle.getPropertyValue('padding-left'));
|
||||
const topPadding = parseInt(elementStyle.getPropertyValue('padding-top'));
|
||||
return [
|
||||
event.clientX - rect.left - leftPadding,
|
||||
event.clientY - rect.top - topPadding
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -20,13 +26,13 @@ export function getCoordsRelativeToElement(event: {clientX: number, clientY: num
|
||||
* 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: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, hasValidCharSize: boolean, actualCellWidth: number, actualCellHeight: number, isSelection?: boolean): [number, number] | undefined {
|
||||
export function getCoords(window: Pick<Window, 'getComputedStyle'>, event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, hasValidCharSize: boolean, actualCellWidth: number, actualCellHeight: number, isSelection?: boolean): [number, number] | undefined {
|
||||
// Coordinates cannot be measured if there are no valid
|
||||
if (!hasValidCharSize) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const coords = getCoordsRelativeToElement(event, element);
|
||||
const coords = getCoordsRelativeToElement(window, event, element);
|
||||
if (!coords) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal as ITerminalApi, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBufferNamespace as IBufferNamespaceApi, IParser, ILinkProvider, IUnicodeHandling, FontWeight, IModes } from 'xterm';
|
||||
import { Terminal as ITerminalApi, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBufferNamespace as IBufferNamespaceApi, IParser, ILinkProvider, IUnicodeHandling, FontWeight, IModes, IDecorationOptions, IDecoration } from 'xterm';
|
||||
import { ITerminal } from 'browser/Types';
|
||||
import { Terminal as TerminalCore } from 'browser/Terminal';
|
||||
import * as Strings from 'browser/LocalizableStrings';
|
||||
@@ -30,17 +30,21 @@ export class Terminal implements ITerminalApi {
|
||||
this._core = new TerminalCore(options);
|
||||
this._addonManager = new AddonManager();
|
||||
|
||||
this._publicOptions = {};
|
||||
this._publicOptions = { ... this._core.options };
|
||||
const getter = (propName: string): any => {
|
||||
return this._core.options[propName];
|
||||
};
|
||||
const setter = (propName: string, value: any): void => {
|
||||
this._checkReadonlyOptions(propName);
|
||||
this._core.options[propName] = value;
|
||||
};
|
||||
|
||||
for (const propName in this._core.options) {
|
||||
Object.defineProperty(this._publicOptions, propName, {
|
||||
get: () => {
|
||||
return this._core.options[propName];
|
||||
},
|
||||
set: (value: any) => {
|
||||
this._checkReadonlyOptions(propName);
|
||||
this._core.options[propName] = value;
|
||||
}
|
||||
});
|
||||
const desc = {
|
||||
get: getter.bind(this, propName),
|
||||
set: setter.bind(this, propName)
|
||||
};
|
||||
Object.defineProperty(this._publicOptions, propName, desc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +58,7 @@ export class Terminal implements ITerminalApi {
|
||||
}
|
||||
|
||||
private _checkProposedApi(): void {
|
||||
if (!this._core.optionsService.options.allowProposedApi) {
|
||||
if (!this._core.optionsService.rawOptions.allowProposedApi) {
|
||||
throw new Error('You must set the allowProposedApi option to true to use proposed API');
|
||||
}
|
||||
}
|
||||
@@ -70,6 +74,7 @@ export class Terminal implements ITerminalApi {
|
||||
public get onScroll(): IEvent<number> { return this._core.onScroll; }
|
||||
public get onSelectionChange(): IEvent<void> { return this._core.onSelectionChange; }
|
||||
public get onTitleChange(): IEvent<string> { return this._core.onTitleChange; }
|
||||
public get onWriteParsed(): IEvent<void> { return this._core.onWriteParsed; }
|
||||
|
||||
public get element(): HTMLElement | undefined { return this._core.element; }
|
||||
public get parser(): IParser {
|
||||
@@ -162,11 +167,16 @@ export class Terminal implements ITerminalApi {
|
||||
this._checkProposedApi();
|
||||
this._core.deregisterCharacterJoiner(joinerId);
|
||||
}
|
||||
public registerMarker(cursorYOffset: number): IMarker | undefined {
|
||||
public registerMarker(cursorYOffset: number = 0): IMarker | undefined {
|
||||
this._checkProposedApi();
|
||||
this._verifyIntegers(cursorYOffset);
|
||||
return this._core.addMarker(cursorYOffset);
|
||||
}
|
||||
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined {
|
||||
this._checkProposedApi();
|
||||
this._verifyPositiveIntegers(decorationOptions.x ?? 0, decorationOptions.width ?? 0, decorationOptions.height ?? 0);
|
||||
return this._core.registerDecoration(decorationOptions);
|
||||
}
|
||||
public addMarker(cursorYOffset: number): IMarker | undefined {
|
||||
return this.registerMarker(cursorYOffset);
|
||||
}
|
||||
@@ -277,4 +287,12 @@ export class Terminal implements ITerminalApi {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _verifyPositiveIntegers(...values: number[]): void {
|
||||
for (const value of values) {
|
||||
if (value && (value === Infinity || isNaN(value) || value % 1 !== 0 || value < 0)) {
|
||||
throw new Error('This API only accepts positive integers');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,18 +4,18 @@
|
||||
*/
|
||||
|
||||
import { IRenderDimensions, IRenderLayer } from 'browser/renderer/Types';
|
||||
import { ICellData } from 'common/Types';
|
||||
import { ICellData, IColor } from 'common/Types';
|
||||
import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants';
|
||||
import { IGlyphIdentifier } from 'browser/renderer/atlas/Types';
|
||||
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/atlas/Constants';
|
||||
import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas';
|
||||
import { acquireCharAtlas } from 'browser/renderer/atlas/CharAtlasCache';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { IColorSet, IColor } from 'browser/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IBufferService, IOptionsService } from 'common/services/Services';
|
||||
import { throwIfFalsy } from 'browser/renderer/RendererUtils';
|
||||
import { channels, color, rgba } from 'browser/Color';
|
||||
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { excludeFromContrastRatioDemands, throwIfFalsy } from 'browser/renderer/RendererUtils';
|
||||
import { channels, color, rgba } from 'common/Color';
|
||||
import { removeElementFromParent } from 'browser/Dom';
|
||||
import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
|
||||
|
||||
@@ -29,6 +29,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
private _scaledCharLeft: number = 0;
|
||||
private _scaledCharTop: number = 0;
|
||||
|
||||
private _selectionStart: [number, number] | undefined;
|
||||
private _selectionEnd: [number, number] | undefined;
|
||||
private _columnSelectMode: boolean = false;
|
||||
|
||||
protected _charAtlas: BaseCharAtlas | undefined;
|
||||
|
||||
/**
|
||||
@@ -52,7 +56,8 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
protected _colors: IColorSet,
|
||||
private _rendererId: number,
|
||||
protected readonly _bufferService: IBufferService,
|
||||
protected readonly _optionsService: IOptionsService
|
||||
protected readonly _optionsService: IOptionsService,
|
||||
protected readonly _decorationService: IDecorationService
|
||||
) {
|
||||
this._canvas = document.createElement('canvas');
|
||||
this._canvas.classList.add(`xterm-${id}-layer`);
|
||||
@@ -79,7 +84,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
public onFocus(): void {}
|
||||
public onCursorMove(): void {}
|
||||
public onGridChanged(startRow: number, endRow: number): void {}
|
||||
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {}
|
||||
|
||||
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
|
||||
this._selectionStart = start;
|
||||
this._selectionEnd = end;
|
||||
this._columnSelectMode = columnSelectMode;
|
||||
}
|
||||
|
||||
public setColors(colorSet: IColorSet): void {
|
||||
this._refreshCharAtlas(colorSet);
|
||||
@@ -112,7 +122,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
this._charAtlas = acquireCharAtlas(this._optionsService.options, this._rendererId, colorSet, this._scaledCharWidth, this._scaledCharHeight);
|
||||
this._charAtlas = acquireCharAtlas(this._optionsService.rawOptions, this._rendererId, colorSet, this._scaledCharWidth, this._scaledCharHeight);
|
||||
this._charAtlas.warmUp();
|
||||
}
|
||||
|
||||
@@ -267,7 +277,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
|
||||
// Draw custom characters if applicable
|
||||
let drawSuccess = false;
|
||||
if (this._optionsService.options.customGlyphs !== false) {
|
||||
if (this._optionsService.rawOptions.customGlyphs !== false) {
|
||||
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight);
|
||||
}
|
||||
|
||||
@@ -294,7 +304,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* @param bold Whether the text is bold.
|
||||
*/
|
||||
protected _drawChars(cell: ICellData, x: number, y: number): void {
|
||||
const contrastColor = this._getContrastColor(cell);
|
||||
const contrastColor = this._getContrastColor(cell, x, y);
|
||||
|
||||
// skip cache right away if we draw in RGB
|
||||
// Note: to avoid bad runtime JoinedCellData will be skipped
|
||||
@@ -315,7 +325,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor();
|
||||
}
|
||||
|
||||
const drawInBrightColor = this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8;
|
||||
const drawInBrightColor = this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8;
|
||||
|
||||
fg += drawInBrightColor ? 8 : 0;
|
||||
this._currentGlyphIdentifier.chars = cell.getChars() || WHITESPACE_CELL_CHAR;
|
||||
@@ -325,7 +335,17 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._currentGlyphIdentifier.bold = !!cell.isBold();
|
||||
this._currentGlyphIdentifier.dim = !!cell.isDim();
|
||||
this._currentGlyphIdentifier.italic = !!cell.isItalic();
|
||||
const atlasDidDraw = this._charAtlas?.draw(this._ctx, this._currentGlyphIdentifier, x * this._scaledCellWidth + this._scaledCharLeft, y * this._scaledCellHeight + this._scaledCharTop);
|
||||
|
||||
// Don't try cache the glyph if it uses any decoration foreground/background override.
|
||||
let hasOverrides = false;
|
||||
for (const d of this._decorationService.getDecorationsAtCell(x, y)) {
|
||||
if (d.backgroundColorRGB || d.foregroundColorRGB) {
|
||||
hasOverrides = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const atlasDidDraw = hasOverrides ? false : this._charAtlas?.draw(this._ctx, this._currentGlyphIdentifier, x * this._scaledCellWidth + this._scaledCharLeft, y * this._scaledCellHeight + this._scaledCharTop);
|
||||
|
||||
if (!atlasDidDraw) {
|
||||
this._drawUncachedChars(cell, x, y);
|
||||
@@ -356,7 +376,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
|
||||
} else {
|
||||
let bg = cell.getBgColor();
|
||||
if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
|
||||
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
|
||||
bg += 8;
|
||||
}
|
||||
this._ctx.fillStyle = this._colors.ansi[bg].css;
|
||||
@@ -370,7 +390,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
|
||||
} else {
|
||||
let fg = cell.getFgColor();
|
||||
if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
|
||||
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
|
||||
fg += 8;
|
||||
}
|
||||
this._ctx.fillStyle = this._colors.ansi[fg].css;
|
||||
@@ -386,7 +406,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
|
||||
// Draw custom characters if applicable
|
||||
let drawSuccess = false;
|
||||
if (this._optionsService.options.customGlyphs !== false) {
|
||||
if (this._optionsService.rawOptions.customGlyphs !== false) {
|
||||
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight);
|
||||
}
|
||||
|
||||
@@ -421,21 +441,48 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* @param isBold If we should use the bold fontWeight.
|
||||
*/
|
||||
protected _getFont(isBold: boolean, isItalic: boolean): string {
|
||||
const fontWeight = isBold ? this._optionsService.options.fontWeightBold : this._optionsService.options.fontWeight;
|
||||
const fontWeight = isBold ? this._optionsService.rawOptions.fontWeightBold : this._optionsService.rawOptions.fontWeight;
|
||||
const fontStyle = isItalic ? 'italic' : '';
|
||||
|
||||
return `${fontStyle} ${fontWeight} ${this._optionsService.options.fontSize * window.devicePixelRatio}px ${this._optionsService.options.fontFamily}`;
|
||||
return `${fontStyle} ${fontWeight} ${this._optionsService.rawOptions.fontSize * window.devicePixelRatio}px ${this._optionsService.rawOptions.fontFamily}`;
|
||||
}
|
||||
|
||||
private _getContrastColor(cell: CellData): IColor | undefined {
|
||||
if (this._optionsService.options.minimumContrastRatio === 1) {
|
||||
private _getContrastColor(cell: CellData, x: number, y: number): IColor | undefined {
|
||||
// Get any decoration foreground/background overrides, this must be fetched before the early
|
||||
// exist but applied after inverse
|
||||
let bgOverride: number | undefined;
|
||||
let fgOverride: number | undefined;
|
||||
let isTop = false;
|
||||
for (const d of this._decorationService.getDecorationsAtCell(x, y)) {
|
||||
if (d.options.layer !== 'top' && isTop) {
|
||||
continue;
|
||||
}
|
||||
if (d.backgroundColorRGB) {
|
||||
bgOverride = d.backgroundColorRGB.rgba;
|
||||
}
|
||||
if (d.foregroundColorRGB) {
|
||||
fgOverride = d.foregroundColorRGB.rgba;
|
||||
}
|
||||
isTop = d.options.layer === 'top';
|
||||
}
|
||||
|
||||
// Apply selection foreground if applicable
|
||||
if (!isTop) {
|
||||
if (this._colors.selectionForeground && this._isCellInSelection(x, y)) {
|
||||
fgOverride = this._colors.selectionForeground.rgba;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bgOverride && !fgOverride && (this._optionsService.rawOptions.minimumContrastRatio === 1 || excludeFromContrastRatioDemands(cell.getCode()))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Try get from cache first
|
||||
const adjustedColor = this._colors.contrastCache.getColor(cell.bg, cell.fg);
|
||||
if (adjustedColor !== undefined) {
|
||||
return adjustedColor || undefined;
|
||||
if (!bgOverride && !fgOverride) {
|
||||
// Try get from cache
|
||||
const adjustedColor = this._colors.contrastCache.getColor(cell.bg, cell.fg);
|
||||
if (adjustedColor !== undefined) {
|
||||
return adjustedColor || undefined;
|
||||
}
|
||||
}
|
||||
|
||||
let fgColor = cell.getFgColor();
|
||||
@@ -453,13 +500,17 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
bgColorMode = temp2;
|
||||
}
|
||||
|
||||
const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, isInverse);
|
||||
const bgRgba = this._resolveBackgroundRgba(bgOverride !== undefined ? Attributes.CM_RGB : bgColorMode, bgOverride ?? bgColor, isInverse);
|
||||
const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, isInverse, isBold);
|
||||
const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._optionsService.options.minimumContrastRatio);
|
||||
let result = rgba.ensureContrastRatio(bgOverride ?? bgRgba, fgOverride ?? fgRgba, this._optionsService.rawOptions.minimumContrastRatio);
|
||||
|
||||
if (!result) {
|
||||
this._colors.contrastCache.setColor(cell.bg, cell.fg, null);
|
||||
return undefined;
|
||||
if (!fgOverride) {
|
||||
this._colors.contrastCache.setColor(cell.bg, cell.fg, null);
|
||||
return undefined;
|
||||
}
|
||||
// If it was an override and there was no contrast change, set as the result
|
||||
result = fgOverride;
|
||||
}
|
||||
|
||||
const color: IColor = {
|
||||
@@ -470,7 +521,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
),
|
||||
rgba: result
|
||||
};
|
||||
this._colors.contrastCache.setColor(cell.bg, cell.fg, color);
|
||||
if (!bgOverride && !fgOverride) {
|
||||
this._colors.contrastCache.setColor(cell.bg, cell.fg, color);
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
@@ -495,7 +548,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
switch (fgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
if (this._optionsService.options.drawBoldTextInBrightColors && bold && fgColor < 8) {
|
||||
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && bold && fgColor < 8) {
|
||||
fgColor += 8;
|
||||
}
|
||||
return this._colors.ansi[fgColor].rgba;
|
||||
@@ -509,5 +562,21 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
return this._colors.foreground.rgba;
|
||||
}
|
||||
}
|
||||
|
||||
private _isCellInSelection(x: number, y: number): boolean {
|
||||
const start = this._selectionStart;
|
||||
const end = this._selectionEnd;
|
||||
if (!start || !end) {
|
||||
return false;
|
||||
}
|
||||
if (this._columnSelectMode) {
|
||||
return x >= start[0] && y >= start[1] &&
|
||||
x < end[0] && y < end[1];
|
||||
}
|
||||
return (y > start[1] && y < end[1]) ||
|
||||
(start[1] === end[1] && y === start[1] && x >= start[0] && x < end[0]) ||
|
||||
(start[1] < end[1] && y === end[1] && x < end[0]) ||
|
||||
(start[1] < end[1] && y === start[1] && x >= start[0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
|
||||
import { ICellData } from 'common/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services';
|
||||
import { IBufferService, IOptionsService, ICoreService, IDecorationService } from 'common/services/Services';
|
||||
import { IEventEmitter } from 'common/EventEmitter';
|
||||
import { ICoreBrowserService } from 'browser/services/Services';
|
||||
|
||||
@@ -40,9 +40,10 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
@IBufferService bufferService: IBufferService,
|
||||
@IOptionsService optionsService: IOptionsService,
|
||||
@ICoreService private readonly _coreService: ICoreService,
|
||||
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
|
||||
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
|
||||
@IDecorationService decorationService: IDecorationService
|
||||
) {
|
||||
super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService);
|
||||
super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService);
|
||||
this._state = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
@@ -94,7 +95,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
public onOptionsChanged(): void {
|
||||
if (this._optionsService.options.cursorBlink) {
|
||||
if (this._optionsService.rawOptions.cursorBlink) {
|
||||
if (!this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager = new CursorBlinkStateManager(this._coreBrowserService.isFocused, () => {
|
||||
this._render(true);
|
||||
@@ -148,7 +149,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
this._clearCursor();
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
const cursorStyle = this._optionsService.options.cursorStyle;
|
||||
const cursorStyle = this._optionsService.rawOptions.cursorStyle;
|
||||
if (cursorStyle && cursorStyle !== 'block') {
|
||||
this._cursorRenderers[cursorStyle](cursorX, viewportRelativeCursorY, this._cell);
|
||||
} else {
|
||||
@@ -174,7 +175,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
if (this._state.x === cursorX &&
|
||||
this._state.y === viewportRelativeCursorY &&
|
||||
this._state.isFocused === this._coreBrowserService.isFocused &&
|
||||
this._state.style === this._optionsService.options.cursorStyle &&
|
||||
this._state.style === this._optionsService.rawOptions.cursorStyle &&
|
||||
this._state.width === this._cell.getWidth()) {
|
||||
return;
|
||||
}
|
||||
@@ -182,13 +183,13 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
this._ctx.save();
|
||||
this._cursorRenderers[this._optionsService.options.cursorStyle || 'block'](cursorX, viewportRelativeCursorY, this._cell);
|
||||
this._cursorRenderers[this._optionsService.rawOptions.cursorStyle || 'block'](cursorX, viewportRelativeCursorY, this._cell);
|
||||
this._ctx.restore();
|
||||
|
||||
this._state.x = cursorX;
|
||||
this._state.y = viewportRelativeCursorY;
|
||||
this._state.isFocused = false;
|
||||
this._state.style = this._optionsService.options.cursorStyle;
|
||||
this._state.style = this._optionsService.rawOptions.cursorStyle;
|
||||
this._state.width = this._cell.getWidth();
|
||||
}
|
||||
|
||||
@@ -213,7 +214,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
private _renderBarCursor(x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._fillLeftLineAtCell(x, y, this._optionsService.options.cursorWidth);
|
||||
this._fillLeftLineAtCell(x, y, this._optionsService.rawOptions.cursorWidth);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
|
||||
@@ -325,6 +325,37 @@ export const boxDrawingDefinitions: { [character: string]: { [fontWeight: number
|
||||
'╰': { [Style.NORMAL]: 'C.5,0,.5,.5,1,.5' }
|
||||
};
|
||||
|
||||
interface IVectorShape {
|
||||
d: string;
|
||||
type: VectorType;
|
||||
/** Padding to apply to the vector's x axis in CSS pixels. */
|
||||
horizontalPadding?: number;
|
||||
}
|
||||
|
||||
const enum VectorType {
|
||||
FILL,
|
||||
STROKE
|
||||
}
|
||||
|
||||
/**
|
||||
* This contains the definitions of the primarily used box drawing characters as vector shapes. The
|
||||
* reason these characters are defined specially is to avoid common problems if a user's font has
|
||||
* not been patched with powerline characters and also to get pixel perfect rendering as rendering
|
||||
* issues can occur around AA/SPAA.
|
||||
*
|
||||
* Original symbols defined in https://github.com/powerline/fontpatcher
|
||||
*/
|
||||
export const powerlineDefinitions: { [index: string]: IVectorShape } = {
|
||||
// Right triangle solid
|
||||
'\u{E0B0}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.FILL },
|
||||
// Right triangle line
|
||||
'\u{E0B1}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.STROKE, horizontalPadding: 0.5 },
|
||||
// Left triangle solid
|
||||
'\u{E0B2}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.FILL },
|
||||
// Left triangle line
|
||||
'\u{E0B3}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.STROKE, horizontalPadding: 0.5 }
|
||||
};
|
||||
|
||||
/**
|
||||
* Try drawing a custom block element or box drawing character, returning whether it was
|
||||
* successfully drawn.
|
||||
@@ -355,6 +386,12 @@ export function tryDrawCustomChar(
|
||||
return true;
|
||||
}
|
||||
|
||||
const powerlineDefinition = powerlineDefinitions[c];
|
||||
if (powerlineDefinition) {
|
||||
drawPowerlineChar(ctx, powerlineDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -414,10 +451,10 @@ function drawPatternChar(
|
||||
let b: number;
|
||||
let a: number;
|
||||
if (fillStyle.startsWith('#')) {
|
||||
r = parseInt(fillStyle.substr(1, 2), 16);
|
||||
g = parseInt(fillStyle.substr(3, 2), 16);
|
||||
b = parseInt(fillStyle.substr(5, 2), 16);
|
||||
a = fillStyle.length > 7 && parseInt(fillStyle.substr(7, 2), 16) || 1;
|
||||
r = parseInt(fillStyle.slice(1, 3), 16);
|
||||
g = parseInt(fillStyle.slice(3, 5), 16);
|
||||
b = parseInt(fillStyle.slice(5, 7), 16);
|
||||
a = fillStyle.length > 7 && parseInt(fillStyle.slice(7, 9), 16) || 1;
|
||||
} else if (fillStyle.startsWith('rgba')) {
|
||||
([r, g, b, a] = fillStyle.substring(5, fillStyle.length - 1).split(',').map(e => parseFloat(e)));
|
||||
} else {
|
||||
@@ -518,6 +555,38 @@ function drawBoxDrawingChar(
|
||||
}
|
||||
}
|
||||
|
||||
function drawPowerlineChar(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
charDefinition: IVectorShape,
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
scaledCellWidth: number,
|
||||
scaledCellHeight: number
|
||||
): void {
|
||||
ctx.beginPath();
|
||||
ctx.lineWidth = window.devicePixelRatio;
|
||||
for (const instruction of charDefinition.d.split(' ')) {
|
||||
const type = instruction[0];
|
||||
const f = svgToCanvasInstructionMap[type];
|
||||
if (!f) {
|
||||
console.error(`Could not find drawing instructions for "${type}"`);
|
||||
continue;
|
||||
}
|
||||
const args: string[] = instruction.substring(1).split(',');
|
||||
if (!args[0] || !args[1]) {
|
||||
continue;
|
||||
}
|
||||
f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset, charDefinition.horizontalPadding));
|
||||
}
|
||||
if (charDefinition.type === VectorType.STROKE) {
|
||||
ctx.strokeStyle = ctx.fillStyle;
|
||||
ctx.stroke();
|
||||
} else {
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function clamp(value: number, max: number, min: number = 0): number {
|
||||
return Math.max(Math.min(value, max), min);
|
||||
}
|
||||
@@ -528,7 +597,7 @@ const svgToCanvasInstructionMap: { [index: string]: any } = {
|
||||
'M': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.moveTo(args[0], args[1])
|
||||
};
|
||||
|
||||
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number): number[] {
|
||||
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, horizontalPadding: number = 0): number[] {
|
||||
const result = args.map(e => parseFloat(e) || parseInt(e));
|
||||
|
||||
if (result.length < 2) {
|
||||
@@ -537,14 +606,14 @@ function translateArgs(args: string[], cellWidth: number, cellHeight: number, xO
|
||||
|
||||
for (let x = 0; x < result.length; x += 2) {
|
||||
// Translate from 0-1 to 0-cellWidth
|
||||
result[x] *= cellWidth;
|
||||
result[x] *= cellWidth - (horizontalPadding * 2 * window.devicePixelRatio);
|
||||
// Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp
|
||||
// line at 100% devicePixelRatio
|
||||
if (result[x] !== 0) {
|
||||
result[x] = clamp(Math.round(result[x] + 0.5) - 0.5, cellWidth, 0);
|
||||
}
|
||||
// Apply the cell's offset (ie. x*cellWidth)
|
||||
result[x] += xOffset;
|
||||
result[x] += xOffset + (horizontalPadding * window.devicePixelRatio);
|
||||
}
|
||||
|
||||
for (let y = 1; y < result.length; y += 2) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||
import { is256Color } from 'browser/renderer/atlas/CharAtlasUtils';
|
||||
import { IColorSet, ILinkifierEvent, ILinkifier, ILinkifier2 } from 'browser/Types';
|
||||
import { IBufferService, IOptionsService } from 'common/services/Services';
|
||||
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
|
||||
export class LinkRenderLayer extends BaseRenderLayer {
|
||||
private _state: ILinkifierEvent | undefined;
|
||||
@@ -21,9 +21,10 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
linkifier: ILinkifier,
|
||||
linkifier2: ILinkifier2,
|
||||
@IBufferService bufferService: IBufferService,
|
||||
@IOptionsService optionsService: IOptionsService
|
||||
@IOptionsService optionsService: IOptionsService,
|
||||
@IDecorationService decorationService: IDecorationService
|
||||
) {
|
||||
super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService);
|
||||
super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService);
|
||||
linkifier.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
|
||||
linkifier.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import { IRenderLayer, IRenderer, IRenderDimensions, IRequestRedrawEvent } from
|
||||
import { LinkRenderLayer } from 'browser/renderer/LinkRenderLayer';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { IColorSet, ILinkifier, ILinkifier2 } from 'browser/Types';
|
||||
import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services';
|
||||
import { IBufferService, IOptionsService, ICoreService, IInstantiationService } from 'common/services/Services';
|
||||
import { ICharSizeService } from 'browser/services/Services';
|
||||
import { IBufferService, IOptionsService, IInstantiationService } from 'common/services/Services';
|
||||
import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
|
||||
@@ -39,7 +39,7 @@ export class Renderer extends Disposable implements IRenderer {
|
||||
@IOptionsService private readonly _optionsService: IOptionsService
|
||||
) {
|
||||
super();
|
||||
const allowTransparency = this._optionsService.options.allowTransparency;
|
||||
const allowTransparency = this._optionsService.rawOptions.allowTransparency;
|
||||
this._renderLayers = [
|
||||
instantiationService.createInstance(TextRenderLayer, this._screenElement, 0, this._colors, allowTransparency, this._id),
|
||||
instantiationService.createInstance(SelectionRenderLayer, this._screenElement, 1, this._colors, this._id),
|
||||
@@ -119,6 +119,10 @@ export class Renderer extends Disposable implements IRenderer {
|
||||
|
||||
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
|
||||
this._runOperation(l => l.onSelectionChanged(start, end, columnSelectMode));
|
||||
// Selection foreground requires a full re-render
|
||||
if (this._colors.selectionForeground) {
|
||||
this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 });
|
||||
}
|
||||
}
|
||||
|
||||
public onCursorMove(): void {
|
||||
@@ -178,18 +182,18 @@ export class Renderer extends Disposable implements IRenderer {
|
||||
// will be floored because since lineHeight can never be lower then 1, there
|
||||
// is a guarentee that the scaled line height will always be larger than
|
||||
// scaled char height.
|
||||
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.options.lineHeight);
|
||||
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.rawOptions.lineHeight);
|
||||
|
||||
// Calculate the y coordinate within a cell that text should draw from in
|
||||
// order to draw in the center of a cell.
|
||||
this.dimensions.scaledCharTop = this._optionsService.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
|
||||
this.dimensions.scaledCharTop = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
|
||||
|
||||
// Calculate the scaled cell width, taking the letterSpacing into account.
|
||||
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.options.letterSpacing);
|
||||
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.rawOptions.letterSpacing);
|
||||
|
||||
// Calculate the x coordinate with a cell that text should draw from in
|
||||
// order to draw in the center of a cell.
|
||||
this.dimensions.scaledCharLeft = Math.floor(this._optionsService.options.letterSpacing / 2);
|
||||
this.dimensions.scaledCharLeft = Math.floor(this._optionsService.rawOptions.letterSpacing / 2);
|
||||
|
||||
// Recalculate the canvas dimensions; scaled* define the actual number of
|
||||
// pixel in the canvas
|
||||
|
||||
@@ -9,3 +9,18 @@ export function throwIfFalsy<T>(value: T | undefined | null): T {
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function isPowerlineGlyph(codepoint: number): boolean {
|
||||
// Only return true for Powerline symbols which require
|
||||
// different padding and should be excluded from minimum contrast
|
||||
// ratio standards
|
||||
return 0xE0A4 <= codepoint && codepoint <= 0xE0D6;
|
||||
}
|
||||
|
||||
function isBoxOrBlockGlyph(codepoint: number): boolean {
|
||||
return (0x2500 <= codepoint && codepoint <= 0x259F);
|
||||
}
|
||||
|
||||
export function excludeFromContrastRatioDemands(codepoint: number): boolean {
|
||||
return isPowerlineGlyph(codepoint) || isBoxOrBlockGlyph(codepoint);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IBufferService, IOptionsService } from 'common/services/Services';
|
||||
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
|
||||
interface ISelectionState {
|
||||
start?: [number, number];
|
||||
@@ -24,9 +24,10 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
colors: IColorSet,
|
||||
rendererId: number,
|
||||
@IBufferService bufferService: IBufferService,
|
||||
@IOptionsService optionsService: IOptionsService
|
||||
@IOptionsService optionsService: IOptionsService,
|
||||
@IDecorationService decorationService: IDecorationService
|
||||
) {
|
||||
super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService);
|
||||
super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService);
|
||||
this._clearState();
|
||||
}
|
||||
|
||||
@@ -53,6 +54,8 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
super.onSelectionChanged(start, end, columnSelectMode);
|
||||
|
||||
// Selection has not changed
|
||||
if (!this._didStateChange(start, end, columnSelectMode, this._bufferService.buffer.ydisp)) {
|
||||
return;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { NULL_CELL_CODE, Content } from 'common/buffer/Constants';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IOptionsService, IBufferService } from 'common/services/Services';
|
||||
import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services';
|
||||
import { ICharacterJoinerService } from 'browser/services/Services';
|
||||
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
|
||||
|
||||
@@ -37,9 +37,10 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
rendererId: number,
|
||||
@IBufferService bufferService: IBufferService,
|
||||
@IOptionsService optionsService: IOptionsService,
|
||||
@ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService
|
||||
@ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService,
|
||||
@IDecorationService decorationService: IDecorationService
|
||||
) {
|
||||
super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService);
|
||||
super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService, decorationService);
|
||||
this._state = new GridCache<CharData>();
|
||||
}
|
||||
|
||||
@@ -176,6 +177,19 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
nextFillStyle = this._colors.ansi[cell.getBgColor()].css;
|
||||
}
|
||||
|
||||
// Get any decoration foreground/background overrides, this must be fetched before the early
|
||||
// exist but applied after inverse
|
||||
let isTop = false;
|
||||
for (const d of this._decorationService.getDecorationsAtCell(x, this._bufferService.buffer.ydisp + y)) {
|
||||
if (d.options.layer !== 'top' && isTop) {
|
||||
continue;
|
||||
}
|
||||
if (d.backgroundColorRGB) {
|
||||
nextFillStyle = d.backgroundColorRGB.css;
|
||||
}
|
||||
isTop = d.options.layer === 'top';
|
||||
}
|
||||
|
||||
if (prevFillStyle === null) {
|
||||
// This is either the first iteration, or the default background was set. Either way, we
|
||||
// don't need to draw anything.
|
||||
@@ -225,7 +239,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
|
||||
} else {
|
||||
let bg = cell.getBgColor();
|
||||
if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
|
||||
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
|
||||
bg += 8;
|
||||
}
|
||||
this._ctx.fillStyle = this._colors.ansi[bg].css;
|
||||
@@ -237,7 +251,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
|
||||
} else {
|
||||
let fg = cell.getFgColor();
|
||||
if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
|
||||
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
|
||||
fg += 8;
|
||||
}
|
||||
this._ctx.fillStyle = this._colors.ansi[fg].css;
|
||||
@@ -271,7 +285,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
public onOptionsChanged(): void {
|
||||
this._setTransparency(this._optionsService.options.allowTransparency);
|
||||
this._setTransparency(this._optionsService.rawOptions.allowTransparency);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,7 +16,7 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number
|
||||
cursor: undefined,
|
||||
cursorAccent: undefined,
|
||||
selection: undefined,
|
||||
ansi: [...colors.ansi]
|
||||
ansi: colors.ansi.slice()
|
||||
};
|
||||
return {
|
||||
devicePixelRatio: window.devicePixelRatio,
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { isFirefox } from 'common/Platform';
|
||||
import { isFirefox, isLegacyEdge } from 'common/Platform';
|
||||
|
||||
export const INVERTED_DEFAULT_COLOR = 257;
|
||||
export const DIM_OPACITY = 0.5;
|
||||
// The text baseline is set conditionally by browser. Using 'ideographic' for Firefox would
|
||||
// The text baseline is set conditionally by browser. Using 'ideographic' for Firefox or Legacy Edge would
|
||||
// result in truncated text (Issue 3353). Using 'bottom' for Chrome would result in slightly
|
||||
// unaligned Powerline fonts (PR 3356#issuecomment-850928179).
|
||||
export const TEXT_BASELINE: CanvasTextBaseline = isFirefox ? 'bottom' : 'ideographic';
|
||||
export const TEXT_BASELINE: CanvasTextBaseline = isFirefox || isLegacyEdge ? 'bottom' : 'ideographic';
|
||||
|
||||
export const CHAR_ATLAS_CELL_SPACING = 1;
|
||||
|
||||
@@ -9,9 +9,9 @@ import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas';
|
||||
import { DEFAULT_ANSI_COLORS } from 'browser/ColorManager';
|
||||
import { LRUMap } from 'browser/renderer/atlas/LRUMap';
|
||||
import { isFirefox, isSafari } from 'common/Platform';
|
||||
import { IColor } from 'browser/Types';
|
||||
import { IColor } from 'common/Types';
|
||||
import { throwIfFalsy } from 'browser/renderer/RendererUtils';
|
||||
import { color } from 'browser/Color';
|
||||
import { color } from 'common/Color';
|
||||
|
||||
// In practice we're probably never going to exhaust a texture this large. For debugging purposes,
|
||||
// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
|
||||
|
||||
@@ -9,9 +9,9 @@ import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { IColorSet, ILinkifierEvent, ILinkifier, ILinkifier2 } from 'browser/Types';
|
||||
import { ICharSizeService } from 'browser/services/Services';
|
||||
import { IOptionsService, IBufferService, IInstantiationService } from 'common/services/Services';
|
||||
import { IOptionsService, IBufferService, IInstantiationService, IDecorationService } from 'common/services/Services';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { color } from 'browser/Color';
|
||||
import { color } from 'common/Color';
|
||||
import { removeElementFromParent } from 'browser/Dom';
|
||||
|
||||
const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
|
||||
@@ -87,11 +87,11 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
this._screenElement.appendChild(this._rowContainer);
|
||||
this._screenElement.appendChild(this._selectionContainer);
|
||||
|
||||
this._linkifier.onShowLinkUnderline(e => this._onLinkHover(e));
|
||||
this._linkifier.onHideLinkUnderline(e => this._onLinkLeave(e));
|
||||
this.register(this._linkifier.onShowLinkUnderline(e => this._onLinkHover(e)));
|
||||
this.register(this._linkifier.onHideLinkUnderline(e => this._onLinkLeave(e)));
|
||||
|
||||
this._linkifier2.onShowLinkUnderline(e => this._onLinkHover(e));
|
||||
this._linkifier2.onHideLinkUnderline(e => this._onLinkLeave(e));
|
||||
this.register(this._linkifier2.onShowLinkUnderline(e => this._onLinkHover(e)));
|
||||
this.register(this._linkifier2.onHideLinkUnderline(e => this._onLinkLeave(e)));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
@@ -107,8 +107,8 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
private _updateDimensions(): void {
|
||||
this.dimensions.scaledCharWidth = this._charSizeService.width * window.devicePixelRatio;
|
||||
this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio);
|
||||
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.options.letterSpacing);
|
||||
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.options.lineHeight);
|
||||
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.rawOptions.letterSpacing);
|
||||
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.rawOptions.lineHeight);
|
||||
this.dimensions.scaledCharLeft = 0;
|
||||
this.dimensions.scaledCharTop = 0;
|
||||
this.dimensions.scaledCanvasWidth = this.dimensions.scaledCellWidth * this._bufferService.cols;
|
||||
@@ -161,16 +161,16 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
let styles =
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` +
|
||||
` color: ${this._colors.foreground.css};` +
|
||||
` font-family: ${this._optionsService.options.fontFamily};` +
|
||||
` font-size: ${this._optionsService.options.fontSize}px;` +
|
||||
` font-family: ${this._optionsService.rawOptions.fontFamily};` +
|
||||
` font-size: ${this._optionsService.rawOptions.fontSize}px;` +
|
||||
`}`;
|
||||
// Text styles
|
||||
styles +=
|
||||
`${this._terminalSelector} span:not(.${BOLD_CLASS}) {` +
|
||||
` font-weight: ${this._optionsService.options.fontWeight};` +
|
||||
` font-weight: ${this._optionsService.rawOptions.fontWeight};` +
|
||||
`}` +
|
||||
`${this._terminalSelector} span.${BOLD_CLASS} {` +
|
||||
` font-weight: ${this._optionsService.options.fontWeightBold};` +
|
||||
` font-weight: ${this._optionsService.rawOptions.fontWeightBold};` +
|
||||
`}` +
|
||||
`${this._terminalSelector} span.${ITALIC_CLASS} {` +
|
||||
` font-style: italic;` +
|
||||
@@ -210,7 +210,7 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
` color: ${this._colors.cursorAccent.css};` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` +
|
||||
` box-shadow: ${this._optionsService.options.cursorWidth}px 0 0 ${this._colors.cursor.css} inset;` +
|
||||
` box-shadow: ${this._optionsService.rawOptions.cursorWidth}px 0 0 ${this._colors.cursor.css} inset;` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` +
|
||||
` box-shadow: 0 -1px 0 ${this._colors.cursor.css} inset;` +
|
||||
@@ -226,7 +226,7 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
`}` +
|
||||
`${this._terminalSelector} .${SELECTION_CLASS} div {` +
|
||||
` position: absolute;` +
|
||||
` background-color: ${this._colors.selectionTransparent.css};` +
|
||||
` background-color: ${this._colors.selectionOpaque.css};` +
|
||||
`}`;
|
||||
// Colors
|
||||
this._colors.ansi.forEach((c, i) => {
|
||||
@@ -281,6 +281,9 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
this._selectionContainer.removeChild(this._selectionContainer.children[0]);
|
||||
}
|
||||
|
||||
this._rowFactory.onSelectionChanged(start, end, columnSelectMode);
|
||||
this.renderRows(0, this._bufferService.rows - 1);
|
||||
|
||||
// Selection does not exist
|
||||
if (!start || !end) {
|
||||
return;
|
||||
@@ -301,8 +304,9 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
const documentFragment = document.createDocumentFragment();
|
||||
|
||||
if (columnSelectMode) {
|
||||
const isXFlipped = start[0] > end[0];
|
||||
documentFragment.appendChild(
|
||||
this._createSelectionElement(viewportCappedStartRow, start[0], end[0], viewportCappedEndRow - viewportCappedStartRow + 1)
|
||||
this._createSelectionElement(viewportCappedStartRow, isXFlipped ? end[0] : start[0], isXFlipped ? start[0] : end[0], viewportCappedEndRow - viewportCappedStartRow + 1)
|
||||
);
|
||||
} else {
|
||||
// Draw first row
|
||||
@@ -356,15 +360,14 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
public renderRows(start: number, end: number): void {
|
||||
const cursorAbsoluteY = this._bufferService.buffer.ybase + this._bufferService.buffer.y;
|
||||
const cursorX = Math.min(this._bufferService.buffer.x, this._bufferService.cols - 1);
|
||||
const cursorBlink = this._optionsService.options.cursorBlink;
|
||||
const cursorBlink = this._optionsService.rawOptions.cursorBlink;
|
||||
|
||||
for (let y = start; y <= end; y++) {
|
||||
const rowElement = this._rowElements[y];
|
||||
rowElement.innerText = '';
|
||||
|
||||
const row = y + this._bufferService.buffer.ydisp;
|
||||
const lineData = this._bufferService.buffer.lines.get(row);
|
||||
const cursorStyle = this._optionsService.options.cursorStyle;
|
||||
const cursorStyle = this._optionsService.rawOptions.cursorStyle;
|
||||
rowElement.appendChild(this._rowFactory.createRow(lineData!, row, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, this._bufferService.cols));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, FgFlags,
|
||||
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||
import { IBufferLine } from 'common/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { MockCoreService, MockOptionsService } from 'common/TestUtils.test';
|
||||
import { css } from 'browser/Color';
|
||||
import { MockCoreService, MockDecorationService, MockOptionsService } from 'common/TestUtils.test';
|
||||
import { css } from 'common/Color';
|
||||
import { MockCharacterJoinerService } from 'browser/TestUtils.test';
|
||||
|
||||
describe('DomRendererRowFactory', () => {
|
||||
@@ -49,7 +49,8 @@ describe('DomRendererRowFactory', () => {
|
||||
} as any,
|
||||
new MockCharacterJoinerService(),
|
||||
new MockOptionsService({ drawBoldTextInBrightColors: true }),
|
||||
new MockCoreService()
|
||||
new MockCoreService(),
|
||||
new MockDecorationService()
|
||||
);
|
||||
lineData = createEmptyLineData(2);
|
||||
});
|
||||
@@ -183,7 +184,7 @@ describe('DomRendererRowFactory', () => {
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-fg-1 xterm-bg-2">a</span>'
|
||||
'<span class="xterm-bg-2 xterm-fg-1">a</span>'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -194,7 +195,7 @@ describe('DomRendererRowFactory', () => {
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-fg-1 xterm-bg-257">a</span>'
|
||||
'<span class="xterm-bg-257 xterm-fg-1">a</span>'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -204,7 +205,7 @@ describe('DomRendererRowFactory', () => {
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-fg-257 xterm-bg-1">a</span>'
|
||||
'<span class="xterm-bg-1 xterm-fg-257">a</span>'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -229,7 +230,7 @@ describe('DomRendererRowFactory', () => {
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span style="color:#010203;background-color:#040506;">a</span>'
|
||||
'<span style="background-color:#040506;color:#010203;">a</span>'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -240,7 +241,27 @@ describe('DomRendererRowFactory', () => {
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span style="color:#040506;background-color:#010203;">a</span>'
|
||||
'<span style="background-color:#010203;color:#040506;">a</span>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectionForeground', () => {
|
||||
it('should force selected cells with content to be rendered above the background', () => {
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
|
||||
rowFactory.onSelectionChanged([1, 0], [2, 0], false);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span>a</span><span class="xterm-decoration-top">b</span>'
|
||||
);
|
||||
});
|
||||
it('should force whitespace cells to be rendered above the background', () => {
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
rowFactory.onSelectionChanged([0, 0], [2, 0], false);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-decoration-top"> </span><span class="xterm-decoration-top">a</span>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IBufferLine } from 'common/Types';
|
||||
import { IBufferLine, ICellData, IColor } from 'common/Types';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||
import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { ICoreService, IOptionsService } from 'common/services/Services';
|
||||
import { color, rgba } from 'browser/Color';
|
||||
import { IColorSet, IColor } from 'browser/Types';
|
||||
import { ICharacterJoinerService } from 'browser/services/Services';
|
||||
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { color, rgba } from 'common/Color';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { ICharacterJoinerService, ISelectionService } from 'browser/services/Services';
|
||||
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
|
||||
import { excludeFromContrastRatioDemands } from 'browser/renderer/RendererUtils';
|
||||
|
||||
export const BOLD_CLASS = 'xterm-bold';
|
||||
export const DIM_CLASS = 'xterm-dim';
|
||||
@@ -27,12 +28,17 @@ export const CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline';
|
||||
export class DomRendererRowFactory {
|
||||
private _workCell: CellData = new CellData();
|
||||
|
||||
private _selectionStart: [number, number] | undefined;
|
||||
private _selectionEnd: [number, number] | undefined;
|
||||
private _columnSelectMode: boolean = false;
|
||||
|
||||
constructor(
|
||||
private readonly _document: Document,
|
||||
private _colors: IColorSet,
|
||||
@ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@ICoreService private readonly _coreService: ICoreService
|
||||
@ICoreService private readonly _coreService: ICoreService,
|
||||
@IDecorationService private readonly _decorationService: IDecorationService
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -40,6 +46,12 @@ export class DomRendererRowFactory {
|
||||
this._colors = colors;
|
||||
}
|
||||
|
||||
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
this._selectionStart = start;
|
||||
this._selectionEnd = end;
|
||||
this._columnSelectMode = columnSelectMode;
|
||||
}
|
||||
|
||||
public createRow(lineData: IBufferLine, row: number, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment {
|
||||
const fragment = this._document.createDocumentFragment();
|
||||
|
||||
@@ -171,14 +183,80 @@ export class DomRendererRowFactory {
|
||||
bgColorMode = temp2;
|
||||
}
|
||||
|
||||
// Apply any decoration foreground/background overrides, this must happen after inverse has
|
||||
// been applied
|
||||
let bgOverride: IColor | undefined;
|
||||
let fgOverride: IColor | undefined;
|
||||
let isTop = false;
|
||||
for (const d of this._decorationService.getDecorationsAtCell(x, row)) {
|
||||
if (d.options.layer !== 'top' && isTop) {
|
||||
continue;
|
||||
}
|
||||
if (d.backgroundColorRGB) {
|
||||
bgColorMode = Attributes.CM_RGB;
|
||||
bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
bgOverride = d.backgroundColorRGB;
|
||||
}
|
||||
if (d.foregroundColorRGB) {
|
||||
fgColorMode = Attributes.CM_RGB;
|
||||
fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
fgOverride = d.foregroundColorRGB;
|
||||
}
|
||||
isTop = d.options.layer === 'top';
|
||||
}
|
||||
|
||||
// Apply selection foreground if applicable
|
||||
const isInSelection = this._isCellInSelection(x, row);
|
||||
if (!isTop) {
|
||||
if (this._colors.selectionForeground && isInSelection) {
|
||||
fgColorMode = Attributes.CM_RGB;
|
||||
fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
|
||||
fgOverride = this._colors.selectionForeground;
|
||||
}
|
||||
}
|
||||
|
||||
// If in the selection, force the element to be above the selection to improve contrast and
|
||||
// support opaque selections
|
||||
if (isInSelection) {
|
||||
bgOverride = this._colors.selectionOpaque;
|
||||
isTop = true;
|
||||
}
|
||||
|
||||
// If it's a top decoration, render above the selection
|
||||
if (isTop) {
|
||||
charElement.classList.add(`xterm-decoration-top`);
|
||||
}
|
||||
|
||||
// Background
|
||||
let resolvedBg: IColor;
|
||||
switch (bgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
resolvedBg = this._colors.ansi[bg];
|
||||
charElement.classList.add(`xterm-bg-${bg}`);
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
resolvedBg = rgba.toColor(bg >> 16, bg >> 8 & 0xFF, bg & 0xFF);
|
||||
this._addStyle(charElement, `background-color:#${padStart((bg >>> 0).toString(16), '0', 6)}`);
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
if (isInverse) {
|
||||
resolvedBg = this._colors.foreground;
|
||||
charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`);
|
||||
} else {
|
||||
resolvedBg = this._colors.background;
|
||||
}
|
||||
}
|
||||
|
||||
// Foreground
|
||||
switch (fgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
if (cell.isBold() && fg < 8 && this._optionsService.options.drawBoldTextInBrightColors) {
|
||||
if (cell.isBold() && fg < 8 && this._optionsService.rawOptions.drawBoldTextInBrightColors) {
|
||||
fg += 8;
|
||||
}
|
||||
if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.ansi[fg])) {
|
||||
if (!this._applyMinimumContrast(charElement, resolvedBg, this._colors.ansi[fg], cell, bgOverride, undefined)) {
|
||||
charElement.classList.add(`xterm-fg-${fg}`);
|
||||
}
|
||||
break;
|
||||
@@ -188,35 +266,19 @@ export class DomRendererRowFactory {
|
||||
(fg >> 8) & 0xFF,
|
||||
(fg ) & 0xFF
|
||||
);
|
||||
if (!this._applyMinimumContrast(charElement, this._colors.background, color)) {
|
||||
if (!this._applyMinimumContrast(charElement, resolvedBg, color, cell, bgOverride, fgOverride)) {
|
||||
this._addStyle(charElement, `color:#${padStart(fg.toString(16), '0', 6)}`);
|
||||
}
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.foreground)) {
|
||||
if (!this._applyMinimumContrast(charElement, resolvedBg, this._colors.foreground, cell, bgOverride, undefined)) {
|
||||
if (isInverse) {
|
||||
charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Background
|
||||
switch (bgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
charElement.classList.add(`xterm-bg-${bg}`);
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
this._addStyle(charElement, `background-color:#${padStart(bg.toString(16), '0', 6)}`);
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
if (isInverse) {
|
||||
charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`);
|
||||
}
|
||||
}
|
||||
|
||||
fragment.appendChild(charElement);
|
||||
|
||||
x = lastCharX;
|
||||
@@ -224,18 +286,21 @@ export class DomRendererRowFactory {
|
||||
return fragment;
|
||||
}
|
||||
|
||||
private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor): boolean {
|
||||
if (this._optionsService.options.minimumContrastRatio === 1) {
|
||||
private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor, cell: ICellData, bgOverride: IColor | undefined, fgOverride: IColor | undefined): boolean {
|
||||
if (this._optionsService.rawOptions.minimumContrastRatio === 1 || excludeFromContrastRatioDemands(cell.getCode())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try get from cache first
|
||||
let adjustedColor = this._colors.contrastCache.getColor(this._workCell.bg, this._workCell.fg);
|
||||
// Try get from cache first, only use the cache when there are no decoration overrides
|
||||
let adjustedColor: IColor | undefined | null = undefined;
|
||||
if (!bgOverride && !fgOverride) {
|
||||
adjustedColor = this._colors.contrastCache.getColor(bg.rgba, fg.rgba);
|
||||
}
|
||||
|
||||
// Calculate and store in cache
|
||||
if (adjustedColor === undefined) {
|
||||
adjustedColor = color.ensureContrastRatio(bg, fg, this._optionsService.options.minimumContrastRatio);
|
||||
this._colors.contrastCache.setColor(this._workCell.bg, this._workCell.fg, adjustedColor ?? null);
|
||||
adjustedColor = color.ensureContrastRatio(bgOverride || bg, fgOverride || fg, this._optionsService.rawOptions.minimumContrastRatio);
|
||||
this._colors.contrastCache.setColor((bgOverride || bg).rgba, (fgOverride || fg).rgba, adjustedColor ?? null);
|
||||
}
|
||||
|
||||
if (adjustedColor) {
|
||||
@@ -249,6 +314,26 @@ export class DomRendererRowFactory {
|
||||
private _addStyle(element: HTMLElement, style: string): void {
|
||||
element.setAttribute('style', `${element.getAttribute('style') || ''}${style};`);
|
||||
}
|
||||
|
||||
private _isCellInSelection(x: number, y: number): boolean {
|
||||
const start = this._selectionStart;
|
||||
const end = this._selectionEnd;
|
||||
if (!start || !end) {
|
||||
return false;
|
||||
}
|
||||
if (this._columnSelectMode) {
|
||||
if (start[0] <= end[0]) {
|
||||
return x >= start[0] && y >= start[1] &&
|
||||
x < end[0] && y <= end[1];
|
||||
}
|
||||
return x < start[0] && y >= start[1] &&
|
||||
x >= end[0] && y <= end[1];
|
||||
}
|
||||
return (y > start[1] && y < end[1]) ||
|
||||
(start[1] === end[1] && y === start[1] && x >= start[0] && x < end[0]) ||
|
||||
(start[1] < end[1] && y === end[1] && x < end[0]) ||
|
||||
(start[1] < end[1] && y === start[1] && x >= start[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function padStart(text: string, padChar: string, length: number): string {
|
||||
|
||||
@@ -116,6 +116,12 @@ describe('SelectionModel', () => {
|
||||
model.selectionStartLength = 4;
|
||||
assert.deepEqual(model.finalSelectionEnd, [2, 3]);
|
||||
});
|
||||
it('should return the end on a different row when start + length overflows onto a following row with selectionEnd inbetween', () => {
|
||||
model.selectionStart = [78, 2];
|
||||
model.selectionEnd = [79, 2];
|
||||
model.selectionStartLength = 4;
|
||||
assert.deepEqual(model.finalSelectionEnd, [2, 3]);
|
||||
});
|
||||
it('should return selection end if selection end is after selection start + length', () => {
|
||||
model.selectionStart = [2, 2];
|
||||
model.selectionStartLength = 2;
|
||||
|
||||
@@ -92,7 +92,12 @@ export class SelectionModel {
|
||||
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]];
|
||||
// Keep the whole wrapped word/line selected if the content wraps multiple lines
|
||||
const startPlusLength = this.selectionStart[0] + this.selectionStartLength;
|
||||
if (startPlusLength > this._bufferService.cols) {
|
||||
return [startPlusLength % this._bufferService.cols, this.selectionStart[1] + Math.floor(startPlusLength / this._bufferService.cols)];
|
||||
}
|
||||
return [Math.max(startPlusLength, this.selectionEnd[0]), this.selectionEnd[1]];
|
||||
}
|
||||
}
|
||||
return this.selectionEnd;
|
||||
|
||||
@@ -69,8 +69,8 @@ class DomMeasureStrategy implements IMeasureStrategy {
|
||||
}
|
||||
|
||||
public measure(): IReadonlyMeasureResult {
|
||||
this._measureElement.style.fontFamily = this._optionsService.options.fontFamily;
|
||||
this._measureElement.style.fontSize = `${this._optionsService.options.fontSize}px`;
|
||||
this._measureElement.style.fontFamily = this._optionsService.rawOptions.fontFamily;
|
||||
this._measureElement.style.fontSize = `${this._optionsService.rawOptions.fontSize}px`;
|
||||
|
||||
// Note that this triggers a synchronous layout
|
||||
const geometry = this._measureElement.getBoundingClientRect();
|
||||
|
||||
@@ -17,6 +17,7 @@ export class MouseService implements IMouseService {
|
||||
|
||||
public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined {
|
||||
return getCoords(
|
||||
window,
|
||||
event,
|
||||
element,
|
||||
colCount,
|
||||
|
||||
@@ -9,8 +9,8 @@ import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { ScreenDprMonitor } from 'browser/ScreenDprMonitor';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { IColorSet, IRenderDebouncer } from 'browser/Types';
|
||||
import { IOptionsService, IBufferService } from 'common/services/Services';
|
||||
import { IColorSet, IRenderDebouncer, IRenderDebouncerWithCallback } from 'browser/Types';
|
||||
import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services';
|
||||
import { ICharSizeService, IRenderService } from 'browser/services/Services';
|
||||
|
||||
interface ISelectionState {
|
||||
@@ -22,7 +22,7 @@ interface ISelectionState {
|
||||
export class RenderService extends Disposable implements IRenderService {
|
||||
public serviceBrand: undefined;
|
||||
|
||||
private _renderDebouncer: IRenderDebouncer;
|
||||
private _renderDebouncer: IRenderDebouncerWithCallback;
|
||||
private _screenDprMonitor: ScreenDprMonitor;
|
||||
|
||||
private _isPaused: boolean = false;
|
||||
@@ -39,8 +39,10 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
|
||||
private _onDimensionsChange = new EventEmitter<IRenderDimensions>();
|
||||
public get onDimensionsChange(): IEvent<IRenderDimensions> { return this._onDimensionsChange.event; }
|
||||
private _onRenderedViewportChange = new EventEmitter<{ start: number, end: number }>();
|
||||
public get onRenderedViewportChange(): IEvent<{ start: number, end: number }> { return this._onRenderedViewportChange.event; }
|
||||
private _onRender = new EventEmitter<{ start: number, end: number }>();
|
||||
public get onRenderedBufferChange(): IEvent<{ start: number, end: number }> { return this._onRender.event; }
|
||||
public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; }
|
||||
private _onRefreshRequest = new EventEmitter<{ start: number, end: number }>();
|
||||
public get onRefreshRequest(): IEvent<{ start: number, end: number }> { return this._onRefreshRequest.event; }
|
||||
|
||||
@@ -52,6 +54,7 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
screenElement: HTMLElement,
|
||||
@IOptionsService optionsService: IOptionsService,
|
||||
@ICharSizeService private readonly _charSizeService: ICharSizeService,
|
||||
@IDecorationService decorationService: IDecorationService,
|
||||
@IBufferService bufferService: IBufferService
|
||||
) {
|
||||
super();
|
||||
@@ -65,10 +68,17 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
this._screenDprMonitor.setListener(() => this.onDevicePixelRatioChange());
|
||||
this.register(this._screenDprMonitor);
|
||||
|
||||
this.register(bufferService.onResize(e => this._fullRefresh()));
|
||||
this.register(optionsService.onOptionChange(() => this._renderer.onOptionsChanged()));
|
||||
this.register(bufferService.onResize(() => this._fullRefresh()));
|
||||
this.register(bufferService.buffers.onBufferActivate(() => this._renderer?.clear()));
|
||||
this.register(optionsService.onOptionChange(() => this._handleOptionsChanged()));
|
||||
this.register(this._charSizeService.onCharSizeChange(() => this.onCharSizeChanged()));
|
||||
|
||||
// Do a full refresh whenever any decoration is added or removed. This may not actually result
|
||||
// in changes but since decorations should be used sparingly or added/removed all in the same
|
||||
// frame this should have minimal performance impact.
|
||||
this.register(decorationService.onDecorationRegistered(() => this._fullRefresh()));
|
||||
this.register(decorationService.onDecorationRemoved(() => this._fullRefresh()));
|
||||
|
||||
// No need to register this as renderer is explicitly disposed in RenderService.dispose
|
||||
this._renderer.onRequestRedraw(e => this.refreshRows(e.start, e.end, true));
|
||||
|
||||
@@ -121,8 +131,9 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
|
||||
// Fire render event only if it was not a redraw
|
||||
if (!this._isNextRenderRedrawOnly) {
|
||||
this._onRender.fire({ start, end });
|
||||
this._onRenderedViewportChange.fire({ start, end });
|
||||
}
|
||||
this._onRender.fire({ start, end });
|
||||
this._isNextRenderRedrawOnly = true;
|
||||
}
|
||||
|
||||
@@ -131,7 +142,7 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
this._fireOnCanvasResize();
|
||||
}
|
||||
|
||||
public changeOptions(): void {
|
||||
private _handleOptionsChanged(): void {
|
||||
this._renderer.onOptionsChanged();
|
||||
this.refreshRows(0, this._rowCount - 1);
|
||||
this._fireOnCanvasResize();
|
||||
@@ -160,6 +171,10 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
this._fullRefresh();
|
||||
}
|
||||
|
||||
public addRefreshCallback(callback: FrameRequestCallback): number {
|
||||
return this._renderDebouncer.addRefreshCallback(callback);
|
||||
}
|
||||
|
||||
private _fullRefresh(): void {
|
||||
if (this._isPaused) {
|
||||
this._needsFullRefresh = true;
|
||||
|
||||
@@ -340,6 +340,9 @@ describe('SelectionService', () => {
|
||||
buffer.lines.set(0, stringToRow('foo bar'));
|
||||
selectionService.selectLineAt(0);
|
||||
assert.equal(selectionService.selectionText, 'foo bar', 'The selected text is correct');
|
||||
assert.deepEqual(selectionService.model.selectionStart, [0, 0]);
|
||||
assert.deepEqual(selectionService.model.selectionEnd, undefined);
|
||||
assert.deepEqual(selectionService.model.selectionStartLength, 20);
|
||||
assert.deepEqual(selectionService.model.finalSelectionStart, [0, 0]);
|
||||
assert.deepEqual(selectionService.model.finalSelectionEnd, [bufferService.cols, 0], 'The actual selection spans the entire column');
|
||||
});
|
||||
@@ -350,6 +353,9 @@ describe('SelectionService', () => {
|
||||
buffer.lines.set(1, line2);
|
||||
selectionService.selectLineAt(0);
|
||||
assert.equal(selectionService.selectionText, 'foobar', 'The selected text is correct');
|
||||
assert.deepEqual(selectionService.model.selectionStart, [0, 0]);
|
||||
assert.deepEqual(selectionService.model.selectionEnd, undefined);
|
||||
assert.deepEqual(selectionService.model.selectionStartLength, 40);
|
||||
assert.deepEqual(selectionService.model.finalSelectionStart, [0, 0]);
|
||||
assert.deepEqual(selectionService.model.finalSelectionEnd, [bufferService.cols, 1], 'The actual selection spans the entire column');
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import { SelectionModel } from 'browser/selection/SelectionModel';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { IMouseService, ISelectionService, IRenderService } from 'browser/services/Services';
|
||||
import { ILinkifier2 } from 'browser/Types';
|
||||
import { IBufferRange, ILinkifier2 } from 'browser/Types';
|
||||
import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services';
|
||||
import { getCoordsRelativeToElement } from 'browser/input/Mouse';
|
||||
import { moveToCellSequence } from 'browser/input/MoveToCell';
|
||||
@@ -207,8 +207,12 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
return '';
|
||||
}
|
||||
|
||||
// For column selection it's not enough to rely on final selection's swapping of reversed
|
||||
// values, it also needs the x coordinates to swap independently of the y coordinate is needed
|
||||
const startCol = start[0] < end[0] ? start[0] : end[0];
|
||||
const endCol = start[0] < end[0] ? end[0] : start[0];
|
||||
for (let i = start[1]; i <= end[1]; i++) {
|
||||
const lineText = buffer.translateBufferLineToString(i, true, start[0], end[0]);
|
||||
const lineText = buffer.translateBufferLineToString(i, true, startCol, endCol);
|
||||
result.push(lineText);
|
||||
}
|
||||
} else {
|
||||
@@ -308,6 +312,15 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
return this._areCoordsInSelection(coords, start, end);
|
||||
}
|
||||
|
||||
public isCellInSelection(x: number, y: number): boolean {
|
||||
const start = this._model.finalSelectionStart;
|
||||
const end = this._model.finalSelectionEnd;
|
||||
if (!start || !end) {
|
||||
return false;
|
||||
}
|
||||
return this._areCoordsInSelection([x, y], start, end);
|
||||
}
|
||||
|
||||
protected _areCoordsInSelection(coords: [number, number], start: [number, number], end: [number, number]): boolean {
|
||||
return (coords[1] > start[1] && coords[1] < end[1]) ||
|
||||
(start[1] === end[1] && coords[1] === start[1] && coords[0] >= start[0] && coords[0] < end[0]) ||
|
||||
@@ -393,7 +406,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _getMouseEventScrollAmount(event: MouseEvent): number {
|
||||
let offset = getCoordsRelativeToElement(event, this._screenElement)[1];
|
||||
let offset = getCoordsRelativeToElement(window, event, this._screenElement)[1];
|
||||
const terminalHeight = this._renderService.dimensions.canvasHeight;
|
||||
if (offset >= 0 && offset <= terminalHeight) {
|
||||
return 0;
|
||||
@@ -414,7 +427,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
*/
|
||||
public shouldForceSelection(event: MouseEvent): boolean {
|
||||
if (Browser.isMac) {
|
||||
return event.altKey && this._optionsService.options.macOptionClickForcesSelection;
|
||||
return event.altKey && this._optionsService.rawOptions.macOptionClickForcesSelection;
|
||||
}
|
||||
|
||||
return event.shiftKey;
|
||||
@@ -567,7 +580,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
* @param event the mouse or keyboard event
|
||||
*/
|
||||
public shouldColumnSelect(event: KeyboardEvent | MouseEvent): boolean {
|
||||
return event.altKey && !(Browser.isMac && this._optionsService.options.macOptionClickForcesSelection);
|
||||
return event.altKey && !(Browser.isMac && this._optionsService.rawOptions.macOptionClickForcesSelection);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -771,6 +784,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
this._model.selectionStart = [col, row];
|
||||
this._model.selectionStartLength = length;
|
||||
this.refresh();
|
||||
this._fireEventIfSelectionChanged();
|
||||
}
|
||||
|
||||
public rightClickSelect(ev: MouseEvent): void {
|
||||
@@ -993,7 +1007,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
if (cell.getWidth() === 0) {
|
||||
return false;
|
||||
}
|
||||
return this._optionsService.options.wordSeparator.indexOf(cell.getChars()) >= 0;
|
||||
return this._optionsService.rawOptions.wordSeparator.indexOf(cell.getChars()) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1002,8 +1016,12 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
*/
|
||||
protected _selectLineAt(line: number): void {
|
||||
const wrappedRange = this._bufferService.buffer.getWrappedRangeForLine(line);
|
||||
const range: IBufferRange = {
|
||||
start: { x: 0, y: wrappedRange.first },
|
||||
end: { x: this._bufferService.cols - 1, y: wrappedRange.last }
|
||||
};
|
||||
this._model.selectionStart = [0, wrappedRange.first];
|
||||
this._model.selectionEnd = [this._bufferService.cols, wrappedRange.last];
|
||||
this._model.selectionStartLength = 0;
|
||||
this._model.selectionEnd = undefined;
|
||||
this._model.selectionStartLength = getRangeLength(range, this._bufferService.cols);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
|
||||
import { IEvent } from 'common/EventEmitter';
|
||||
import { IRenderDimensions, IRenderer } from 'browser/renderer/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IColorSet, IRenderDebouncer } from 'browser/Types';
|
||||
import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
|
||||
import { createDecorator } from 'common/services/ServiceRegistry';
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { IDecorationOptions, IDecoration } from 'xterm';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
|
||||
export const ICharSizeService = createDecorator<ICharSizeService>('CharSizeService');
|
||||
export interface ICharSizeService {
|
||||
@@ -47,15 +49,20 @@ export interface IRenderService extends IDisposable {
|
||||
* Fires when buffer changes are rendered. This does not fire when only cursor
|
||||
* or selections are rendered.
|
||||
*/
|
||||
onRenderedBufferChange: IEvent<{ start: number, end: number }>;
|
||||
onRenderedViewportChange: IEvent<{ start: number, end: number }>;
|
||||
/**
|
||||
* Fires on render
|
||||
*/
|
||||
onRender: IEvent<{ start: number, end: number }>;
|
||||
onRefreshRequest: IEvent<{ start: number, end: number }>;
|
||||
|
||||
dimensions: IRenderDimensions;
|
||||
|
||||
addRefreshCallback(callback: FrameRequestCallback): number;
|
||||
|
||||
refreshRows(start: number, end: number): void;
|
||||
clearTextureAtlas(): void;
|
||||
resize(cols: number, rows: number): void;
|
||||
changeOptions(): void;
|
||||
setRenderer(renderer: IRenderer): void;
|
||||
setColors(colors: IColorSet): void;
|
||||
onDevicePixelRatioChange(): void;
|
||||
@@ -95,6 +102,7 @@ export interface ISelectionService {
|
||||
shouldForceSelection(event: MouseEvent): boolean;
|
||||
refresh(isLinuxMouseSelection?: boolean): void;
|
||||
onMouseDown(event: MouseEvent): void;
|
||||
isCellInSelection(x: number, y: number): boolean;
|
||||
}
|
||||
|
||||
export const ISoundService = createDecorator<ISoundService>('SoundService');
|
||||
|
||||
@@ -34,7 +34,7 @@ export class SoundService implements ISoundService {
|
||||
return;
|
||||
}
|
||||
const bellAudioSource = ctx.createBufferSource();
|
||||
ctx.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._optionsService.options.bellSound)), (buffer) => {
|
||||
ctx.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._optionsService.rawOptions.bellSound)), (buffer) => {
|
||||
bellAudioSource.buffer = buffer;
|
||||
bellAudioSource.connect(ctx.destination);
|
||||
bellAudioSource.start(0);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { channels, color, css, rgb, rgba, toPaddedHex, contrastRatio } from 'browser/Color';
|
||||
import { channels, color, css, rgb, rgba, toPaddedHex, contrastRatio } from 'common/Color';
|
||||
|
||||
describe('Color', () => {
|
||||
|
||||
@@ -153,6 +153,44 @@ describe('Color', () => {
|
||||
|
||||
describe('css', () => {
|
||||
describe('toColor', () => {
|
||||
it('should convert the #rgb format to an IColor', () => {
|
||||
assert.deepEqual(css.toColor('#000'), { css: '#000000', rgba: 0x000000FF });
|
||||
assert.deepEqual(css.toColor('#111'), { css: '#111111', rgba: 0x111111FF });
|
||||
assert.deepEqual(css.toColor('#222'), { css: '#222222', rgba: 0x222222FF });
|
||||
assert.deepEqual(css.toColor('#333'), { css: '#333333', rgba: 0x333333FF });
|
||||
assert.deepEqual(css.toColor('#444'), { css: '#444444', rgba: 0x444444FF });
|
||||
assert.deepEqual(css.toColor('#555'), { css: '#555555', rgba: 0x555555FF });
|
||||
assert.deepEqual(css.toColor('#666'), { css: '#666666', rgba: 0x666666FF });
|
||||
assert.deepEqual(css.toColor('#777'), { css: '#777777', rgba: 0x777777FF });
|
||||
assert.deepEqual(css.toColor('#888'), { css: '#888888', rgba: 0x888888FF });
|
||||
assert.deepEqual(css.toColor('#999'), { css: '#999999', rgba: 0x999999FF });
|
||||
assert.deepEqual(css.toColor('#aaa'), { css: '#aaaaaa', rgba: 0xaaaaaaFF });
|
||||
assert.deepEqual(css.toColor('#bbb'), { css: '#bbbbbb', rgba: 0xbbbbbbFF });
|
||||
assert.deepEqual(css.toColor('#ccc'), { css: '#cccccc', rgba: 0xccccccFF });
|
||||
assert.deepEqual(css.toColor('#ddd'), { css: '#dddddd', rgba: 0xddddddFF });
|
||||
assert.deepEqual(css.toColor('#eee'), { css: '#eeeeee', rgba: 0xeeeeeeFF });
|
||||
assert.deepEqual(css.toColor('#fff'), { css: '#ffffff', rgba: 0xffffffFF });
|
||||
assert.deepEqual(css.toColor('#fff'), { css: '#ffffff', rgba: 0xffffffFF });
|
||||
});
|
||||
it('should convert the #rgb format to an IColor', () => {
|
||||
assert.deepEqual(css.toColor('#0000'), { css: '#00000000', rgba: 0x00000000 });
|
||||
assert.deepEqual(css.toColor('#1111'), { css: '#11111111', rgba: 0x11111111 });
|
||||
assert.deepEqual(css.toColor('#2222'), { css: '#22222222', rgba: 0x22222222 });
|
||||
assert.deepEqual(css.toColor('#3333'), { css: '#33333333', rgba: 0x33333333 });
|
||||
assert.deepEqual(css.toColor('#4444'), { css: '#44444444', rgba: 0x44444444 });
|
||||
assert.deepEqual(css.toColor('#5555'), { css: '#55555555', rgba: 0x55555555 });
|
||||
assert.deepEqual(css.toColor('#6666'), { css: '#66666666', rgba: 0x66666666 });
|
||||
assert.deepEqual(css.toColor('#7777'), { css: '#77777777', rgba: 0x77777777 });
|
||||
assert.deepEqual(css.toColor('#8888'), { css: '#88888888', rgba: 0x88888888 });
|
||||
assert.deepEqual(css.toColor('#9999'), { css: '#99999999', rgba: 0x99999999 });
|
||||
assert.deepEqual(css.toColor('#aaaa'), { css: '#aaaaaaaa', rgba: 0xaaaaaaaa });
|
||||
assert.deepEqual(css.toColor('#bbbb'), { css: '#bbbbbbbb', rgba: 0xbbbbbbbb });
|
||||
assert.deepEqual(css.toColor('#cccc'), { css: '#cccccccc', rgba: 0xcccccccc });
|
||||
assert.deepEqual(css.toColor('#dddd'), { css: '#dddddddd', rgba: 0xdddddddd });
|
||||
assert.deepEqual(css.toColor('#eeee'), { css: '#eeeeeeee', rgba: 0xeeeeeeee });
|
||||
assert.deepEqual(css.toColor('#ffff'), { css: '#ffffffff', rgba: 0xffffffff });
|
||||
assert.deepEqual(css.toColor('#ffff'), { css: '#ffffffff', rgba: 0xffffffff });
|
||||
});
|
||||
it('should convert the #rrggbb format to an IColor', () => {
|
||||
assert.deepEqual(css.toColor('#000000'), { css: '#000000', rgba: 0x000000FF });
|
||||
assert.deepEqual(css.toColor('#101010'), { css: '#101010', rgba: 0x101010FF });
|
||||
@@ -191,6 +229,20 @@ describe('Color', () => {
|
||||
assert.deepEqual(css.toColor('#f0f0f0f0'), { css: '#f0f0f0f0', rgba: 0xf0f0f0f0 });
|
||||
assert.deepEqual(css.toColor('#ffffffff'), { css: '#ffffffff', rgba: 0xffffffff });
|
||||
});
|
||||
it('should convert the rgb() format to an IColor', () => {
|
||||
assert.deepEqual(css.toColor('rgb(0, 0, 0)'), { css: '#000000ff', rgba: 0x000000ff });
|
||||
assert.deepEqual(css.toColor('rgb(80, 0, 0)'), { css: '#500000ff', rgba: 0x500000ff });
|
||||
assert.deepEqual(css.toColor('rgb(0, 80, 0)'), { css: '#005000ff', rgba: 0x005000ff });
|
||||
assert.deepEqual(css.toColor('rgb(0, 0, 80)'), { css: '#000050ff', rgba: 0x000050ff });
|
||||
assert.deepEqual(css.toColor('rgb(255, 255, 255)'), { css: '#ffffffff', rgba: 0xffffffff });
|
||||
});
|
||||
it('should convert the rgba() format to an IColor', () => {
|
||||
assert.deepEqual(css.toColor('rgba(0, 0, 0, 0)'), { css: '#00000000', rgba: 0x00000000 });
|
||||
assert.deepEqual(css.toColor('rgba(80, 0, 0, 0.5)'), { css: '#50000080', rgba: 0x50000080 });
|
||||
assert.deepEqual(css.toColor('rgba(0, 80, 0, 0.5)'), { css: '#00500080', rgba: 0x00500080 });
|
||||
assert.deepEqual(css.toColor('rgba(0, 0, 80, 0.5)'), { css: '#00005080', rgba: 0x00005080 });
|
||||
assert.deepEqual(css.toColor('rgba(255, 255, 255, 1)'), { css: '#ffffffff', rgba: 0xffffffff });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IColor } from 'browser/Types';
|
||||
import { IColorRGB } from 'common/Types';
|
||||
import { IColor, IColorRGB } from 'common/Types';
|
||||
|
||||
/**
|
||||
* Helper functions where the source type is "channels" (individual color channels as numbers).
|
||||
@@ -95,17 +94,40 @@ export namespace color {
|
||||
*/
|
||||
export namespace css {
|
||||
export function toColor(css: string): IColor {
|
||||
switch (css.length) {
|
||||
case 7: // #rrggbb
|
||||
return {
|
||||
css,
|
||||
rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0
|
||||
};
|
||||
case 9: // #rrggbbaa
|
||||
return {
|
||||
css,
|
||||
rgba: parseInt(css.slice(1), 16) >>> 0
|
||||
};
|
||||
if (css.match(/#[0-9a-f]{3,8}/i)) {
|
||||
switch (css.length) {
|
||||
case 4: { // #rgb
|
||||
const r = parseInt(css.slice(1, 2).repeat(2), 16);
|
||||
const g = parseInt(css.slice(2, 3).repeat(2), 16);
|
||||
const b = parseInt(css.slice(3, 4).repeat(2), 16);
|
||||
return rgba.toColor(r, g, b);
|
||||
}
|
||||
case 5: { // #rgba
|
||||
const r = parseInt(css.slice(1, 2).repeat(2), 16);
|
||||
const g = parseInt(css.slice(2, 3).repeat(2), 16);
|
||||
const b = parseInt(css.slice(3, 4).repeat(2), 16);
|
||||
const a = parseInt(css.slice(4, 5).repeat(2), 16);
|
||||
return rgba.toColor(r, g, b, a);
|
||||
}
|
||||
case 7: // #rrggbb
|
||||
return {
|
||||
css,
|
||||
rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0
|
||||
};
|
||||
case 9: // #rrggbbaa
|
||||
return {
|
||||
css,
|
||||
rgba: parseInt(css.slice(1), 16) >>> 0
|
||||
};
|
||||
}
|
||||
}
|
||||
const rgbaMatch = css.match(/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(,\s*(0|1|\d?\.(\d+))\s*)?\)/);
|
||||
if (rgbaMatch) { // rgb() or rgba()
|
||||
const r = parseInt(rgbaMatch[1]);
|
||||
const g = parseInt(rgbaMatch[2]);
|
||||
const b = parseInt(rgbaMatch[3]);
|
||||
const a = Math.round((rgbaMatch[5] === undefined ? 1 : parseFloat(rgbaMatch[5])) * 0xFF);
|
||||
return rgba.toColor(r, g, b, a);
|
||||
}
|
||||
throw new Error('css.toColor: Unsupported css format');
|
||||
}
|
||||
@@ -151,15 +173,42 @@ export namespace rgb {
|
||||
* Helper functions where the source type is "rgba" (number: 0xrrggbbaa).
|
||||
*/
|
||||
export namespace rgba {
|
||||
/**
|
||||
* Given a foreground color and a background color, either increase or reduce the luminance of the
|
||||
* foreground color until the specified contrast ratio is met. If pure white or black is hit
|
||||
* without the contrast ratio being met, go the other direction using the background color as the
|
||||
* foreground color and take either the first or second result depending on which has the higher
|
||||
* contrast ratio.
|
||||
*
|
||||
* `undefined` will be returned if the contrast ratio is already met.
|
||||
*
|
||||
* @param bgRgba The background color in rgba format.
|
||||
* @param fgRgba The foreground color in rgba format.
|
||||
* @param ratio The contrast ratio to achieve.
|
||||
*/
|
||||
export function ensureContrastRatio(bgRgba: number, fgRgba: number, ratio: number): number | undefined {
|
||||
const bgL = rgb.relativeLuminance(bgRgba >> 8);
|
||||
const fgL = rgb.relativeLuminance(fgRgba >> 8);
|
||||
const cr = contrastRatio(bgL, fgL);
|
||||
if (cr < ratio) {
|
||||
if (fgL < bgL) {
|
||||
return reduceLuminance(bgRgba, fgRgba, ratio);
|
||||
const resultA = reduceLuminance(bgRgba, fgRgba, ratio);
|
||||
const resultARatio = contrastRatio(bgL, rgb.relativeLuminance(resultA >> 8));
|
||||
if (resultARatio < ratio) {
|
||||
const resultB = increaseLuminance(bgRgba, bgRgba, ratio);
|
||||
const resultBRatio = contrastRatio(bgL, rgb.relativeLuminance(resultB >> 8));
|
||||
return resultARatio > resultBRatio ? resultA : resultB;
|
||||
}
|
||||
return resultA;
|
||||
}
|
||||
return increaseLuminance(bgRgba, fgRgba, ratio);
|
||||
const resultA = increaseLuminance(bgRgba, fgRgba, ratio);
|
||||
const resultARatio = contrastRatio(bgL, rgb.relativeLuminance(resultA >> 8));
|
||||
if (resultARatio < ratio) {
|
||||
const resultB = reduceLuminance(bgRgba, bgRgba, ratio);
|
||||
const resultBRatio = contrastRatio(bgL, rgb.relativeLuminance(resultB >> 8));
|
||||
return resultARatio > resultBRatio ? resultA : resultB;
|
||||
}
|
||||
return resultA;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -173,13 +222,13 @@ export namespace rgba {
|
||||
let fgR = (fgRgba >> 24) & 0xFF;
|
||||
let fgG = (fgRgba >> 16) & 0xFF;
|
||||
let fgB = (fgRgba >> 8) & 0xFF;
|
||||
let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB));
|
||||
let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgG, fgB), rgb.relativeLuminance2(bgR, bgG, bgB));
|
||||
while (cr < ratio && (fgR > 0 || fgG > 0 || fgB > 0)) {
|
||||
// Reduce by 10% until the ratio is hit
|
||||
fgR -= Math.max(0, Math.ceil(fgR * 0.1));
|
||||
fgG -= Math.max(0, Math.ceil(fgG * 0.1));
|
||||
fgB -= Math.max(0, Math.ceil(fgB * 0.1));
|
||||
cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB));
|
||||
cr = contrastRatio(rgb.relativeLuminance2(fgR, fgG, fgB), rgb.relativeLuminance2(bgR, bgG, bgB));
|
||||
}
|
||||
return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;
|
||||
}
|
||||
@@ -193,13 +242,13 @@ export namespace rgba {
|
||||
let fgR = (fgRgba >> 24) & 0xFF;
|
||||
let fgG = (fgRgba >> 16) & 0xFF;
|
||||
let fgB = (fgRgba >> 8) & 0xFF;
|
||||
let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB));
|
||||
let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgG, fgB), rgb.relativeLuminance2(bgR, bgG, bgB));
|
||||
while (cr < ratio && (fgR < 0xFF || fgG < 0xFF || fgB < 0xFF)) {
|
||||
// Increase by 10% until the ratio is hit
|
||||
fgR = Math.min(0xFF, fgR + Math.ceil((255 - fgR) * 0.1));
|
||||
fgG = Math.min(0xFF, fgG + Math.ceil((255 - fgG) * 0.1));
|
||||
fgB = Math.min(0xFF, fgB + Math.ceil((255 - fgB) * 0.1));
|
||||
cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB));
|
||||
cr = contrastRatio(rgb.relativeLuminance2(fgR, fgG, fgB), rgb.relativeLuminance2(bgR, bgG, bgB));
|
||||
}
|
||||
return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;
|
||||
}
|
||||
@@ -209,10 +258,10 @@ export namespace rgba {
|
||||
return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF];
|
||||
}
|
||||
|
||||
export function toColor(r: number, g: number, b: number): IColor {
|
||||
export function toColor(r: number, g: number, b: number, a?: number): IColor {
|
||||
return {
|
||||
css: channels.toCss(r, g, b),
|
||||
rgba: channels.toRgba(r, g, b)
|
||||
css: channels.toCss(r, g, b, a),
|
||||
rgba: channels.toRgba(r, g, b, a)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,8 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
private _onResize = new EventEmitter<{ cols: number, rows: number }>();
|
||||
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; }
|
||||
protected _onScroll = new EventEmitter<IScrollEvent, void>();
|
||||
public get onWriteParsed(): IEvent<void> { return this._onWriteParsed.event; }
|
||||
protected _onWriteParsed = new EventEmitter<void>();
|
||||
/**
|
||||
* Internally we track the source of the scroll but this is meaningless outside the library so
|
||||
* it's filtered out.
|
||||
@@ -138,6 +140,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
|
||||
// Setup WriteBuffer
|
||||
this._writeBuffer = new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult));
|
||||
this.register(forwardEvent(this._writeBuffer.onWriteParsed, this._onWriteParsed));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
@@ -247,7 +250,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
}
|
||||
|
||||
protected _setup(): void {
|
||||
if (this.optionsService.options.windowsMode) {
|
||||
if (this.optionsService.rawOptions.windowsMode) {
|
||||
this._enableWindowsMode();
|
||||
}
|
||||
}
|
||||
@@ -267,7 +270,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
this.buffers.resize(this.cols, this.rows);
|
||||
break;
|
||||
case 'windowsMode':
|
||||
if (this.optionsService.options.windowsMode) {
|
||||
if (this.optionsService.rawOptions.windowsMode) {
|
||||
this._enableWindowsMode();
|
||||
} else {
|
||||
this._windowsMode?.dispose();
|
||||
|
||||
+12
-11
@@ -177,8 +177,8 @@ class DECRQSS implements IDcsHandler {
|
||||
break;
|
||||
case ' q': // DECSCUSR
|
||||
const STYLES: { [key: string]: number } = { 'block': 2, 'underline': 4, 'bar': 6 };
|
||||
let style = STYLES[this._optionsService.options.cursorStyle];
|
||||
style -= this._optionsService.options.cursorBlink ? 1 : 0;
|
||||
let style = STYLES[this._optionsService.rawOptions.cursorStyle];
|
||||
style -= this._optionsService.rawOptions.cursorBlink ? 1 : 0;
|
||||
this._coreService.triggerDataEvent(`${C0.ESC}P1$r${style} q${C0.ESC}\\`);
|
||||
break;
|
||||
default:
|
||||
@@ -546,7 +546,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
|
||||
// Log debug data, the log level gate is to prevent extra work in this hot path
|
||||
if (this._logService.logLevel <= LogLevelEnum.DEBUG) {
|
||||
this._logService.debug(`parsing data${typeof data === 'string' ? ` "${data}"` : ''}`, typeof data === 'string'
|
||||
this._logService.debug(`parsing data${typeof data === 'string' ? ` "${data}"` : ` "${Array.prototype.map.call(data, e => String.fromCharCode(e)).join('')}"`}`, typeof data === 'string'
|
||||
? data.split('').map(e => e.charCodeAt(0))
|
||||
: data
|
||||
);
|
||||
@@ -603,7 +603,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
let code: number;
|
||||
let chWidth: number;
|
||||
const charset = this._charsetService.charset;
|
||||
const screenReaderMode = this._optionsService.options.screenReaderMode;
|
||||
const screenReaderMode = this._optionsService.rawOptions.screenReaderMode;
|
||||
const cols = this._bufferService.cols;
|
||||
const wraparoundMode = this._coreService.decPrivateModes.wraparound;
|
||||
const insertMode = this._coreService.modes.insertMode;
|
||||
@@ -745,7 +745,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
if (id.final === 't' && !id.prefix && !id.intermediates) {
|
||||
// security: always check whether window option is allowed
|
||||
return this._parser.registerCsiHandler(id, params => {
|
||||
if (!paramToWindowOption(params.params[0], this._optionsService.options.windowOptions)) {
|
||||
if (!paramToWindowOption(params.params[0], this._optionsService.rawOptions.windowOptions)) {
|
||||
return true;
|
||||
}
|
||||
return callback(params);
|
||||
@@ -800,7 +800,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
*/
|
||||
public lineFeed(): boolean {
|
||||
this._dirtyRowService.markDirty(this._activeBuffer.y);
|
||||
if (this._optionsService.options.convertEol) {
|
||||
if (this._optionsService.rawOptions.convertEol) {
|
||||
this._activeBuffer.x = 0;
|
||||
}
|
||||
this._activeBuffer.y++;
|
||||
@@ -904,7 +904,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
}
|
||||
const originalX = this._activeBuffer.x;
|
||||
this._activeBuffer.x = this._activeBuffer.nextStop();
|
||||
if (this._optionsService.options.screenReaderMode) {
|
||||
if (this._optionsService.rawOptions.screenReaderMode) {
|
||||
this._onA11yTab.fire(this._activeBuffer.x - originalX);
|
||||
}
|
||||
return true;
|
||||
@@ -1231,6 +1231,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
private _resetBufferLine(y: number): void {
|
||||
const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;
|
||||
line.fill(this._activeBuffer.getNullCell(this._eraseAttrData()));
|
||||
this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase + y);
|
||||
line.isWrapped = false;
|
||||
}
|
||||
|
||||
@@ -1763,7 +1764,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* @param term The terminal name to evaluate
|
||||
*/
|
||||
private _is(term: string): boolean {
|
||||
return (this._optionsService.options.termName + '').indexOf(term) === 0;
|
||||
return (this._optionsService.rawOptions.termName + '').indexOf(term) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1929,7 +1930,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* This is only active if 'SetWinLines' (24) is enabled
|
||||
* through `options.windowsOptions`.
|
||||
*/
|
||||
if (this._optionsService.options.windowOptions.setWinLines) {
|
||||
if (this._optionsService.rawOptions.windowOptions.setWinLines) {
|
||||
this._bufferService.resize(132, this._bufferService.rows);
|
||||
this._onRequestReset.fire();
|
||||
}
|
||||
@@ -2163,7 +2164,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* This is only active if 'SetWinLines' (24) is enabled
|
||||
* through `options.windowsOptions`.
|
||||
*/
|
||||
if (this._optionsService.options.windowOptions.setWinLines) {
|
||||
if (this._optionsService.rawOptions.windowOptions.setWinLines) {
|
||||
this._bufferService.resize(80, this._bufferService.rows);
|
||||
this._onRequestReset.fire();
|
||||
}
|
||||
@@ -2740,7 +2741,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* Ps >= 24 not implemented
|
||||
*/
|
||||
public windowOptions(params: IParams): boolean {
|
||||
if (!paramToWindowOption(params.params[0], this._optionsService.options.windowOptions)) {
|
||||
if (!paramToWindowOption(params.params[0], this._optionsService.rawOptions.windowOptions)) {
|
||||
return true;
|
||||
}
|
||||
const second = (params.length > 1) ? params.params[1] : 0;
|
||||
|
||||
@@ -18,6 +18,7 @@ const userAgent = (isNode) ? 'node' : navigator.userAgent;
|
||||
const platform = (isNode) ? 'node' : navigator.platform;
|
||||
|
||||
export const isFirefox = userAgent.includes('Firefox');
|
||||
export const isLegacyEdge = userAgent.includes('Edge');
|
||||
export const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent);
|
||||
|
||||
// Find the users platform. We use this to interpret the meta key
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { SortedList } from 'common/SortedList';
|
||||
|
||||
const deepStrictEqual = assert.deepStrictEqual;
|
||||
|
||||
describe('SortedList', () => {
|
||||
let list: SortedList<number>;
|
||||
function assertList(expected: number[]): void {
|
||||
deepStrictEqual(Array.from(list.values()), expected);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
list = new SortedList<number>(e => e);
|
||||
});
|
||||
|
||||
describe('insert', () => {
|
||||
it('should maintain sorted values', () => {
|
||||
list.insert(10);
|
||||
assertList([10]);
|
||||
list.insert(8);
|
||||
assertList([8, 10]);
|
||||
list.insert(15);
|
||||
assertList([8, 10, 15]);
|
||||
list.insert(2);
|
||||
assertList([2, 8, 10, 15]);
|
||||
list.insert(1);
|
||||
assertList([1, 2, 8, 10, 15]);
|
||||
list.insert(6);
|
||||
assertList([1, 2, 6, 8, 10, 15]);
|
||||
});
|
||||
it('should allow duplicates of the same key', () => {
|
||||
list.insert(5);
|
||||
assertList([5]);
|
||||
list.insert(5);
|
||||
assertList([5, 5]);
|
||||
list.insert(8);
|
||||
assertList([5, 5, 8]);
|
||||
list.insert(5);
|
||||
assertList([5, 5, 5, 8]);
|
||||
list.insert(8);
|
||||
assertList([5, 5, 5, 8, 8]);
|
||||
list.insert(6);
|
||||
assertList([5, 5, 5, 6, 8, 8]);
|
||||
});
|
||||
});
|
||||
it('delete', () => {
|
||||
list.insert(1);
|
||||
list.insert(2);
|
||||
list.insert(4);
|
||||
list.insert(3);
|
||||
list.insert(5);
|
||||
assertList([1, 2, 3, 4, 5]);
|
||||
list.delete(1);
|
||||
assertList([2, 3, 4, 5]);
|
||||
list.delete(3);
|
||||
assertList([2, 4, 5]);
|
||||
list.delete(4);
|
||||
assertList([2, 5]);
|
||||
list.delete(5);
|
||||
assertList([2]);
|
||||
list.delete(2);
|
||||
assertList([]);
|
||||
});
|
||||
it('getKeyIterator', () => {
|
||||
list.insert(5);
|
||||
list.insert(5);
|
||||
list.insert(8);
|
||||
list.insert(5);
|
||||
list.insert(8);
|
||||
list.insert(6);
|
||||
assertList([5, 5, 5, 6, 8, 8]);
|
||||
deepStrictEqual(Array.from(list.getKeyIterator(1)), []);
|
||||
deepStrictEqual(Array.from(list.getKeyIterator(5)), [5, 5, 5]);
|
||||
deepStrictEqual(Array.from(list.getKeyIterator(6)), [6]);
|
||||
deepStrictEqual(Array.from(list.getKeyIterator(8)), [8, 8]);
|
||||
deepStrictEqual(Array.from(list.getKeyIterator(9)), []);
|
||||
});
|
||||
it('clear', () => {
|
||||
list.insert(1);
|
||||
list.insert(2);
|
||||
list.insert(4);
|
||||
list.insert(3);
|
||||
list.insert(5);
|
||||
list.clear();
|
||||
assertList([]);
|
||||
});
|
||||
it('custom key', () => {
|
||||
const customList = new SortedList<{ key: number }>(e => e.key);
|
||||
customList.insert({ key: 5 });
|
||||
customList.insert({ key: 2 });
|
||||
customList.insert({ key: 10 });
|
||||
customList.insert({ key: 5 });
|
||||
customList.insert({ key: 6 });
|
||||
deepStrictEqual(Array.from(customList.values()), [
|
||||
{ key: 2 },
|
||||
{ key: 5 },
|
||||
{ key: 5 },
|
||||
{ key: 6 },
|
||||
{ key: 10 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* A generic list that is maintained in sorted order and allows values with duplicate keys. This
|
||||
* list is based on binary search and as such locating a key will take O(log n) amortized, this
|
||||
* includes the by key iterator.
|
||||
*/
|
||||
export class SortedList<T> {
|
||||
private readonly _array: T[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly _getKey: (value: T) => number
|
||||
) {
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._array.length = 0;
|
||||
}
|
||||
|
||||
public insert(value: T): void {
|
||||
if (this._array.length === 0) {
|
||||
this._array.push(value);
|
||||
return;
|
||||
}
|
||||
const i = this._search(this._getKey(value), 0, this._array.length - 1);
|
||||
this._array.splice(i, 0, value);
|
||||
}
|
||||
|
||||
public delete(value: T): boolean {
|
||||
if (this._array.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const key = this._getKey(value);
|
||||
let i = this._search(key, 0, this._array.length - 1);
|
||||
if (this._getKey(this._array[i]) !== key) {
|
||||
return false;
|
||||
}
|
||||
do {
|
||||
if (this._array[i] === value) {
|
||||
this._array.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
} while (++i < this._array.length && this._getKey(this._array[i]) === key);
|
||||
return false;
|
||||
}
|
||||
|
||||
public *getKeyIterator(key: number): IterableIterator<T> {
|
||||
if (this._array.length === 0) {
|
||||
return;
|
||||
}
|
||||
let i = this._search(key, 0, this._array.length - 1);
|
||||
if (i < 0 || i >= this._array.length) {
|
||||
return;
|
||||
}
|
||||
if (this._getKey(this._array[i]) !== key) {
|
||||
return;
|
||||
}
|
||||
do {
|
||||
yield this._array[i];
|
||||
} while (++i < this._array.length && this._getKey(this._array[i]) === key);
|
||||
}
|
||||
|
||||
public values(): IterableIterator<T> {
|
||||
return this._array.values();
|
||||
}
|
||||
|
||||
private _search(key: number, min: number, max: number): number {
|
||||
if (max < min) {
|
||||
return min;
|
||||
}
|
||||
let mid = Math.floor((min + max) / 2);
|
||||
if (this._getKey(this._array[mid]) > key) {
|
||||
return this._search(key, min, mid - 1);
|
||||
}
|
||||
if (this._getKey(this._array[mid]) < key) {
|
||||
return this._search(key, mid + 1, max);
|
||||
}
|
||||
// Value found! Since keys can be duplicates, move the result index back to the lowest index
|
||||
// that matches the key.
|
||||
while (mid > 0 && this._getKey(this._array[mid - 1]) === key) {
|
||||
mid--;
|
||||
}
|
||||
return mid;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum } from 'common/services/Services';
|
||||
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration } from 'common/services/Services';
|
||||
import { IEvent, EventEmitter } from 'common/EventEmitter';
|
||||
import { clone } from 'common/Clone';
|
||||
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
|
||||
@@ -11,6 +11,7 @@ import { IBufferSet, IBuffer } from 'common/buffer/Types';
|
||||
import { BufferSet } from 'common/buffer/BufferSet';
|
||||
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes, IAttributeData } from 'common/Types';
|
||||
import { UnicodeV6 } from 'common/input/UnicodeV6';
|
||||
import { IDecorationOptions, IDecoration } from 'xterm';
|
||||
|
||||
export class MockBufferService implements IBufferService {
|
||||
public serviceBrand: any;
|
||||
@@ -120,12 +121,13 @@ export class MockLogService implements ILogService {
|
||||
|
||||
export class MockOptionsService implements IOptionsService {
|
||||
public serviceBrand: any;
|
||||
public options: ITerminalOptions = clone(DEFAULT_OPTIONS);
|
||||
public readonly rawOptions: ITerminalOptions = clone(DEFAULT_OPTIONS);
|
||||
public options: ITerminalOptions = this.rawOptions;
|
||||
public onOptionChange: IEvent<string> = new EventEmitter<string>().event;
|
||||
constructor(testOptions?: Partial<ITerminalOptions>) {
|
||||
if (testOptions) {
|
||||
for (const key of Object.keys(testOptions)) {
|
||||
this.options[key] = testOptions[key];
|
||||
this.rawOptions[key] = testOptions[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,3 +159,15 @@ export class MockUnicodeService implements IUnicodeService {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
export class MockDecorationService implements IDecorationService {
|
||||
public serviceBrand: any;
|
||||
public get decorations(): IterableIterator<IInternalDecoration> { return [].values(); }
|
||||
public onDecorationRegistered = new EventEmitter<IInternalDecoration>().event;
|
||||
public onDecorationRemoved = new EventEmitter<IInternalDecoration>().event;
|
||||
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { return undefined; }
|
||||
public reset(): void { }
|
||||
public *getDecorationsAtLine(line: number): IterableIterator<IInternalDecoration> { }
|
||||
public *getDecorationsAtCell(x: number, line: number): IterableIterator<IInternalDecoration> { }
|
||||
public dispose(): void { }
|
||||
}
|
||||
|
||||
Vendored
+6
@@ -50,6 +50,7 @@ export interface IKeyboardEvent {
|
||||
keyCode: number;
|
||||
key: string;
|
||||
type: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface IScrollEvent {
|
||||
@@ -102,6 +103,11 @@ export interface ICharset {
|
||||
}
|
||||
|
||||
export type CharData = [number, string, number, number];
|
||||
|
||||
export interface IColor {
|
||||
css: string;
|
||||
rgba: number; // 32-bit int with rgba in each byte
|
||||
}
|
||||
export type IColorRGB = [number, number, number];
|
||||
|
||||
export interface IExtendedAttrs {
|
||||
|
||||
@@ -43,6 +43,7 @@ export class Buffer implements IBuffer {
|
||||
private _whitespaceCell: ICellData = CellData.fromCharData([0, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE]);
|
||||
private _cols: number;
|
||||
private _rows: number;
|
||||
private _isClearing: boolean = false;
|
||||
|
||||
constructor(
|
||||
private _hasScrollback: boolean,
|
||||
@@ -107,7 +108,7 @@ export class Buffer implements IBuffer {
|
||||
return rows;
|
||||
}
|
||||
|
||||
const correctBufferLength = rows + this._optionsService.options.scrollback;
|
||||
const correctBufferLength = rows + this._optionsService.rawOptions.scrollback;
|
||||
|
||||
return correctBufferLength > MAX_BUFFER_SIZE ? MAX_BUFFER_SIZE : correctBufferLength;
|
||||
}
|
||||
@@ -172,7 +173,7 @@ export class Buffer implements IBuffer {
|
||||
if (this._rows < newRows) {
|
||||
for (let y = this._rows; y < newRows; y++) {
|
||||
if (this.lines.length < newRows + this.ybase) {
|
||||
if (this._optionsService.options.windowsMode) {
|
||||
if (this._optionsService.rawOptions.windowsMode) {
|
||||
// Just add the new missing rows on Windows as conpty reprints the screen with it's
|
||||
// view of the world. Once a line enters scrollback for conpty it remains there
|
||||
this.lines.push(new BufferLine(newCols, nullCell));
|
||||
@@ -252,7 +253,7 @@ export class Buffer implements IBuffer {
|
||||
}
|
||||
|
||||
private get _isReflowEnabled(): boolean {
|
||||
return this._hasScrollback && !this._optionsService.options.windowsMode;
|
||||
return this._hasScrollback && !this._optionsService.rawOptions.windowsMode;
|
||||
}
|
||||
|
||||
private _reflow(newCols: number, newRows: number): void {
|
||||
@@ -367,6 +368,11 @@ export class Buffer implements IBuffer {
|
||||
let srcCol = lastLineLength;
|
||||
while (srcLineIndex >= 0) {
|
||||
const cellsToCopy = Math.min(srcCol, destCol);
|
||||
if (wrappedLines[destLineIndex] === undefined) {
|
||||
// Sanity check that the line exists, this has been known to fail for an unknown reason
|
||||
// which would stop the reflow from happening if an exception would throw.
|
||||
break;
|
||||
}
|
||||
wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy, true);
|
||||
destCol -= cellsToCopy;
|
||||
if (destCol === 0) {
|
||||
@@ -550,7 +556,7 @@ export class Buffer implements IBuffer {
|
||||
i = 0;
|
||||
}
|
||||
|
||||
for (; i < this._cols; i += this._optionsService.options.tabStopWidth) {
|
||||
for (; i < this._cols; i += this._optionsService.rawOptions.tabStopWidth) {
|
||||
this.tabs[i] = true;
|
||||
}
|
||||
}
|
||||
@@ -579,6 +585,33 @@ export class Buffer implements IBuffer {
|
||||
return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears markers on single line.
|
||||
* @param y The line to clear.
|
||||
*/
|
||||
public clearMarkers(y: number): void {
|
||||
this._isClearing = true;
|
||||
for (let i = 0; i < this.markers.length; i++) {
|
||||
if (this.markers[i].line === y) {
|
||||
this.markers[i].dispose();
|
||||
this.markers.splice(i--, 1);
|
||||
}
|
||||
}
|
||||
this._isClearing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears markers on all lines
|
||||
*/
|
||||
public clearAllMarkers(): void {
|
||||
this._isClearing = true;
|
||||
for (let i = 0; i < this.markers.length; i++) {
|
||||
this.markers[i].dispose();
|
||||
this.markers.splice(i--, 1);
|
||||
}
|
||||
this._isClearing = false;
|
||||
}
|
||||
|
||||
public addMarker(y: number): Marker {
|
||||
const marker = new Marker(y);
|
||||
this.markers.push(marker);
|
||||
@@ -610,7 +643,9 @@ export class Buffer implements IBuffer {
|
||||
}
|
||||
|
||||
private _removeMarker(marker: Marker): void {
|
||||
this.markers.splice(this.markers.indexOf(marker), 1);
|
||||
if (!this._isClearing) {
|
||||
this.markers.splice(this.markers.indexOf(marker), 1);
|
||||
}
|
||||
}
|
||||
|
||||
public iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator {
|
||||
@@ -633,7 +668,7 @@ export class Buffer implements IBuffer {
|
||||
export class BufferStringIterator implements IBufferStringIterator {
|
||||
private _current: number;
|
||||
|
||||
constructor (
|
||||
constructor(
|
||||
private _buffer: IBuffer,
|
||||
private _trimRight: boolean,
|
||||
private _startIndex: number = 0,
|
||||
|
||||
Vendored
+3
-1
@@ -10,7 +10,7 @@ import { IEvent } from 'common/EventEmitter';
|
||||
export type BufferIndex = [number, number];
|
||||
|
||||
export interface IBufferStringIteratorResult {
|
||||
range: {first: number, last: number};
|
||||
range: { first: number, last: number };
|
||||
content: string;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@ export interface IBuffer {
|
||||
getNullCell(attr?: IAttributeData): ICellData;
|
||||
getWhitespaceCell(attr?: IAttributeData): ICellData;
|
||||
addMarker(y: number): IMarker;
|
||||
clearMarkers(y: number): void;
|
||||
clearAllMarkers(): void;
|
||||
}
|
||||
|
||||
export interface IBufferSet extends IDisposable {
|
||||
|
||||
@@ -148,3 +148,6 @@ export namespace C1 {
|
||||
/** Application Program Command */
|
||||
export const APC = '\x9f';
|
||||
}
|
||||
export namespace C1_ESCAPED {
|
||||
export const ST = `${C0.ESC}\\`;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user