Compare commits
736 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e27fafb824 | ||
|
|
485554cd0c | ||
|
|
f3239fe1d5 | ||
|
|
dd8e5ee543 | ||
|
|
5de5e4b6f2 | ||
|
|
b7561c6cef | ||
|
|
ea69dcd42a | ||
|
|
ce51196eab | ||
|
|
e4c0fc6ad5 | ||
|
|
b52db22544 | ||
|
|
f934370e7f | ||
|
|
be24e58926 | ||
|
|
e538beb920 | ||
|
|
a64ba8b687 | ||
|
|
e7f1398f3a | ||
|
|
b0033bb6d4 | ||
|
|
ecba761e18 | ||
|
|
deb679b8f5 | ||
|
|
9c1f58ee89 | ||
|
|
adeb7e6864 | ||
|
|
7913a1ea22 | ||
|
|
3e1c559b2d | ||
|
|
950b06674f | ||
|
|
f2cef0b795 | ||
|
|
373fe6fadf | ||
|
|
055d48cfb2 | ||
|
|
2660d37ad8 | ||
|
|
e165f1e16c | ||
|
|
aee3bb98f2 | ||
|
|
8d7a57a01e | ||
|
|
d74658fdb5 | ||
|
|
06ba1c64cf | ||
|
|
5e64f1aca8 | ||
|
|
5f2ac61401 | ||
|
|
d6ed2ba642 | ||
|
|
ef596c64f8 | ||
|
|
08c3fddc65 | ||
|
|
bb3fc8efd7 | ||
|
|
9422e27f97 | ||
|
|
63a401ac5d | ||
|
|
057dc62b90 | ||
|
|
a93502bb64 | ||
|
|
e71b989041 | ||
|
|
3a82d0d8e1 | ||
|
|
abd05cc82e | ||
|
|
1a672929e0 | ||
|
|
ed88fdcea2 | ||
|
|
6ad9ff10c1 | ||
|
|
ac0d55222f | ||
|
|
9ccf2f3f58 | ||
|
|
b5ee095da9 | ||
|
|
a9937ee8be | ||
|
|
d346b1bfd9 | ||
|
|
30225678c0 | ||
|
|
66694b4c9a | ||
|
|
8b53868f8a | ||
|
|
9e4b118214 | ||
|
|
310def2923 | ||
|
|
67991b413c | ||
|
|
6fbbbab7ba | ||
|
|
b768a3977c | ||
|
|
7daa4b5b04 | ||
|
|
a6dd9a20d4 | ||
|
|
9602bc6f8e | ||
|
|
5941f5fca0 | ||
|
|
5a889b04df | ||
|
|
89ead1c44d | ||
|
|
c16820166b | ||
|
|
58e45dd9be | ||
|
|
aa543a4b0a | ||
|
|
e70b728758 | ||
|
|
2d5e72251e | ||
|
|
d7fcb049d4 | ||
|
|
2ea78c5ade | ||
|
|
a0a558318c | ||
|
|
747d9e8784 | ||
|
|
c7eb6a6a60 | ||
|
|
7244fe9c7f | ||
|
|
8ee106e6aa | ||
|
|
4992a8a407 | ||
|
|
b44ae46559 | ||
|
|
dff812b38e | ||
|
|
9f6c53b547 | ||
|
|
b1de9a945d | ||
|
|
e8bbd370e4 | ||
|
|
8d7bb8b1a3 | ||
|
|
5c3ae8808b | ||
|
|
eb353648e6 | ||
|
|
a1597578ff | ||
|
|
0742640b39 | ||
|
|
1a53d5b7ba | ||
|
|
f933d54469 | ||
|
|
ce6f3d7f3e | ||
|
|
ea263822fa | ||
|
|
e5c520a265 | ||
|
|
794d214eee | ||
|
|
3cab32d201 | ||
|
|
cf62d26ed8 | ||
|
|
e39be35e17 | ||
|
|
56496c2585 | ||
|
|
089542c6f4 | ||
|
|
67f672d0cc | ||
|
|
94e9c7fd5b | ||
|
|
2b36ab0de7 | ||
|
|
1f762e482d | ||
|
|
b19d92e918 | ||
|
|
9bbe67f0ea | ||
|
|
7357b3ff2a | ||
|
|
10548c2038 | ||
|
|
943571af2a | ||
|
|
2dbee1d914 | ||
|
|
d7a78e14ac | ||
|
|
571d0386e2 | ||
|
|
1875a0e349 | ||
|
|
d0f7e5f075 | ||
|
|
f37f839330 | ||
|
|
7340e83059 | ||
|
|
fee7657fd7 | ||
|
|
b10f06d084 | ||
|
|
f9f75e98f8 | ||
|
|
e5faaeb2f2 | ||
|
|
5a53eeef63 | ||
|
|
85a13fa477 | ||
|
|
8728d3292d | ||
|
|
29b63ae4c6 | ||
|
|
4b22e49ce1 | ||
|
|
fe28abe8cf | ||
|
|
e56609cf0c | ||
|
|
eb65a5d29a | ||
|
|
f8c2620166 | ||
|
|
587a908225 | ||
|
|
bf044506ed | ||
|
|
870fa5f278 | ||
|
|
d383ff30ce | ||
|
|
7bc8eb4f3d | ||
|
|
984e366c32 | ||
|
|
0bcd0a3f08 | ||
|
|
d7ecbdcc1d | ||
|
|
d8b888c9cb | ||
|
|
b2f0c78924 | ||
|
|
5d45c5711d | ||
|
|
b3b56c36d0 | ||
|
|
ad1db117e6 | ||
|
|
508b9dc024 | ||
|
|
496066db59 | ||
|
|
2b31a48ef9 | ||
|
|
ed361f2d1a | ||
|
|
8dc9197324 | ||
|
|
05a6137549 | ||
|
|
a4027aacb5 | ||
|
|
7f8e76e0f1 | ||
|
|
8270e8e758 | ||
|
|
a080ae98c6 | ||
|
|
f499a1dfc2 | ||
|
|
6d6a82655a | ||
|
|
ba75007259 | ||
|
|
984378e12c | ||
|
|
7c9e4e513c | ||
|
|
137fbd0088 | ||
|
|
7f786ca8a6 | ||
|
|
89bbfb8154 | ||
|
|
6057d819b0 | ||
|
|
93a516d588 | ||
|
|
accf90e843 | ||
|
|
cbc162acf5 | ||
|
|
835af35839 | ||
|
|
d3521650d3 | ||
|
|
3040cfece1 | ||
|
|
f5d4e26799 | ||
|
|
cbd9e186b5 | ||
|
|
43db9e826b | ||
|
|
6f26fa013a | ||
|
|
13ed9dc1f1 | ||
|
|
8937d877e3 | ||
|
|
63238a2938 | ||
|
|
b949b30f24 | ||
|
|
56930972fe | ||
|
|
07a4cfeefd | ||
|
|
fe5465a265 | ||
|
|
6dd23c250b | ||
|
|
e9a750be71 | ||
|
|
9fc2ddb8da | ||
|
|
cf81f5a555 | ||
|
|
ce4142eab3 | ||
|
|
a3df597155 | ||
|
|
adeea9da66 | ||
|
|
a85e400b35 | ||
|
|
393009a05c | ||
|
|
11e3874b4a | ||
|
|
3149a4297c | ||
|
|
4f774e2bde | ||
|
|
78564dcc68 | ||
|
|
d5a17053df | ||
|
|
e3ecd87081 | ||
|
|
7b453beebc | ||
|
|
b9d1ca4341 | ||
|
|
304afc1813 | ||
|
|
dcf26acaac | ||
|
|
da460edb8b | ||
|
|
9164c5f239 | ||
|
|
c47340000d | ||
|
|
3e59c61a34 | ||
|
|
435d405d10 | ||
|
|
a86ba57983 | ||
|
|
5d8ed535be | ||
|
|
b9551ae8b1 | ||
|
|
06d2cdc20d | ||
|
|
0faf5308ac | ||
|
|
1544da887e | ||
|
|
e31205c95e | ||
|
|
275b7e8d4f | ||
|
|
2c3efdea8c | ||
|
|
a888620e5f | ||
|
|
a93f5e5fb4 | ||
|
|
3c26f67ea3 | ||
|
|
bc906fef9c | ||
|
|
699dafbbd4 | ||
|
|
8492c6e7ac | ||
|
|
13ecd16685 | ||
|
|
61b806e485 | ||
|
|
04d577e326 | ||
|
|
60f7169008 | ||
|
|
eec1748dc7 | ||
|
|
91c786a8db | ||
|
|
8534a9cc41 | ||
|
|
99317bbd62 | ||
|
|
89c0b358a7 | ||
|
|
17094ec542 | ||
|
|
5d2c4807db | ||
|
|
c6dd797f4e | ||
|
|
afec4152f4 | ||
|
|
40da3b233f | ||
|
|
1e8ef8a4c1 | ||
|
|
4053d683d9 | ||
|
|
788bb4a368 | ||
|
|
636931373e | ||
|
|
870b73aa36 | ||
|
|
a138955943 | ||
|
|
5d8d7de68d | ||
|
|
55910c0d79 | ||
|
|
466a377e1d | ||
|
|
614ee4eac7 | ||
|
|
697e641e8e | ||
|
|
323e1f7367 | ||
|
|
f4b9772ec2 | ||
|
|
29bc2db6e8 | ||
|
|
34edbc7934 | ||
|
|
0a37d40fad | ||
|
|
ab5db0bc1e | ||
|
|
e4f18947de | ||
|
|
9e8ef31452 | ||
|
|
ca0d7e5e1f | ||
|
|
cd65031cda | ||
|
|
c41b958829 | ||
|
|
88d663a253 | ||
|
|
f0fe346e15 | ||
|
|
6685d5aa7d | ||
|
|
7d2b74a93b | ||
|
|
5f819b6edc | ||
|
|
c9cbc2fe1e | ||
|
|
a2ee38f37b | ||
|
|
3914d1d072 | ||
|
|
63f171200e | ||
|
|
528d64d3cc | ||
|
|
fb492a9fb8 | ||
|
|
ae147a379d | ||
|
|
31eeffa5a7 | ||
|
|
9cd4e5ba04 | ||
|
|
6444fcd442 | ||
|
|
db33e4935a | ||
|
|
a293e9c0c5 | ||
|
|
38df091b06 | ||
|
|
dcd05ef96b | ||
|
|
80f3173fbd | ||
|
|
0fc2db6d6e | ||
|
|
7660159164 | ||
|
|
de679cae78 | ||
|
|
abf96e6ad6 | ||
|
|
64e2f6d506 | ||
|
|
ec39c9d335 | ||
|
|
3e2f684545 | ||
|
|
4c22774694 | ||
|
|
f898dc6dae | ||
|
|
e8570b5c26 | ||
|
|
f8ef605cbd | ||
|
|
f4115ddc3c | ||
|
|
368b4447ff | ||
|
|
2930ea8fb0 | ||
|
|
4bea16eb31 | ||
|
|
cec0c5912c | ||
|
|
80abd84050 | ||
|
|
1bdaeda43e | ||
|
|
4ab307f0a1 | ||
|
|
5118f27a90 | ||
|
|
bcdb4ffd88 | ||
|
|
7bbaa1d930 | ||
|
|
ae0fa75abe | ||
|
|
59121a238a | ||
|
|
437145afbe | ||
|
|
fbba417f09 | ||
|
|
95137ecb2a | ||
|
|
e23965e7c9 | ||
|
|
9cbb680fb2 | ||
|
|
7bcce23dc9 | ||
|
|
6c5b27af1d | ||
|
|
e1a2897d53 | ||
|
|
ad05c0cc7a | ||
|
|
60e2c6bc52 | ||
|
|
06e241117c | ||
|
|
e38c1814d5 | ||
|
|
4ed96bb5a6 | ||
|
|
bf9daf1529 | ||
|
|
358a6ff66c | ||
|
|
08e9f3e1e3 | ||
|
|
523cbe781b | ||
|
|
119d44caf7 | ||
|
|
2d1ff8f606 | ||
|
|
1b67f19edc | ||
|
|
920daa8a8f | ||
|
|
163ce95171 | ||
|
|
174b37cdf0 | ||
|
|
04ffca95c6 | ||
|
|
9e15c57f91 | ||
|
|
4efdc53d9f | ||
|
|
0b1c27956b | ||
|
|
fe571f1d70 | ||
|
|
6ab795c629 | ||
|
|
52b8e3d1a2 | ||
|
|
418a9a3d66 | ||
|
|
85674ba506 | ||
|
|
6645e2820c | ||
|
|
c984b39aaa | ||
|
|
2adf11e204 | ||
|
|
cdbcbdfe6d | ||
|
|
44cd0be068 | ||
|
|
1e7184ea07 | ||
|
|
4dd0752e80 | ||
|
|
0639c8331c | ||
|
|
49d1c9d1ba | ||
|
|
f5c775fcd1 | ||
|
|
8432daef6a | ||
|
|
f35c419f43 | ||
|
|
77defe6e28 | ||
|
|
c8b43e3078 | ||
|
|
6caf016df9 | ||
|
|
75dd37d873 | ||
|
|
ceff57d02f | ||
|
|
a758bd4f8d | ||
|
|
5b31c1ba4e | ||
|
|
7524974f19 | ||
|
|
da09247e5e | ||
|
|
9c74deb9ec | ||
|
|
d9da8effd4 | ||
|
|
c8d5e19492 | ||
|
|
cb97b7cd1d | ||
|
|
eeba0993aa | ||
|
|
5e516f59c0 | ||
|
|
1ed1ec21dd | ||
|
|
e9c385e7a6 | ||
|
|
91a7bbbba2 | ||
|
|
65711b2256 | ||
|
|
67686dd1c2 | ||
|
|
cbe136c0cb | ||
|
|
b7535dfba4 | ||
|
|
dc81b5f57a | ||
|
|
b4ebe179f9 | ||
|
|
dd38eb1264 | ||
|
|
ec54010e3c | ||
|
|
98f726974e | ||
|
|
4ee404a0af | ||
|
|
87d16c271e | ||
|
|
daedf179b2 | ||
|
|
a7634ccd5f | ||
|
|
5f8e406c18 | ||
|
|
a88cff4fa0 | ||
|
|
6a44a7448e | ||
|
|
fa379885f1 | ||
|
|
bd6e972d0f | ||
|
|
6d9bf802e2 | ||
|
|
ad33111a22 | ||
|
|
39cc0cac93 | ||
|
|
102926d171 | ||
|
|
09c0c3a0e7 | ||
|
|
416033a01c | ||
|
|
02f42f2877 | ||
|
|
88e3d87098 | ||
|
|
4578938ea1 | ||
|
|
a02a29944c | ||
|
|
6965117dd8 | ||
|
|
cff610e1ec | ||
|
|
42eba7268d | ||
|
|
e37908cf3b | ||
|
|
8354d1520d | ||
|
|
45d6f5ab04 | ||
|
|
8f90d42723 | ||
|
|
703e8e626d | ||
|
|
b1ed9c88a4 | ||
|
|
026c3476db | ||
|
|
a13e2518b8 | ||
|
|
45d1690f6e | ||
|
|
0be897d5ac | ||
|
|
811696670a | ||
|
|
3426d46b69 | ||
|
|
0e93bc41dd | ||
|
|
bd573e0651 | ||
|
|
5ae46709b0 | ||
|
|
ee693a8d2b | ||
|
|
512a10b037 | ||
|
|
0c714210ff | ||
|
|
e668ff8bcd | ||
|
|
853b636435 | ||
|
|
733e0cb21b | ||
|
|
3b536f153f | ||
|
|
47c467dafc | ||
|
|
b841b3eb79 | ||
|
|
faba276fdc | ||
|
|
2463077b2d | ||
|
|
924e1578ea | ||
|
|
36546463e6 | ||
|
|
1445ce10b5 | ||
|
|
748b1ba602 | ||
|
|
d3f28166cb | ||
|
|
eacd2a45bb | ||
|
|
df1804b215 | ||
|
|
0ed488d93b | ||
|
|
fcbd7f9a5a | ||
|
|
2449834868 | ||
|
|
cb942a0e2f | ||
|
|
a1412166f0 | ||
|
|
1a91aa8194 | ||
|
|
5ec003530f | ||
|
|
4cc1556ca4 | ||
|
|
29b616f4cc | ||
|
|
88e0fe6f88 | ||
|
|
7537c3b6d4 | ||
|
|
1803bd77ef | ||
|
|
9d7039ed51 | ||
|
|
2c17ae9aa6 | ||
|
|
b9edde7b26 | ||
|
|
cc78ae14d4 | ||
|
|
93de2bcfed | ||
|
|
e0998dbfda | ||
|
|
815cc7ee91 | ||
|
|
fbc307cd5e | ||
|
|
a5039cad65 | ||
|
|
6ce76ca13e | ||
|
|
4bd43e67ef | ||
|
|
b307a7e91d | ||
|
|
9930e92412 | ||
|
|
21aba54dc3 | ||
|
|
d78d5712be | ||
|
|
c8ad5b68e0 | ||
|
|
cd2c3c3606 | ||
|
|
9f29eeda03 | ||
|
|
f453928b44 | ||
|
|
74cdd32c58 | ||
|
|
f8cf534812 | ||
|
|
ad26362a82 | ||
|
|
fc2ae42f4b | ||
|
|
d249618ee6 | ||
|
|
09a53a0c64 | ||
|
|
2f78d93383 | ||
|
|
2f43ef67fd | ||
|
|
f42fd8e1bb | ||
|
|
861893b7b6 | ||
|
|
10b3fae2c3 | ||
|
|
bf7acb5f34 | ||
|
|
543ebb7e4e | ||
|
|
0d8c68ae1d | ||
|
|
c47855424f | ||
|
|
f7532c785e | ||
|
|
a07fe3aa58 | ||
|
|
1e49b56626 | ||
|
|
8c0541b455 | ||
|
|
0854976691 | ||
|
|
53a7da9d3f | ||
|
|
cea8107242 | ||
|
|
a743c2d8d7 | ||
|
|
afdac15572 | ||
|
|
e88d3bb97e | ||
|
|
fb17d1ed3f | ||
|
|
2cf44d30b7 | ||
|
|
03bd6d6c33 | ||
|
|
9bb195e177 | ||
|
|
a7186c643f | ||
|
|
3a9b69077e | ||
|
|
d19d3bbe45 | ||
|
|
2b9db911c7 | ||
|
|
e0bf5337ca | ||
|
|
a6e530511d | ||
|
|
294769be35 | ||
|
|
bfecdb7bc0 | ||
|
|
73afb29b04 | ||
|
|
22172be2c0 | ||
|
|
9e651ee127 | ||
|
|
d969f38850 | ||
|
|
f0db748ba1 | ||
|
|
2e2bce7322 | ||
|
|
091ed9ab47 | ||
|
|
63089badf1 | ||
|
|
7a79df7a24 | ||
|
|
bcf38e6bb5 | ||
|
|
a0287920e5 | ||
|
|
3269b9925f | ||
|
|
a0ea5b38a0 | ||
|
|
005a7076af | ||
|
|
e1d4bcf013 | ||
|
|
6b7ee10287 | ||
|
|
6df266348e | ||
|
|
4002be882f | ||
|
|
23fbeaf978 | ||
|
|
66e27b7420 | ||
|
|
ce71ed3959 | ||
|
|
843972ceca | ||
|
|
68223bdb67 | ||
|
|
2f39dee28b | ||
|
|
612b4404a9 | ||
|
|
cfe6103daf | ||
|
|
ca4086b844 | ||
|
|
c13a26ff7b | ||
|
|
cfaab6cfb6 | ||
|
|
b621c9b857 | ||
|
|
7474813a17 | ||
|
|
b25c3eb740 | ||
|
|
447f710570 | ||
|
|
6f5ca6064b | ||
|
|
c844fcdc09 | ||
|
|
b0afc80678 | ||
|
|
a023950f28 | ||
|
|
8e74cc178e | ||
|
|
61d8848b31 | ||
|
|
dfbfa86548 | ||
|
|
2664dad2bc | ||
|
|
8d5e3fb159 | ||
|
|
8d1a4a6a24 | ||
|
|
c04151f999 | ||
|
|
0b63d882ce | ||
|
|
6aa346dec8 | ||
|
|
bef09696f6 | ||
|
|
1a8b23e118 | ||
|
|
f39942863b | ||
|
|
5094380c83 | ||
|
|
643545e91e | ||
|
|
0e51365770 | ||
|
|
401b59be5c | ||
|
|
0a6293bcda | ||
|
|
0f1eb3dd2e | ||
|
|
856768a43c | ||
|
|
08e0444ee4 | ||
|
|
b80887dabe | ||
|
|
572e571927 | ||
|
|
5a9dea5299 | ||
|
|
9ba24794c7 | ||
|
|
84d257470a | ||
|
|
4967a8d5ef | ||
|
|
b10c82c015 | ||
|
|
213aa36e1c | ||
|
|
c5956a0363 | ||
|
|
8230dd9a3b | ||
|
|
cb18131432 | ||
|
|
707ffe8ff3 | ||
|
|
00b5cc472e | ||
|
|
1c3bf90a8a | ||
|
|
e60500dd7c | ||
|
|
88d0c04444 | ||
|
|
198f6694b7 | ||
|
|
d9283efbe6 | ||
|
|
18354c5e04 | ||
|
|
52a4c15c14 | ||
|
|
7dd9b9539e | ||
|
|
092689ed56 | ||
|
|
880b3f087f | ||
|
|
d25ec39a23 | ||
|
|
712616d167 | ||
|
|
1cc7615d06 | ||
|
|
76ee44748e | ||
|
|
7d1ba6455b | ||
|
|
7b12c1c9e0 | ||
|
|
862b988d56 | ||
|
|
2cb8b0fcd3 | ||
|
|
3bd4542bce | ||
|
|
213b94afd4 | ||
|
|
8b1b35913a | ||
|
|
0a704b8d67 | ||
|
|
b4bc7906d2 | ||
|
|
d2f4d37af8 | ||
|
|
3498e92d1c | ||
|
|
763ab4d5f1 | ||
|
|
53872a6024 | ||
|
|
314c97715d | ||
|
|
131979dff0 | ||
|
|
34f85b5690 | ||
|
|
cebab56c94 | ||
|
|
296944e34d | ||
|
|
3154ccbafe | ||
|
|
e644c0876e | ||
|
|
5832153712 | ||
|
|
b6e6dafca7 | ||
|
|
d6bc05cad0 | ||
|
|
c9cbeafc05 | ||
|
|
364fab7b5f | ||
|
|
c278503166 | ||
|
|
2e61a586b6 | ||
|
|
e605a5ead2 | ||
|
|
6f97a9be3b | ||
|
|
227c612dac | ||
|
|
c8e47a8c63 | ||
|
|
d721c2ba4b | ||
|
|
3f11b8af56 | ||
|
|
4e32fabfdc | ||
|
|
fe786f3366 | ||
|
|
b9c459e800 | ||
|
|
b2aab0c773 | ||
|
|
f49c9db423 | ||
|
|
6e882bcd02 | ||
|
|
068aa1adb3 | ||
|
|
47ad9baebc | ||
|
|
84d789b8ac | ||
|
|
0159019850 | ||
|
|
1f2eb9ddbc | ||
|
|
d75f415b25 | ||
|
|
4fecab6d4b | ||
|
|
e0897cd019 | ||
|
|
a939535d95 | ||
|
|
59bbe43a46 | ||
|
|
b2caf9e905 | ||
|
|
7dcf30c954 | ||
|
|
118f137f18 | ||
|
|
0fff7d9166 | ||
|
|
62ec105bff | ||
|
|
c2b44537aa | ||
|
|
f33d30cb9d | ||
|
|
8b9488bacb | ||
|
|
2f4d8932dc | ||
|
|
78bbb83448 | ||
|
|
61b9179fb1 | ||
|
|
a72bdac7df | ||
|
|
0ff87e603f | ||
|
|
2d6285a6e1 | ||
|
|
44e0a00734 | ||
|
|
595dbd44ae | ||
|
|
1ec31738e6 | ||
|
|
baf636a4a4 | ||
|
|
9384823e47 | ||
|
|
8b5089c759 | ||
|
|
941d935c4a | ||
|
|
c07d794249 | ||
|
|
9dc3c74260 | ||
|
|
a26b066788 | ||
|
|
306ebb256c | ||
|
|
258b89bb70 | ||
|
|
20a77f4c5e | ||
|
|
9a7ecfbc4f | ||
|
|
8d3f42de52 | ||
|
|
a66b81d60a | ||
|
|
89392cd23d | ||
|
|
1995bd89a6 | ||
|
|
2c57703ad6 | ||
|
|
882c8ce696 | ||
|
|
f5aa07aac9 | ||
|
|
61e06487b7 | ||
|
|
f0353d6aba | ||
|
|
0e62ddbb65 | ||
|
|
40c861c249 | ||
|
|
78d97a3db2 | ||
|
|
1aee7bdb1d | ||
|
|
b8994c2a89 | ||
|
|
6e5ec2a00d | ||
|
|
2919cbe9cb | ||
|
|
f59be5fecf | ||
|
|
3228a55329 | ||
|
|
b571eae4f3 | ||
|
|
6212ebad9b | ||
|
|
9c1b01521a | ||
|
|
78c158e1a4 | ||
|
|
a82a12fd14 | ||
|
|
2cbb313467 | ||
|
|
e1556893f7 | ||
|
|
927118726c | ||
|
|
2952f2c905 | ||
|
|
acb29eb273 | ||
|
|
a1e576343e | ||
|
|
9bc08e446b | ||
|
|
f3cd710f21 | ||
|
|
efc85d1b75 | ||
|
|
9c74be3bf2 | ||
|
|
ce8741977b | ||
|
|
d12387b753 | ||
|
|
50afb2d65f | ||
|
|
ee78d6f17b | ||
|
|
7091e0c567 | ||
|
|
ac76706aa7 | ||
|
|
fcb217b9e8 | ||
|
|
9977248926 | ||
|
|
0c10d6c82d | ||
|
|
bc076c1cc1 | ||
|
|
a7a73a5b0b | ||
|
|
c539069cbb | ||
|
|
f1db618be2 | ||
|
|
79ba217485 | ||
|
|
ef4fc42d93 | ||
|
|
5bfbeb55c0 | ||
|
|
4069db4959 | ||
|
|
7d5425e142 | ||
|
|
de8218314c | ||
|
|
1a92a19954 | ||
|
|
0674e76864 | ||
|
|
60abc5f090 | ||
|
|
e8a2885721 | ||
|
|
5dc47c625e | ||
|
|
64445c7d1c | ||
|
|
50c77daa0b | ||
|
|
c3ff489fee | ||
|
|
6384950d56 | ||
|
|
b49a268031 | ||
|
|
2d6d10f920 | ||
|
|
580bad2042 | ||
|
|
9759f9e947 | ||
|
|
ab4f90a20a | ||
|
|
7105589904 | ||
|
|
59ed535cdf | ||
|
|
60a8e74430 | ||
|
|
6ba4af3e26 | ||
|
|
3ae5ba09fd | ||
|
|
401bdf0ba1 | ||
|
|
087ff28d0d | ||
|
|
715faaaceb | ||
|
|
2c6aeaed7c | ||
|
|
559774d6ac | ||
|
|
282195b13e | ||
|
|
eb9d7c8660 | ||
|
|
eea0f35d38 | ||
|
|
37eae2ba67 | ||
|
|
81a85e9c79 | ||
|
|
cdb268e656 | ||
|
|
30e2e2014d |
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -32,6 +32,11 @@ jobs:
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Download rust-analyzer
|
||||
run: |
|
||||
script/download-rust-analyzer
|
||||
echo "$PWD/vendor/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --workspace --no-fail-fast
|
||||
|
||||
@@ -63,6 +68,9 @@ jobs:
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Download rust-analyzer
|
||||
run: script/download-rust-analyzer
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle
|
||||
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -2,5 +2,6 @@
|
||||
/zed.xcworkspace
|
||||
.DS_Store
|
||||
/script/node_modules
|
||||
/server/.env.toml
|
||||
/server/static/styles.css
|
||||
/crates/server/.env.toml
|
||||
/crates/server/static/styles.css
|
||||
/vendor/bin
|
||||
|
||||
1222
Cargo.lock
generated
1222
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,6 @@ default-members = ["crates/zed"]
|
||||
|
||||
[patch.crates-io]
|
||||
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "d72771a19f4143530b1cfd23808e344f1276e176" }
|
||||
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457
|
||||
cocoa = { git = "https://github.com/servo/core-foundation-rs", rev = "025dcb3c0d1ef01530f57ef65f3b1deb948f5737" }
|
||||
cocoa-foundation = { git = "https://github.com/servo/core-foundation-rs", rev = "025dcb3c0d1ef01530f57ef65f3b1deb948f5737" }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.55-bullseye as builder
|
||||
FROM rust:1.56-bullseye as builder
|
||||
WORKDIR app
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash -
|
||||
RUN apt-get install -y nodejs
|
||||
|
||||
2
Procfile
Normal file
2
Procfile
Normal file
@@ -0,0 +1,2 @@
|
||||
web: cd ../zed.dev && PORT=3000 npx next dev
|
||||
collab: cd crates/server && cargo run
|
||||
44
README.md
44
README.md
@@ -6,9 +6,41 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
|
||||
|
||||
## Development tips
|
||||
|
||||
### Testing against locally-running servers
|
||||
|
||||
Make sure you have `zed.dev` cloned as a sibling to this repo.
|
||||
|
||||
```
|
||||
cd ..
|
||||
git clone https://github.com/zed-industries/zed.dev
|
||||
```
|
||||
|
||||
Make sure your local database is created, migrated, and seeded with initial data. Install [Postgres](https://postgresapp.com), then from the `zed` repository root, run:
|
||||
|
||||
```
|
||||
script/sqlx database create
|
||||
script/sqlx migrate run
|
||||
script/seed-db
|
||||
```
|
||||
|
||||
Run `zed.dev` and the collaboration server.
|
||||
|
||||
```
|
||||
brew install foreman
|
||||
foreman start
|
||||
```
|
||||
|
||||
If you want to run Zed pointed at the local servers, you can run:
|
||||
|
||||
```
|
||||
script/zed_with_local_servers
|
||||
# or...
|
||||
script/zed_with_local_servers --release
|
||||
```
|
||||
|
||||
### Dump element JSON
|
||||
|
||||
If you trigger `cmd-shift-i`, Zed will copy a JSON representation of the current window contents to the clipboard. You can paste this in a tool like [DJSON](https://chrome.google.com/webstore/detail/djson-json-viewer-formatt/chaeijjekipecdajnijdldjjipaegdjc?hl=en) to navigate the state of on-screen elements in a structured way.
|
||||
If you trigger `cmd-alt-i`, Zed will copy a JSON representation of the current window contents to the clipboard. You can paste this in a tool like [DJSON](https://chrome.google.com/webstore/detail/djson-json-viewer-formatt/chaeijjekipecdajnijdldjjipaegdjc?hl=en) to navigate the state of on-screen elements in a structured way.
|
||||
|
||||
## Roadmap
|
||||
|
||||
@@ -26,12 +58,12 @@ Establish basic infrastructure for building the app bundle and uploading an arti
|
||||
|
||||
[Tracking issue](https://github.com/zed-industries/zed/issues/6)
|
||||
|
||||
Turn the minimal text editor into a collaborative *code* editor. This will include the minimal features that the Zed team needs to collaborate in Zed to build Zed without net loss in developer productivity. This includes productivity-critical features such as:
|
||||
Turn the minimal text editor into a collaborative _code_ editor. This will include the minimal features that the Zed team needs to collaborate in Zed to build Zed without net loss in developer productivity. This includes productivity-critical features such as:
|
||||
|
||||
* Syntax highlighting and syntax-aware editing and navigation
|
||||
* The ability to see and edit non-local working copies of a repository
|
||||
* Language server support for Rust code navigation, refactoring, diagnostics, etc.
|
||||
* Project browsing and project-wide search and replace
|
||||
- Syntax highlighting and syntax-aware editing and navigation
|
||||
- The ability to see and edit non-local working copies of a repository
|
||||
- Language server support for Rust code navigation, refactoring, diagnostics, etc.
|
||||
- Project browsing and project-wide search and replace
|
||||
|
||||
We want to tackle collaboration fairly early so that the rest of the design of the product can flow around that assumption. We could probably produce a single-player code editor more quickly, but at the risk of having collaboration feel more "bolted on" when we eventually add it.
|
||||
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
use crate::Point;
|
||||
|
||||
use super::{Buffer, Content};
|
||||
use anyhow::Result;
|
||||
use std::{cmp::Ordering, ops::Range};
|
||||
use sum_tree::Bias;
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
|
||||
pub struct Anchor {
|
||||
pub offset: usize,
|
||||
pub bias: Bias,
|
||||
pub version: clock::Global,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AnchorMap<T> {
|
||||
pub(crate) version: clock::Global,
|
||||
pub(crate) entries: Vec<((usize, Bias), T)>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AnchorSet(pub(crate) AnchorMap<()>);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AnchorRangeMap<T> {
|
||||
pub(crate) version: clock::Global,
|
||||
pub(crate) entries: Vec<(Range<(usize, Bias)>, T)>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AnchorRangeSet(pub(crate) AnchorRangeMap<()>);
|
||||
|
||||
impl Anchor {
|
||||
pub fn min() -> Self {
|
||||
Self {
|
||||
offset: 0,
|
||||
bias: Bias::Left,
|
||||
version: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max() -> Self {
|
||||
Self {
|
||||
offset: usize::MAX,
|
||||
bias: Bias::Right,
|
||||
version: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cmp<'a>(&self, other: &Anchor, buffer: impl Into<Content<'a>>) -> Result<Ordering> {
|
||||
let buffer = buffer.into();
|
||||
|
||||
if self == other {
|
||||
return Ok(Ordering::Equal);
|
||||
}
|
||||
|
||||
let offset_comparison = if self.version == other.version {
|
||||
self.offset.cmp(&other.offset)
|
||||
} else {
|
||||
buffer
|
||||
.full_offset_for_anchor(self)
|
||||
.cmp(&buffer.full_offset_for_anchor(other))
|
||||
};
|
||||
|
||||
Ok(offset_comparison.then_with(|| self.bias.cmp(&other.bias)))
|
||||
}
|
||||
|
||||
pub fn bias_left(&self, buffer: &Buffer) -> Anchor {
|
||||
if self.bias == Bias::Left {
|
||||
self.clone()
|
||||
} else {
|
||||
buffer.anchor_before(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bias_right(&self, buffer: &Buffer) -> Anchor {
|
||||
if self.bias == Bias::Right {
|
||||
self.clone()
|
||||
} else {
|
||||
buffer.anchor_after(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AnchorMap<T> {
|
||||
pub fn to_points<'a>(
|
||||
&'a self,
|
||||
content: impl Into<Content<'a>> + 'a,
|
||||
) -> impl Iterator<Item = (Point, &'a T)> + 'a {
|
||||
let content = content.into();
|
||||
content
|
||||
.summaries_for_anchors(self)
|
||||
.map(move |(sum, value)| (sum.lines, value))
|
||||
}
|
||||
|
||||
pub fn version(&self) -> &clock::Global {
|
||||
&self.version
|
||||
}
|
||||
}
|
||||
|
||||
impl AnchorSet {
|
||||
pub fn to_points<'a>(
|
||||
&'a self,
|
||||
content: impl Into<Content<'a>> + 'a,
|
||||
) -> impl Iterator<Item = Point> + 'a {
|
||||
self.0.to_points(content).map(move |(point, _)| point)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AnchorRangeMap<T> {
|
||||
pub fn to_point_ranges<'a>(
|
||||
&'a self,
|
||||
content: impl Into<Content<'a>> + 'a,
|
||||
) -> impl Iterator<Item = (Range<Point>, &'a T)> + 'a {
|
||||
let content = content.into();
|
||||
content
|
||||
.summaries_for_anchor_ranges(self)
|
||||
.map(move |(range, value)| ((range.start.lines..range.end.lines), value))
|
||||
}
|
||||
|
||||
pub fn version(&self) -> &clock::Global {
|
||||
&self.version
|
||||
}
|
||||
}
|
||||
|
||||
impl AnchorRangeSet {
|
||||
pub fn to_point_ranges<'a>(
|
||||
&'a self,
|
||||
content: impl Into<Content<'a>> + 'a,
|
||||
) -> impl Iterator<Item = Range<Point>> + 'a {
|
||||
self.0.to_point_ranges(content).map(|(range, _)| range)
|
||||
}
|
||||
|
||||
pub fn version(&self) -> &clock::Global {
|
||||
self.0.version()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AnchorRangeExt {
|
||||
fn cmp<'a>(&self, b: &Range<Anchor>, buffer: impl Into<Content<'a>>) -> Result<Ordering>;
|
||||
}
|
||||
|
||||
impl AnchorRangeExt for Range<Anchor> {
|
||||
fn cmp<'a>(&self, other: &Range<Anchor>, buffer: impl Into<Content<'a>>) -> Result<Ordering> {
|
||||
let buffer = buffer.into();
|
||||
Ok(match self.start.cmp(&other.start, &buffer)? {
|
||||
Ordering::Equal => other.end.cmp(&self.end, buffer)?,
|
||||
ord @ _ => ord,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
use crate::HighlightMap;
|
||||
use anyhow::Result;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use std::{path::Path, str, sync::Arc};
|
||||
use theme::SyntaxTheme;
|
||||
use tree_sitter::{Language as Grammar, Query};
|
||||
pub use tree_sitter::{Parser, Tree};
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
pub struct LanguageConfig {
|
||||
pub name: String,
|
||||
pub path_suffixes: Vec<String>,
|
||||
pub brackets: Vec<BracketPair>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct BracketPair {
|
||||
pub start: String,
|
||||
pub end: String,
|
||||
pub close: bool,
|
||||
pub newline: bool,
|
||||
}
|
||||
|
||||
pub struct Language {
|
||||
pub(crate) config: LanguageConfig,
|
||||
pub(crate) grammar: Grammar,
|
||||
pub(crate) highlights_query: Query,
|
||||
pub(crate) brackets_query: Query,
|
||||
pub(crate) indents_query: Query,
|
||||
pub(crate) highlight_map: Mutex<HighlightMap>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct LanguageRegistry {
|
||||
languages: Vec<Arc<Language>>,
|
||||
}
|
||||
|
||||
impl LanguageRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn add(&mut self, language: Arc<Language>) {
|
||||
self.languages.push(language);
|
||||
}
|
||||
|
||||
pub fn set_theme(&self, theme: &SyntaxTheme) {
|
||||
for language in &self.languages {
|
||||
language.set_theme(theme);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_language(&self, path: impl AsRef<Path>) -> Option<&Arc<Language>> {
|
||||
let path = path.as_ref();
|
||||
let filename = path.file_name().and_then(|name| name.to_str());
|
||||
let extension = path.extension().and_then(|name| name.to_str());
|
||||
let path_suffixes = [extension, filename];
|
||||
self.languages.iter().find(|language| {
|
||||
language
|
||||
.config
|
||||
.path_suffixes
|
||||
.iter()
|
||||
.any(|suffix| path_suffixes.contains(&Some(suffix.as_str())))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Language {
|
||||
pub fn new(config: LanguageConfig, grammar: Grammar) -> Self {
|
||||
Self {
|
||||
config,
|
||||
brackets_query: Query::new(grammar, "").unwrap(),
|
||||
highlights_query: Query::new(grammar, "").unwrap(),
|
||||
indents_query: Query::new(grammar, "").unwrap(),
|
||||
grammar,
|
||||
highlight_map: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_highlights_query(mut self, source: &str) -> Result<Self> {
|
||||
self.highlights_query = Query::new(self.grammar, source)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn with_brackets_query(mut self, source: &str) -> Result<Self> {
|
||||
self.brackets_query = Query::new(self.grammar, source)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn with_indents_query(mut self, source: &str) -> Result<Self> {
|
||||
self.indents_query = Query::new(self.grammar, source)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
self.config.name.as_str()
|
||||
}
|
||||
|
||||
pub fn brackets(&self) -> &[BracketPair] {
|
||||
&self.config.brackets
|
||||
}
|
||||
|
||||
pub fn highlight_map(&self) -> HighlightMap {
|
||||
self.highlight_map.lock().clone()
|
||||
}
|
||||
|
||||
pub fn set_theme(&self, theme: &SyntaxTheme) {
|
||||
*self.highlight_map.lock() =
|
||||
HighlightMap::new(self.highlights_query.capture_names(), theme);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_select_language() {
|
||||
let grammar = tree_sitter_rust::language();
|
||||
let registry = LanguageRegistry {
|
||||
languages: vec![
|
||||
Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".to_string(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
grammar,
|
||||
)),
|
||||
Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Make".to_string(),
|
||||
path_suffixes: vec!["Makefile".to_string(), "mk".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
grammar,
|
||||
)),
|
||||
],
|
||||
};
|
||||
|
||||
// matching file extension
|
||||
assert_eq!(
|
||||
registry.select_language("zed/lib.rs").map(|l| l.name()),
|
||||
Some("Rust")
|
||||
);
|
||||
assert_eq!(
|
||||
registry.select_language("zed/lib.mk").map(|l| l.name()),
|
||||
Some("Make")
|
||||
);
|
||||
|
||||
// matching filename
|
||||
assert_eq!(
|
||||
registry.select_language("zed/Makefile").map(|l| l.name()),
|
||||
Some("Make")
|
||||
);
|
||||
|
||||
// matching suffix that is not the full file extension or filename
|
||||
assert_eq!(registry.select_language("zed/cars").map(|l| l.name()), None);
|
||||
assert_eq!(
|
||||
registry.select_language("zed/a.cars").map(|l| l.name()),
|
||||
None
|
||||
);
|
||||
assert_eq!(registry.select_language("zed/sumk").map(|l| l.name()), None);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,75 +0,0 @@
|
||||
use crate::{Anchor, Buffer, Point, ToOffset as _, ToPoint as _};
|
||||
use std::{cmp::Ordering, mem, ops::Range};
|
||||
|
||||
pub type SelectionSetId = clock::Lamport;
|
||||
pub type SelectionsVersion = usize;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum SelectionGoal {
|
||||
None,
|
||||
Column(u32),
|
||||
ColumnRange { start: u32, end: u32 },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Selection {
|
||||
pub id: usize,
|
||||
pub start: Anchor,
|
||||
pub end: Anchor,
|
||||
pub reversed: bool,
|
||||
pub goal: SelectionGoal,
|
||||
}
|
||||
|
||||
impl Selection {
|
||||
pub fn head(&self) -> &Anchor {
|
||||
if self.reversed {
|
||||
&self.start
|
||||
} else {
|
||||
&self.end
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_head(&mut self, buffer: &Buffer, cursor: Anchor) {
|
||||
if cursor.cmp(self.tail(), buffer).unwrap() < Ordering::Equal {
|
||||
if !self.reversed {
|
||||
mem::swap(&mut self.start, &mut self.end);
|
||||
self.reversed = true;
|
||||
}
|
||||
self.start = cursor;
|
||||
} else {
|
||||
if self.reversed {
|
||||
mem::swap(&mut self.start, &mut self.end);
|
||||
self.reversed = false;
|
||||
}
|
||||
self.end = cursor;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tail(&self) -> &Anchor {
|
||||
if self.reversed {
|
||||
&self.end
|
||||
} else {
|
||||
&self.start
|
||||
}
|
||||
}
|
||||
|
||||
pub fn point_range(&self, buffer: &Buffer) -> Range<Point> {
|
||||
let start = self.start.to_point(buffer);
|
||||
let end = self.end.to_point(buffer);
|
||||
if self.reversed {
|
||||
end..start
|
||||
} else {
|
||||
start..end
|
||||
}
|
||||
}
|
||||
|
||||
pub fn offset_range(&self, buffer: &Buffer) -> Range<usize> {
|
||||
let start = self.start.to_offset(buffer);
|
||||
let end = self.end.to_offset(buffer);
|
||||
if self.reversed {
|
||||
end..start
|
||||
} else {
|
||||
start..end
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
mod buffer;
|
||||
mod syntax;
|
||||
@@ -1,790 +0,0 @@
|
||||
use crate::*;
|
||||
use clock::ReplicaId;
|
||||
use rand::prelude::*;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
cmp::Ordering,
|
||||
env,
|
||||
iter::Iterator,
|
||||
mem,
|
||||
rc::Rc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_edit(cx: &mut gpui::MutableAppContext) {
|
||||
cx.add_model(|cx| {
|
||||
let mut buffer = Buffer::new(0, "abc", cx);
|
||||
assert_eq!(buffer.text(), "abc");
|
||||
buffer.edit(vec![3..3], "def", cx);
|
||||
assert_eq!(buffer.text(), "abcdef");
|
||||
buffer.edit(vec![0..0], "ghi", cx);
|
||||
assert_eq!(buffer.text(), "ghiabcdef");
|
||||
buffer.edit(vec![5..5], "jkl", cx);
|
||||
assert_eq!(buffer.text(), "ghiabjklcdef");
|
||||
buffer.edit(vec![6..7], "", cx);
|
||||
assert_eq!(buffer.text(), "ghiabjlcdef");
|
||||
buffer.edit(vec![4..9], "mno", cx);
|
||||
assert_eq!(buffer.text(), "ghiamnoef");
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_edit_events(cx: &mut gpui::MutableAppContext) {
|
||||
let mut now = Instant::now();
|
||||
let buffer_1_events = Rc::new(RefCell::new(Vec::new()));
|
||||
let buffer_2_events = Rc::new(RefCell::new(Vec::new()));
|
||||
|
||||
let buffer1 = cx.add_model(|cx| Buffer::new(0, "abcdef", cx));
|
||||
let buffer2 = cx.add_model(|cx| Buffer::new(1, "abcdef", cx));
|
||||
let buffer_ops = buffer1.update(cx, |buffer, cx| {
|
||||
let buffer_1_events = buffer_1_events.clone();
|
||||
cx.subscribe(&buffer1, move |_, _, event, _| {
|
||||
buffer_1_events.borrow_mut().push(event.clone())
|
||||
})
|
||||
.detach();
|
||||
let buffer_2_events = buffer_2_events.clone();
|
||||
cx.subscribe(&buffer2, move |_, _, event, _| {
|
||||
buffer_2_events.borrow_mut().push(event.clone())
|
||||
})
|
||||
.detach();
|
||||
|
||||
// An edit emits an edited event, followed by a dirtied event,
|
||||
// since the buffer was previously in a clean state.
|
||||
buffer.edit(Some(2..4), "XYZ", cx);
|
||||
|
||||
// An empty transaction does not emit any events.
|
||||
buffer.start_transaction(None).unwrap();
|
||||
buffer.end_transaction(None, cx).unwrap();
|
||||
|
||||
// A transaction containing two edits emits one edited event.
|
||||
now += Duration::from_secs(1);
|
||||
buffer.start_transaction_at(None, now).unwrap();
|
||||
buffer.edit(Some(5..5), "u", cx);
|
||||
buffer.edit(Some(6..6), "w", cx);
|
||||
buffer.end_transaction_at(None, now, cx).unwrap();
|
||||
|
||||
// Undoing a transaction emits one edited event.
|
||||
buffer.undo(cx);
|
||||
|
||||
buffer.operations.clone()
|
||||
});
|
||||
|
||||
// Incorporating a set of remote ops emits a single edited event,
|
||||
// followed by a dirtied event.
|
||||
buffer2.update(cx, |buffer, cx| {
|
||||
buffer.apply_ops(buffer_ops, cx).unwrap();
|
||||
});
|
||||
|
||||
let buffer_1_events = buffer_1_events.borrow();
|
||||
assert_eq!(
|
||||
*buffer_1_events,
|
||||
vec![Event::Edited, Event::Dirtied, Event::Edited, Event::Edited]
|
||||
);
|
||||
|
||||
let buffer_2_events = buffer_2_events.borrow();
|
||||
assert_eq!(*buffer_2_events, vec![Event::Edited, Event::Dirtied]);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_edits(cx: &mut gpui::MutableAppContext, mut rng: StdRng) {
|
||||
let operations = env::var("OPERATIONS")
|
||||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||||
.unwrap_or(10);
|
||||
|
||||
let reference_string_len = rng.gen_range(0..3);
|
||||
let mut reference_string = RandomCharIter::new(&mut rng)
|
||||
.take(reference_string_len)
|
||||
.collect::<String>();
|
||||
cx.add_model(|cx| {
|
||||
let mut buffer = Buffer::new(0, reference_string.as_str(), cx);
|
||||
buffer.history.group_interval = Duration::from_millis(rng.gen_range(0..=200));
|
||||
let mut buffer_versions = Vec::new();
|
||||
log::info!(
|
||||
"buffer text {:?}, version: {:?}",
|
||||
buffer.text(),
|
||||
buffer.version()
|
||||
);
|
||||
|
||||
for _i in 0..operations {
|
||||
let (old_ranges, new_text) = buffer.randomly_mutate(&mut rng, cx);
|
||||
for old_range in old_ranges.iter().rev() {
|
||||
reference_string.replace_range(old_range.clone(), &new_text);
|
||||
}
|
||||
assert_eq!(buffer.text(), reference_string);
|
||||
log::info!(
|
||||
"buffer text {:?}, version: {:?}",
|
||||
buffer.text(),
|
||||
buffer.version()
|
||||
);
|
||||
|
||||
if rng.gen_bool(0.25) {
|
||||
buffer.randomly_undo_redo(&mut rng, cx);
|
||||
reference_string = buffer.text();
|
||||
log::info!(
|
||||
"buffer text {:?}, version: {:?}",
|
||||
buffer.text(),
|
||||
buffer.version()
|
||||
);
|
||||
}
|
||||
|
||||
let range = buffer.random_byte_range(0, &mut rng);
|
||||
assert_eq!(
|
||||
buffer.text_summary_for_range(range.clone()),
|
||||
TextSummary::from(&reference_string[range])
|
||||
);
|
||||
|
||||
if rng.gen_bool(0.3) {
|
||||
buffer_versions.push(buffer.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for mut old_buffer in buffer_versions {
|
||||
let edits = buffer
|
||||
.edits_since(old_buffer.version.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
log::info!(
|
||||
"mutating old buffer version {:?}, text: {:?}, edits since: {:?}",
|
||||
old_buffer.version(),
|
||||
old_buffer.text(),
|
||||
edits,
|
||||
);
|
||||
|
||||
let mut delta = 0_isize;
|
||||
for edit in edits {
|
||||
let old_start = (edit.old_bytes.start as isize + delta) as usize;
|
||||
let new_text: String = buffer.text_for_range(edit.new_bytes.clone()).collect();
|
||||
old_buffer.edit(
|
||||
Some(old_start..old_start + edit.deleted_bytes()),
|
||||
new_text,
|
||||
cx,
|
||||
);
|
||||
delta += edit.delta();
|
||||
}
|
||||
assert_eq!(old_buffer.text(), buffer.text());
|
||||
}
|
||||
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_line_len(cx: &mut gpui::MutableAppContext) {
|
||||
cx.add_model(|cx| {
|
||||
let mut buffer = Buffer::new(0, "", cx);
|
||||
buffer.edit(vec![0..0], "abcd\nefg\nhij", cx);
|
||||
buffer.edit(vec![12..12], "kl\nmno", cx);
|
||||
buffer.edit(vec![18..18], "\npqrs\n", cx);
|
||||
buffer.edit(vec![18..21], "\nPQ", cx);
|
||||
|
||||
assert_eq!(buffer.line_len(0), 4);
|
||||
assert_eq!(buffer.line_len(1), 3);
|
||||
assert_eq!(buffer.line_len(2), 5);
|
||||
assert_eq!(buffer.line_len(3), 3);
|
||||
assert_eq!(buffer.line_len(4), 4);
|
||||
assert_eq!(buffer.line_len(5), 0);
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_text_summary_for_range(cx: &mut gpui::MutableAppContext) {
|
||||
cx.add_model(|cx| {
|
||||
let buffer = Buffer::new(0, "ab\nefg\nhklm\nnopqrs\ntuvwxyz", cx);
|
||||
assert_eq!(
|
||||
buffer.text_summary_for_range(1..3),
|
||||
TextSummary {
|
||||
bytes: 2,
|
||||
lines: Point::new(1, 0),
|
||||
first_line_chars: 1,
|
||||
last_line_chars: 0,
|
||||
longest_row: 0,
|
||||
longest_row_chars: 1,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.text_summary_for_range(1..12),
|
||||
TextSummary {
|
||||
bytes: 11,
|
||||
lines: Point::new(3, 0),
|
||||
first_line_chars: 1,
|
||||
last_line_chars: 0,
|
||||
longest_row: 2,
|
||||
longest_row_chars: 4,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.text_summary_for_range(0..20),
|
||||
TextSummary {
|
||||
bytes: 20,
|
||||
lines: Point::new(4, 1),
|
||||
first_line_chars: 2,
|
||||
last_line_chars: 1,
|
||||
longest_row: 3,
|
||||
longest_row_chars: 6,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.text_summary_for_range(0..22),
|
||||
TextSummary {
|
||||
bytes: 22,
|
||||
lines: Point::new(4, 3),
|
||||
first_line_chars: 2,
|
||||
last_line_chars: 3,
|
||||
longest_row: 3,
|
||||
longest_row_chars: 6,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.text_summary_for_range(7..22),
|
||||
TextSummary {
|
||||
bytes: 15,
|
||||
lines: Point::new(2, 3),
|
||||
first_line_chars: 4,
|
||||
last_line_chars: 3,
|
||||
longest_row: 1,
|
||||
longest_row_chars: 6,
|
||||
}
|
||||
);
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_chars_at(cx: &mut gpui::MutableAppContext) {
|
||||
cx.add_model(|cx| {
|
||||
let mut buffer = Buffer::new(0, "", cx);
|
||||
buffer.edit(vec![0..0], "abcd\nefgh\nij", cx);
|
||||
buffer.edit(vec![12..12], "kl\nmno", cx);
|
||||
buffer.edit(vec![18..18], "\npqrs", cx);
|
||||
buffer.edit(vec![18..21], "\nPQ", cx);
|
||||
|
||||
let chars = buffer.chars_at(Point::new(0, 0));
|
||||
assert_eq!(chars.collect::<String>(), "abcd\nefgh\nijkl\nmno\nPQrs");
|
||||
|
||||
let chars = buffer.chars_at(Point::new(1, 0));
|
||||
assert_eq!(chars.collect::<String>(), "efgh\nijkl\nmno\nPQrs");
|
||||
|
||||
let chars = buffer.chars_at(Point::new(2, 0));
|
||||
assert_eq!(chars.collect::<String>(), "ijkl\nmno\nPQrs");
|
||||
|
||||
let chars = buffer.chars_at(Point::new(3, 0));
|
||||
assert_eq!(chars.collect::<String>(), "mno\nPQrs");
|
||||
|
||||
let chars = buffer.chars_at(Point::new(4, 0));
|
||||
assert_eq!(chars.collect::<String>(), "PQrs");
|
||||
|
||||
// Regression test:
|
||||
let mut buffer = Buffer::new(0, "", cx);
|
||||
buffer.edit(vec![0..0], "[workspace]\nmembers = [\n \"xray_core\",\n \"xray_server\",\n \"xray_cli\",\n \"xray_wasm\",\n]\n", cx);
|
||||
buffer.edit(vec![60..60], "\n", cx);
|
||||
|
||||
let chars = buffer.chars_at(Point::new(6, 0));
|
||||
assert_eq!(chars.collect::<String>(), " \"xray_wasm\",\n]\n");
|
||||
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_anchors(cx: &mut gpui::MutableAppContext) {
|
||||
cx.add_model(|cx| {
|
||||
let mut buffer = Buffer::new(0, "", cx);
|
||||
buffer.edit(vec![0..0], "abc", cx);
|
||||
let left_anchor = buffer.anchor_before(2);
|
||||
let right_anchor = buffer.anchor_after(2);
|
||||
|
||||
buffer.edit(vec![1..1], "def\n", cx);
|
||||
assert_eq!(buffer.text(), "adef\nbc");
|
||||
assert_eq!(left_anchor.to_offset(&buffer), 6);
|
||||
assert_eq!(right_anchor.to_offset(&buffer), 6);
|
||||
assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 });
|
||||
assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 1 });
|
||||
|
||||
buffer.edit(vec![2..3], "", cx);
|
||||
assert_eq!(buffer.text(), "adf\nbc");
|
||||
assert_eq!(left_anchor.to_offset(&buffer), 5);
|
||||
assert_eq!(right_anchor.to_offset(&buffer), 5);
|
||||
assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 });
|
||||
assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 1 });
|
||||
|
||||
buffer.edit(vec![5..5], "ghi\n", cx);
|
||||
assert_eq!(buffer.text(), "adf\nbghi\nc");
|
||||
assert_eq!(left_anchor.to_offset(&buffer), 5);
|
||||
assert_eq!(right_anchor.to_offset(&buffer), 9);
|
||||
assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 });
|
||||
assert_eq!(right_anchor.to_point(&buffer), Point { row: 2, column: 0 });
|
||||
|
||||
buffer.edit(vec![7..9], "", cx);
|
||||
assert_eq!(buffer.text(), "adf\nbghc");
|
||||
assert_eq!(left_anchor.to_offset(&buffer), 5);
|
||||
assert_eq!(right_anchor.to_offset(&buffer), 7);
|
||||
assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 },);
|
||||
assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 3 });
|
||||
|
||||
// Ensure anchoring to a point is equivalent to anchoring to an offset.
|
||||
assert_eq!(
|
||||
buffer.anchor_before(Point { row: 0, column: 0 }),
|
||||
buffer.anchor_before(0)
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.anchor_before(Point { row: 0, column: 1 }),
|
||||
buffer.anchor_before(1)
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.anchor_before(Point { row: 0, column: 2 }),
|
||||
buffer.anchor_before(2)
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.anchor_before(Point { row: 0, column: 3 }),
|
||||
buffer.anchor_before(3)
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.anchor_before(Point { row: 1, column: 0 }),
|
||||
buffer.anchor_before(4)
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.anchor_before(Point { row: 1, column: 1 }),
|
||||
buffer.anchor_before(5)
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.anchor_before(Point { row: 1, column: 2 }),
|
||||
buffer.anchor_before(6)
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.anchor_before(Point { row: 1, column: 3 }),
|
||||
buffer.anchor_before(7)
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.anchor_before(Point { row: 1, column: 4 }),
|
||||
buffer.anchor_before(8)
|
||||
);
|
||||
|
||||
// Comparison between anchors.
|
||||
let anchor_at_offset_0 = buffer.anchor_before(0);
|
||||
let anchor_at_offset_1 = buffer.anchor_before(1);
|
||||
let anchor_at_offset_2 = buffer.anchor_before(2);
|
||||
|
||||
assert_eq!(
|
||||
anchor_at_offset_0
|
||||
.cmp(&anchor_at_offset_0, &buffer)
|
||||
.unwrap(),
|
||||
Ordering::Equal
|
||||
);
|
||||
assert_eq!(
|
||||
anchor_at_offset_1
|
||||
.cmp(&anchor_at_offset_1, &buffer)
|
||||
.unwrap(),
|
||||
Ordering::Equal
|
||||
);
|
||||
assert_eq!(
|
||||
anchor_at_offset_2
|
||||
.cmp(&anchor_at_offset_2, &buffer)
|
||||
.unwrap(),
|
||||
Ordering::Equal
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
anchor_at_offset_0
|
||||
.cmp(&anchor_at_offset_1, &buffer)
|
||||
.unwrap(),
|
||||
Ordering::Less
|
||||
);
|
||||
assert_eq!(
|
||||
anchor_at_offset_1
|
||||
.cmp(&anchor_at_offset_2, &buffer)
|
||||
.unwrap(),
|
||||
Ordering::Less
|
||||
);
|
||||
assert_eq!(
|
||||
anchor_at_offset_0
|
||||
.cmp(&anchor_at_offset_2, &buffer)
|
||||
.unwrap(),
|
||||
Ordering::Less
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
anchor_at_offset_1
|
||||
.cmp(&anchor_at_offset_0, &buffer)
|
||||
.unwrap(),
|
||||
Ordering::Greater
|
||||
);
|
||||
assert_eq!(
|
||||
anchor_at_offset_2
|
||||
.cmp(&anchor_at_offset_1, &buffer)
|
||||
.unwrap(),
|
||||
Ordering::Greater
|
||||
);
|
||||
assert_eq!(
|
||||
anchor_at_offset_2
|
||||
.cmp(&anchor_at_offset_0, &buffer)
|
||||
.unwrap(),
|
||||
Ordering::Greater
|
||||
);
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_anchors_at_start_and_end(cx: &mut gpui::MutableAppContext) {
|
||||
cx.add_model(|cx| {
|
||||
let mut buffer = Buffer::new(0, "", cx);
|
||||
let before_start_anchor = buffer.anchor_before(0);
|
||||
let after_end_anchor = buffer.anchor_after(0);
|
||||
|
||||
buffer.edit(vec![0..0], "abc", cx);
|
||||
assert_eq!(buffer.text(), "abc");
|
||||
assert_eq!(before_start_anchor.to_offset(&buffer), 0);
|
||||
assert_eq!(after_end_anchor.to_offset(&buffer), 3);
|
||||
|
||||
let after_start_anchor = buffer.anchor_after(0);
|
||||
let before_end_anchor = buffer.anchor_before(3);
|
||||
|
||||
buffer.edit(vec![3..3], "def", cx);
|
||||
buffer.edit(vec![0..0], "ghi", cx);
|
||||
assert_eq!(buffer.text(), "ghiabcdef");
|
||||
assert_eq!(before_start_anchor.to_offset(&buffer), 0);
|
||||
assert_eq!(after_start_anchor.to_offset(&buffer), 3);
|
||||
assert_eq!(before_end_anchor.to_offset(&buffer), 6);
|
||||
assert_eq!(after_end_anchor.to_offset(&buffer), 9);
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_apply_diff(mut cx: gpui::TestAppContext) {
|
||||
let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n";
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
|
||||
|
||||
let text = "a\nccc\ndddd\nffffff\n";
|
||||
let diff = buffer.read_with(&cx, |b, cx| b.diff(text.into(), cx)).await;
|
||||
buffer.update(&mut cx, |b, cx| b.apply_diff(diff, cx));
|
||||
cx.read(|cx| assert_eq!(buffer.read(cx).text(), text));
|
||||
|
||||
let text = "a\n1\n\nccc\ndd2dd\nffffff\n";
|
||||
let diff = buffer.read_with(&cx, |b, cx| b.diff(text.into(), cx)).await;
|
||||
buffer.update(&mut cx, |b, cx| b.apply_diff(diff, cx));
|
||||
cx.read(|cx| assert_eq!(buffer.read(cx).text(), text));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_undo_redo(cx: &mut gpui::MutableAppContext) {
|
||||
cx.add_model(|cx| {
|
||||
let mut buffer = Buffer::new(0, "1234", cx);
|
||||
// Set group interval to zero so as to not group edits in the undo stack.
|
||||
buffer.history.group_interval = Duration::from_secs(0);
|
||||
|
||||
buffer.edit(vec![1..1], "abx", cx);
|
||||
buffer.edit(vec![3..4], "yzef", cx);
|
||||
buffer.edit(vec![3..5], "cd", cx);
|
||||
assert_eq!(buffer.text(), "1abcdef234");
|
||||
|
||||
let transactions = buffer.history.undo_stack.clone();
|
||||
assert_eq!(transactions.len(), 3);
|
||||
|
||||
buffer.undo_or_redo(transactions[0].clone(), cx).unwrap();
|
||||
assert_eq!(buffer.text(), "1cdef234");
|
||||
buffer.undo_or_redo(transactions[0].clone(), cx).unwrap();
|
||||
assert_eq!(buffer.text(), "1abcdef234");
|
||||
|
||||
buffer.undo_or_redo(transactions[1].clone(), cx).unwrap();
|
||||
assert_eq!(buffer.text(), "1abcdx234");
|
||||
buffer.undo_or_redo(transactions[2].clone(), cx).unwrap();
|
||||
assert_eq!(buffer.text(), "1abx234");
|
||||
buffer.undo_or_redo(transactions[1].clone(), cx).unwrap();
|
||||
assert_eq!(buffer.text(), "1abyzef234");
|
||||
buffer.undo_or_redo(transactions[2].clone(), cx).unwrap();
|
||||
assert_eq!(buffer.text(), "1abcdef234");
|
||||
|
||||
buffer.undo_or_redo(transactions[2].clone(), cx).unwrap();
|
||||
assert_eq!(buffer.text(), "1abyzef234");
|
||||
buffer.undo_or_redo(transactions[0].clone(), cx).unwrap();
|
||||
assert_eq!(buffer.text(), "1yzef234");
|
||||
buffer.undo_or_redo(transactions[1].clone(), cx).unwrap();
|
||||
assert_eq!(buffer.text(), "1234");
|
||||
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_history(cx: &mut gpui::MutableAppContext) {
|
||||
cx.add_model(|cx| {
|
||||
let mut now = Instant::now();
|
||||
let mut buffer = Buffer::new(0, "123456", cx);
|
||||
|
||||
let set_id =
|
||||
buffer.add_selection_set(buffer.selections_from_ranges(vec![4..4]).unwrap(), cx);
|
||||
buffer.start_transaction_at(Some(set_id), now).unwrap();
|
||||
buffer.edit(vec![2..4], "cd", cx);
|
||||
buffer.end_transaction_at(Some(set_id), now, cx).unwrap();
|
||||
assert_eq!(buffer.text(), "12cd56");
|
||||
assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![4..4]);
|
||||
|
||||
buffer.start_transaction_at(Some(set_id), now).unwrap();
|
||||
buffer
|
||||
.update_selection_set(
|
||||
set_id,
|
||||
buffer.selections_from_ranges(vec![1..3]).unwrap(),
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
buffer.edit(vec![4..5], "e", cx);
|
||||
buffer.end_transaction_at(Some(set_id), now, cx).unwrap();
|
||||
assert_eq!(buffer.text(), "12cde6");
|
||||
assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]);
|
||||
|
||||
now += buffer.history.group_interval + Duration::from_millis(1);
|
||||
buffer.start_transaction_at(Some(set_id), now).unwrap();
|
||||
buffer
|
||||
.update_selection_set(
|
||||
set_id,
|
||||
buffer.selections_from_ranges(vec![2..2]).unwrap(),
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
buffer.edit(vec![0..1], "a", cx);
|
||||
buffer.edit(vec![1..1], "b", cx);
|
||||
buffer.end_transaction_at(Some(set_id), now, cx).unwrap();
|
||||
assert_eq!(buffer.text(), "ab2cde6");
|
||||
assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![3..3]);
|
||||
|
||||
// Last transaction happened past the group interval, undo it on its
|
||||
// own.
|
||||
buffer.undo(cx);
|
||||
assert_eq!(buffer.text(), "12cde6");
|
||||
assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]);
|
||||
|
||||
// First two transactions happened within the group interval, undo them
|
||||
// together.
|
||||
buffer.undo(cx);
|
||||
assert_eq!(buffer.text(), "123456");
|
||||
assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![4..4]);
|
||||
|
||||
// Redo the first two transactions together.
|
||||
buffer.redo(cx);
|
||||
assert_eq!(buffer.text(), "12cde6");
|
||||
assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]);
|
||||
|
||||
// Redo the last transaction on its own.
|
||||
buffer.redo(cx);
|
||||
assert_eq!(buffer.text(), "ab2cde6");
|
||||
assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![3..3]);
|
||||
|
||||
buffer.start_transaction_at(None, now).unwrap();
|
||||
buffer.end_transaction_at(None, now, cx).unwrap();
|
||||
buffer.undo(cx);
|
||||
assert_eq!(buffer.text(), "12cde6");
|
||||
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_concurrent_edits(cx: &mut gpui::MutableAppContext) {
|
||||
let text = "abcdef";
|
||||
|
||||
let buffer1 = cx.add_model(|cx| Buffer::new(1, text, cx));
|
||||
let buffer2 = cx.add_model(|cx| Buffer::new(2, text, cx));
|
||||
let buffer3 = cx.add_model(|cx| Buffer::new(3, text, cx));
|
||||
|
||||
let buf1_op = buffer1.update(cx, |buffer, cx| {
|
||||
buffer.edit(vec![1..2], "12", cx);
|
||||
assert_eq!(buffer.text(), "a12cdef");
|
||||
buffer.operations.last().unwrap().clone()
|
||||
});
|
||||
let buf2_op = buffer2.update(cx, |buffer, cx| {
|
||||
buffer.edit(vec![3..4], "34", cx);
|
||||
assert_eq!(buffer.text(), "abc34ef");
|
||||
buffer.operations.last().unwrap().clone()
|
||||
});
|
||||
let buf3_op = buffer3.update(cx, |buffer, cx| {
|
||||
buffer.edit(vec![5..6], "56", cx);
|
||||
assert_eq!(buffer.text(), "abcde56");
|
||||
buffer.operations.last().unwrap().clone()
|
||||
});
|
||||
|
||||
buffer1.update(cx, |buffer, _| {
|
||||
buffer.apply_op(buf2_op.clone()).unwrap();
|
||||
buffer.apply_op(buf3_op.clone()).unwrap();
|
||||
});
|
||||
buffer2.update(cx, |buffer, _| {
|
||||
buffer.apply_op(buf1_op.clone()).unwrap();
|
||||
buffer.apply_op(buf3_op.clone()).unwrap();
|
||||
});
|
||||
buffer3.update(cx, |buffer, _| {
|
||||
buffer.apply_op(buf1_op.clone()).unwrap();
|
||||
buffer.apply_op(buf2_op.clone()).unwrap();
|
||||
});
|
||||
|
||||
assert_eq!(buffer1.read(cx).text(), "a12c34e56");
|
||||
assert_eq!(buffer2.read(cx).text(), "a12c34e56");
|
||||
assert_eq!(buffer3.read(cx).text(), "a12c34e56");
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_concurrent_edits(cx: &mut gpui::MutableAppContext, mut rng: StdRng) {
|
||||
let peers = env::var("PEERS")
|
||||
.map(|i| i.parse().expect("invalid `PEERS` variable"))
|
||||
.unwrap_or(5);
|
||||
let operations = env::var("OPERATIONS")
|
||||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||||
.unwrap_or(10);
|
||||
|
||||
let base_text_len = rng.gen_range(0..10);
|
||||
let base_text = RandomCharIter::new(&mut rng)
|
||||
.take(base_text_len)
|
||||
.collect::<String>();
|
||||
let mut replica_ids = Vec::new();
|
||||
let mut buffers = Vec::new();
|
||||
let mut network = Network::new(rng.clone());
|
||||
|
||||
for i in 0..peers {
|
||||
let buffer = cx.add_model(|cx| {
|
||||
let mut buf = Buffer::new(i as ReplicaId, base_text.as_str(), cx);
|
||||
buf.history.group_interval = Duration::from_millis(rng.gen_range(0..=200));
|
||||
buf
|
||||
});
|
||||
buffers.push(buffer);
|
||||
replica_ids.push(i as u16);
|
||||
network.add_peer(i as u16);
|
||||
}
|
||||
|
||||
log::info!("initial text: {:?}", base_text);
|
||||
|
||||
let mut mutation_count = operations;
|
||||
loop {
|
||||
let replica_index = rng.gen_range(0..peers);
|
||||
let replica_id = replica_ids[replica_index];
|
||||
buffers[replica_index].update(cx, |buffer, cx| match rng.gen_range(0..=100) {
|
||||
0..=50 if mutation_count != 0 => {
|
||||
buffer.randomly_mutate(&mut rng, cx);
|
||||
network.broadcast(buffer.replica_id, mem::take(&mut buffer.operations));
|
||||
log::info!("buffer {} text: {:?}", buffer.replica_id, buffer.text());
|
||||
mutation_count -= 1;
|
||||
}
|
||||
51..=70 if mutation_count != 0 => {
|
||||
buffer.randomly_undo_redo(&mut rng, cx);
|
||||
network.broadcast(buffer.replica_id, mem::take(&mut buffer.operations));
|
||||
mutation_count -= 1;
|
||||
}
|
||||
71..=100 if network.has_unreceived(replica_id) => {
|
||||
let ops = network.receive(replica_id);
|
||||
if !ops.is_empty() {
|
||||
log::info!(
|
||||
"peer {} applying {} ops from the network.",
|
||||
replica_id,
|
||||
ops.len()
|
||||
);
|
||||
buffer.apply_ops(ops, cx).unwrap();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
if mutation_count == 0 && network.is_idle() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let first_buffer = buffers[0].read(cx);
|
||||
for buffer in &buffers[1..] {
|
||||
let buffer = buffer.read(cx);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
first_buffer.text(),
|
||||
"Replica {} text != Replica 0 text",
|
||||
buffer.replica_id
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.selection_sets().collect::<HashMap<_, _>>(),
|
||||
first_buffer.selection_sets().collect::<HashMap<_, _>>()
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.all_selection_ranges().collect::<HashMap<_, _>>(),
|
||||
first_buffer
|
||||
.all_selection_ranges()
|
||||
.collect::<HashMap<_, _>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Envelope<T: Clone> {
|
||||
message: T,
|
||||
sender: ReplicaId,
|
||||
}
|
||||
|
||||
struct Network<T: Clone, R: rand::Rng> {
|
||||
inboxes: std::collections::BTreeMap<ReplicaId, Vec<Envelope<T>>>,
|
||||
all_messages: Vec<T>,
|
||||
rng: R,
|
||||
}
|
||||
|
||||
impl<T: Clone, R: rand::Rng> Network<T, R> {
|
||||
fn new(rng: R) -> Self {
|
||||
Network {
|
||||
inboxes: Default::default(),
|
||||
all_messages: Vec::new(),
|
||||
rng,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_peer(&mut self, id: ReplicaId) {
|
||||
self.inboxes.insert(id, Vec::new());
|
||||
}
|
||||
|
||||
fn is_idle(&self) -> bool {
|
||||
self.inboxes.values().all(|i| i.is_empty())
|
||||
}
|
||||
|
||||
fn broadcast(&mut self, sender: ReplicaId, messages: Vec<T>) {
|
||||
for (replica, inbox) in self.inboxes.iter_mut() {
|
||||
if *replica != sender {
|
||||
for message in &messages {
|
||||
let min_index = inbox
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.find_map(|(index, envelope)| {
|
||||
if sender == envelope.sender {
|
||||
Some(index + 1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
// Insert one or more duplicates of this message *after* the previous
|
||||
// message delivered by this replica.
|
||||
for _ in 0..self.rng.gen_range(1..4) {
|
||||
let insertion_index = self.rng.gen_range(min_index..inbox.len() + 1);
|
||||
inbox.insert(
|
||||
insertion_index,
|
||||
Envelope {
|
||||
message: message.clone(),
|
||||
sender,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.all_messages.extend(messages);
|
||||
}
|
||||
|
||||
fn has_unreceived(&self, receiver: ReplicaId) -> bool {
|
||||
!self.inboxes[&receiver].is_empty()
|
||||
}
|
||||
|
||||
fn receive(&mut self, receiver: ReplicaId) -> Vec<T> {
|
||||
let inbox = self.inboxes.get_mut(&receiver).unwrap();
|
||||
let count = self.rng.gen_range(0..inbox.len() + 1);
|
||||
inbox
|
||||
.drain(0..count)
|
||||
.map(|envelope| envelope.message)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -1,380 +0,0 @@
|
||||
use crate::*;
|
||||
use gpui::{ModelHandle, MutableAppContext};
|
||||
use unindent::Unindent as _;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_reparse(mut cx: gpui::TestAppContext) {
|
||||
let buffer = cx.add_model(|cx| {
|
||||
let text = "fn a() {}".into();
|
||||
Buffer::from_history(0, History::new(text), None, Some(rust_lang()), cx)
|
||||
});
|
||||
|
||||
// Wait for the initial text to parse
|
||||
buffer
|
||||
.condition(&cx, |buffer, _| !buffer.is_parsing())
|
||||
.await;
|
||||
assert_eq!(
|
||||
get_tree_sexp(&buffer, &cx),
|
||||
concat!(
|
||||
"(source_file (function_item name: (identifier) ",
|
||||
"parameters: (parameters) ",
|
||||
"body: (block)))"
|
||||
)
|
||||
);
|
||||
|
||||
buffer.update(&mut cx, |buffer, _| {
|
||||
buffer.set_sync_parse_timeout(Duration::ZERO)
|
||||
});
|
||||
|
||||
// Perform some edits (add parameter and variable reference)
|
||||
// Parsing doesn't begin until the transaction is complete
|
||||
buffer.update(&mut cx, |buf, cx| {
|
||||
buf.start_transaction(None).unwrap();
|
||||
|
||||
let offset = buf.text().find(")").unwrap();
|
||||
buf.edit(vec![offset..offset], "b: C", cx);
|
||||
assert!(!buf.is_parsing());
|
||||
|
||||
let offset = buf.text().find("}").unwrap();
|
||||
buf.edit(vec![offset..offset], " d; ", cx);
|
||||
assert!(!buf.is_parsing());
|
||||
|
||||
buf.end_transaction(None, cx).unwrap();
|
||||
assert_eq!(buf.text(), "fn a(b: C) { d; }");
|
||||
assert!(buf.is_parsing());
|
||||
});
|
||||
buffer
|
||||
.condition(&cx, |buffer, _| !buffer.is_parsing())
|
||||
.await;
|
||||
assert_eq!(
|
||||
get_tree_sexp(&buffer, &cx),
|
||||
concat!(
|
||||
"(source_file (function_item name: (identifier) ",
|
||||
"parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
|
||||
"body: (block (identifier))))"
|
||||
)
|
||||
);
|
||||
|
||||
// Perform a series of edits without waiting for the current parse to complete:
|
||||
// * turn identifier into a field expression
|
||||
// * turn field expression into a method call
|
||||
// * add a turbofish to the method call
|
||||
buffer.update(&mut cx, |buf, cx| {
|
||||
let offset = buf.text().find(";").unwrap();
|
||||
buf.edit(vec![offset..offset], ".e", cx);
|
||||
assert_eq!(buf.text(), "fn a(b: C) { d.e; }");
|
||||
assert!(buf.is_parsing());
|
||||
});
|
||||
buffer.update(&mut cx, |buf, cx| {
|
||||
let offset = buf.text().find(";").unwrap();
|
||||
buf.edit(vec![offset..offset], "(f)", cx);
|
||||
assert_eq!(buf.text(), "fn a(b: C) { d.e(f); }");
|
||||
assert!(buf.is_parsing());
|
||||
});
|
||||
buffer.update(&mut cx, |buf, cx| {
|
||||
let offset = buf.text().find("(f)").unwrap();
|
||||
buf.edit(vec![offset..offset], "::<G>", cx);
|
||||
assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }");
|
||||
assert!(buf.is_parsing());
|
||||
});
|
||||
buffer
|
||||
.condition(&cx, |buffer, _| !buffer.is_parsing())
|
||||
.await;
|
||||
assert_eq!(
|
||||
get_tree_sexp(&buffer, &cx),
|
||||
concat!(
|
||||
"(source_file (function_item name: (identifier) ",
|
||||
"parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
|
||||
"body: (block (call_expression ",
|
||||
"function: (generic_function ",
|
||||
"function: (field_expression value: (identifier) field: (field_identifier)) ",
|
||||
"type_arguments: (type_arguments (type_identifier))) ",
|
||||
"arguments: (arguments (identifier))))))",
|
||||
)
|
||||
);
|
||||
|
||||
buffer.update(&mut cx, |buf, cx| {
|
||||
buf.undo(cx);
|
||||
assert_eq!(buf.text(), "fn a() {}");
|
||||
assert!(buf.is_parsing());
|
||||
});
|
||||
buffer
|
||||
.condition(&cx, |buffer, _| !buffer.is_parsing())
|
||||
.await;
|
||||
assert_eq!(
|
||||
get_tree_sexp(&buffer, &cx),
|
||||
concat!(
|
||||
"(source_file (function_item name: (identifier) ",
|
||||
"parameters: (parameters) ",
|
||||
"body: (block)))"
|
||||
)
|
||||
);
|
||||
|
||||
buffer.update(&mut cx, |buf, cx| {
|
||||
buf.redo(cx);
|
||||
assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }");
|
||||
assert!(buf.is_parsing());
|
||||
});
|
||||
buffer
|
||||
.condition(&cx, |buffer, _| !buffer.is_parsing())
|
||||
.await;
|
||||
assert_eq!(
|
||||
get_tree_sexp(&buffer, &cx),
|
||||
concat!(
|
||||
"(source_file (function_item name: (identifier) ",
|
||||
"parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
|
||||
"body: (block (call_expression ",
|
||||
"function: (generic_function ",
|
||||
"function: (field_expression value: (identifier) field: (field_identifier)) ",
|
||||
"type_arguments: (type_arguments (type_identifier))) ",
|
||||
"arguments: (arguments (identifier))))))",
|
||||
)
|
||||
);
|
||||
|
||||
fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> String {
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
buffer.syntax_tree().unwrap().root_node().to_sexp()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||
let buffer = cx.add_model(|cx| {
|
||||
let text = "
|
||||
mod x {
|
||||
mod y {
|
||||
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent()
|
||||
.into();
|
||||
Buffer::from_history(0, History::new(text), None, Some(rust_lang()), cx)
|
||||
});
|
||||
let buffer = buffer.read(cx);
|
||||
assert_eq!(
|
||||
buffer.enclosing_bracket_point_ranges(Point::new(1, 6)..Point::new(1, 6)),
|
||||
Some((
|
||||
Point::new(0, 6)..Point::new(0, 7),
|
||||
Point::new(4, 0)..Point::new(4, 1)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.enclosing_bracket_point_ranges(Point::new(1, 10)..Point::new(1, 10)),
|
||||
Some((
|
||||
Point::new(1, 10)..Point::new(1, 11),
|
||||
Point::new(3, 4)..Point::new(3, 5)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.enclosing_bracket_point_ranges(Point::new(3, 5)..Point::new(3, 5)),
|
||||
Some((
|
||||
Point::new(1, 10)..Point::new(1, 11),
|
||||
Point::new(3, 4)..Point::new(3, 5)
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_edit_with_autoindent(cx: &mut MutableAppContext) {
|
||||
cx.add_model(|cx| {
|
||||
let text = "fn a() {}".into();
|
||||
let mut buffer = Buffer::from_history(0, History::new(text), None, Some(rust_lang()), cx);
|
||||
|
||||
buffer.edit_with_autoindent([8..8], "\n\n", cx);
|
||||
assert_eq!(buffer.text(), "fn a() {\n \n}");
|
||||
|
||||
buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 4)], "b()\n", cx);
|
||||
assert_eq!(buffer.text(), "fn a() {\n b()\n \n}");
|
||||
|
||||
buffer.edit_with_autoindent([Point::new(2, 4)..Point::new(2, 4)], ".c", cx);
|
||||
assert_eq!(buffer.text(), "fn a() {\n b()\n .c\n}");
|
||||
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_autoindent_moves_selections(cx: &mut MutableAppContext) {
|
||||
cx.add_model(|cx| {
|
||||
let text = History::new("fn a() {}".into());
|
||||
let mut buffer = Buffer::from_history(0, text, None, Some(rust_lang()), cx);
|
||||
|
||||
let selection_set_id = buffer.add_selection_set(Vec::new(), cx);
|
||||
buffer.start_transaction(Some(selection_set_id)).unwrap();
|
||||
buffer.edit_with_autoindent([5..5, 9..9], "\n\n", cx);
|
||||
buffer
|
||||
.update_selection_set(
|
||||
selection_set_id,
|
||||
vec![
|
||||
Selection {
|
||||
id: 0,
|
||||
start: buffer.anchor_before(Point::new(1, 0)),
|
||||
end: buffer.anchor_before(Point::new(1, 0)),
|
||||
reversed: false,
|
||||
goal: SelectionGoal::None,
|
||||
},
|
||||
Selection {
|
||||
id: 1,
|
||||
start: buffer.anchor_before(Point::new(4, 0)),
|
||||
end: buffer.anchor_before(Point::new(4, 0)),
|
||||
reversed: false,
|
||||
goal: SelectionGoal::None,
|
||||
},
|
||||
],
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(buffer.text(), "fn a(\n\n) {}\n\n");
|
||||
|
||||
// Ending the transaction runs the auto-indent. The selection
|
||||
// at the start of the auto-indented row is pushed to the right.
|
||||
buffer.end_transaction(Some(selection_set_id), cx).unwrap();
|
||||
assert_eq!(buffer.text(), "fn a(\n \n) {}\n\n");
|
||||
let selection_ranges = buffer
|
||||
.selection_set(selection_set_id)
|
||||
.unwrap()
|
||||
.selections
|
||||
.iter()
|
||||
.map(|selection| selection.point_range(&buffer))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(selection_ranges[0], empty(Point::new(1, 4)));
|
||||
assert_eq!(selection_ranges[1], empty(Point::new(4, 0)));
|
||||
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut MutableAppContext) {
|
||||
cx.add_model(|cx| {
|
||||
let text = "
|
||||
fn a() {
|
||||
c;
|
||||
d;
|
||||
}
|
||||
"
|
||||
.unindent()
|
||||
.into();
|
||||
let mut buffer = Buffer::from_history(0, History::new(text), None, Some(rust_lang()), cx);
|
||||
|
||||
// Lines 2 and 3 don't match the indentation suggestion. When editing these lines,
|
||||
// their indentation is not adjusted.
|
||||
buffer.edit_with_autoindent([empty(Point::new(1, 1)), empty(Point::new(2, 1))], "()", cx);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
"
|
||||
fn a() {
|
||||
c();
|
||||
d();
|
||||
}
|
||||
"
|
||||
.unindent()
|
||||
);
|
||||
|
||||
// When appending new content after these lines, the indentation is based on the
|
||||
// preceding lines' actual indentation.
|
||||
buffer.edit_with_autoindent(
|
||||
[empty(Point::new(1, 1)), empty(Point::new(2, 1))],
|
||||
"\n.f\n.g",
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
"
|
||||
fn a() {
|
||||
c
|
||||
.f
|
||||
.g();
|
||||
d
|
||||
.f
|
||||
.g();
|
||||
}
|
||||
"
|
||||
.unindent()
|
||||
);
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppContext) {
|
||||
cx.add_model(|cx| {
|
||||
let text = History::new(
|
||||
"
|
||||
fn a() {}
|
||||
"
|
||||
.unindent()
|
||||
.into(),
|
||||
);
|
||||
let mut buffer = Buffer::from_history(0, text, None, Some(rust_lang()), cx);
|
||||
|
||||
buffer.edit_with_autoindent([5..5], "\nb", cx);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
"
|
||||
fn a(
|
||||
b) {}
|
||||
"
|
||||
.unindent()
|
||||
);
|
||||
|
||||
// The indentation suggestion changed because `@end` node (a close paren)
|
||||
// is now at the beginning of the line.
|
||||
buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 5)], "", cx);
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
"
|
||||
fn a(
|
||||
) {}
|
||||
"
|
||||
.unindent()
|
||||
);
|
||||
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contiguous_ranges() {
|
||||
assert_eq!(
|
||||
contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12], 100).collect::<Vec<_>>(),
|
||||
&[1..4, 5..7, 9..13]
|
||||
);
|
||||
|
||||
// Respects the `max_len` parameter
|
||||
assert_eq!(
|
||||
contiguous_ranges([2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31], 3).collect::<Vec<_>>(),
|
||||
&[2..5, 5..8, 8..10, 23..26, 26..27, 30..32],
|
||||
);
|
||||
}
|
||||
|
||||
fn rust_lang() -> Arc<Language> {
|
||||
Arc::new(
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".to_string(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
tree_sitter_rust::language(),
|
||||
)
|
||||
.with_indents_query(
|
||||
r#"
|
||||
(call_expression) @indent
|
||||
(field_expression) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.with_brackets_query(r#" ("{" @open "}" @close) "#)
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
fn empty(point: Point) -> Range<Point> {
|
||||
point..point
|
||||
}
|
||||
@@ -3,6 +3,9 @@ name = "chat_panel"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
path = "src/chat_panel.rs"
|
||||
|
||||
[dependencies]
|
||||
client = { path = "../client" }
|
||||
editor = { path = "../editor" }
|
||||
|
||||
@@ -56,13 +56,14 @@ impl ChatPanel {
|
||||
4,
|
||||
{
|
||||
let settings = settings.clone();
|
||||
move |_| {
|
||||
Arc::new(move |_| {
|
||||
let settings = settings.borrow();
|
||||
EditorSettings {
|
||||
tab_size: settings.tab_size,
|
||||
style: settings.theme.chat_panel.input_editor.as_editor(),
|
||||
soft_wrap: editor::SoftWrap::EditorWidth,
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
cx,
|
||||
)
|
||||
@@ -95,7 +96,7 @@ impl ChatPanel {
|
||||
});
|
||||
|
||||
let mut message_list = ListState::new(0, Orientation::Bottom, 1000., {
|
||||
let this = cx.handle().downgrade();
|
||||
let this = cx.weak_handle();
|
||||
move |ix, cx| {
|
||||
let this = this.upgrade(cx).unwrap().read(cx);
|
||||
let message = this.active_channel.as_ref().unwrap().0.read(cx).message(ix);
|
||||
@@ -232,7 +233,7 @@ impl ChatPanel {
|
||||
Empty::new().boxed()
|
||||
};
|
||||
|
||||
Expanded::new(1., messages).boxed()
|
||||
Flexible::new(1., true, messages).boxed()
|
||||
}
|
||||
|
||||
fn render_message(&self, message: &ChannelMessage) -> ElementBox {
|
||||
@@ -3,8 +3,11 @@ name = "client"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
path = "src/client.rs"
|
||||
|
||||
[features]
|
||||
test-support = ["rpc/test-support"]
|
||||
test-support = ["gpui/test-support", "rpc/test-support"]
|
||||
|
||||
[dependencies]
|
||||
gpui = { path = "../gpui" }
|
||||
@@ -13,7 +16,7 @@ rpc = { path = "../rpc" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
anyhow = "1.0.38"
|
||||
async-recursion = "0.3"
|
||||
async-tungstenite = { version = "0.14", features = ["async-tls"] }
|
||||
async-tungstenite = { version = "0.16", features = ["async-tls"] }
|
||||
futures = "0.3"
|
||||
image = "0.23"
|
||||
lazy_static = "1.4.0"
|
||||
@@ -26,3 +29,7 @@ surf = "2.2"
|
||||
thiserror = "1.0.29"
|
||||
time = "0.3"
|
||||
tiny_http = "0.8"
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
|
||||
@@ -599,8 +599,8 @@ mod tests {
|
||||
#[gpui::test]
|
||||
async fn test_channel_messages(mut cx: TestAppContext) {
|
||||
let user_id = 5;
|
||||
let mut client = Client::new();
|
||||
let http_client = FakeHttpClient::new(|_| async move { Ok(Response::new(404)) });
|
||||
let mut client = Client::new(http_client.clone());
|
||||
let server = FakeServer::for_client(user_id, &mut client, &cx).await;
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
||||
|
||||
|
||||
@@ -11,10 +11,12 @@ use async_tungstenite::tungstenite::{
|
||||
error::Error as WebsocketError,
|
||||
http::{Request, StatusCode},
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use gpui::{action, AsyncAppContext, Entity, ModelContext, MutableAppContext, Task};
|
||||
use http::HttpClient;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::RwLock;
|
||||
use postage::{prelude::Stream, watch};
|
||||
use postage::watch;
|
||||
use rand::prelude::*;
|
||||
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage};
|
||||
use std::{
|
||||
@@ -26,7 +28,7 @@ use std::{
|
||||
sync::{Arc, Weak},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use surf::Url;
|
||||
use surf::{http::Method, Url};
|
||||
use thiserror::Error;
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
@@ -36,7 +38,7 @@ pub use user::*;
|
||||
|
||||
lazy_static! {
|
||||
static ref ZED_SERVER_URL: String =
|
||||
std::env::var("ZED_SERVER_URL").unwrap_or("https://zed.dev:443".to_string());
|
||||
std::env::var("ZED_SERVER_URL").unwrap_or("https://zed.dev".to_string());
|
||||
static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
|
||||
.ok()
|
||||
.and_then(|s| if s.is_empty() { None } else { Some(s) });
|
||||
@@ -54,6 +56,7 @@ pub fn init(rpc: Arc<Client>, cx: &mut MutableAppContext) {
|
||||
|
||||
pub struct Client {
|
||||
peer: Arc<Peer>,
|
||||
http: Arc<dyn HttpClient>,
|
||||
state: RwLock<ClientState>,
|
||||
authenticate:
|
||||
Option<Box<dyn 'static + Send + Sync + Fn(&AsyncAppContext) -> Task<Result<Credentials>>>>,
|
||||
@@ -122,14 +125,14 @@ struct ClientState {
|
||||
status: (watch::Sender<Status>, watch::Receiver<Status>),
|
||||
entity_id_extractors: HashMap<TypeId, Box<dyn Send + Sync + Fn(&dyn AnyTypedEnvelope) -> u64>>,
|
||||
model_handlers: HashMap<
|
||||
(TypeId, u64),
|
||||
Box<dyn Send + Sync + FnMut(Box<dyn AnyTypedEnvelope>, &mut AsyncAppContext)>,
|
||||
(TypeId, Option<u64>),
|
||||
Option<Box<dyn Send + Sync + FnMut(Box<dyn AnyTypedEnvelope>, &mut AsyncAppContext)>>,
|
||||
>,
|
||||
_maintain_connection: Option<Task<()>>,
|
||||
heartbeat_interval: Duration,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Credentials {
|
||||
pub user_id: u64,
|
||||
pub access_token: String,
|
||||
@@ -150,28 +153,23 @@ impl Default for ClientState {
|
||||
|
||||
pub struct Subscription {
|
||||
client: Weak<Client>,
|
||||
id: (TypeId, u64),
|
||||
id: (TypeId, Option<u64>),
|
||||
}
|
||||
|
||||
impl Drop for Subscription {
|
||||
fn drop(&mut self) {
|
||||
if let Some(client) = self.client.upgrade() {
|
||||
drop(
|
||||
client
|
||||
.state
|
||||
.write()
|
||||
.model_handlers
|
||||
.remove(&self.id)
|
||||
.unwrap(),
|
||||
);
|
||||
let mut state = client.state.write();
|
||||
let _ = state.model_handlers.remove(&self.id).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new() -> Arc<Self> {
|
||||
pub fn new(http: Arc<dyn HttpClient>) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
peer: Peer::new(),
|
||||
http,
|
||||
state: Default::default(),
|
||||
authenticate: None,
|
||||
establish_connection: None,
|
||||
@@ -269,20 +267,13 @@ impl Client {
|
||||
+ Sync
|
||||
+ FnMut(&mut M, TypedEnvelope<T>, Arc<Self>, &mut ModelContext<M>) -> Result<()>,
|
||||
{
|
||||
let subscription_id = (TypeId::of::<T>(), Default::default());
|
||||
let subscription_id = (TypeId::of::<T>(), None);
|
||||
let client = self.clone();
|
||||
let mut state = self.state.write();
|
||||
let model = cx.handle().downgrade();
|
||||
let prev_extractor = state
|
||||
.entity_id_extractors
|
||||
.insert(subscription_id.0, Box::new(|_| Default::default()));
|
||||
if prev_extractor.is_some() {
|
||||
panic!("registered a handler for the same entity twice")
|
||||
}
|
||||
|
||||
state.model_handlers.insert(
|
||||
let model = cx.weak_handle();
|
||||
let prev_handler = state.model_handlers.insert(
|
||||
subscription_id,
|
||||
Box::new(move |envelope, cx| {
|
||||
Some(Box::new(move |envelope, cx| {
|
||||
if let Some(model) = model.upgrade(cx) {
|
||||
let envelope = envelope.into_any().downcast::<TypedEnvelope<T>>().unwrap();
|
||||
model.update(cx, |model, cx| {
|
||||
@@ -291,8 +282,11 @@ impl Client {
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
})),
|
||||
);
|
||||
if prev_handler.is_some() {
|
||||
panic!("registered handler for the same message twice");
|
||||
}
|
||||
|
||||
Subscription {
|
||||
client: Arc::downgrade(self),
|
||||
@@ -314,10 +308,10 @@ impl Client {
|
||||
+ Sync
|
||||
+ FnMut(&mut M, TypedEnvelope<T>, Arc<Self>, &mut ModelContext<M>) -> Result<()>,
|
||||
{
|
||||
let subscription_id = (TypeId::of::<T>(), remote_id);
|
||||
let subscription_id = (TypeId::of::<T>(), Some(remote_id));
|
||||
let client = self.clone();
|
||||
let mut state = self.state.write();
|
||||
let model = cx.handle().downgrade();
|
||||
let model = cx.weak_handle();
|
||||
state
|
||||
.entity_id_extractors
|
||||
.entry(subscription_id.0)
|
||||
@@ -332,7 +326,7 @@ impl Client {
|
||||
});
|
||||
let prev_handler = state.model_handlers.insert(
|
||||
subscription_id,
|
||||
Box::new(move |envelope, cx| {
|
||||
Some(Box::new(move |envelope, cx| {
|
||||
if let Some(model) = model.upgrade(cx) {
|
||||
let envelope = envelope.into_any().downcast::<TypedEnvelope<T>>().unwrap();
|
||||
model.update(cx, |model, cx| {
|
||||
@@ -341,7 +335,7 @@ impl Client {
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
})),
|
||||
);
|
||||
if prev_handler.is_some() {
|
||||
panic!("registered a handler for the same entity twice")
|
||||
@@ -353,6 +347,10 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
|
||||
read_credentials_from_keychain(cx).is_some()
|
||||
}
|
||||
|
||||
#[async_recursion(?Send)]
|
||||
pub async fn authenticate_and_connect(
|
||||
self: &Arc<Self>,
|
||||
@@ -403,7 +401,6 @@ impl Client {
|
||||
|
||||
match self.establish_connection(&credentials, cx).await {
|
||||
Ok(conn) => {
|
||||
log::info!("connected to rpc address {}", *ZED_SERVER_URL);
|
||||
self.state.write().credentials = Some(credentials.clone());
|
||||
if !used_keychain && IMPERSONATE_LOGIN.is_none() {
|
||||
write_credentials_to_keychain(&credentials, cx).log_err();
|
||||
@@ -440,29 +437,29 @@ impl Client {
|
||||
let mut cx = cx.clone();
|
||||
let this = self.clone();
|
||||
async move {
|
||||
while let Some(message) = incoming.recv().await {
|
||||
while let Some(message) = incoming.next().await {
|
||||
let mut state = this.state.write();
|
||||
if let Some(extract_entity_id) =
|
||||
let payload_type_id = message.payload_type_id();
|
||||
let entity_id = if let Some(extract_entity_id) =
|
||||
state.entity_id_extractors.get(&message.payload_type_id())
|
||||
{
|
||||
let payload_type_id = message.payload_type_id();
|
||||
let entity_id = (extract_entity_id)(message.as_ref());
|
||||
let handler_key = (payload_type_id, entity_id);
|
||||
if let Some(mut handler) = state.model_handlers.remove(&handler_key) {
|
||||
drop(state); // Avoid deadlocks if the handler interacts with rpc::Client
|
||||
let start_time = Instant::now();
|
||||
log::info!("RPC client message {}", message.payload_type_name());
|
||||
(handler)(message, &mut cx);
|
||||
log::info!(
|
||||
"RPC message handled. duration:{:?}",
|
||||
start_time.elapsed()
|
||||
);
|
||||
this.state
|
||||
.write()
|
||||
.model_handlers
|
||||
.insert(handler_key, handler);
|
||||
} else {
|
||||
log::info!("unhandled message {}", message.payload_type_name());
|
||||
Some((extract_entity_id)(message.as_ref()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let handler_key = (payload_type_id, entity_id);
|
||||
if let Some(handler) = state.model_handlers.get_mut(&handler_key) {
|
||||
let mut handler = handler.take().unwrap();
|
||||
drop(state); // Avoid deadlocks if the handler interacts with rpc::Client
|
||||
let start_time = Instant::now();
|
||||
log::info!("RPC client message {}", message.payload_type_name());
|
||||
(handler)(message, &mut cx);
|
||||
log::info!("RPC message handled. duration:{:?}", start_time.elapsed());
|
||||
|
||||
let mut state = this.state.write();
|
||||
if state.model_handlers.contains_key(&handler_key) {
|
||||
state.model_handlers.insert(handler_key, Some(handler));
|
||||
}
|
||||
} else {
|
||||
log::info!("unhandled message {}", message.payload_type_name());
|
||||
@@ -521,20 +518,57 @@ impl Client {
|
||||
format!("{} {}", credentials.user_id, credentials.access_token),
|
||||
)
|
||||
.header("X-Zed-Protocol-Version", rpc::PROTOCOL_VERSION);
|
||||
|
||||
let http = self.http.clone();
|
||||
cx.background().spawn(async move {
|
||||
if let Some(host) = ZED_SERVER_URL.strip_prefix("https://") {
|
||||
let stream = smol::net::TcpStream::connect(host).await?;
|
||||
let request = request.uri(format!("wss://{}/rpc", host)).body(())?;
|
||||
let (stream, _) =
|
||||
async_tungstenite::async_tls::client_async_tls(request, stream).await?;
|
||||
Ok(Connection::new(stream))
|
||||
} else if let Some(host) = ZED_SERVER_URL.strip_prefix("http://") {
|
||||
let stream = smol::net::TcpStream::connect(host).await?;
|
||||
let request = request.uri(format!("ws://{}/rpc", host)).body(())?;
|
||||
let (stream, _) = async_tungstenite::client_async(request, stream).await?;
|
||||
Ok(Connection::new(stream))
|
||||
} else {
|
||||
Err(anyhow!("invalid server url: {}", *ZED_SERVER_URL))?
|
||||
let mut rpc_url = format!("{}/rpc", *ZED_SERVER_URL);
|
||||
let rpc_request = surf::Request::new(
|
||||
Method::Get,
|
||||
surf::Url::parse(&rpc_url).context("invalid ZED_SERVER_URL")?,
|
||||
);
|
||||
let rpc_response = http.send(rpc_request).await?;
|
||||
|
||||
if rpc_response.status().is_redirection() {
|
||||
rpc_url = rpc_response
|
||||
.header("Location")
|
||||
.ok_or_else(|| anyhow!("missing location header in /rpc response"))?
|
||||
.as_str()
|
||||
.to_string();
|
||||
}
|
||||
// Until we switch the zed.dev domain to point to the new Next.js app, there
|
||||
// will be no redirect required, and the app will connect directly to
|
||||
// wss://zed.dev/rpc.
|
||||
else if rpc_response.status() != surf::StatusCode::UpgradeRequired {
|
||||
Err(anyhow!(
|
||||
"unexpected /rpc response status {}",
|
||||
rpc_response.status()
|
||||
))?
|
||||
}
|
||||
|
||||
let mut rpc_url = surf::Url::parse(&rpc_url).context("invalid rpc url")?;
|
||||
let rpc_host = rpc_url
|
||||
.host_str()
|
||||
.zip(rpc_url.port_or_known_default())
|
||||
.ok_or_else(|| anyhow!("missing host in rpc url"))?;
|
||||
let stream = smol::net::TcpStream::connect(rpc_host).await?;
|
||||
|
||||
log::info!("connected to rpc endpoint {}", rpc_url);
|
||||
|
||||
match rpc_url.scheme() {
|
||||
"https" => {
|
||||
rpc_url.set_scheme("wss").unwrap();
|
||||
let request = request.uri(rpc_url.as_str()).body(())?;
|
||||
let (stream, _) =
|
||||
async_tungstenite::async_tls::client_async_tls(request, stream).await?;
|
||||
Ok(Connection::new(stream))
|
||||
}
|
||||
"http" => {
|
||||
rpc_url.set_scheme("ws").unwrap();
|
||||
let request = request.uri(rpc_url.as_str()).body(())?;
|
||||
let (stream, _) = async_tungstenite::client_async(request, stream).await?;
|
||||
Ok(Connection::new(stream))
|
||||
}
|
||||
_ => Err(anyhow!("invalid rpc url: {}", rpc_url))?,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -561,7 +595,7 @@ impl Client {
|
||||
// Open the Zed sign-in page in the user's browser, with query parameters that indicate
|
||||
// that the user is signing in from a Zed app running on the same device.
|
||||
let mut url = format!(
|
||||
"{}/sign_in?native_app_port={}&native_app_public_key={}",
|
||||
"{}/native_app_signin?native_app_port={}&native_app_public_key={}",
|
||||
*ZED_SERVER_URL, port, public_key_string
|
||||
);
|
||||
|
||||
@@ -592,9 +626,16 @@ impl Client {
|
||||
user_id = Some(value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let post_auth_url =
|
||||
format!("{}/native_app_signin_succeeded", *ZED_SERVER_URL);
|
||||
req.respond(
|
||||
tiny_http::Response::from_string(LOGIN_RESPONSE).with_header(
|
||||
tiny_http::Header::from_bytes("Content-Type", "text/html").unwrap(),
|
||||
tiny_http::Response::empty(302).with_header(
|
||||
tiny_http::Header::from_bytes(
|
||||
&b"Location"[..],
|
||||
post_auth_url.as_bytes(),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
)
|
||||
.context("failed to respond to login http request")?;
|
||||
@@ -621,9 +662,9 @@ impl Client {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) -> Result<()> {
|
||||
pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) -> Result<()> {
|
||||
let conn_id = self.connection_id()?;
|
||||
self.peer.disconnect(conn_id).await;
|
||||
self.peer.disconnect(conn_id);
|
||||
self.set_status(Status::SignedOut, cx);
|
||||
Ok(())
|
||||
}
|
||||
@@ -651,6 +692,14 @@ impl Client {
|
||||
) -> impl Future<Output = Result<()>> {
|
||||
self.peer.respond(receipt, response)
|
||||
}
|
||||
|
||||
pub fn respond_with_error<T: RequestMessage>(
|
||||
&self,
|
||||
receipt: Receipt<T>,
|
||||
error: proto::Error,
|
||||
) -> impl Future<Output = Result<()>> {
|
||||
self.peer.respond_with_error(receipt, error)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
|
||||
@@ -694,17 +743,10 @@ pub fn decode_worktree_url(url: &str) -> Option<(u64, String)> {
|
||||
Some((id, access_token.to_string()))
|
||||
}
|
||||
|
||||
const LOGIN_RESPONSE: &'static str = "
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<script>window.close();</script>
|
||||
</html>
|
||||
";
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test::FakeServer;
|
||||
use crate::test::{FakeHttpClient, FakeServer};
|
||||
use gpui::TestAppContext;
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
@@ -712,7 +754,7 @@ mod tests {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
let user_id = 5;
|
||||
let mut client = Client::new();
|
||||
let mut client = Client::new(FakeHttpClient::with_404_response());
|
||||
let server = FakeServer::for_client(user_id, &mut client, &cx).await;
|
||||
|
||||
cx.foreground().advance_clock(Duration::from_secs(10));
|
||||
@@ -723,7 +765,7 @@ mod tests {
|
||||
let ping = server.receive::<proto::Ping>().await.unwrap();
|
||||
server.respond(ping.receipt(), proto::Ack {}).await;
|
||||
|
||||
client.disconnect(&cx.to_async()).await.unwrap();
|
||||
client.disconnect(&cx.to_async()).unwrap();
|
||||
assert!(server.receive::<proto::Ping>().await.is_err());
|
||||
}
|
||||
|
||||
@@ -732,27 +774,27 @@ mod tests {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
let user_id = 5;
|
||||
let mut client = Client::new();
|
||||
let mut client = Client::new(FakeHttpClient::with_404_response());
|
||||
let server = FakeServer::for_client(user_id, &mut client, &cx).await;
|
||||
let mut status = client.status();
|
||||
assert!(matches!(
|
||||
status.recv().await,
|
||||
status.next().await,
|
||||
Some(Status::Connected { .. })
|
||||
));
|
||||
assert_eq!(server.auth_count(), 1);
|
||||
|
||||
server.forbid_connections();
|
||||
server.disconnect().await;
|
||||
while !matches!(status.recv().await, Some(Status::ReconnectionError { .. })) {}
|
||||
server.disconnect();
|
||||
while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {}
|
||||
|
||||
server.allow_connections();
|
||||
cx.foreground().advance_clock(Duration::from_secs(10));
|
||||
while !matches!(status.recv().await, Some(Status::Connected { .. })) {}
|
||||
while !matches!(status.next().await, Some(Status::Connected { .. })) {}
|
||||
assert_eq!(server.auth_count(), 1); // Client reused the cached credentials when reconnecting
|
||||
|
||||
server.forbid_connections();
|
||||
server.disconnect().await;
|
||||
while !matches!(status.recv().await, Some(Status::ReconnectionError { .. })) {}
|
||||
server.disconnect();
|
||||
while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {}
|
||||
|
||||
// Clear cached credentials after authentication fails
|
||||
server.roll_access_token();
|
||||
@@ -760,7 +802,7 @@ mod tests {
|
||||
cx.foreground().advance_clock(Duration::from_secs(10));
|
||||
assert_eq!(server.auth_count(), 1);
|
||||
cx.foreground().advance_clock(Duration::from_secs(10));
|
||||
while !matches!(status.recv().await, Some(Status::Connected { .. })) {}
|
||||
while !matches!(status.next().await, Some(Status::Connected { .. })) {}
|
||||
assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token
|
||||
}
|
||||
|
||||
@@ -774,4 +816,113 @@ mod tests {
|
||||
);
|
||||
assert_eq!(decode_worktree_url("not://the-right-format"), None);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_subscribing_to_entity(mut cx: TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
let user_id = 5;
|
||||
let mut client = Client::new(FakeHttpClient::with_404_response());
|
||||
let server = FakeServer::for_client(user_id, &mut client, &cx).await;
|
||||
|
||||
let model = cx.add_model(|_| Model { subscription: None });
|
||||
let (mut done_tx1, mut done_rx1) = postage::oneshot::channel();
|
||||
let (mut done_tx2, mut done_rx2) = postage::oneshot::channel();
|
||||
let _subscription1 = model.update(&mut cx, |_, cx| {
|
||||
client.subscribe_to_entity(
|
||||
1,
|
||||
cx,
|
||||
move |_, _: TypedEnvelope<proto::UnshareProject>, _, _| {
|
||||
postage::sink::Sink::try_send(&mut done_tx1, ()).unwrap();
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
});
|
||||
let _subscription2 = model.update(&mut cx, |_, cx| {
|
||||
client.subscribe_to_entity(
|
||||
2,
|
||||
cx,
|
||||
move |_, _: TypedEnvelope<proto::UnshareProject>, _, _| {
|
||||
postage::sink::Sink::try_send(&mut done_tx2, ()).unwrap();
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
// Ensure dropping a subscription for the same entity type still allows receiving of
|
||||
// messages for other entity IDs of the same type.
|
||||
let subscription3 = model.update(&mut cx, |_, cx| {
|
||||
client.subscribe_to_entity(
|
||||
3,
|
||||
cx,
|
||||
move |_, _: TypedEnvelope<proto::UnshareProject>, _, _| Ok(()),
|
||||
)
|
||||
});
|
||||
drop(subscription3);
|
||||
|
||||
server.send(proto::UnshareProject { project_id: 1 }).await;
|
||||
server.send(proto::UnshareProject { project_id: 2 }).await;
|
||||
done_rx1.next().await.unwrap();
|
||||
done_rx2.next().await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_subscribing_after_dropping_subscription(mut cx: TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
let user_id = 5;
|
||||
let mut client = Client::new(FakeHttpClient::with_404_response());
|
||||
let server = FakeServer::for_client(user_id, &mut client, &cx).await;
|
||||
|
||||
let model = cx.add_model(|_| Model { subscription: None });
|
||||
let (mut done_tx1, _done_rx1) = postage::oneshot::channel();
|
||||
let (mut done_tx2, mut done_rx2) = postage::oneshot::channel();
|
||||
let subscription1 = model.update(&mut cx, |_, cx| {
|
||||
client.subscribe(cx, move |_, _: TypedEnvelope<proto::Ping>, _, _| {
|
||||
postage::sink::Sink::try_send(&mut done_tx1, ()).unwrap();
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
drop(subscription1);
|
||||
let _subscription2 = model.update(&mut cx, |_, cx| {
|
||||
client.subscribe(cx, move |_, _: TypedEnvelope<proto::Ping>, _, _| {
|
||||
postage::sink::Sink::try_send(&mut done_tx2, ()).unwrap();
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
server.send(proto::Ping {}).await;
|
||||
done_rx2.next().await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dropping_subscription_in_handler(mut cx: TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
let user_id = 5;
|
||||
let mut client = Client::new(FakeHttpClient::with_404_response());
|
||||
let server = FakeServer::for_client(user_id, &mut client, &cx).await;
|
||||
|
||||
let model = cx.add_model(|_| Model { subscription: None });
|
||||
let (mut done_tx, mut done_rx) = postage::oneshot::channel();
|
||||
model.update(&mut cx, |model, cx| {
|
||||
model.subscription = Some(client.subscribe(
|
||||
cx,
|
||||
move |model, _: TypedEnvelope<proto::Ping>, _, _| {
|
||||
model.subscription.take();
|
||||
postage::sink::Sink::try_send(&mut done_tx, ()).unwrap();
|
||||
Ok(())
|
||||
},
|
||||
));
|
||||
});
|
||||
server.send(proto::Ping {}).await;
|
||||
done_rx.next().await.unwrap();
|
||||
}
|
||||
|
||||
struct Model {
|
||||
subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl Entity for Model {
|
||||
type Event = ();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
use super::Client;
|
||||
use super::*;
|
||||
use crate::http::{HttpClient, Request, Response, ServerResponse};
|
||||
use futures::{future::BoxFuture, Future};
|
||||
use gpui::TestAppContext;
|
||||
use futures::{future::BoxFuture, stream::BoxStream, Future, StreamExt};
|
||||
use gpui::{ModelHandle, TestAppContext};
|
||||
use parking_lot::Mutex;
|
||||
use postage::{mpsc, prelude::Stream};
|
||||
use rpc::{proto, ConnectionId, Peer, Receipt, TypedEnvelope};
|
||||
use std::fmt;
|
||||
use std::sync::atomic::Ordering::SeqCst;
|
||||
@@ -15,7 +14,7 @@ use std::sync::{
|
||||
|
||||
pub struct FakeServer {
|
||||
peer: Arc<Peer>,
|
||||
incoming: Mutex<Option<mpsc::Receiver<Box<dyn proto::AnyTypedEnvelope>>>>,
|
||||
incoming: Mutex<Option<BoxStream<'static, Box<dyn proto::AnyTypedEnvelope>>>>,
|
||||
connection_id: Mutex<Option<ConnectionId>>,
|
||||
forbid_connections: AtomicBool,
|
||||
auth_count: AtomicUsize,
|
||||
@@ -72,8 +71,8 @@ impl FakeServer {
|
||||
server
|
||||
}
|
||||
|
||||
pub async fn disconnect(&self) {
|
||||
self.peer.disconnect(self.connection_id()).await;
|
||||
pub fn disconnect(&self) {
|
||||
self.peer.disconnect(self.connection_id());
|
||||
self.connection_id.lock().take();
|
||||
self.incoming.lock().take();
|
||||
}
|
||||
@@ -129,7 +128,7 @@ impl FakeServer {
|
||||
.lock()
|
||||
.as_mut()
|
||||
.expect("not connected")
|
||||
.recv()
|
||||
.next()
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("other half hung up"))?;
|
||||
let type_name = message.payload_type_name();
|
||||
@@ -155,6 +154,24 @@ impl FakeServer {
|
||||
fn connection_id(&self) -> ConnectionId {
|
||||
self.connection_id.lock().expect("not connected")
|
||||
}
|
||||
|
||||
pub async fn build_user_store(
|
||||
&self,
|
||||
client: Arc<Client>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> ModelHandle<UserStore> {
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client, http_client, cx));
|
||||
assert_eq!(
|
||||
self.receive::<proto::GetUsers>()
|
||||
.await
|
||||
.unwrap()
|
||||
.payload
|
||||
.user_ids,
|
||||
&[self.user_id]
|
||||
);
|
||||
user_store
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FakeHttpClient {
|
||||
@@ -172,6 +189,10 @@ impl FakeHttpClient {
|
||||
handler: Box::new(move |req| Box::pin(handler(req))),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_404_response() -> Arc<dyn HttpClient> {
|
||||
Self::new(|_| async move { Ok(ServerResponse::new(404)) })
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for FakeHttpClient {
|
||||
|
||||
@@ -20,26 +20,26 @@ pub struct User {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Collaborator {
|
||||
pub struct Contact {
|
||||
pub user: Arc<User>,
|
||||
pub worktrees: Vec<WorktreeMetadata>,
|
||||
pub projects: Vec<ProjectMetadata>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WorktreeMetadata {
|
||||
pub struct ProjectMetadata {
|
||||
pub id: u64,
|
||||
pub root_name: String,
|
||||
pub is_shared: bool,
|
||||
pub worktree_root_names: Vec<String>,
|
||||
pub guests: Vec<Arc<User>>,
|
||||
}
|
||||
|
||||
pub struct UserStore {
|
||||
users: HashMap<u64, Arc<User>>,
|
||||
current_user: watch::Receiver<Option<Arc<User>>>,
|
||||
collaborators: Arc<[Collaborator]>,
|
||||
rpc: Arc<Client>,
|
||||
contacts: Arc<[Contact]>,
|
||||
client: Arc<Client>,
|
||||
http: Arc<dyn HttpClient>,
|
||||
_maintain_collaborators: Task<()>,
|
||||
_maintain_contacts: Task<()>,
|
||||
_maintain_current_user: Task<()>,
|
||||
}
|
||||
|
||||
@@ -50,39 +50,43 @@ impl Entity for UserStore {
|
||||
}
|
||||
|
||||
impl UserStore {
|
||||
pub fn new(rpc: Arc<Client>, http: Arc<dyn HttpClient>, cx: &mut ModelContext<Self>) -> Self {
|
||||
pub fn new(
|
||||
client: Arc<Client>,
|
||||
http: Arc<dyn HttpClient>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let (mut current_user_tx, current_user_rx) = watch::channel();
|
||||
let (mut update_collaborators_tx, mut update_collaborators_rx) =
|
||||
watch::channel::<Option<proto::UpdateCollaborators>>();
|
||||
let update_collaborators_subscription = rpc.subscribe(
|
||||
let (mut update_contacts_tx, mut update_contacts_rx) =
|
||||
watch::channel::<Option<proto::UpdateContacts>>();
|
||||
let update_contacts_subscription = client.subscribe(
|
||||
cx,
|
||||
move |_: &mut Self, msg: TypedEnvelope<proto::UpdateCollaborators>, _, _| {
|
||||
let _ = update_collaborators_tx.blocking_send(Some(msg.payload));
|
||||
move |_: &mut Self, msg: TypedEnvelope<proto::UpdateContacts>, _, _| {
|
||||
let _ = update_contacts_tx.blocking_send(Some(msg.payload));
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
Self {
|
||||
users: Default::default(),
|
||||
current_user: current_user_rx,
|
||||
collaborators: Arc::from([]),
|
||||
rpc: rpc.clone(),
|
||||
contacts: Arc::from([]),
|
||||
client: client.clone(),
|
||||
http,
|
||||
_maintain_collaborators: cx.spawn_weak(|this, mut cx| async move {
|
||||
let _subscription = update_collaborators_subscription;
|
||||
while let Some(message) = update_collaborators_rx.recv().await {
|
||||
_maintain_contacts: cx.spawn_weak(|this, mut cx| async move {
|
||||
let _subscription = update_contacts_subscription;
|
||||
while let Some(message) = update_contacts_rx.recv().await {
|
||||
if let Some((message, this)) = message.zip(this.upgrade(&cx)) {
|
||||
this.update(&mut cx, |this, cx| this.update_collaborators(message, cx))
|
||||
this.update(&mut cx, |this, cx| this.update_contacts(message, cx))
|
||||
.log_err()
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}),
|
||||
_maintain_current_user: cx.spawn_weak(|this, mut cx| async move {
|
||||
let mut status = rpc.status();
|
||||
let mut status = client.status();
|
||||
while let Some(status) = status.recv().await {
|
||||
match status {
|
||||
Status::Connected { .. } => {
|
||||
if let Some((this, user_id)) = this.upgrade(&cx).zip(rpc.user_id()) {
|
||||
if let Some((this, user_id)) = this.upgrade(&cx).zip(client.user_id()) {
|
||||
let user = this
|
||||
.update(&mut cx, |this, cx| this.fetch_user(user_id, cx))
|
||||
.log_err()
|
||||
@@ -100,35 +104,29 @@ impl UserStore {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_collaborators(
|
||||
fn update_contacts(
|
||||
&mut self,
|
||||
message: proto::UpdateCollaborators,
|
||||
message: proto::UpdateContacts,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let mut user_ids = HashSet::new();
|
||||
for collaborator in &message.collaborators {
|
||||
user_ids.insert(collaborator.user_id);
|
||||
user_ids.extend(
|
||||
collaborator
|
||||
.worktrees
|
||||
.iter()
|
||||
.flat_map(|w| &w.guests)
|
||||
.copied(),
|
||||
);
|
||||
for contact in &message.contacts {
|
||||
user_ids.insert(contact.user_id);
|
||||
user_ids.extend(contact.projects.iter().flat_map(|w| &w.guests).copied());
|
||||
}
|
||||
|
||||
let load_users = self.load_users(user_ids.into_iter().collect(), cx);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
load_users.await?;
|
||||
|
||||
let mut collaborators = Vec::new();
|
||||
for collaborator in message.collaborators {
|
||||
collaborators.push(Collaborator::from_proto(collaborator, &this, &mut cx).await?);
|
||||
let mut contacts = Vec::new();
|
||||
for contact in message.contacts {
|
||||
contacts.push(Contact::from_proto(contact, &this, &mut cx).await?);
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
collaborators.sort_by(|a, b| a.user.github_login.cmp(&b.user.github_login));
|
||||
this.collaborators = collaborators.into();
|
||||
contacts.sort_by(|a, b| a.user.github_login.cmp(&b.user.github_login));
|
||||
this.contacts = contacts.into();
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
@@ -136,8 +134,8 @@ impl UserStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn collaborators(&self) -> &Arc<[Collaborator]> {
|
||||
&self.collaborators
|
||||
pub fn contacts(&self) -> &Arc<[Contact]> {
|
||||
&self.contacts
|
||||
}
|
||||
|
||||
pub fn load_users(
|
||||
@@ -145,7 +143,7 @@ impl UserStore {
|
||||
mut user_ids: Vec<u64>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let rpc = self.rpc.clone();
|
||||
let rpc = self.client.clone();
|
||||
let http = self.http.clone();
|
||||
user_ids.retain(|id| !self.users.contains_key(id));
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
@@ -212,21 +210,21 @@ impl User {
|
||||
}
|
||||
}
|
||||
|
||||
impl Collaborator {
|
||||
impl Contact {
|
||||
async fn from_proto(
|
||||
collaborator: proto::Collaborator,
|
||||
contact: proto::Contact,
|
||||
user_store: &ModelHandle<UserStore>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Self> {
|
||||
let user = user_store
|
||||
.update(cx, |user_store, cx| {
|
||||
user_store.fetch_user(collaborator.user_id, cx)
|
||||
user_store.fetch_user(contact.user_id, cx)
|
||||
})
|
||||
.await?;
|
||||
let mut worktrees = Vec::new();
|
||||
for worktree in collaborator.worktrees {
|
||||
let mut projects = Vec::new();
|
||||
for project in contact.projects {
|
||||
let mut guests = Vec::new();
|
||||
for participant_id in worktree.guests {
|
||||
for participant_id in project.guests {
|
||||
guests.push(
|
||||
user_store
|
||||
.update(cx, |user_store, cx| {
|
||||
@@ -235,14 +233,14 @@ impl Collaborator {
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
worktrees.push(WorktreeMetadata {
|
||||
id: worktree.id,
|
||||
root_name: worktree.root_name,
|
||||
is_shared: worktree.is_shared,
|
||||
projects.push(ProjectMetadata {
|
||||
id: project.id,
|
||||
worktree_root_names: project.worktree_root_names.clone(),
|
||||
is_shared: project.is_shared,
|
||||
guests,
|
||||
});
|
||||
}
|
||||
Ok(Self { user, worktrees })
|
||||
Ok(Self { user, projects })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ name = "clock"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
path = "src/clock.rs"
|
||||
|
||||
[dependencies]
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
rpc = { path = "../rpc" }
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cmp::{self, Ordering},
|
||||
fmt,
|
||||
fmt, iter,
|
||||
ops::{Add, AddAssign},
|
||||
slice,
|
||||
};
|
||||
|
||||
pub type ReplicaId = u16;
|
||||
@@ -22,6 +21,15 @@ pub struct Lamport {
|
||||
}
|
||||
|
||||
impl Local {
|
||||
pub const MIN: Self = Self {
|
||||
replica_id: ReplicaId::MIN,
|
||||
value: Seq::MIN,
|
||||
};
|
||||
pub const MAX: Self = Self {
|
||||
replica_id: ReplicaId::MAX,
|
||||
value: Seq::MAX,
|
||||
};
|
||||
|
||||
pub fn new(replica_id: ReplicaId) -> Self {
|
||||
Self {
|
||||
replica_id,
|
||||
@@ -59,7 +67,7 @@ impl<'a> AddAssign<&'a Local> for Local {
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Hash, Eq, PartialEq)]
|
||||
pub struct Global(SmallVec<[Local; 3]>);
|
||||
pub struct Global(SmallVec<[u32; 8]>);
|
||||
|
||||
impl From<Vec<rpc::proto::VectorClockEntry>> for Global {
|
||||
fn from(message: Vec<rpc::proto::VectorClockEntry>) -> Self {
|
||||
@@ -86,81 +94,125 @@ impl<'a> From<&'a Global> for Vec<rpc::proto::VectorClockEntry> {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Global> for Vec<rpc::proto::VectorClockEntry> {
|
||||
fn from(version: Global) -> Self {
|
||||
(&version).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Global {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn get(&self, replica_id: ReplicaId) -> Seq {
|
||||
self.0
|
||||
.iter()
|
||||
.find(|t| t.replica_id == replica_id)
|
||||
.map_or(0, |t| t.value)
|
||||
self.0.get(replica_id as usize).copied().unwrap_or(0) as Seq
|
||||
}
|
||||
|
||||
pub fn observe(&mut self, timestamp: Local) {
|
||||
if let Some(entry) = self
|
||||
.0
|
||||
.iter_mut()
|
||||
.find(|t| t.replica_id == timestamp.replica_id)
|
||||
{
|
||||
entry.value = cmp::max(entry.value, timestamp.value);
|
||||
} else {
|
||||
self.0.push(timestamp);
|
||||
if timestamp.value > 0 {
|
||||
let new_len = timestamp.replica_id as usize + 1;
|
||||
if new_len > self.0.len() {
|
||||
self.0.resize(new_len, 0);
|
||||
}
|
||||
|
||||
let entry = &mut self.0[timestamp.replica_id as usize];
|
||||
*entry = cmp::max(*entry, timestamp.value);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn join(&mut self, other: &Self) {
|
||||
for timestamp in other.0.iter() {
|
||||
self.observe(*timestamp);
|
||||
if other.0.len() > self.0.len() {
|
||||
self.0.resize(other.0.len(), 0);
|
||||
}
|
||||
|
||||
for (left, right) in self.0.iter_mut().zip(&other.0) {
|
||||
*left = cmp::max(*left, *right);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn meet(&mut self, other: &Self) {
|
||||
for timestamp in other.0.iter() {
|
||||
if let Some(entry) = self
|
||||
.0
|
||||
.iter_mut()
|
||||
.find(|t| t.replica_id == timestamp.replica_id)
|
||||
{
|
||||
entry.value = cmp::min(entry.value, timestamp.value);
|
||||
} else {
|
||||
self.0.push(*timestamp);
|
||||
if other.0.len() > self.0.len() {
|
||||
self.0.resize(other.0.len(), 0);
|
||||
}
|
||||
|
||||
let mut new_len = 0;
|
||||
for (ix, (left, right)) in self
|
||||
.0
|
||||
.iter_mut()
|
||||
.zip(other.0.iter().chain(iter::repeat(&0)))
|
||||
.enumerate()
|
||||
{
|
||||
if *left == 0 {
|
||||
*left = *right;
|
||||
} else if *right > 0 {
|
||||
*left = cmp::min(*left, *right);
|
||||
}
|
||||
|
||||
if *left != 0 {
|
||||
new_len = ix + 1;
|
||||
}
|
||||
}
|
||||
self.0.resize(new_len, 0);
|
||||
}
|
||||
|
||||
pub fn observed(&self, timestamp: Local) -> bool {
|
||||
self.get(timestamp.replica_id) >= timestamp.value
|
||||
}
|
||||
|
||||
pub fn changed_since(&self, other: &Self) -> bool {
|
||||
self.0.iter().any(|t| t.value > other.get(t.replica_id))
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> slice::Iter<Local> {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Global {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
let mut global_ordering = Ordering::Equal;
|
||||
|
||||
for timestamp in self.0.iter().chain(other.0.iter()) {
|
||||
let ordering = self
|
||||
.get(timestamp.replica_id)
|
||||
.cmp(&other.get(timestamp.replica_id));
|
||||
if ordering != Ordering::Equal {
|
||||
if global_ordering == Ordering::Equal {
|
||||
global_ordering = ordering;
|
||||
} else if ordering != global_ordering {
|
||||
return None;
|
||||
pub fn observed_any(&self, other: &Self) -> bool {
|
||||
let mut lhs = self.0.iter();
|
||||
let mut rhs = other.0.iter();
|
||||
loop {
|
||||
if let Some(left) = lhs.next() {
|
||||
if let Some(right) = rhs.next() {
|
||||
if *right > 0 && left >= right {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(global_ordering)
|
||||
pub fn observed_all(&self, other: &Self) -> bool {
|
||||
let mut lhs = self.0.iter();
|
||||
let mut rhs = other.0.iter();
|
||||
loop {
|
||||
if let Some(left) = lhs.next() {
|
||||
if let Some(right) = rhs.next() {
|
||||
if left < right {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return rhs.next().is_none();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn changed_since(&self, other: &Self) -> bool {
|
||||
if self.0.len() > other.0.len() {
|
||||
return true;
|
||||
}
|
||||
for (left, right) in self.0.iter().zip(other.0.iter()) {
|
||||
if left > right {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn iter<'a>(&'a self) -> impl 'a + Iterator<Item = Local> {
|
||||
self.0.iter().enumerate().map(|(replica_id, seq)| Local {
|
||||
replica_id: replica_id as ReplicaId,
|
||||
value: *seq,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,11 +265,11 @@ impl fmt::Debug for Lamport {
|
||||
impl fmt::Debug for Global {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Global {{")?;
|
||||
for (i, element) in self.0.iter().enumerate() {
|
||||
if i > 0 {
|
||||
for timestamp in self.iter() {
|
||||
if timestamp.replica_id > 0 {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
write!(f, "{}: {}", element.replica_id, element.value)?;
|
||||
write!(f, "{}: {}", timestamp.replica_id, timestamp.value)?;
|
||||
}
|
||||
write!(f, "}}")
|
||||
}
|
||||
13
crates/collections/Cargo.toml
Normal file
13
crates/collections/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "collections"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/collections.rs"
|
||||
|
||||
[features]
|
||||
test-support = ["seahash"]
|
||||
|
||||
[dependencies]
|
||||
seahash = { version = "4.1", optional = true }
|
||||
26
crates/collections/src/collections.rs
Normal file
26
crates/collections/src/collections.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
#[cfg(feature = "test-support")]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct DeterministicState;
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
impl std::hash::BuildHasher for DeterministicState {
|
||||
type Hasher = seahash::SeaHasher;
|
||||
|
||||
fn build_hasher(&self) -> Self::Hasher {
|
||||
seahash::SeaHasher::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub type HashMap<K, V> = std::collections::HashMap<K, V, DeterministicState>;
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub type HashSet<T> = std::collections::HashSet<T, DeterministicState>;
|
||||
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
pub type HashMap<K, V> = std::collections::HashMap<K, V>;
|
||||
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
pub type HashSet<T> = std::collections::HashSet<T>;
|
||||
|
||||
pub use std::collections::*;
|
||||
@@ -1,8 +1,11 @@
|
||||
[package]
|
||||
name = "people_panel"
|
||||
name = "contacts_panel"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
path = "src/contacts_panel.rs"
|
||||
|
||||
[dependencies]
|
||||
client = { path = "../client" }
|
||||
gpui = { path = "../gpui" }
|
||||
@@ -1,131 +1,70 @@
|
||||
use client::{Collaborator, UserStore};
|
||||
use std::sync::Arc;
|
||||
|
||||
use client::{Contact, UserStore};
|
||||
use gpui::{
|
||||
action,
|
||||
elements::*,
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
platform::CursorStyle,
|
||||
Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext, RenderContext,
|
||||
Subscription, View, ViewContext,
|
||||
Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, View,
|
||||
ViewContext,
|
||||
};
|
||||
use postage::watch;
|
||||
use theme::Theme;
|
||||
use workspace::{Settings, Workspace};
|
||||
use workspace::{AppState, JoinProject, JoinProjectParams, Settings};
|
||||
|
||||
action!(JoinWorktree, u64);
|
||||
action!(LeaveWorktree, u64);
|
||||
action!(ShareWorktree, u64);
|
||||
action!(UnshareWorktree, u64);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(PeoplePanel::share_worktree);
|
||||
cx.add_action(PeoplePanel::unshare_worktree);
|
||||
cx.add_action(PeoplePanel::join_worktree);
|
||||
cx.add_action(PeoplePanel::leave_worktree);
|
||||
}
|
||||
|
||||
pub struct PeoplePanel {
|
||||
collaborators: ListState,
|
||||
pub struct ContactsPanel {
|
||||
contacts: ListState,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
settings: watch::Receiver<Settings>,
|
||||
_maintain_collaborators: Subscription,
|
||||
_maintain_contacts: Subscription,
|
||||
}
|
||||
|
||||
impl PeoplePanel {
|
||||
pub fn new(
|
||||
user_store: ModelHandle<UserStore>,
|
||||
settings: watch::Receiver<Settings>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
impl ContactsPanel {
|
||||
pub fn new(app_state: Arc<AppState>, cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
collaborators: ListState::new(
|
||||
user_store.read(cx).collaborators().len(),
|
||||
contacts: ListState::new(
|
||||
app_state.user_store.read(cx).contacts().len(),
|
||||
Orientation::Top,
|
||||
1000.,
|
||||
{
|
||||
let user_store = user_store.clone();
|
||||
let settings = settings.clone();
|
||||
let app_state = app_state.clone();
|
||||
move |ix, cx| {
|
||||
let user_store = user_store.read(cx);
|
||||
let collaborators = user_store.collaborators().clone();
|
||||
let user_store = app_state.user_store.read(cx);
|
||||
let contacts = user_store.contacts().clone();
|
||||
let current_user_id = user_store.current_user().map(|user| user.id);
|
||||
Self::render_collaborator(
|
||||
&collaborators[ix],
|
||||
&contacts[ix],
|
||||
current_user_id,
|
||||
&settings.borrow().theme,
|
||||
app_state.clone(),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
_maintain_collaborators: cx.observe(&user_store, Self::update_collaborators),
|
||||
user_store,
|
||||
settings,
|
||||
_maintain_contacts: cx.observe(&app_state.user_store, Self::update_contacts),
|
||||
user_store: app_state.user_store.clone(),
|
||||
settings: app_state.settings.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn share_worktree(
|
||||
workspace: &mut Workspace,
|
||||
action: &ShareWorktree,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
workspace
|
||||
.project()
|
||||
.update(cx, |p, cx| p.share_worktree(action.0, cx));
|
||||
}
|
||||
|
||||
fn unshare_worktree(
|
||||
workspace: &mut Workspace,
|
||||
action: &UnshareWorktree,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
workspace
|
||||
.project()
|
||||
.update(cx, |p, cx| p.unshare_worktree(action.0, cx));
|
||||
}
|
||||
|
||||
fn join_worktree(
|
||||
workspace: &mut Workspace,
|
||||
action: &JoinWorktree,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
workspace
|
||||
.project()
|
||||
.update(cx, |p, cx| p.add_remote_worktree(action.0, cx).detach());
|
||||
}
|
||||
|
||||
fn leave_worktree(
|
||||
workspace: &mut Workspace,
|
||||
action: &LeaveWorktree,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
workspace
|
||||
.project()
|
||||
.update(cx, |p, cx| p.close_remote_worktree(action.0, cx));
|
||||
}
|
||||
|
||||
fn update_collaborators(&mut self, _: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) {
|
||||
self.collaborators
|
||||
.reset(self.user_store.read(cx).collaborators().len());
|
||||
fn update_contacts(&mut self, _: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) {
|
||||
self.contacts
|
||||
.reset(self.user_store.read(cx).contacts().len());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_collaborator(
|
||||
collaborator: &Collaborator,
|
||||
collaborator: &Contact,
|
||||
current_user_id: Option<u64>,
|
||||
theme: &Theme,
|
||||
app_state: Arc<AppState>,
|
||||
cx: &mut LayoutContext,
|
||||
) -> ElementBox {
|
||||
let theme = &theme.people_panel;
|
||||
let worktree_count = collaborator.worktrees.len();
|
||||
let theme = &app_state.settings.borrow().theme.contacts_panel;
|
||||
let project_count = collaborator.projects.len();
|
||||
let font_cache = cx.font_cache();
|
||||
let line_height = theme.unshared_worktree.name.text.line_height(font_cache);
|
||||
let cap_height = theme.unshared_worktree.name.text.cap_height(font_cache);
|
||||
let baseline_offset = theme
|
||||
.unshared_worktree
|
||||
.name
|
||||
.text
|
||||
.baseline_offset(font_cache)
|
||||
+ (theme.unshared_worktree.height - line_height) / 2.;
|
||||
let line_height = theme.unshared_project.name.text.line_height(font_cache);
|
||||
let cap_height = theme.unshared_project.name.text.cap_height(font_cache);
|
||||
let baseline_offset = theme.unshared_project.name.text.baseline_offset(font_cache)
|
||||
+ (theme.unshared_project.height - line_height) / 2.;
|
||||
let tree_branch_width = theme.tree_branch_width;
|
||||
let tree_branch_color = theme.tree_branch_color;
|
||||
let host_avatar_height = theme
|
||||
@@ -161,11 +100,11 @@ impl PeoplePanel {
|
||||
)
|
||||
.with_children(
|
||||
collaborator
|
||||
.worktrees
|
||||
.projects
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, worktree)| {
|
||||
let worktree_id = worktree.id;
|
||||
.map(|(ix, project)| {
|
||||
let project_id = project.id;
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
@@ -182,7 +121,7 @@ impl PeoplePanel {
|
||||
vec2f(start_x, start_y),
|
||||
vec2f(
|
||||
start_x + tree_branch_width,
|
||||
if ix + 1 == worktree_count {
|
||||
if ix + 1 == project_count {
|
||||
end_y
|
||||
} else {
|
||||
bounds.max_y()
|
||||
@@ -210,28 +149,28 @@ impl PeoplePanel {
|
||||
.with_child({
|
||||
let is_host = Some(collaborator.user.id) == current_user_id;
|
||||
let is_guest = !is_host
|
||||
&& worktree
|
||||
&& project
|
||||
.guests
|
||||
.iter()
|
||||
.any(|guest| Some(guest.id) == current_user_id);
|
||||
let is_shared = worktree.is_shared;
|
||||
let is_shared = project.is_shared;
|
||||
let app_state = app_state.clone();
|
||||
|
||||
MouseEventHandler::new::<PeoplePanel, _, _, _>(
|
||||
worktree_id as usize,
|
||||
MouseEventHandler::new::<ContactsPanel, _, _, _>(
|
||||
project_id as usize,
|
||||
cx,
|
||||
|mouse_state, _| {
|
||||
let style = match (worktree.is_shared, mouse_state.hovered)
|
||||
{
|
||||
(false, false) => &theme.unshared_worktree,
|
||||
(false, true) => &theme.hovered_unshared_worktree,
|
||||
(true, false) => &theme.shared_worktree,
|
||||
(true, true) => &theme.hovered_shared_worktree,
|
||||
let style = match (project.is_shared, mouse_state.hovered) {
|
||||
(false, false) => &theme.unshared_project,
|
||||
(false, true) => &theme.hovered_unshared_project,
|
||||
(true, false) => &theme.shared_project,
|
||||
(true, true) => &theme.hovered_shared_project,
|
||||
};
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(
|
||||
worktree.root_name.clone(),
|
||||
project.worktree_root_names.join(", "),
|
||||
style.name.text.clone(),
|
||||
)
|
||||
.aligned()
|
||||
@@ -240,7 +179,7 @@ impl PeoplePanel {
|
||||
.with_style(style.name.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(worktree.guests.iter().filter_map(
|
||||
.with_children(project.guests.iter().filter_map(
|
||||
|participant| {
|
||||
participant.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
@@ -268,23 +207,18 @@ impl PeoplePanel {
|
||||
CursorStyle::Arrow
|
||||
})
|
||||
.on_click(move |cx| {
|
||||
if is_shared {
|
||||
if is_host {
|
||||
cx.dispatch_action(UnshareWorktree(worktree_id));
|
||||
} else if is_guest {
|
||||
cx.dispatch_action(LeaveWorktree(worktree_id));
|
||||
} else {
|
||||
cx.dispatch_action(JoinWorktree(worktree_id))
|
||||
}
|
||||
} else if is_host {
|
||||
cx.dispatch_action(ShareWorktree(worktree_id));
|
||||
if !is_host && !is_guest {
|
||||
cx.dispatch_global_action(JoinProject(JoinProjectParams {
|
||||
project_id,
|
||||
app_state: app_state.clone(),
|
||||
}));
|
||||
}
|
||||
})
|
||||
.expanded(1.0)
|
||||
.flexible(1., true)
|
||||
.boxed()
|
||||
})
|
||||
.constrained()
|
||||
.with_height(theme.unshared_worktree.height)
|
||||
.with_height(theme.unshared_project.height)
|
||||
.boxed()
|
||||
}),
|
||||
)
|
||||
@@ -294,18 +228,18 @@ impl PeoplePanel {
|
||||
|
||||
pub enum Event {}
|
||||
|
||||
impl Entity for PeoplePanel {
|
||||
impl Entity for ContactsPanel {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for PeoplePanel {
|
||||
impl View for ContactsPanel {
|
||||
fn ui_name() -> &'static str {
|
||||
"PeoplePanel"
|
||||
"ContactsPanel"
|
||||
}
|
||||
|
||||
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = &self.settings.borrow().theme.people_panel;
|
||||
Container::new(List::new(self.collaborators.clone()).boxed())
|
||||
let theme = &self.settings.borrow().theme.contacts_panel;
|
||||
Container::new(List::new(self.contacts.clone()).boxed())
|
||||
.with_style(theme.container)
|
||||
.boxed()
|
||||
}
|
||||
27
crates/diagnostics/Cargo.toml
Normal file
27
crates/diagnostics/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "diagnostics"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/diagnostics.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
collections = { path = "../collections" }
|
||||
editor = { path = "../editor" }
|
||||
language = { path = "../language" }
|
||||
gpui = { path = "../gpui" }
|
||||
project = { path = "../project" }
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
postage = { version = "0.4", features = ["futures-traits"] }
|
||||
|
||||
[dev-dependencies]
|
||||
unindent = "0.1"
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
1106
crates/diagnostics/src/diagnostics.rs
Normal file
1106
crates/diagnostics/src/diagnostics.rs
Normal file
File diff suppressed because it is too large
Load Diff
87
crates/diagnostics/src/items.rs
Normal file
87
crates/diagnostics/src/items.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use gpui::{
|
||||
elements::*, platform::CursorStyle, Entity, ModelHandle, RenderContext, View, ViewContext,
|
||||
};
|
||||
use postage::watch;
|
||||
use project::Project;
|
||||
use std::fmt::Write;
|
||||
use workspace::{Settings, StatusItemView};
|
||||
|
||||
pub struct DiagnosticSummary {
|
||||
settings: watch::Receiver<Settings>,
|
||||
summary: project::DiagnosticSummary,
|
||||
in_progress: bool,
|
||||
}
|
||||
|
||||
impl DiagnosticSummary {
|
||||
pub fn new(
|
||||
project: &ModelHandle<Project>,
|
||||
settings: watch::Receiver<Settings>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
cx.subscribe(project, |this, project, event, cx| match event {
|
||||
project::Event::DiskBasedDiagnosticsUpdated { .. } => {
|
||||
this.summary = project.read(cx).diagnostic_summary(cx);
|
||||
cx.notify();
|
||||
}
|
||||
project::Event::DiskBasedDiagnosticsStarted => {
|
||||
this.in_progress = true;
|
||||
cx.notify();
|
||||
}
|
||||
project::Event::DiskBasedDiagnosticsFinished => {
|
||||
this.in_progress = false;
|
||||
cx.notify();
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
Self {
|
||||
settings,
|
||||
summary: project.read(cx).diagnostic_summary(cx),
|
||||
in_progress: project.read(cx).is_running_disk_based_diagnostics(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for DiagnosticSummary {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for DiagnosticSummary {
|
||||
fn ui_name() -> &'static str {
|
||||
"DiagnosticSummary"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
enum Tag {}
|
||||
|
||||
let theme = &self.settings.borrow().theme.project_diagnostics;
|
||||
let mut message = String::new();
|
||||
if self.in_progress {
|
||||
message.push_str("Checking... ");
|
||||
}
|
||||
write!(
|
||||
message,
|
||||
"Errors: {}, Warnings: {}",
|
||||
self.summary.error_count, self.summary.warning_count
|
||||
)
|
||||
.unwrap();
|
||||
MouseEventHandler::new::<Tag, _, _, _>(0, cx, |_, _| {
|
||||
Label::new(message, theme.status_bar_item.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.status_bar_item.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(|cx| cx.dispatch_action(crate::Deploy))
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for DiagnosticSummary {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
_: Option<&dyn workspace::ItemViewHandle>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,51 @@
|
||||
[package]
|
||||
name = "editor"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/editor.rs"
|
||||
|
||||
[features]
|
||||
test-support = ["buffer/test-support", "gpui/test-support"]
|
||||
test-support = [
|
||||
"rand",
|
||||
"text/test-support",
|
||||
"language/test-support",
|
||||
"gpui/test-support",
|
||||
"util/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
buffer = { path = "../buffer" }
|
||||
text = { path = "../text" }
|
||||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
project = { path = "../project" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
aho-corasick = "0.7"
|
||||
anyhow = "1.0"
|
||||
itertools = "0.10"
|
||||
lazy_static = "1.4"
|
||||
log = "0.4"
|
||||
parking_lot = "0.11"
|
||||
postage = { version = "0.4", features = ["futures-traits"] }
|
||||
rand = { version = "0.8.3", optional = true }
|
||||
serde = { version = "1", features = ["derive", "rc"] }
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "1.2"
|
||||
|
||||
[dev-dependencies]
|
||||
buffer = { path = "../buffer", features = ["test-support"] }
|
||||
text = { path = "../text", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
ctor = "0.1"
|
||||
env_logger = "0.8"
|
||||
rand = "0.8"
|
||||
unindent = "0.1.7"
|
||||
tree-sitter = "0.19"
|
||||
tree-sitter-rust = "0.19"
|
||||
tree-sitter = "0.20"
|
||||
tree-sitter-rust = "0.20"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1440
crates/editor/src/display_map/block_map.rs
Normal file
1440
crates/editor/src/display_map/block_map.rs
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,17 @@
|
||||
use super::fold_map::{self, FoldEdit, FoldPoint, Snapshot as FoldSnapshot};
|
||||
use buffer::{rope, HighlightId};
|
||||
use super::fold_map::{self, FoldEdit, FoldPoint, FoldSnapshot, ToFoldPoint};
|
||||
use crate::MultiBufferSnapshot;
|
||||
use language::{rope, Chunk};
|
||||
use parking_lot::Mutex;
|
||||
use std::{mem, ops::Range};
|
||||
use std::{cmp, mem, ops::Range};
|
||||
use sum_tree::Bias;
|
||||
use text::Point;
|
||||
use theme::SyntaxTheme;
|
||||
|
||||
pub struct TabMap(Mutex<Snapshot>);
|
||||
pub struct TabMap(Mutex<TabSnapshot>);
|
||||
|
||||
impl TabMap {
|
||||
pub fn new(input: FoldSnapshot, tab_size: usize) -> (Self, Snapshot) {
|
||||
let snapshot = Snapshot {
|
||||
pub fn new(input: FoldSnapshot, tab_size: usize) -> (Self, TabSnapshot) {
|
||||
let snapshot = TabSnapshot {
|
||||
fold_snapshot: input,
|
||||
tab_size,
|
||||
};
|
||||
@@ -19,9 +22,10 @@ impl TabMap {
|
||||
&self,
|
||||
fold_snapshot: FoldSnapshot,
|
||||
mut fold_edits: Vec<FoldEdit>,
|
||||
) -> (Snapshot, Vec<Edit>) {
|
||||
) -> (TabSnapshot, Vec<TabEdit>) {
|
||||
let mut old_snapshot = self.0.lock();
|
||||
let new_snapshot = Snapshot {
|
||||
let max_offset = old_snapshot.fold_snapshot.len();
|
||||
let new_snapshot = TabSnapshot {
|
||||
fold_snapshot,
|
||||
tab_size: old_snapshot.tab_size,
|
||||
};
|
||||
@@ -31,19 +35,19 @@ impl TabMap {
|
||||
let mut delta = 0;
|
||||
for chunk in old_snapshot
|
||||
.fold_snapshot
|
||||
.chunks_at(fold_edit.old_bytes.end)
|
||||
.chunks(fold_edit.old.end..max_offset, None)
|
||||
{
|
||||
let patterns: &[_] = &['\t', '\n'];
|
||||
if let Some(ix) = chunk.find(patterns) {
|
||||
if &chunk[ix..ix + 1] == "\t" {
|
||||
fold_edit.old_bytes.end.0 += delta + ix + 1;
|
||||
fold_edit.new_bytes.end.0 += delta + ix + 1;
|
||||
if let Some(ix) = chunk.text.find(patterns) {
|
||||
if &chunk.text[ix..ix + 1] == "\t" {
|
||||
fold_edit.old.end.0 += delta + ix + 1;
|
||||
fold_edit.new.end.0 += delta + ix + 1;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
delta += chunk.len();
|
||||
delta += chunk.text.len();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,9 +56,9 @@ impl TabMap {
|
||||
let (prev_edits, next_edits) = fold_edits.split_at_mut(ix);
|
||||
let prev_edit = prev_edits.last_mut().unwrap();
|
||||
let edit = &next_edits[0];
|
||||
if prev_edit.old_bytes.end >= edit.old_bytes.start {
|
||||
prev_edit.old_bytes.end = edit.old_bytes.end;
|
||||
prev_edit.new_bytes.end = edit.new_bytes.end;
|
||||
if prev_edit.old.end >= edit.old.start {
|
||||
prev_edit.old.end = edit.old.end;
|
||||
prev_edit.new.end = edit.new.end;
|
||||
fold_edits.remove(ix);
|
||||
} else {
|
||||
ix += 1;
|
||||
@@ -62,25 +66,13 @@ impl TabMap {
|
||||
}
|
||||
|
||||
for fold_edit in fold_edits {
|
||||
let old_start = fold_edit
|
||||
.old_bytes
|
||||
.start
|
||||
.to_point(&old_snapshot.fold_snapshot);
|
||||
let old_end = fold_edit
|
||||
.old_bytes
|
||||
.end
|
||||
.to_point(&old_snapshot.fold_snapshot);
|
||||
let new_start = fold_edit
|
||||
.new_bytes
|
||||
.start
|
||||
.to_point(&new_snapshot.fold_snapshot);
|
||||
let new_end = fold_edit
|
||||
.new_bytes
|
||||
.end
|
||||
.to_point(&new_snapshot.fold_snapshot);
|
||||
tab_edits.push(Edit {
|
||||
old_lines: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
|
||||
new_lines: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
|
||||
let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot);
|
||||
let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
|
||||
let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot);
|
||||
let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
|
||||
tab_edits.push(TabEdit {
|
||||
old: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
|
||||
new: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -90,12 +82,16 @@ impl TabMap {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Snapshot {
|
||||
pub struct TabSnapshot {
|
||||
pub fold_snapshot: FoldSnapshot,
|
||||
pub tab_size: usize,
|
||||
}
|
||||
|
||||
impl Snapshot {
|
||||
impl TabSnapshot {
|
||||
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
|
||||
self.fold_snapshot.buffer_snapshot()
|
||||
}
|
||||
|
||||
pub fn text_summary(&self) -> TextSummary {
|
||||
self.text_summary_for_range(TabPoint::zero()..self.max_point())
|
||||
}
|
||||
@@ -108,28 +104,31 @@ impl Snapshot {
|
||||
.text_summary_for_range(input_start..input_end);
|
||||
|
||||
let mut first_line_chars = 0;
|
||||
let mut first_line_bytes = 0;
|
||||
for c in self.chunks_at(range.start).flat_map(|chunk| chunk.chars()) {
|
||||
if c == '\n'
|
||||
|| (range.start.row() == range.end.row() && first_line_bytes == range.end.column())
|
||||
{
|
||||
let line_end = if range.start.row() == range.end.row() {
|
||||
range.end
|
||||
} else {
|
||||
self.max_point()
|
||||
};
|
||||
for c in self
|
||||
.chunks(range.start..line_end, None)
|
||||
.flat_map(|chunk| chunk.text.chars())
|
||||
{
|
||||
if c == '\n' {
|
||||
break;
|
||||
}
|
||||
first_line_chars += 1;
|
||||
first_line_bytes += c.len_utf8() as u32;
|
||||
}
|
||||
|
||||
let mut last_line_chars = 0;
|
||||
let mut last_line_bytes = 0;
|
||||
for c in self
|
||||
.chunks_at(TabPoint::new(range.end.row(), 0).max(range.start))
|
||||
.flat_map(|chunk| chunk.chars())
|
||||
{
|
||||
if last_line_bytes == range.end.column() {
|
||||
break;
|
||||
if range.start.row() == range.end.row() {
|
||||
last_line_chars = first_line_chars;
|
||||
} else {
|
||||
for _ in self
|
||||
.chunks(TabPoint::new(range.end.row(), 0)..range.end, None)
|
||||
.flat_map(|chunk| chunk.text.chars())
|
||||
{
|
||||
last_line_chars += 1;
|
||||
}
|
||||
last_line_chars += 1;
|
||||
last_line_bytes += c.len_utf8() as u32;
|
||||
}
|
||||
|
||||
TextSummary {
|
||||
@@ -145,21 +144,11 @@ impl Snapshot {
|
||||
self.fold_snapshot.version
|
||||
}
|
||||
|
||||
pub fn chunks_at(&self, point: TabPoint) -> Chunks {
|
||||
let (point, expanded_char_column, to_next_stop) = self.to_fold_point(point, Bias::Left);
|
||||
let fold_chunks = self
|
||||
.fold_snapshot
|
||||
.chunks_at(point.to_offset(&self.fold_snapshot));
|
||||
Chunks {
|
||||
fold_chunks,
|
||||
column: expanded_char_column,
|
||||
tab_size: self.tab_size,
|
||||
chunk: &SPACES[0..to_next_stop],
|
||||
skip_leading_tab: to_next_stop > 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn highlighted_chunks(&mut self, range: Range<TabPoint>) -> HighlightedChunks {
|
||||
pub fn chunks<'a>(
|
||||
&'a self,
|
||||
range: Range<TabPoint>,
|
||||
theme: Option<&'a SyntaxTheme>,
|
||||
) -> TabChunks<'a> {
|
||||
let (input_start, expanded_char_column, to_next_stop) =
|
||||
self.to_fold_point(range.start, Bias::Left);
|
||||
let input_start = input_start.to_offset(&self.fold_snapshot);
|
||||
@@ -167,25 +156,35 @@ impl Snapshot {
|
||||
.to_fold_point(range.end, Bias::Right)
|
||||
.0
|
||||
.to_offset(&self.fold_snapshot);
|
||||
HighlightedChunks {
|
||||
fold_chunks: self
|
||||
.fold_snapshot
|
||||
.highlighted_chunks(input_start..input_end),
|
||||
let to_next_stop = if range.start.0 + Point::new(0, to_next_stop as u32) > range.end.0 {
|
||||
(range.end.column() - range.start.column()) as usize
|
||||
} else {
|
||||
to_next_stop
|
||||
};
|
||||
|
||||
TabChunks {
|
||||
fold_chunks: self.fold_snapshot.chunks(input_start..input_end, theme),
|
||||
column: expanded_char_column,
|
||||
output_position: range.start.0,
|
||||
max_output_position: range.end.0,
|
||||
tab_size: self.tab_size,
|
||||
chunk: &SPACES[0..to_next_stop],
|
||||
chunk: Chunk {
|
||||
text: &SPACES[0..to_next_stop],
|
||||
..Default::default()
|
||||
},
|
||||
skip_leading_tab: to_next_stop > 0,
|
||||
style_id: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer_rows(&self, row: u32) -> fold_map::BufferRows {
|
||||
pub fn buffer_rows(&self, row: u32) -> fold_map::FoldBufferRows {
|
||||
self.fold_snapshot.buffer_rows(row)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn text(&self) -> String {
|
||||
self.chunks_at(Default::default()).collect()
|
||||
self.chunks(TabPoint::zero()..self.max_point(), None)
|
||||
.map(|chunk| chunk.text)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn max_point(&self) -> TabPoint {
|
||||
@@ -205,6 +204,10 @@ impl Snapshot {
|
||||
TabPoint::new(input.row(), expanded as u32)
|
||||
}
|
||||
|
||||
pub fn from_point(&self, point: Point, bias: Bias) -> TabPoint {
|
||||
self.to_tab_point(point.to_fold_point(&self.fold_snapshot, bias))
|
||||
}
|
||||
|
||||
pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, usize, usize) {
|
||||
let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
|
||||
let expanded = output.column() as usize;
|
||||
@@ -217,6 +220,12 @@ impl Snapshot {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
|
||||
self.to_fold_point(point, bias)
|
||||
.0
|
||||
.to_buffer_point(&self.fold_snapshot)
|
||||
}
|
||||
|
||||
fn expand_tabs(chars: impl Iterator<Item = char>, column: usize, tab_size: usize) -> usize {
|
||||
let mut expanded_chars = 0;
|
||||
let mut expanded_bytes = 0;
|
||||
@@ -306,11 +315,7 @@ impl From<super::Point> for TabPoint {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Edit {
|
||||
pub old_lines: Range<TabPoint>,
|
||||
pub new_lines: Range<TabPoint>,
|
||||
}
|
||||
pub type TabEdit = text::Edit<TabPoint>;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct TextSummary {
|
||||
@@ -364,23 +369,25 @@ impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
|
||||
// Handles a tab width <= 16
|
||||
const SPACES: &'static str = " ";
|
||||
|
||||
pub struct Chunks<'a> {
|
||||
fold_chunks: fold_map::Chunks<'a>,
|
||||
chunk: &'a str,
|
||||
pub struct TabChunks<'a> {
|
||||
fold_chunks: fold_map::FoldChunks<'a>,
|
||||
chunk: Chunk<'a>,
|
||||
column: usize,
|
||||
output_position: Point,
|
||||
max_output_position: Point,
|
||||
tab_size: usize,
|
||||
skip_leading_tab: bool,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Chunks<'a> {
|
||||
type Item = &'a str;
|
||||
impl<'a> Iterator for TabChunks<'a> {
|
||||
type Item = Chunk<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.chunk.is_empty() {
|
||||
if self.chunk.text.is_empty() {
|
||||
if let Some(chunk) = self.fold_chunks.next() {
|
||||
self.chunk = chunk;
|
||||
if self.skip_leading_tab {
|
||||
self.chunk = &self.chunk[1..];
|
||||
self.chunk.text = &self.chunk.text[1..];
|
||||
self.skip_leading_tab = false;
|
||||
}
|
||||
} else {
|
||||
@@ -388,88 +395,121 @@ impl<'a> Iterator for Chunks<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
for (ix, c) in self.chunk.char_indices() {
|
||||
for (ix, c) in self.chunk.text.char_indices() {
|
||||
match c {
|
||||
'\t' => {
|
||||
if ix > 0 {
|
||||
let (prefix, suffix) = self.chunk.split_at(ix);
|
||||
self.chunk = suffix;
|
||||
return Some(prefix);
|
||||
let (prefix, suffix) = self.chunk.text.split_at(ix);
|
||||
self.chunk.text = suffix;
|
||||
return Some(Chunk {
|
||||
text: prefix,
|
||||
..self.chunk
|
||||
});
|
||||
} else {
|
||||
self.chunk = &self.chunk[1..];
|
||||
let len = self.tab_size - self.column % self.tab_size;
|
||||
self.chunk.text = &self.chunk.text[1..];
|
||||
let mut len = self.tab_size - self.column % self.tab_size;
|
||||
let next_output_position = cmp::min(
|
||||
self.output_position + Point::new(0, len as u32),
|
||||
self.max_output_position,
|
||||
);
|
||||
len = (next_output_position.column - self.output_position.column) as usize;
|
||||
self.column += len;
|
||||
return Some(&SPACES[0..len]);
|
||||
self.output_position = next_output_position;
|
||||
return Some(Chunk {
|
||||
text: &SPACES[0..len],
|
||||
..self.chunk
|
||||
});
|
||||
}
|
||||
}
|
||||
'\n' => self.column = 0,
|
||||
_ => self.column += 1,
|
||||
}
|
||||
}
|
||||
|
||||
let result = Some(self.chunk);
|
||||
self.chunk = "";
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HighlightedChunks<'a> {
|
||||
fold_chunks: fold_map::HighlightedChunks<'a>,
|
||||
chunk: &'a str,
|
||||
style_id: HighlightId,
|
||||
column: usize,
|
||||
tab_size: usize,
|
||||
skip_leading_tab: bool,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for HighlightedChunks<'a> {
|
||||
type Item = (&'a str, HighlightId);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.chunk.is_empty() {
|
||||
if let Some((chunk, style_id)) = self.fold_chunks.next() {
|
||||
self.chunk = chunk;
|
||||
self.style_id = style_id;
|
||||
if self.skip_leading_tab {
|
||||
self.chunk = &self.chunk[1..];
|
||||
self.skip_leading_tab = false;
|
||||
'\n' => {
|
||||
self.column = 0;
|
||||
self.output_position += Point::new(1, 0);
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
for (ix, c) in self.chunk.char_indices() {
|
||||
match c {
|
||||
'\t' => {
|
||||
if ix > 0 {
|
||||
let (prefix, suffix) = self.chunk.split_at(ix);
|
||||
self.chunk = suffix;
|
||||
return Some((prefix, self.style_id));
|
||||
} else {
|
||||
self.chunk = &self.chunk[1..];
|
||||
let len = self.tab_size - self.column % self.tab_size;
|
||||
self.column += len;
|
||||
return Some((&SPACES[0..len], self.style_id));
|
||||
}
|
||||
_ => {
|
||||
self.column += 1;
|
||||
self.output_position.column += c.len_utf8() as u32;
|
||||
}
|
||||
'\n' => self.column = 0,
|
||||
_ => self.column += 1,
|
||||
}
|
||||
}
|
||||
|
||||
Some((mem::take(&mut self.chunk), mem::take(&mut self.style_id)))
|
||||
Some(mem::take(&mut self.chunk))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{display_map::fold_map::FoldMap, MultiBuffer};
|
||||
use rand::{prelude::StdRng, Rng};
|
||||
use text::{RandomCharIter, Rope};
|
||||
|
||||
#[test]
|
||||
fn test_expand_tabs() {
|
||||
assert_eq!(Snapshot::expand_tabs("\t".chars(), 0, 4), 0);
|
||||
assert_eq!(Snapshot::expand_tabs("\t".chars(), 1, 4), 4);
|
||||
assert_eq!(Snapshot::expand_tabs("\ta".chars(), 2, 4), 5);
|
||||
assert_eq!(TabSnapshot::expand_tabs("\t".chars(), 0, 4), 0);
|
||||
assert_eq!(TabSnapshot::expand_tabs("\t".chars(), 1, 4), 4);
|
||||
assert_eq!(TabSnapshot::expand_tabs("\ta".chars(), 2, 4), 5);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_tabs(cx: &mut gpui::MutableAppContext, mut rng: StdRng) {
|
||||
let tab_size = rng.gen_range(1..=4);
|
||||
let len = rng.gen_range(0..30);
|
||||
let buffer = if rng.gen() {
|
||||
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
|
||||
MultiBuffer::build_simple(&text, cx)
|
||||
} else {
|
||||
MultiBuffer::build_random(&mut rng, cx)
|
||||
};
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
log::info!("Buffer text: {:?}", buffer_snapshot.text());
|
||||
|
||||
let (mut fold_map, _) = FoldMap::new(buffer_snapshot.clone());
|
||||
fold_map.randomly_mutate(&mut rng);
|
||||
let (folds_snapshot, _) = fold_map.read(buffer_snapshot.clone(), vec![]);
|
||||
log::info!("FoldMap text: {:?}", folds_snapshot.text());
|
||||
|
||||
let (_, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size);
|
||||
let text = Rope::from(tabs_snapshot.text().as_str());
|
||||
log::info!(
|
||||
"TabMap text (tab size: {}): {:?}",
|
||||
tab_size,
|
||||
tabs_snapshot.text(),
|
||||
);
|
||||
|
||||
for _ in 0..5 {
|
||||
let end_row = rng.gen_range(0..=text.max_point().row);
|
||||
let end_column = rng.gen_range(0..=text.line_len(end_row));
|
||||
let mut end = TabPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right));
|
||||
let start_row = rng.gen_range(0..=text.max_point().row);
|
||||
let start_column = rng.gen_range(0..=text.line_len(start_row));
|
||||
let mut start =
|
||||
TabPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left));
|
||||
if start > end {
|
||||
mem::swap(&mut start, &mut end);
|
||||
}
|
||||
|
||||
let expected_text = text
|
||||
.chunks_in_range(text.point_to_offset(start.0)..text.point_to_offset(end.0))
|
||||
.collect::<String>();
|
||||
let expected_summary = TextSummary::from(expected_text.as_str());
|
||||
assert_eq!(
|
||||
expected_text,
|
||||
tabs_snapshot
|
||||
.chunks(start..end, None)
|
||||
.map(|c| c.text)
|
||||
.collect::<String>(),
|
||||
"chunks({:?}..{:?})",
|
||||
start,
|
||||
end
|
||||
);
|
||||
|
||||
let mut actual_summary = tabs_snapshot.text_summary_for_range(start..end);
|
||||
if tab_size > 1 && folds_snapshot.text().contains('\t') {
|
||||
actual_summary.longest_row = expected_summary.longest_row;
|
||||
actual_summary.longest_row_chars = expected_summary.longest_row_chars;
|
||||
}
|
||||
|
||||
assert_eq!(actual_summary, expected_summary,);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
6293
crates/editor/src/editor.rs
Normal file
6293
crates/editor/src/editor.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,14 @@
|
||||
use super::{
|
||||
DisplayPoint, Editor, EditorMode, EditorSettings, EditorStyle, Input, Scroll, Select,
|
||||
SelectPhase, Snapshot, MAX_LINE_LEN,
|
||||
display_map::{BlockContext, ToDisplayPoint},
|
||||
Anchor, DisplayPoint, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, Input,
|
||||
Scroll, Select, SelectPhase, SoftWrap, ToPoint, MAX_LINE_LEN,
|
||||
};
|
||||
use buffer::HighlightId;
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use gpui::{
|
||||
color::Color,
|
||||
elements::layout_highlighted_chunks,
|
||||
fonts::HighlightStyle,
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
@@ -14,14 +17,14 @@ use gpui::{
|
||||
json::{self, ToJson},
|
||||
keymap::Keystroke,
|
||||
text_layout::{self, RunStyle, TextLayoutCache},
|
||||
AppContext, Axis, Border, Element, Event, EventContext, FontCache, LayoutContext,
|
||||
AppContext, Axis, Border, Element, ElementBox, Event, EventContext, FontCache, LayoutContext,
|
||||
MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
|
||||
};
|
||||
use json::json;
|
||||
use language::Bias;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cmp::{self, Ordering},
|
||||
collections::{BTreeMap, HashMap},
|
||||
fmt::Write,
|
||||
ops::Range,
|
||||
};
|
||||
@@ -47,26 +50,48 @@ impl EditorElement {
|
||||
self.view.upgrade(cx).unwrap().update(cx, f)
|
||||
}
|
||||
|
||||
fn snapshot(&self, cx: &mut MutableAppContext) -> Snapshot {
|
||||
fn snapshot(&self, cx: &mut MutableAppContext) -> EditorSnapshot {
|
||||
self.update_view(cx, |view, cx| view.snapshot(cx))
|
||||
}
|
||||
|
||||
fn mouse_down(
|
||||
&self,
|
||||
position: Vector2F,
|
||||
cmd: bool,
|
||||
alt: bool,
|
||||
shift: bool,
|
||||
mut click_count: usize,
|
||||
layout: &mut LayoutState,
|
||||
paint: &mut PaintState,
|
||||
cx: &mut EventContext,
|
||||
) -> bool {
|
||||
if paint.text_bounds.contains_point(position) {
|
||||
let snapshot = self.snapshot(cx.app);
|
||||
let position = paint.point_for_position(&snapshot, layout, position);
|
||||
cx.dispatch_action(Select(SelectPhase::Begin { position, add: cmd }));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
if paint.gutter_bounds.contains_point(position) {
|
||||
click_count = 3; // Simulate triple-click when clicking the gutter to select lines
|
||||
} else if !paint.text_bounds.contains_point(position) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let snapshot = self.snapshot(cx.app);
|
||||
let (position, overshoot) = paint.point_for_position(&snapshot, layout, position);
|
||||
|
||||
if shift && alt {
|
||||
cx.dispatch_action(Select(SelectPhase::BeginColumnar {
|
||||
position,
|
||||
overshoot,
|
||||
}));
|
||||
} else if shift {
|
||||
cx.dispatch_action(Select(SelectPhase::Extend {
|
||||
position,
|
||||
click_count,
|
||||
}));
|
||||
} else {
|
||||
cx.dispatch_action(Select(SelectPhase::Begin {
|
||||
position,
|
||||
add: alt,
|
||||
click_count,
|
||||
}));
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn mouse_up(&self, _position: Vector2F, cx: &mut EventContext) -> bool {
|
||||
@@ -118,10 +143,11 @@ impl EditorElement {
|
||||
let font_cache = cx.font_cache.clone();
|
||||
let text_layout_cache = cx.text_layout_cache.clone();
|
||||
let snapshot = self.snapshot(cx.app);
|
||||
let position = paint.point_for_position(&snapshot, layout, position);
|
||||
let (position, overshoot) = paint.point_for_position(&snapshot, layout, position);
|
||||
|
||||
cx.dispatch_action(Select(SelectPhase::Update {
|
||||
position,
|
||||
overshoot,
|
||||
scroll_position: (snapshot.scroll_position() + scroll_delta).clamp(
|
||||
Vector2F::zero(),
|
||||
layout.scroll_max(&font_cache, &text_layout_cache),
|
||||
@@ -238,6 +264,24 @@ impl EditorElement {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(highlighted_rows) = &layout.highlighted_rows {
|
||||
let origin = vec2f(
|
||||
bounds.origin_x(),
|
||||
bounds.origin_y() + (layout.line_height * highlighted_rows.start as f32)
|
||||
- scroll_top,
|
||||
);
|
||||
let size = vec2f(
|
||||
bounds.width(),
|
||||
layout.line_height * highlighted_rows.len() as f32,
|
||||
);
|
||||
cx.scene.push_quad(Quad {
|
||||
bounds: RectF::new(origin, size),
|
||||
background: Some(style.highlighted_line_background),
|
||||
border: Border::default(),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,22 +331,16 @@ impl EditorElement {
|
||||
let content_origin = bounds.origin() + layout.text_offset;
|
||||
|
||||
for (replica_id, selections) in &layout.selections {
|
||||
let style_ix = *replica_id as usize % (style.guest_selections.len() + 1);
|
||||
let style = if style_ix == 0 {
|
||||
&style.selection
|
||||
} else {
|
||||
&style.guest_selections[style_ix - 1]
|
||||
};
|
||||
let style = style.replica_selection_style(*replica_id);
|
||||
|
||||
for selection in selections {
|
||||
if selection.start != selection.end {
|
||||
let range_start = cmp::min(selection.start, selection.end);
|
||||
let range_end = cmp::max(selection.start, selection.end);
|
||||
let row_range = if range_end.column() == 0 {
|
||||
cmp::max(range_start.row(), start_row)..cmp::min(range_end.row(), end_row)
|
||||
let row_range = if selection.end.column() == 0 {
|
||||
cmp::max(selection.start.row(), start_row)
|
||||
..cmp::min(selection.end.row(), end_row)
|
||||
} else {
|
||||
cmp::max(range_start.row(), start_row)
|
||||
..cmp::min(range_end.row() + 1, end_row)
|
||||
cmp::max(selection.start.row(), start_row)
|
||||
..cmp::min(selection.end.row() + 1, end_row)
|
||||
};
|
||||
|
||||
let selection = Selection {
|
||||
@@ -315,16 +353,18 @@ impl EditorElement {
|
||||
.map(|row| {
|
||||
let line_layout = &layout.line_layouts[(row - start_row) as usize];
|
||||
SelectionLine {
|
||||
start_x: if row == range_start.row() {
|
||||
start_x: if row == selection.start.row() {
|
||||
content_origin.x()
|
||||
+ line_layout.x_for_index(range_start.column() as usize)
|
||||
+ line_layout
|
||||
.x_for_index(selection.start.column() as usize)
|
||||
- scroll_left
|
||||
} else {
|
||||
content_origin.x() - scroll_left
|
||||
},
|
||||
end_x: if row == range_end.row() {
|
||||
end_x: if row == selection.end.row() {
|
||||
content_origin.x()
|
||||
+ line_layout.x_for_index(range_end.column() as usize)
|
||||
+ line_layout
|
||||
.x_for_index(selection.end.column() as usize)
|
||||
- scroll_left
|
||||
} else {
|
||||
content_origin.x()
|
||||
@@ -341,13 +381,13 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
if view.show_local_cursors() || *replica_id != local_replica_id {
|
||||
let cursor_position = selection.end;
|
||||
let cursor_position = selection.head();
|
||||
if (start_row..end_row).contains(&cursor_position.row()) {
|
||||
let cursor_row_layout =
|
||||
&layout.line_layouts[(selection.end.row() - start_row) as usize];
|
||||
let x = cursor_row_layout.x_for_index(selection.end.column() as usize)
|
||||
&layout.line_layouts[(cursor_position.row() - start_row) as usize];
|
||||
let x = cursor_row_layout.x_for_index(cursor_position.column() as usize)
|
||||
- scroll_left;
|
||||
let y = selection.end.row() as f32 * layout.line_height - scroll_top;
|
||||
let y = cursor_position.row() as f32 * layout.line_height - scroll_top;
|
||||
cursors.push(Cursor {
|
||||
color: style.cursor,
|
||||
origin: content_origin + vec2f(x, y),
|
||||
@@ -381,8 +421,26 @@ impl EditorElement {
|
||||
cx.scene.pop_layer();
|
||||
}
|
||||
|
||||
fn max_line_number_width(&self, snapshot: &Snapshot, cx: &LayoutContext) -> f32 {
|
||||
let digit_count = (snapshot.buffer_row_count() as f32).log10().floor() as usize + 1;
|
||||
fn paint_blocks(
|
||||
&mut self,
|
||||
bounds: RectF,
|
||||
visible_bounds: RectF,
|
||||
layout: &mut LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) {
|
||||
let scroll_position = layout.snapshot.scroll_position();
|
||||
let scroll_left = scroll_position.x() * layout.em_width;
|
||||
let scroll_top = scroll_position.y() * layout.line_height;
|
||||
|
||||
for (row, element) in &mut layout.blocks {
|
||||
let origin = bounds.origin()
|
||||
+ vec2f(-scroll_left, *row as f32 * layout.line_height - scroll_top);
|
||||
element.paint(origin, visible_bounds, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &LayoutContext) -> f32 {
|
||||
let digit_count = (snapshot.max_buffer_row() as f32).log10().floor() as usize + 1;
|
||||
let style = &self.settings.style;
|
||||
|
||||
cx.text_layout_cache
|
||||
@@ -394,24 +452,25 @@ impl EditorElement {
|
||||
RunStyle {
|
||||
font_id: style.text.font_id,
|
||||
color: Color::black(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
},
|
||||
)],
|
||||
)
|
||||
.width()
|
||||
}
|
||||
|
||||
fn layout_line_numbers(
|
||||
fn layout_rows(
|
||||
&self,
|
||||
rows: Range<u32>,
|
||||
active_rows: &BTreeMap<u32, bool>,
|
||||
snapshot: &Snapshot,
|
||||
snapshot: &EditorSnapshot,
|
||||
cx: &LayoutContext,
|
||||
) -> Vec<Option<text_layout::Line>> {
|
||||
let style = &self.settings.style;
|
||||
let mut layouts = Vec::with_capacity(rows.len());
|
||||
let include_line_numbers = snapshot.mode == EditorMode::Full;
|
||||
let mut line_number_layouts = Vec::with_capacity(rows.len());
|
||||
let mut line_number = String::new();
|
||||
for (ix, (buffer_row, soft_wrapped)) in snapshot
|
||||
for (ix, row) in snapshot
|
||||
.buffer_rows(rows.start)
|
||||
.take((rows.end - rows.start) as usize)
|
||||
.enumerate()
|
||||
@@ -422,33 +481,35 @@ impl EditorElement {
|
||||
} else {
|
||||
style.line_number
|
||||
};
|
||||
if soft_wrapped {
|
||||
layouts.push(None);
|
||||
if let Some(buffer_row) = row {
|
||||
if include_line_numbers {
|
||||
line_number.clear();
|
||||
write!(&mut line_number, "{}", buffer_row + 1).unwrap();
|
||||
line_number_layouts.push(Some(cx.text_layout_cache.layout_str(
|
||||
&line_number,
|
||||
style.text.font_size,
|
||||
&[(
|
||||
line_number.len(),
|
||||
RunStyle {
|
||||
font_id: style.text.font_id,
|
||||
color,
|
||||
underline: None,
|
||||
},
|
||||
)],
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
line_number.clear();
|
||||
write!(&mut line_number, "{}", buffer_row + 1).unwrap();
|
||||
layouts.push(Some(cx.text_layout_cache.layout_str(
|
||||
&line_number,
|
||||
style.text.font_size,
|
||||
&[(
|
||||
line_number.len(),
|
||||
RunStyle {
|
||||
font_id: style.text.font_id,
|
||||
color,
|
||||
underline: false,
|
||||
},
|
||||
)],
|
||||
)));
|
||||
line_number_layouts.push(None);
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
line_number_layouts
|
||||
}
|
||||
|
||||
fn layout_lines(
|
||||
&mut self,
|
||||
mut rows: Range<u32>,
|
||||
snapshot: &mut Snapshot,
|
||||
snapshot: &mut EditorSnapshot,
|
||||
cx: &LayoutContext,
|
||||
) -> Vec<text_layout::Line> {
|
||||
rows.end = cmp::min(rows.end, snapshot.max_point().row() + 1);
|
||||
@@ -476,83 +537,90 @@ impl EditorElement {
|
||||
RunStyle {
|
||||
font_id: placeholder_style.font_id,
|
||||
color: placeholder_style.color,
|
||||
underline: false,
|
||||
underline: None,
|
||||
},
|
||||
)],
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
let style = &self.settings.style;
|
||||
let mut prev_font_properties = style.text.font_properties.clone();
|
||||
let mut prev_font_id = style.text.font_id;
|
||||
|
||||
let mut layouts = Vec::with_capacity(rows.len());
|
||||
let mut line = String::new();
|
||||
let mut styles = Vec::new();
|
||||
let mut row = rows.start;
|
||||
let mut line_exceeded_max_len = false;
|
||||
let chunks = snapshot.highlighted_chunks_for_rows(rows.clone());
|
||||
|
||||
'outer: for (chunk, style_ix) in chunks.chain(Some(("\n", HighlightId::default()))) {
|
||||
for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
|
||||
if ix > 0 {
|
||||
layouts.push(cx.text_layout_cache.layout_str(
|
||||
&line,
|
||||
style.text.font_size,
|
||||
&styles,
|
||||
));
|
||||
line.clear();
|
||||
styles.clear();
|
||||
row += 1;
|
||||
line_exceeded_max_len = false;
|
||||
if row == rows.end {
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
|
||||
if !line_chunk.is_empty() && !line_exceeded_max_len {
|
||||
let highlight_style = style_ix
|
||||
.style(&style.syntax)
|
||||
.unwrap_or(style.text.clone().into());
|
||||
// Avoid a lookup if the font properties match the previous ones.
|
||||
let font_id = if highlight_style.font_properties == prev_font_properties {
|
||||
prev_font_id
|
||||
} else {
|
||||
let style = &self.settings.style;
|
||||
let chunks = snapshot
|
||||
.chunks(rows.clone(), Some(&style.syntax))
|
||||
.map(|chunk| {
|
||||
let highlight = if let Some(severity) = chunk.diagnostic {
|
||||
let underline = Some(super::diagnostic_style(severity, true, style).text);
|
||||
if let Some(mut highlight) = chunk.highlight_style {
|
||||
highlight.underline = underline;
|
||||
Some(highlight)
|
||||
} else {
|
||||
Some(HighlightStyle {
|
||||
underline,
|
||||
color: style.text.color,
|
||||
font_properties: style.text.font_properties,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
cx.font_cache
|
||||
.select_font(
|
||||
style.text.font_family_id,
|
||||
&highlight_style.font_properties,
|
||||
)
|
||||
.unwrap_or(style.text.font_id)
|
||||
chunk.highlight_style
|
||||
};
|
||||
(chunk.text, highlight)
|
||||
});
|
||||
layout_highlighted_chunks(
|
||||
chunks,
|
||||
&style.text,
|
||||
&cx.text_layout_cache,
|
||||
&cx.font_cache,
|
||||
MAX_LINE_LEN,
|
||||
rows.len() as usize,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn layout_blocks(
|
||||
&mut self,
|
||||
rows: Range<u32>,
|
||||
snapshot: &EditorSnapshot,
|
||||
width: f32,
|
||||
line_number_x: f32,
|
||||
text_x: f32,
|
||||
line_height: f32,
|
||||
style: &EditorStyle,
|
||||
line_layouts: &[text_layout::Line],
|
||||
cx: &mut LayoutContext,
|
||||
) -> Vec<(u32, ElementBox)> {
|
||||
snapshot
|
||||
.blocks_in_range(rows.clone())
|
||||
.map(|(start_row, block)| {
|
||||
let anchor_row = block
|
||||
.position()
|
||||
.to_point(&snapshot.buffer_snapshot)
|
||||
.to_display_point(snapshot)
|
||||
.row();
|
||||
|
||||
let anchor_x = text_x
|
||||
+ if rows.contains(&anchor_row) {
|
||||
line_layouts[(anchor_row - rows.start) as usize]
|
||||
.x_for_index(block.column() as usize)
|
||||
} else {
|
||||
layout_line(anchor_row, snapshot, style, cx.text_layout_cache)
|
||||
.x_for_index(block.column() as usize)
|
||||
};
|
||||
|
||||
if line.len() + line_chunk.len() > MAX_LINE_LEN {
|
||||
let mut chunk_len = MAX_LINE_LEN - line.len();
|
||||
while !line_chunk.is_char_boundary(chunk_len) {
|
||||
chunk_len -= 1;
|
||||
}
|
||||
line_chunk = &line_chunk[..chunk_len];
|
||||
line_exceeded_max_len = true;
|
||||
}
|
||||
|
||||
line.push_str(line_chunk);
|
||||
styles.push((
|
||||
line_chunk.len(),
|
||||
RunStyle {
|
||||
font_id,
|
||||
color: highlight_style.color,
|
||||
underline: highlight_style.underline,
|
||||
},
|
||||
));
|
||||
prev_font_id = font_id;
|
||||
prev_font_properties = highlight_style.font_properties;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
let mut element = block.render(&BlockContext {
|
||||
cx,
|
||||
anchor_x,
|
||||
line_number_x,
|
||||
});
|
||||
element.layout(
|
||||
SizeConstraint {
|
||||
min: Vector2F::zero(),
|
||||
max: vec2f(width, block.height() as f32 * line_height),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
(start_row, element)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -587,8 +655,13 @@ impl Element for EditorElement {
|
||||
let text_width = size.x() - gutter_width;
|
||||
let text_offset = vec2f(-style.text.descent(cx.font_cache), 0.);
|
||||
let em_width = style.text.em_width(cx.font_cache);
|
||||
let em_advance = style.text.em_advance(cx.font_cache);
|
||||
let overscroll = vec2f(em_width, 0.);
|
||||
let wrap_width = text_width - text_offset.x() - overscroll.x() - em_width;
|
||||
let wrap_width = match self.settings.soft_wrap {
|
||||
SoftWrap::None => None,
|
||||
SoftWrap::EditorWidth => Some(text_width - text_offset.x() - overscroll.x() - em_width),
|
||||
SoftWrap::Column(column) => Some(column as f32 * em_advance),
|
||||
};
|
||||
let snapshot = self.update_view(cx.app, |view, cx| {
|
||||
if view.set_wrap_width(wrap_width, cx) {
|
||||
view.snapshot(cx)
|
||||
@@ -622,49 +695,73 @@ impl Element for EditorElement {
|
||||
let scroll_top = scroll_position.y() * line_height;
|
||||
let end_row = ((scroll_top + size.y()) / line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen
|
||||
|
||||
let mut selections = HashMap::new();
|
||||
let mut active_rows = BTreeMap::new();
|
||||
self.update_view(cx.app, |view, cx| {
|
||||
for selection_set_id in view.active_selection_sets(cx).collect::<Vec<_>>() {
|
||||
let mut set = Vec::new();
|
||||
for selection in view.selections_in_range(
|
||||
selection_set_id,
|
||||
DisplayPoint::new(start_row, 0)..DisplayPoint::new(end_row, 0),
|
||||
cx,
|
||||
) {
|
||||
set.push(selection.clone());
|
||||
if selection_set_id == view.selection_set_id {
|
||||
let is_empty = selection.start == selection.end;
|
||||
let mut selection_start;
|
||||
let mut selection_end;
|
||||
if selection.start < selection.end {
|
||||
selection_start = selection.start;
|
||||
selection_end = selection.end;
|
||||
} else {
|
||||
selection_start = selection.end;
|
||||
selection_end = selection.start;
|
||||
};
|
||||
selection_start = snapshot.prev_row_boundary(selection_start).0;
|
||||
selection_end = snapshot.next_row_boundary(selection_end).0;
|
||||
for row in cmp::max(selection_start.row(), start_row)
|
||||
..=cmp::min(selection_end.row(), end_row)
|
||||
{
|
||||
let contains_non_empty_selection =
|
||||
active_rows.entry(row).or_insert(!is_empty);
|
||||
*contains_non_empty_selection |= !is_empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
let start_anchor = if start_row == 0 {
|
||||
Anchor::min()
|
||||
} else {
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left))
|
||||
};
|
||||
let end_anchor = if end_row > snapshot.max_point().row() {
|
||||
Anchor::max()
|
||||
} else {
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
|
||||
};
|
||||
|
||||
selections.insert(selection_set_id.replica_id, set);
|
||||
let mut selections = HashMap::default();
|
||||
let mut active_rows = BTreeMap::new();
|
||||
let mut highlighted_rows = None;
|
||||
self.update_view(cx.app, |view, cx| {
|
||||
highlighted_rows = view.highlighted_rows();
|
||||
let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
let local_selections = view
|
||||
.local_selections_in_range(start_anchor.clone()..end_anchor.clone(), &display_map);
|
||||
for selection in &local_selections {
|
||||
let is_empty = selection.start == selection.end;
|
||||
let selection_start = snapshot.prev_line_boundary(selection.start).1;
|
||||
let selection_end = snapshot.next_line_boundary(selection.end).1;
|
||||
for row in cmp::max(selection_start.row(), start_row)
|
||||
..=cmp::min(selection_end.row(), end_row)
|
||||
{
|
||||
let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty);
|
||||
*contains_non_empty_selection |= !is_empty;
|
||||
}
|
||||
}
|
||||
selections.insert(
|
||||
view.replica_id(cx),
|
||||
local_selections
|
||||
.into_iter()
|
||||
.map(|selection| crate::Selection {
|
||||
id: selection.id,
|
||||
goal: selection.goal,
|
||||
reversed: selection.reversed,
|
||||
start: selection.start.to_display_point(&display_map),
|
||||
end: selection.end.to_display_point(&display_map),
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
|
||||
for (replica_id, selection) in display_map
|
||||
.buffer_snapshot
|
||||
.remote_selections_in_range(&(start_anchor..end_anchor))
|
||||
{
|
||||
selections
|
||||
.entry(replica_id)
|
||||
.or_insert(Vec::new())
|
||||
.push(crate::Selection {
|
||||
id: selection.id,
|
||||
goal: selection.goal,
|
||||
reversed: selection.reversed,
|
||||
start: selection.start.to_display_point(&display_map),
|
||||
end: selection.end.to_display_point(&display_map),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let line_number_layouts = if snapshot.mode == EditorMode::Full {
|
||||
self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let line_number_layouts = self.layout_rows(start_row..end_row, &active_rows, &snapshot, cx);
|
||||
|
||||
let mut max_visible_line_width = 0.0;
|
||||
let line_layouts = self.layout_lines(start_row..end_row, &mut snapshot, cx);
|
||||
@@ -674,6 +771,18 @@ impl Element for EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
let blocks = self.layout_blocks(
|
||||
start_row..end_row,
|
||||
&snapshot,
|
||||
size.x(),
|
||||
gutter_padding,
|
||||
gutter_width + text_offset.x(),
|
||||
line_height,
|
||||
&style,
|
||||
&line_layouts,
|
||||
cx,
|
||||
);
|
||||
|
||||
let mut layout = LayoutState {
|
||||
size,
|
||||
gutter_size,
|
||||
@@ -684,10 +793,13 @@ impl Element for EditorElement {
|
||||
snapshot,
|
||||
style: self.settings.style.clone(),
|
||||
active_rows,
|
||||
highlighted_rows,
|
||||
line_layouts,
|
||||
line_number_layouts,
|
||||
blocks,
|
||||
line_height,
|
||||
em_width,
|
||||
em_advance,
|
||||
selections,
|
||||
max_visible_line_width,
|
||||
};
|
||||
@@ -740,11 +852,13 @@ impl Element for EditorElement {
|
||||
self.paint_gutter(gutter_bounds, visible_bounds, layout, cx);
|
||||
}
|
||||
self.paint_text(text_bounds, visible_bounds, layout, cx);
|
||||
self.paint_blocks(bounds, visible_bounds, layout, cx);
|
||||
|
||||
cx.scene.pop_layer();
|
||||
|
||||
Some(PaintState {
|
||||
bounds,
|
||||
gutter_bounds,
|
||||
text_bounds,
|
||||
})
|
||||
} else {
|
||||
@@ -762,9 +876,13 @@ impl Element for EditorElement {
|
||||
) -> bool {
|
||||
if let (Some(layout), Some(paint)) = (layout, paint) {
|
||||
match event {
|
||||
Event::LeftMouseDown { position, cmd } => {
|
||||
self.mouse_down(*position, *cmd, layout, paint, cx)
|
||||
}
|
||||
Event::LeftMouseDown {
|
||||
position,
|
||||
alt,
|
||||
shift,
|
||||
click_count,
|
||||
..
|
||||
} => self.mouse_down(*position, *alt, *shift, *click_count, layout, paint, cx),
|
||||
Event::LeftMouseUp { position } => self.mouse_up(*position, cx),
|
||||
Event::LeftMouseDragged { position } => {
|
||||
self.mouse_dragged(*position, layout, paint, cx)
|
||||
@@ -804,13 +922,16 @@ pub struct LayoutState {
|
||||
gutter_padding: f32,
|
||||
text_size: Vector2F,
|
||||
style: EditorStyle,
|
||||
snapshot: Snapshot,
|
||||
snapshot: EditorSnapshot,
|
||||
active_rows: BTreeMap<u32, bool>,
|
||||
highlighted_rows: Option<Range<u32>>,
|
||||
line_layouts: Vec<text_layout::Line>,
|
||||
line_number_layouts: Vec<Option<text_layout::Line>>,
|
||||
blocks: Vec<(u32, ElementBox)>,
|
||||
line_height: f32,
|
||||
em_width: f32,
|
||||
selections: HashMap<ReplicaId, Vec<Range<DisplayPoint>>>,
|
||||
em_advance: f32,
|
||||
selections: HashMap<ReplicaId, Vec<text::Selection<DisplayPoint>>>,
|
||||
overscroll: Vector2F,
|
||||
text_offset: Vector2F,
|
||||
max_visible_line_width: f32,
|
||||
@@ -819,7 +940,8 @@ pub struct LayoutState {
|
||||
impl LayoutState {
|
||||
fn scroll_width(&self, layout_cache: &TextLayoutCache) -> f32 {
|
||||
let row = self.snapshot.longest_row();
|
||||
let longest_line_width = self.layout_line(row, &self.snapshot, layout_cache).width();
|
||||
let longest_line_width =
|
||||
layout_line(row, &self.snapshot, &self.style, layout_cache).width();
|
||||
longest_line_width.max(self.max_visible_line_width) + self.overscroll.x()
|
||||
}
|
||||
|
||||
@@ -834,50 +956,51 @@ impl LayoutState {
|
||||
max_row.saturating_sub(1) as f32,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn layout_line(
|
||||
&self,
|
||||
row: u32,
|
||||
snapshot: &Snapshot,
|
||||
layout_cache: &TextLayoutCache,
|
||||
) -> text_layout::Line {
|
||||
let mut line = snapshot.line(row);
|
||||
fn layout_line(
|
||||
row: u32,
|
||||
snapshot: &EditorSnapshot,
|
||||
style: &EditorStyle,
|
||||
layout_cache: &TextLayoutCache,
|
||||
) -> text_layout::Line {
|
||||
let mut line = snapshot.line(row);
|
||||
|
||||
if line.len() > MAX_LINE_LEN {
|
||||
let mut len = MAX_LINE_LEN;
|
||||
while !line.is_char_boundary(len) {
|
||||
len -= 1;
|
||||
}
|
||||
line.truncate(len);
|
||||
if line.len() > MAX_LINE_LEN {
|
||||
let mut len = MAX_LINE_LEN;
|
||||
while !line.is_char_boundary(len) {
|
||||
len -= 1;
|
||||
}
|
||||
|
||||
layout_cache.layout_str(
|
||||
&line,
|
||||
self.style.text.font_size,
|
||||
&[(
|
||||
snapshot.line_len(row) as usize,
|
||||
RunStyle {
|
||||
font_id: self.style.text.font_id,
|
||||
color: Color::black(),
|
||||
underline: false,
|
||||
},
|
||||
)],
|
||||
)
|
||||
line.truncate(len);
|
||||
}
|
||||
|
||||
layout_cache.layout_str(
|
||||
&line,
|
||||
style.text.font_size,
|
||||
&[(
|
||||
snapshot.line_len(row) as usize,
|
||||
RunStyle {
|
||||
font_id: style.text.font_id,
|
||||
color: Color::black(),
|
||||
underline: None,
|
||||
},
|
||||
)],
|
||||
)
|
||||
}
|
||||
|
||||
pub struct PaintState {
|
||||
bounds: RectF,
|
||||
gutter_bounds: RectF,
|
||||
text_bounds: RectF,
|
||||
}
|
||||
|
||||
impl PaintState {
|
||||
fn point_for_position(
|
||||
&self,
|
||||
snapshot: &Snapshot,
|
||||
snapshot: &EditorSnapshot,
|
||||
layout: &LayoutState,
|
||||
position: Vector2F,
|
||||
) -> DisplayPoint {
|
||||
) -> (DisplayPoint, u32) {
|
||||
let scroll_position = snapshot.scroll_position();
|
||||
let position = position - self.text_bounds.origin();
|
||||
let y = position.y().max(0.0).min(layout.size.y());
|
||||
@@ -889,12 +1012,13 @@ impl PaintState {
|
||||
let column = if x >= 0.0 {
|
||||
line.index_for_x(x)
|
||||
.map(|ix| ix as u32)
|
||||
.unwrap_or(snapshot.line_len(row))
|
||||
.unwrap_or_else(|| snapshot.line_len(row))
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let overshoot = (0f32.max(x - line.width()) / layout.em_advance) as u32;
|
||||
|
||||
DisplayPoint::new(row, column)
|
||||
(DisplayPoint::new(row, column), overshoot)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1039,23 +1163,20 @@ fn scale_horizontal_mouse_autoscroll_delta(delta: f32) -> f32 {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
test::sample_text,
|
||||
{Editor, EditorSettings},
|
||||
};
|
||||
use buffer::Buffer;
|
||||
use crate::{Editor, EditorSettings, MultiBuffer};
|
||||
use std::sync::Arc;
|
||||
use util::test::sample_text;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) {
|
||||
let settings = EditorSettings::test(cx);
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6), cx));
|
||||
let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
|
||||
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
|
||||
Editor::for_buffer(
|
||||
buffer,
|
||||
{
|
||||
let settings = settings.clone();
|
||||
move |_| settings.clone()
|
||||
Arc::new(move |_| settings.clone())
|
||||
},
|
||||
cx,
|
||||
)
|
||||
@@ -1066,7 +1187,7 @@ mod tests {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let mut presenter = cx.build_presenter(window_id, 30.);
|
||||
let mut layout_cx = presenter.build_layout_context(false, cx);
|
||||
element.layout_line_numbers(0..6, &Default::default(), &snapshot, &mut layout_cx)
|
||||
element.layout_rows(0..6, &Default::default(), &snapshot, &mut layout_cx)
|
||||
});
|
||||
assert_eq!(layouts.len(), 6);
|
||||
}
|
||||
|
||||
390
crates/editor/src/items.rs
Normal file
390
crates/editor/src/items.rs
Normal file
@@ -0,0 +1,390 @@
|
||||
use crate::{Autoscroll, Editor, Event};
|
||||
use crate::{MultiBuffer, ToPoint as _};
|
||||
use anyhow::Result;
|
||||
use gpui::{
|
||||
elements::*, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, RenderContext,
|
||||
Subscription, Task, View, ViewContext, ViewHandle, WeakModelHandle,
|
||||
};
|
||||
use language::{Buffer, Diagnostic, File as _};
|
||||
use postage::watch;
|
||||
use project::{File, ProjectPath, Worktree};
|
||||
use std::fmt::Write;
|
||||
use std::path::Path;
|
||||
use text::{Point, Selection};
|
||||
use util::TryFutureExt;
|
||||
use workspace::{
|
||||
ItemHandle, ItemView, ItemViewHandle, PathOpener, Settings, StatusItemView, WeakItemHandle,
|
||||
Workspace,
|
||||
};
|
||||
|
||||
pub struct BufferOpener;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BufferItemHandle(pub ModelHandle<Buffer>);
|
||||
|
||||
#[derive(Clone)]
|
||||
struct WeakBufferItemHandle(WeakModelHandle<Buffer>);
|
||||
|
||||
impl PathOpener for BufferOpener {
|
||||
fn open(
|
||||
&self,
|
||||
worktree: &mut Worktree,
|
||||
project_path: ProjectPath,
|
||||
cx: &mut ModelContext<Worktree>,
|
||||
) -> Option<Task<Result<Box<dyn ItemHandle>>>> {
|
||||
let buffer = worktree.open_buffer(project_path.path, cx);
|
||||
let task = cx.spawn(|_, _| async move {
|
||||
let buffer = buffer.await?;
|
||||
Ok(Box::new(BufferItemHandle(buffer)) as Box<dyn ItemHandle>)
|
||||
});
|
||||
Some(task)
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemHandle for BufferItemHandle {
|
||||
fn add_view(
|
||||
&self,
|
||||
window_id: usize,
|
||||
workspace: &Workspace,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Box<dyn ItemViewHandle> {
|
||||
let buffer = cx.add_model(|cx| MultiBuffer::singleton(self.0.clone(), cx));
|
||||
let weak_buffer = buffer.downgrade();
|
||||
Box::new(cx.add_view(window_id, |cx| {
|
||||
Editor::for_buffer(
|
||||
buffer,
|
||||
crate::settings_builder(weak_buffer, workspace.settings()),
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
fn boxed_clone(&self) -> Box<dyn ItemHandle> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn to_any(&self) -> gpui::AnyModelHandle {
|
||||
self.0.clone().into()
|
||||
}
|
||||
|
||||
fn downgrade(&self) -> Box<dyn workspace::WeakItemHandle> {
|
||||
Box::new(WeakBufferItemHandle(self.0.downgrade()))
|
||||
}
|
||||
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
|
||||
File::from_dyn(self.0.read(cx).file()).map(|f| ProjectPath {
|
||||
worktree_id: f.worktree_id(cx),
|
||||
path: f.path().clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn id(&self) -> usize {
|
||||
self.0.id()
|
||||
}
|
||||
}
|
||||
|
||||
impl WeakItemHandle for WeakBufferItemHandle {
|
||||
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
|
||||
self.0
|
||||
.upgrade(cx)
|
||||
.map(|buffer| Box::new(BufferItemHandle(buffer)) as Box<dyn ItemHandle>)
|
||||
}
|
||||
|
||||
fn id(&self) -> usize {
|
||||
self.0.id()
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemView for Editor {
|
||||
type ItemHandle = BufferItemHandle;
|
||||
|
||||
fn item_handle(&self, cx: &AppContext) -> Self::ItemHandle {
|
||||
BufferItemHandle(self.buffer.read(cx).as_singleton().unwrap())
|
||||
}
|
||||
|
||||
fn title(&self, cx: &AppContext) -> String {
|
||||
let filename = self
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.file(cx)
|
||||
.and_then(|file| file.file_name());
|
||||
if let Some(name) = filename {
|
||||
name.to_string_lossy().into()
|
||||
} else {
|
||||
"untitled".into()
|
||||
}
|
||||
}
|
||||
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
|
||||
File::from_dyn(self.buffer().read(cx).file(cx)).map(|file| ProjectPath {
|
||||
worktree_id: file.worktree_id(cx),
|
||||
path: file.path().clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Some(self.clone(cx))
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &AppContext) -> bool {
|
||||
self.buffer().read(cx).read(cx).is_dirty()
|
||||
}
|
||||
|
||||
fn has_conflict(&self, cx: &AppContext) -> bool {
|
||||
self.buffer().read(cx).read(cx).has_conflict()
|
||||
}
|
||||
|
||||
fn can_save(&self, cx: &AppContext) -> bool {
|
||||
self.project_path(cx).is_some()
|
||||
}
|
||||
|
||||
fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
|
||||
let buffer = self.buffer().clone();
|
||||
Ok(cx.spawn(|editor, mut cx| async move {
|
||||
buffer
|
||||
.update(&mut cx, |buffer, cx| buffer.format(cx).log_err())
|
||||
.await;
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
editor.request_autoscroll(Autoscroll::Fit, cx)
|
||||
});
|
||||
buffer
|
||||
.update(&mut cx, |buffer, cx| buffer.save(cx))?
|
||||
.await?;
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn can_save_as(&self, _: &AppContext) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
&mut self,
|
||||
worktree: ModelHandle<Worktree>,
|
||||
path: &Path,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let buffer = self
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.expect("cannot call save_as on an excerpt list")
|
||||
.clone();
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let handle = cx.handle();
|
||||
let text = buffer.as_rope().clone();
|
||||
let version = buffer.version();
|
||||
|
||||
let save_as = worktree.update(cx, |worktree, cx| {
|
||||
worktree
|
||||
.as_local_mut()
|
||||
.unwrap()
|
||||
.save_buffer_as(handle, path, text, cx)
|
||||
});
|
||||
|
||||
cx.spawn(|buffer, mut cx| async move {
|
||||
save_as.await.map(|new_file| {
|
||||
let (language, language_server) = worktree.update(&mut cx, |worktree, cx| {
|
||||
let worktree = worktree.as_local_mut().unwrap();
|
||||
let language = worktree
|
||||
.language_registry()
|
||||
.select_language(new_file.full_path())
|
||||
.cloned();
|
||||
let language_server = language
|
||||
.as_ref()
|
||||
.and_then(|language| worktree.register_language(language, cx));
|
||||
(language, language_server.clone())
|
||||
});
|
||||
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.did_save(version, new_file.mtime, Some(Box::new(new_file)), cx);
|
||||
buffer.set_language(language, language_server, cx);
|
||||
});
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn should_activate_item_on_event(event: &Event) -> bool {
|
||||
matches!(event, Event::Activate)
|
||||
}
|
||||
|
||||
fn should_close_item_on_event(event: &Event) -> bool {
|
||||
matches!(event, Event::Closed)
|
||||
}
|
||||
|
||||
fn should_update_tab_on_event(event: &Event) -> bool {
|
||||
matches!(
|
||||
event,
|
||||
Event::Saved | Event::Dirtied | Event::FileHandleChanged
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CursorPosition {
|
||||
position: Option<Point>,
|
||||
selected_count: usize,
|
||||
settings: watch::Receiver<Settings>,
|
||||
_observe_active_editor: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl CursorPosition {
|
||||
pub fn new(settings: watch::Receiver<Settings>) -> Self {
|
||||
Self {
|
||||
position: None,
|
||||
selected_count: 0,
|
||||
settings,
|
||||
_observe_active_editor: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_position(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
|
||||
let editor = editor.read(cx);
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
|
||||
self.selected_count = 0;
|
||||
let mut last_selection: Option<Selection<usize>> = None;
|
||||
for selection in editor.local_selections::<usize>(cx) {
|
||||
self.selected_count += selection.end - selection.start;
|
||||
if last_selection
|
||||
.as_ref()
|
||||
.map_or(true, |last_selection| selection.id > last_selection.id)
|
||||
{
|
||||
last_selection = Some(selection);
|
||||
}
|
||||
}
|
||||
self.position = last_selection.map(|s| s.head().to_point(&buffer));
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for CursorPosition {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for CursorPosition {
|
||||
fn ui_name() -> &'static str {
|
||||
"CursorPosition"
|
||||
}
|
||||
|
||||
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
||||
if let Some(position) = self.position {
|
||||
let theme = &self.settings.borrow().theme.workspace.status_bar;
|
||||
let mut text = format!("{},{}", position.row + 1, position.column + 1);
|
||||
if self.selected_count > 0 {
|
||||
write!(text, " ({} selected)", self.selected_count).unwrap();
|
||||
}
|
||||
Label::new(text, theme.cursor_position.clone()).boxed()
|
||||
} else {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for CursorPosition {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemViewHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(editor) = active_pane_item.and_then(|item| item.to_any().downcast::<Editor>()) {
|
||||
self._observe_active_editor = Some(cx.observe(&editor, Self::update_position));
|
||||
self.update_position(editor, cx);
|
||||
} else {
|
||||
self.position = None;
|
||||
self._observe_active_editor = None;
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DiagnosticMessage {
|
||||
settings: watch::Receiver<Settings>,
|
||||
diagnostic: Option<Diagnostic>,
|
||||
_observe_active_editor: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl DiagnosticMessage {
|
||||
pub fn new(settings: watch::Receiver<Settings>) -> Self {
|
||||
Self {
|
||||
diagnostic: None,
|
||||
settings,
|
||||
_observe_active_editor: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
|
||||
let editor = editor.read(cx);
|
||||
let buffer = editor.buffer().read(cx);
|
||||
let cursor_position = editor.newest_selection::<usize>(&buffer.read(cx)).head();
|
||||
let new_diagnostic = buffer
|
||||
.read(cx)
|
||||
.diagnostics_in_range::<_, usize>(cursor_position..cursor_position)
|
||||
.filter(|entry| !entry.range.is_empty())
|
||||
.min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
|
||||
.map(|entry| entry.diagnostic);
|
||||
if new_diagnostic != self.diagnostic {
|
||||
self.diagnostic = new_diagnostic;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for DiagnosticMessage {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for DiagnosticMessage {
|
||||
fn ui_name() -> &'static str {
|
||||
"DiagnosticMessage"
|
||||
}
|
||||
|
||||
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
||||
if let Some(diagnostic) = &self.diagnostic {
|
||||
let theme = &self.settings.borrow().theme.workspace.status_bar;
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Svg::new("icons/warning.svg")
|
||||
.with_color(theme.diagnostic_icon_color)
|
||||
.constrained()
|
||||
.with_height(theme.diagnostic_icon_size)
|
||||
.contained()
|
||||
.with_margin_right(theme.diagnostic_icon_spacing)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(
|
||||
diagnostic.message.lines().next().unwrap().to_string(),
|
||||
theme.diagnostic_message.clone(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed()
|
||||
} else {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for DiagnosticMessage {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemViewHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(editor) = active_pane_item.and_then(|item| item.to_any().downcast::<Editor>()) {
|
||||
self._observe_active_editor = Some(cx.observe(&editor, Self::update));
|
||||
self.update(editor, cx);
|
||||
} else {
|
||||
self.diagnostic = Default::default();
|
||||
self._observe_active_editor = None;
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,9 @@
|
||||
use super::{Bias, DisplayMapSnapshot, DisplayPoint, SelectionGoal};
|
||||
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
|
||||
use crate::ToPoint;
|
||||
use anyhow::Result;
|
||||
use std::{cmp, ops::Range};
|
||||
|
||||
pub fn left(map: &DisplayMapSnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
|
||||
pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
|
||||
if point.column() > 0 {
|
||||
*point.column_mut() -= 1;
|
||||
} else if point.row() > 0 {
|
||||
@@ -11,7 +13,7 @@ pub fn left(map: &DisplayMapSnapshot, mut point: DisplayPoint) -> Result<Display
|
||||
Ok(map.clip_point(point, Bias::Left))
|
||||
}
|
||||
|
||||
pub fn right(map: &DisplayMapSnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
|
||||
pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
|
||||
let max_column = map.line_len(point.row());
|
||||
if point.column() < max_column {
|
||||
*point.column_mut() += 1;
|
||||
@@ -23,21 +25,26 @@ pub fn right(map: &DisplayMapSnapshot, mut point: DisplayPoint) -> Result<Displa
|
||||
}
|
||||
|
||||
pub fn up(
|
||||
map: &DisplayMapSnapshot,
|
||||
mut point: DisplayPoint,
|
||||
map: &DisplaySnapshot,
|
||||
start: DisplayPoint,
|
||||
goal: SelectionGoal,
|
||||
) -> Result<(DisplayPoint, SelectionGoal)> {
|
||||
let goal_column = if let SelectionGoal::Column(column) = goal {
|
||||
let mut goal_column = if let SelectionGoal::Column(column) = goal {
|
||||
column
|
||||
} else {
|
||||
map.column_to_chars(point.row(), point.column())
|
||||
map.column_to_chars(start.row(), start.column())
|
||||
};
|
||||
|
||||
if point.row() > 0 {
|
||||
*point.row_mut() -= 1;
|
||||
let prev_row = start.row().saturating_sub(1);
|
||||
let mut point = map.clip_point(
|
||||
DisplayPoint::new(prev_row, map.line_len(prev_row)),
|
||||
Bias::Left,
|
||||
);
|
||||
if point.row() < start.row() {
|
||||
*point.column_mut() = map.column_from_chars(point.row(), goal_column);
|
||||
} else {
|
||||
point = DisplayPoint::new(0, 0);
|
||||
goal_column = 0;
|
||||
}
|
||||
|
||||
let clip_bias = if point.column() == map.line_len(point.row()) {
|
||||
@@ -53,22 +60,23 @@ pub fn up(
|
||||
}
|
||||
|
||||
pub fn down(
|
||||
map: &DisplayMapSnapshot,
|
||||
mut point: DisplayPoint,
|
||||
map: &DisplaySnapshot,
|
||||
start: DisplayPoint,
|
||||
goal: SelectionGoal,
|
||||
) -> Result<(DisplayPoint, SelectionGoal)> {
|
||||
let max_point = map.max_point();
|
||||
let goal_column = if let SelectionGoal::Column(column) = goal {
|
||||
let mut goal_column = if let SelectionGoal::Column(column) = goal {
|
||||
column
|
||||
} else {
|
||||
map.column_to_chars(point.row(), point.column())
|
||||
map.column_to_chars(start.row(), start.column())
|
||||
};
|
||||
|
||||
if point.row() < max_point.row() {
|
||||
*point.row_mut() += 1;
|
||||
let next_row = start.row() + 1;
|
||||
let mut point = map.clip_point(DisplayPoint::new(next_row, 0), Bias::Right);
|
||||
if point.row() > start.row() {
|
||||
*point.column_mut() = map.column_from_chars(point.row(), goal_column);
|
||||
} else {
|
||||
point = max_point;
|
||||
point = map.max_point();
|
||||
goal_column = map.column_to_chars(point.row(), point.column())
|
||||
}
|
||||
|
||||
let clip_bias = if point.column() == map.line_len(point.row()) {
|
||||
@@ -84,27 +92,24 @@ pub fn down(
|
||||
}
|
||||
|
||||
pub fn line_beginning(
|
||||
map: &DisplayMapSnapshot,
|
||||
map: &DisplaySnapshot,
|
||||
point: DisplayPoint,
|
||||
toggle_indent: bool,
|
||||
) -> Result<DisplayPoint> {
|
||||
) -> DisplayPoint {
|
||||
let (indent, is_blank) = map.line_indent(point.row());
|
||||
if toggle_indent && !is_blank && point.column() != indent {
|
||||
Ok(DisplayPoint::new(point.row(), indent))
|
||||
DisplayPoint::new(point.row(), indent)
|
||||
} else {
|
||||
Ok(DisplayPoint::new(point.row(), 0))
|
||||
DisplayPoint::new(point.row(), 0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn line_end(map: &DisplayMapSnapshot, point: DisplayPoint) -> Result<DisplayPoint> {
|
||||
pub fn line_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||
let line_end = DisplayPoint::new(point.row(), map.line_len(point.row()));
|
||||
Ok(map.clip_point(line_end, Bias::Left))
|
||||
map.clip_point(line_end, Bias::Left)
|
||||
}
|
||||
|
||||
pub fn prev_word_boundary(
|
||||
map: &DisplayMapSnapshot,
|
||||
mut point: DisplayPoint,
|
||||
) -> Result<DisplayPoint> {
|
||||
pub fn prev_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
||||
let mut line_start = 0;
|
||||
if point.row() > 0 {
|
||||
if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
|
||||
@@ -114,7 +119,7 @@ pub fn prev_word_boundary(
|
||||
|
||||
if point.column() == line_start {
|
||||
if point.row() == 0 {
|
||||
return Ok(DisplayPoint::new(0, 0));
|
||||
return DisplayPoint::new(0, 0);
|
||||
} else {
|
||||
let row = point.row() - 1;
|
||||
point = map.clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left);
|
||||
@@ -140,13 +145,10 @@ pub fn prev_word_boundary(
|
||||
prev_char_kind = char_kind;
|
||||
column += c.len_utf8() as u32;
|
||||
}
|
||||
Ok(boundary)
|
||||
boundary
|
||||
}
|
||||
|
||||
pub fn next_word_boundary(
|
||||
map: &DisplayMapSnapshot,
|
||||
mut point: DisplayPoint,
|
||||
) -> Result<DisplayPoint> {
|
||||
pub fn next_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
||||
let mut prev_char_kind = None;
|
||||
for c in map.chars_at(point) {
|
||||
let char_kind = char_kind(c);
|
||||
@@ -170,14 +172,54 @@ pub fn next_word_boundary(
|
||||
}
|
||||
prev_char_kind = Some(char_kind);
|
||||
}
|
||||
Ok(point)
|
||||
map.clip_point(point, Bias::Right)
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
|
||||
let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
|
||||
let text = &map.buffer_snapshot;
|
||||
let next_char_kind = text.chars_at(ix).next().map(char_kind);
|
||||
let prev_char_kind = text.reversed_chars_at(ix).next().map(char_kind);
|
||||
prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
|
||||
}
|
||||
|
||||
pub fn surrounding_word(map: &DisplaySnapshot, point: DisplayPoint) -> Range<DisplayPoint> {
|
||||
let mut start = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
|
||||
let mut end = start;
|
||||
|
||||
let text = &map.buffer_snapshot;
|
||||
let mut next_chars = text.chars_at(start).peekable();
|
||||
let mut prev_chars = text.reversed_chars_at(start).peekable();
|
||||
let word_kind = cmp::max(
|
||||
prev_chars.peek().copied().map(char_kind),
|
||||
next_chars.peek().copied().map(char_kind),
|
||||
);
|
||||
|
||||
for ch in prev_chars {
|
||||
if Some(char_kind(ch)) == word_kind {
|
||||
start -= ch.len_utf8();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for ch in next_chars {
|
||||
if Some(char_kind(ch)) == word_kind {
|
||||
end += ch.len_utf8();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
start.to_point(&map.buffer_snapshot).to_display_point(map)
|
||||
..end.to_point(&map.buffer_snapshot).to_display_point(map)
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
|
||||
enum CharKind {
|
||||
Newline,
|
||||
Whitespace,
|
||||
Punctuation,
|
||||
Whitespace,
|
||||
Word,
|
||||
}
|
||||
|
||||
@@ -196,7 +238,120 @@ fn char_kind(c: char) -> CharKind {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{display_map::DisplayMap, Buffer};
|
||||
use crate::{
|
||||
display_map::{BlockDisposition, BlockProperties},
|
||||
Buffer, DisplayMap, ExcerptProperties, MultiBuffer,
|
||||
};
|
||||
use gpui::{elements::Empty, Element};
|
||||
use language::Point;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) {
|
||||
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
|
||||
let font_id = cx
|
||||
.font_cache()
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefg\nhijkl\nmn", cx));
|
||||
let mut excerpt1_header_position = None;
|
||||
let mut excerpt2_header_position = None;
|
||||
let multibuffer = cx.add_model(|cx| {
|
||||
let mut multibuffer = MultiBuffer::new(0);
|
||||
let excerpt1_id = multibuffer.push_excerpt(
|
||||
ExcerptProperties {
|
||||
buffer: &buffer,
|
||||
range: Point::new(0, 0)..Point::new(1, 4),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
let excerpt2_id = multibuffer.push_excerpt(
|
||||
ExcerptProperties {
|
||||
buffer: &buffer,
|
||||
range: Point::new(2, 0)..Point::new(3, 2),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
|
||||
excerpt1_header_position = Some(
|
||||
multibuffer
|
||||
.read(cx)
|
||||
.anchor_in_excerpt(excerpt1_id, language::Anchor::min()),
|
||||
);
|
||||
excerpt2_header_position = Some(
|
||||
multibuffer
|
||||
.read(cx)
|
||||
.anchor_in_excerpt(excerpt2_id, language::Anchor::min()),
|
||||
);
|
||||
multibuffer
|
||||
});
|
||||
|
||||
let display_map =
|
||||
cx.add_model(|cx| DisplayMap::new(multibuffer, 2, font_id, 14.0, None, cx));
|
||||
display_map.update(cx, |display_map, cx| {
|
||||
display_map.insert_blocks(
|
||||
[
|
||||
BlockProperties {
|
||||
position: excerpt1_header_position.unwrap(),
|
||||
height: 2,
|
||||
render: Arc::new(|_| Empty::new().boxed()),
|
||||
disposition: BlockDisposition::Above,
|
||||
},
|
||||
BlockProperties {
|
||||
position: excerpt2_header_position.unwrap(),
|
||||
height: 3,
|
||||
render: Arc::new(|_| Empty::new().boxed()),
|
||||
disposition: BlockDisposition::Above,
|
||||
},
|
||||
],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\n\nhijkl\nmn");
|
||||
|
||||
// Can't move up into the first excerpt's header
|
||||
assert_eq!(
|
||||
up(&snapshot, DisplayPoint::new(2, 2), SelectionGoal::Column(2)).unwrap(),
|
||||
(DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
|
||||
);
|
||||
assert_eq!(
|
||||
up(&snapshot, DisplayPoint::new(2, 0), SelectionGoal::None).unwrap(),
|
||||
(DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
|
||||
);
|
||||
|
||||
// Move up and down within first excerpt
|
||||
assert_eq!(
|
||||
up(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(4)).unwrap(),
|
||||
(DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
|
||||
);
|
||||
assert_eq!(
|
||||
down(&snapshot, DisplayPoint::new(2, 3), SelectionGoal::Column(4)).unwrap(),
|
||||
(DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
|
||||
);
|
||||
|
||||
// Move up and down across second excerpt's header
|
||||
assert_eq!(
|
||||
up(&snapshot, DisplayPoint::new(7, 5), SelectionGoal::Column(5)).unwrap(),
|
||||
(DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
|
||||
);
|
||||
assert_eq!(
|
||||
down(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(5)).unwrap(),
|
||||
(DisplayPoint::new(7, 5), SelectionGoal::Column(5)),
|
||||
);
|
||||
|
||||
// Can't move down off the end
|
||||
assert_eq!(
|
||||
down(&snapshot, DisplayPoint::new(8, 0), SelectionGoal::Column(0)).unwrap(),
|
||||
(DisplayPoint::new(8, 2), SelectionGoal::Column(2)),
|
||||
);
|
||||
assert_eq!(
|
||||
down(&snapshot, DisplayPoint::new(8, 2), SelectionGoal::Column(2)).unwrap(),
|
||||
(DisplayPoint::new(8, 2), SelectionGoal::Column(2)),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
|
||||
@@ -208,50 +363,122 @@ mod tests {
|
||||
.unwrap();
|
||||
let font_size = 14.0;
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, "a bcΔ defγ hi—jk", cx));
|
||||
let buffer = MultiBuffer::build_simple("a bcΔ defγ hi—jk", cx);
|
||||
let display_map =
|
||||
cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
|
||||
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
assert_eq!(
|
||||
prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)).unwrap(),
|
||||
prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)),
|
||||
DisplayPoint::new(0, 7)
|
||||
);
|
||||
assert_eq!(
|
||||
prev_word_boundary(&snapshot, DisplayPoint::new(0, 7)).unwrap(),
|
||||
prev_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
|
||||
DisplayPoint::new(0, 2)
|
||||
);
|
||||
assert_eq!(
|
||||
prev_word_boundary(&snapshot, DisplayPoint::new(0, 6)).unwrap(),
|
||||
prev_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
|
||||
DisplayPoint::new(0, 2)
|
||||
);
|
||||
assert_eq!(
|
||||
prev_word_boundary(&snapshot, DisplayPoint::new(0, 2)).unwrap(),
|
||||
prev_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
|
||||
DisplayPoint::new(0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
prev_word_boundary(&snapshot, DisplayPoint::new(0, 1)).unwrap(),
|
||||
prev_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
|
||||
DisplayPoint::new(0, 0)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
next_word_boundary(&snapshot, DisplayPoint::new(0, 0)).unwrap(),
|
||||
next_word_boundary(&snapshot, DisplayPoint::new(0, 0)),
|
||||
DisplayPoint::new(0, 1)
|
||||
);
|
||||
assert_eq!(
|
||||
next_word_boundary(&snapshot, DisplayPoint::new(0, 1)).unwrap(),
|
||||
next_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
|
||||
DisplayPoint::new(0, 6)
|
||||
);
|
||||
assert_eq!(
|
||||
next_word_boundary(&snapshot, DisplayPoint::new(0, 2)).unwrap(),
|
||||
next_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
|
||||
DisplayPoint::new(0, 6)
|
||||
);
|
||||
assert_eq!(
|
||||
next_word_boundary(&snapshot, DisplayPoint::new(0, 6)).unwrap(),
|
||||
next_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
|
||||
DisplayPoint::new(0, 12)
|
||||
);
|
||||
assert_eq!(
|
||||
next_word_boundary(&snapshot, DisplayPoint::new(0, 7)).unwrap(),
|
||||
next_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
|
||||
DisplayPoint::new(0, 12)
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
|
||||
let tab_size = 4;
|
||||
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
|
||||
let font_id = cx
|
||||
.font_cache()
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let font_size = 14.0;
|
||||
let buffer = MultiBuffer::build_simple("lorem ipsum dolor\n sit", cx);
|
||||
let display_map =
|
||||
cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
|
||||
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(0, 0)),
|
||||
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(0, 2)),
|
||||
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(0, 5)),
|
||||
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(0, 6)),
|
||||
DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(0, 7)),
|
||||
DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(0, 11)),
|
||||
DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(0, 13)),
|
||||
DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14)
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(0, 14)),
|
||||
DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(0, 17)),
|
||||
DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(0, 19)),
|
||||
DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(1, 0)),
|
||||
DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4)
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(1, 1)),
|
||||
DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4)
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(1, 6)),
|
||||
DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7)
|
||||
);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, DisplayPoint::new(1, 7)),
|
||||
DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
3155
crates/editor/src/multi_buffer.rs
Normal file
3155
crates/editor/src/multi_buffer.rs
Normal file
File diff suppressed because it is too large
Load Diff
141
crates/editor/src/multi_buffer/anchor.rs
Normal file
141
crates/editor/src/multi_buffer/anchor.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||
use anyhow::Result;
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
ops::{Range, Sub},
|
||||
};
|
||||
use sum_tree::Bias;
|
||||
use text::{rope::TextDimension, Point};
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
|
||||
pub struct Anchor {
|
||||
pub(crate) buffer_id: usize,
|
||||
pub(crate) excerpt_id: ExcerptId,
|
||||
pub(crate) text_anchor: text::Anchor,
|
||||
}
|
||||
|
||||
impl Anchor {
|
||||
pub fn min() -> Self {
|
||||
Self {
|
||||
buffer_id: 0,
|
||||
excerpt_id: ExcerptId::min(),
|
||||
text_anchor: text::Anchor::min(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max() -> Self {
|
||||
Self {
|
||||
buffer_id: 0,
|
||||
excerpt_id: ExcerptId::max(),
|
||||
text_anchor: text::Anchor::max(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn excerpt_id(&self) -> &ExcerptId {
|
||||
&self.excerpt_id
|
||||
}
|
||||
|
||||
pub fn cmp<'a>(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Result<Ordering> {
|
||||
let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id);
|
||||
if excerpt_id_cmp.is_eq() {
|
||||
if self.excerpt_id == ExcerptId::min() || self.excerpt_id == ExcerptId::max() {
|
||||
Ok(Ordering::Equal)
|
||||
} else if let Some((buffer_id, buffer_snapshot)) =
|
||||
snapshot.buffer_snapshot_for_excerpt(&self.excerpt_id)
|
||||
{
|
||||
// Even though the anchor refers to a valid excerpt the underlying buffer might have
|
||||
// changed. In that case, treat the anchor as if it were at the start of that
|
||||
// excerpt.
|
||||
if self.buffer_id == buffer_id && other.buffer_id == buffer_id {
|
||||
self.text_anchor.cmp(&other.text_anchor, buffer_snapshot)
|
||||
} else if self.buffer_id == buffer_id {
|
||||
Ok(Ordering::Greater)
|
||||
} else if other.buffer_id == buffer_id {
|
||||
Ok(Ordering::Less)
|
||||
} else {
|
||||
Ok(Ordering::Equal)
|
||||
}
|
||||
} else {
|
||||
Ok(Ordering::Equal)
|
||||
}
|
||||
} else {
|
||||
Ok(excerpt_id_cmp)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
|
||||
if self.text_anchor.bias != Bias::Left {
|
||||
if let Some((buffer_id, buffer_snapshot)) =
|
||||
snapshot.buffer_snapshot_for_excerpt(&self.excerpt_id)
|
||||
{
|
||||
if self.buffer_id == buffer_id {
|
||||
return Self {
|
||||
buffer_id: self.buffer_id,
|
||||
excerpt_id: self.excerpt_id.clone(),
|
||||
text_anchor: self.text_anchor.bias_left(buffer_snapshot),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
self.clone()
|
||||
}
|
||||
|
||||
pub fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
|
||||
if self.text_anchor.bias != Bias::Right {
|
||||
if let Some((buffer_id, buffer_snapshot)) =
|
||||
snapshot.buffer_snapshot_for_excerpt(&self.excerpt_id)
|
||||
{
|
||||
if self.buffer_id == buffer_id {
|
||||
return Self {
|
||||
buffer_id: self.buffer_id,
|
||||
excerpt_id: self.excerpt_id.clone(),
|
||||
text_anchor: self.text_anchor.bias_right(buffer_snapshot),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
self.clone()
|
||||
}
|
||||
|
||||
pub fn summary<D>(&self, snapshot: &MultiBufferSnapshot) -> D
|
||||
where
|
||||
D: TextDimension + Ord + Sub<D, Output = D>,
|
||||
{
|
||||
snapshot.summary_for_anchor(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOffset for Anchor {
|
||||
fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize {
|
||||
self.summary(snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPoint for Anchor {
|
||||
fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point {
|
||||
self.summary(snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AnchorRangeExt {
|
||||
fn cmp(&self, b: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Result<Ordering>;
|
||||
fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<usize>;
|
||||
fn to_point(&self, content: &MultiBufferSnapshot) -> Range<Point>;
|
||||
}
|
||||
|
||||
impl AnchorRangeExt for Range<Anchor> {
|
||||
fn cmp(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Result<Ordering> {
|
||||
Ok(match self.start.cmp(&other.start, buffer)? {
|
||||
Ordering::Equal => other.end.cmp(&self.end, buffer)?,
|
||||
ord @ _ => ord,
|
||||
})
|
||||
}
|
||||
|
||||
fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<usize> {
|
||||
self.start.to_offset(&content)..self.end.to_offset(&content)
|
||||
}
|
||||
|
||||
fn to_point(&self, content: &MultiBufferSnapshot) -> Range<Point> {
|
||||
self.start.to_point(&content)..self.end.to_point(&content)
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,6 @@
|
||||
use gpui::{Entity, ModelHandle};
|
||||
use smol::channel;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
pub fn sample_text(rows: usize, cols: usize) -> String {
|
||||
let mut text = String::new();
|
||||
for row in 0..rows {
|
||||
let c: char = ('a' as u32 + row as u32) as u8 as char;
|
||||
let mut line = c.to_string().repeat(cols);
|
||||
if row < rows - 1 {
|
||||
line.push('\n');
|
||||
}
|
||||
text += &line;
|
||||
}
|
||||
text
|
||||
}
|
||||
|
||||
pub struct Observer<T>(PhantomData<T>);
|
||||
|
||||
impl<T: 'static> Entity for Observer<T> {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl<T: Entity> Observer<T> {
|
||||
pub fn new(
|
||||
handle: &ModelHandle<T>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) -> (ModelHandle<Self>, channel::Receiver<()>) {
|
||||
let (notify_tx, notify_rx) = channel::unbounded();
|
||||
let observer = cx.add_model(|cx| {
|
||||
cx.observe(handle, move |_, _, _| {
|
||||
let _ = notify_tx.try_send(());
|
||||
})
|
||||
.detach();
|
||||
Observer(PhantomData)
|
||||
});
|
||||
(observer, notify_rx)
|
||||
}
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
// std::env::set_var("RUST_LOG", "info");
|
||||
env_logger::init();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ name = "file_finder"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
path = "src/file_finder.rs"
|
||||
|
||||
[dependencies]
|
||||
editor = { path = "../editor" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
@@ -14,5 +17,6 @@ workspace = { path = "../workspace" }
|
||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
serde_json = { version = "1.0.64", features = ["preserve_order"] }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
|
||||
@@ -3,16 +3,12 @@ use fuzzy::PathMatch;
|
||||
use gpui::{
|
||||
action,
|
||||
elements::*,
|
||||
keymap::{
|
||||
self,
|
||||
menu::{SelectNext, SelectPrev},
|
||||
Binding,
|
||||
},
|
||||
keymap::{self, Binding},
|
||||
AppContext, Axis, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use postage::watch;
|
||||
use project::{Project, ProjectPath};
|
||||
use project::{Project, ProjectPath, WorktreeId};
|
||||
use std::{
|
||||
cmp,
|
||||
path::Path,
|
||||
@@ -22,7 +18,10 @@ use std::{
|
||||
},
|
||||
};
|
||||
use util::post_inc;
|
||||
use workspace::{Settings, Workspace};
|
||||
use workspace::{
|
||||
menu::{Confirm, SelectNext, SelectPrev},
|
||||
Settings, Workspace,
|
||||
};
|
||||
|
||||
pub struct FileFinder {
|
||||
handle: WeakViewHandle<Self>,
|
||||
@@ -40,7 +39,6 @@ pub struct FileFinder {
|
||||
}
|
||||
|
||||
action!(Toggle);
|
||||
action!(Confirm);
|
||||
action!(Select, ProjectPath);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
@@ -53,7 +51,6 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_bindings(vec![
|
||||
Binding::new("cmd-p", Toggle, None),
|
||||
Binding::new("escape", Toggle, Some("FileFinder")),
|
||||
Binding::new("enter", Confirm, Some("FileFinder")),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -83,7 +80,7 @@ impl View for FileFinder {
|
||||
.with_style(settings.theme.selector.input_editor.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(Flexible::new(1.0, self.render_matches()).boxed())
|
||||
.with_child(Flexible::new(1.0, false, self.render_matches()).boxed())
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(settings.theme.selector.container)
|
||||
@@ -175,6 +172,7 @@ impl FileFinder {
|
||||
.with_child(
|
||||
Flexible::new(
|
||||
1.0,
|
||||
false,
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Label::new(file_name.to_string(), style.label.clone())
|
||||
@@ -195,7 +193,7 @@ impl FileFinder {
|
||||
.with_style(style.container);
|
||||
|
||||
let action = Select(ProjectPath {
|
||||
worktree_id: path_match.worktree_id,
|
||||
worktree_id: WorktreeId::from_usize(path_match.worktree_id),
|
||||
path: path_match.path.clone(),
|
||||
});
|
||||
EventHandler::new(container.boxed())
|
||||
@@ -249,8 +247,8 @@ impl FileFinder {
|
||||
match event {
|
||||
Event::Selected(project_path) => {
|
||||
workspace
|
||||
.open_entry(project_path.clone(), cx)
|
||||
.map(|d| d.detach());
|
||||
.open_path(project_path.clone(), cx)
|
||||
.detach_and_log_err(cx);
|
||||
workspace.dismiss_modal(cx);
|
||||
}
|
||||
Event::Dismissed => {
|
||||
@@ -270,13 +268,14 @@ impl FileFinder {
|
||||
Editor::single_line(
|
||||
{
|
||||
let settings = settings.clone();
|
||||
move |_| {
|
||||
Arc::new(move |_| {
|
||||
let settings = settings.borrow();
|
||||
EditorSettings {
|
||||
style: settings.theme.selector.input_editor.as_editor(),
|
||||
tab_size: settings.tab_size,
|
||||
soft_wrap: editor::SoftWrap::None,
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
cx,
|
||||
)
|
||||
@@ -285,7 +284,7 @@ impl FileFinder {
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
handle: cx.handle().downgrade(),
|
||||
handle: cx.weak_handle(),
|
||||
settings,
|
||||
project,
|
||||
query_editor,
|
||||
@@ -351,7 +350,8 @@ impl FileFinder {
|
||||
let mat = &self.matches[selected_index];
|
||||
self.selected = Some((mat.worktree_id, mat.path.clone()));
|
||||
}
|
||||
self.list_state.scroll_to(selected_index);
|
||||
self.list_state
|
||||
.scroll_to(ScrollTarget::Show(selected_index));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -362,14 +362,15 @@ impl FileFinder {
|
||||
let mat = &self.matches[selected_index];
|
||||
self.selected = Some((mat.worktree_id, mat.path.clone()));
|
||||
}
|
||||
self.list_state.scroll_to(selected_index);
|
||||
self.list_state
|
||||
.scroll_to(ScrollTarget::Show(selected_index));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||
if let Some(m) = self.matches.get(self.selected_index()) {
|
||||
cx.emit(Event::Selected(ProjectPath {
|
||||
worktree_id: m.worktree_id,
|
||||
worktree_id: WorktreeId::from_usize(m.worktree_id),
|
||||
path: m.path.clone(),
|
||||
}));
|
||||
}
|
||||
@@ -413,7 +414,8 @@ impl FileFinder {
|
||||
}
|
||||
self.latest_search_query = query;
|
||||
self.latest_search_did_cancel = did_cancel;
|
||||
self.list_state.scroll_to(self.selected_index());
|
||||
self.list_state
|
||||
.scroll_to(ScrollTarget::Show(self.selected_index()));
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -429,7 +431,14 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_matching_paths(mut cx: gpui::TestAppContext) {
|
||||
let params = cx.update(WorkspaceParams::test);
|
||||
let mut path_openers = Vec::new();
|
||||
cx.update(|cx| {
|
||||
super::init(cx);
|
||||
editor::init(cx, &mut path_openers);
|
||||
});
|
||||
|
||||
let mut params = cx.update(WorkspaceParams::test);
|
||||
params.path_openers = Arc::from(path_openers);
|
||||
params
|
||||
.fs
|
||||
.as_fake()
|
||||
@@ -443,10 +452,6 @@ mod tests {
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
cx.update(|cx| {
|
||||
super::init(cx);
|
||||
editor::init(cx);
|
||||
});
|
||||
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
|
||||
workspace
|
||||
@@ -4,6 +4,9 @@ version = "2.0.2"
|
||||
license = "MIT"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
path = "src/fsevent.rs"
|
||||
|
||||
[dependencies]
|
||||
bitflags = "1"
|
||||
fsevent-sys = "3.0.2"
|
||||
|
||||
@@ -3,6 +3,9 @@ name = "fuzzy"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
path = "src/fuzzy.rs"
|
||||
|
||||
[dependencies]
|
||||
gpui = { path = "../gpui" }
|
||||
util = { path = "../util" }
|
||||
|
||||
@@ -9,6 +9,7 @@ impl CharBag {
|
||||
}
|
||||
|
||||
fn insert(&mut self, c: char) {
|
||||
let c = c.to_ascii_lowercase();
|
||||
if c >= 'a' && c <= 'z' {
|
||||
let mut count = self.0;
|
||||
let idx = c as u8 - 'a' as u8;
|
||||
|
||||
@@ -55,6 +55,7 @@ pub struct PathMatch {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct StringMatchCandidate {
|
||||
pub id: usize,
|
||||
pub string: String,
|
||||
pub char_bag: CharBag,
|
||||
}
|
||||
@@ -109,6 +110,7 @@ impl<'a> MatchCandidate for &'a StringMatchCandidate {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct StringMatch {
|
||||
pub candidate_id: usize,
|
||||
pub score: f64,
|
||||
pub positions: Vec<usize>,
|
||||
pub string: String,
|
||||
@@ -116,7 +118,7 @@ pub struct StringMatch {
|
||||
|
||||
impl PartialEq for StringMatch {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.score.eq(&other.score)
|
||||
self.cmp(other).is_eq()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,13 +135,13 @@ impl Ord for StringMatch {
|
||||
self.score
|
||||
.partial_cmp(&other.score)
|
||||
.unwrap_or(Ordering::Equal)
|
||||
.then_with(|| self.string.cmp(&other.string))
|
||||
.then_with(|| self.candidate_id.cmp(&other.candidate_id))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for PathMatch {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.score.eq(&other.score)
|
||||
self.cmp(other).is_eq()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,8 +189,8 @@ pub async fn match_strings(
|
||||
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
|
||||
let cancel_flag = &cancel_flag;
|
||||
scope.spawn(async move {
|
||||
let segment_start = segment_idx * segment_size;
|
||||
let segment_end = segment_start + segment_size;
|
||||
let segment_start = cmp::min(segment_idx * segment_size, candidates.len());
|
||||
let segment_end = cmp::min(segment_start + segment_size, candidates.len());
|
||||
let mut matcher = Matcher::new(
|
||||
query,
|
||||
lowercase_query,
|
||||
@@ -330,6 +332,7 @@ impl<'a> Matcher<'a> {
|
||||
results,
|
||||
cancel_flag,
|
||||
|candidate, score| StringMatch {
|
||||
candidate_id: candidate.id,
|
||||
score,
|
||||
positions: Vec::new(),
|
||||
string: candidate.string.to_string(),
|
||||
@@ -433,13 +436,17 @@ impl<'a> Matcher<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn find_last_positions(&mut self, prefix: &[char], path: &[char]) -> bool {
|
||||
let mut path = path.iter();
|
||||
let mut prefix_iter = prefix.iter();
|
||||
for (i, char) in self.query.iter().enumerate().rev() {
|
||||
if let Some(j) = path.rposition(|c| c == char) {
|
||||
self.last_positions[i] = j + prefix.len();
|
||||
} else if let Some(j) = prefix_iter.rposition(|c| c == char) {
|
||||
fn find_last_positions(
|
||||
&mut self,
|
||||
lowercase_prefix: &[char],
|
||||
lowercase_candidate: &[char],
|
||||
) -> bool {
|
||||
let mut lowercase_prefix = lowercase_prefix.iter();
|
||||
let mut lowercase_candidate = lowercase_candidate.iter();
|
||||
for (i, char) in self.lowercase_query.iter().enumerate().rev() {
|
||||
if let Some(j) = lowercase_candidate.rposition(|c| c == char) {
|
||||
self.last_positions[i] = j + lowercase_prefix.len();
|
||||
} else if let Some(j) = lowercase_prefix.rposition(|c| c == char) {
|
||||
self.last_positions[i] = j;
|
||||
} else {
|
||||
return false;
|
||||
14
crates/go_to_line/Cargo.toml
Normal file
14
crates/go_to_line/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "go_to_line"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
path = "src/go_to_line.rs"
|
||||
|
||||
[dependencies]
|
||||
text = { path = "../text" }
|
||||
editor = { path = "../editor" }
|
||||
gpui = { path = "../gpui" }
|
||||
workspace = { path = "../workspace" }
|
||||
postage = { version = "0.4", features = ["futures-traits"] }
|
||||
227
crates/go_to_line/src/go_to_line.rs
Normal file
227
crates/go_to_line/src/go_to_line.rs
Normal file
@@ -0,0 +1,227 @@
|
||||
use editor::{display_map::ToDisplayPoint, Autoscroll, Editor, EditorSettings};
|
||||
use gpui::{
|
||||
action, elements::*, geometry::vector::Vector2F, keymap::Binding, Axis, Entity,
|
||||
MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use postage::watch;
|
||||
use std::sync::Arc;
|
||||
use text::{Bias, Point, Selection};
|
||||
use workspace::{Settings, Workspace};
|
||||
|
||||
action!(Toggle);
|
||||
action!(Confirm);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_bindings([
|
||||
Binding::new("ctrl-g", Toggle, Some("Editor")),
|
||||
Binding::new("escape", Toggle, Some("GoToLine")),
|
||||
Binding::new("enter", Confirm, Some("GoToLine")),
|
||||
]);
|
||||
cx.add_action(GoToLine::toggle);
|
||||
cx.add_action(GoToLine::confirm);
|
||||
}
|
||||
|
||||
pub struct GoToLine {
|
||||
settings: watch::Receiver<Settings>,
|
||||
line_editor: ViewHandle<Editor>,
|
||||
active_editor: ViewHandle<Editor>,
|
||||
restore_state: Option<RestoreState>,
|
||||
line_selection_id: Option<usize>,
|
||||
cursor_point: Point,
|
||||
max_point: Point,
|
||||
}
|
||||
|
||||
struct RestoreState {
|
||||
scroll_position: Vector2F,
|
||||
selections: Vec<Selection<usize>>,
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
Dismissed,
|
||||
}
|
||||
|
||||
impl GoToLine {
|
||||
pub fn new(
|
||||
active_editor: ViewHandle<Editor>,
|
||||
settings: watch::Receiver<Settings>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let line_editor = cx.add_view(|cx| {
|
||||
Editor::single_line(
|
||||
{
|
||||
let settings = settings.clone();
|
||||
Arc::new(move |_| {
|
||||
let settings = settings.borrow();
|
||||
EditorSettings {
|
||||
tab_size: settings.tab_size,
|
||||
style: settings.theme.selector.input_editor.as_editor(),
|
||||
soft_wrap: editor::SoftWrap::None,
|
||||
}
|
||||
})
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.subscribe(&line_editor, Self::on_line_editor_event)
|
||||
.detach();
|
||||
|
||||
let (restore_state, cursor_point, max_point) = active_editor.update(cx, |editor, cx| {
|
||||
let restore_state = Some(RestoreState {
|
||||
scroll_position: editor.scroll_position(cx),
|
||||
selections: editor.local_selections::<usize>(cx),
|
||||
});
|
||||
|
||||
let buffer = editor.buffer().read(cx).read(cx);
|
||||
(
|
||||
restore_state,
|
||||
editor.newest_selection(&buffer).head(),
|
||||
buffer.max_point(),
|
||||
)
|
||||
});
|
||||
|
||||
Self {
|
||||
settings: settings.clone(),
|
||||
line_editor,
|
||||
active_editor,
|
||||
restore_state,
|
||||
line_selection_id: None,
|
||||
cursor_point,
|
||||
max_point,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||
workspace.toggle_modal(cx, |cx, workspace| {
|
||||
let editor = workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.to_any()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
let view = cx.add_view(|cx| GoToLine::new(editor, workspace.settings.clone(), cx));
|
||||
cx.subscribe(&view, Self::on_event).detach();
|
||||
view
|
||||
});
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||
self.restore_state.take();
|
||||
cx.emit(Event::Dismissed);
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
workspace: &mut Workspace,
|
||||
_: ViewHandle<Self>,
|
||||
event: &Event,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
match event {
|
||||
Event::Dismissed => workspace.dismiss_modal(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_line_editor_event(
|
||||
&mut self,
|
||||
_: ViewHandle<Editor>,
|
||||
event: &editor::Event,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
||||
editor::Event::Edited => {
|
||||
let line_editor = self.line_editor.read(cx).buffer().read(cx).read(cx).text();
|
||||
let mut components = line_editor.trim().split(&[',', ':'][..]);
|
||||
let row = components.next().and_then(|row| row.parse::<u32>().ok());
|
||||
let column = components.next().and_then(|row| row.parse::<u32>().ok());
|
||||
if let Some(point) = row.map(|row| {
|
||||
Point::new(
|
||||
row.saturating_sub(1),
|
||||
column.map(|column| column.saturating_sub(1)).unwrap_or(0),
|
||||
)
|
||||
}) {
|
||||
self.line_selection_id = self.active_editor.update(cx, |active_editor, cx| {
|
||||
let snapshot = active_editor.snapshot(cx).display_snapshot;
|
||||
let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
|
||||
let display_point = point.to_display_point(&snapshot);
|
||||
let row = display_point.row();
|
||||
active_editor.select_ranges([point..point], Some(Autoscroll::Center), cx);
|
||||
active_editor.set_highlighted_rows(Some(row..row + 1));
|
||||
Some(
|
||||
active_editor
|
||||
.newest_selection::<usize>(&snapshot.buffer_snapshot)
|
||||
.id,
|
||||
)
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for GoToLine {
|
||||
type Event = Event;
|
||||
|
||||
fn release(&mut self, cx: &mut MutableAppContext) {
|
||||
let line_selection_id = self.line_selection_id.take();
|
||||
let restore_state = self.restore_state.take();
|
||||
self.active_editor.update(cx, |editor, cx| {
|
||||
editor.set_highlighted_rows(None);
|
||||
if let Some((line_selection_id, restore_state)) = line_selection_id.zip(restore_state) {
|
||||
let newest_selection =
|
||||
editor.newest_selection::<usize>(&editor.buffer().read(cx).read(cx));
|
||||
if line_selection_id == newest_selection.id {
|
||||
editor.set_scroll_position(restore_state.scroll_position, cx);
|
||||
editor.update_selections(restore_state.selections, None, cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl View for GoToLine {
|
||||
fn ui_name() -> &'static str {
|
||||
"GoToLine"
|
||||
}
|
||||
|
||||
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = &self.settings.borrow().theme.selector;
|
||||
|
||||
let label = format!(
|
||||
"{},{} of {} lines",
|
||||
self.cursor_point.row + 1,
|
||||
self.cursor_point.column + 1,
|
||||
self.max_point.row + 1
|
||||
);
|
||||
|
||||
Align::new(
|
||||
ConstrainedBox::new(
|
||||
Container::new(
|
||||
Flex::new(Axis::Vertical)
|
||||
.with_child(
|
||||
Container::new(ChildView::new(self.line_editor.id()).boxed())
|
||||
.with_style(theme.input_editor.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(Label::new(label, theme.empty.label.clone()).boxed())
|
||||
.with_style(theme.empty.container)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(theme.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_max_width(500.0)
|
||||
.boxed(),
|
||||
)
|
||||
.top()
|
||||
.named("go to line")
|
||||
}
|
||||
|
||||
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.focus(&self.line_editor);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,9 @@ edition = "2018"
|
||||
name = "gpui"
|
||||
version = "0.1.0"
|
||||
|
||||
[lib]
|
||||
path = "src/gpui.rs"
|
||||
|
||||
[features]
|
||||
test-support = ["env_logger"]
|
||||
|
||||
@@ -15,6 +18,7 @@ backtrace = "0.3"
|
||||
ctor = "0.1"
|
||||
env_logger = { version = "0.8", optional = true }
|
||||
etagere = "0.2"
|
||||
futures = "0.3"
|
||||
image = "0.23"
|
||||
lazy_static = "1.4.0"
|
||||
log = "0.4"
|
||||
@@ -34,7 +38,7 @@ smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "1.2"
|
||||
time = { version = "0.3" }
|
||||
tiny-skia = "0.5"
|
||||
tree-sitter = "0.19"
|
||||
tree-sitter = "0.20"
|
||||
usvg = "0.14"
|
||||
waker-fn = "1.1.0"
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ impl gpui::Element for TextElement {
|
||||
.select_font(family, &Default::default())
|
||||
.unwrap(),
|
||||
color: Color::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
};
|
||||
let bold = RunStyle {
|
||||
font_id: cx
|
||||
@@ -76,7 +76,7 @@ impl gpui::Element for TextElement {
|
||||
)
|
||||
.unwrap(),
|
||||
color: Color::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
};
|
||||
|
||||
let text = "Hello world!";
|
||||
|
||||
@@ -14,7 +14,7 @@ include = ["bindings/rust/*", "grammar.js", "queries/*", "src/*"]
|
||||
path = "bindings/rust/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
tree-sitter = "0.19.3"
|
||||
tree-sitter = "0.20"
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.0"
|
||||
|
||||
@@ -23,6 +23,7 @@ use std::{
|
||||
mem,
|
||||
ops::{Deref, DerefMut},
|
||||
path::{Path, PathBuf},
|
||||
pin::Pin,
|
||||
rc::{self, Rc},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering::SeqCst},
|
||||
@@ -35,6 +36,12 @@ pub trait Entity: 'static {
|
||||
type Event;
|
||||
|
||||
fn release(&mut self, _: &mut MutableAppContext) {}
|
||||
fn app_will_quit(
|
||||
&mut self,
|
||||
_: &mut MutableAppContext,
|
||||
) -> Option<Pin<Box<dyn 'static + Future<Output = ()>>>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub trait View: Entity + Sized {
|
||||
@@ -198,8 +205,6 @@ pub struct App(Rc<RefCell<MutableAppContext>>);
|
||||
#[derive(Clone)]
|
||||
pub struct AsyncAppContext(Rc<RefCell<MutableAppContext>>);
|
||||
|
||||
pub struct BackgroundAppContext(*const RefCell<MutableAppContext>);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TestAppContext {
|
||||
cx: Rc<RefCell<MutableAppContext>>,
|
||||
@@ -220,20 +225,29 @@ impl App {
|
||||
asset_source,
|
||||
))));
|
||||
|
||||
let cx = app.0.clone();
|
||||
foreground_platform.on_menu_command(Box::new(move |action| {
|
||||
let mut cx = cx.borrow_mut();
|
||||
if let Some(key_window_id) = cx.cx.platform.key_window_id() {
|
||||
if let Some((presenter, _)) = cx.presenters_and_platform_windows.get(&key_window_id)
|
||||
{
|
||||
let presenter = presenter.clone();
|
||||
let path = presenter.borrow().dispatch_path(cx.as_ref());
|
||||
cx.dispatch_action_any(key_window_id, &path, action);
|
||||
foreground_platform.on_quit(Box::new({
|
||||
let cx = app.0.clone();
|
||||
move || {
|
||||
cx.borrow_mut().quit();
|
||||
}
|
||||
}));
|
||||
foreground_platform.on_menu_command(Box::new({
|
||||
let cx = app.0.clone();
|
||||
move |action| {
|
||||
let mut cx = cx.borrow_mut();
|
||||
if let Some(key_window_id) = cx.cx.platform.key_window_id() {
|
||||
if let Some((presenter, _)) =
|
||||
cx.presenters_and_platform_windows.get(&key_window_id)
|
||||
{
|
||||
let presenter = presenter.clone();
|
||||
let path = presenter.borrow().dispatch_path(cx.as_ref());
|
||||
cx.dispatch_action_any(key_window_id, &path, action);
|
||||
} else {
|
||||
cx.dispatch_global_action_any(action);
|
||||
}
|
||||
} else {
|
||||
cx.dispatch_global_action_any(action);
|
||||
}
|
||||
} else {
|
||||
cx.dispatch_global_action_any(action);
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -265,6 +279,18 @@ impl App {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_quit<F>(self, mut callback: F) -> Self
|
||||
where
|
||||
F: 'static + FnMut(&mut MutableAppContext),
|
||||
{
|
||||
let cx = self.0.clone();
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.foreground_platform
|
||||
.on_quit(Box::new(move || callback(&mut *cx.borrow_mut())));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_event<F>(self, mut callback: F) -> Self
|
||||
where
|
||||
F: 'static + FnMut(Event, &mut MutableAppContext) -> bool,
|
||||
@@ -316,10 +342,8 @@ impl App {
|
||||
|
||||
fn update<T, F: FnOnce(&mut MutableAppContext) -> T>(&mut self, callback: F) -> T {
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.pending_flushes += 1;
|
||||
let result = callback(&mut *state);
|
||||
let result = state.update(callback);
|
||||
state.pending_notifications.clear();
|
||||
state.flush_effects();
|
||||
result
|
||||
}
|
||||
}
|
||||
@@ -380,11 +404,7 @@ impl TestAppContext {
|
||||
T: Entity,
|
||||
F: FnOnce(&mut ModelContext<T>) -> T,
|
||||
{
|
||||
let mut state = self.cx.borrow_mut();
|
||||
state.pending_flushes += 1;
|
||||
let handle = state.add_model(build_model);
|
||||
state.flush_effects();
|
||||
handle
|
||||
self.cx.borrow_mut().add_model(build_model)
|
||||
}
|
||||
|
||||
pub fn add_window<T, F>(&mut self, build_root_view: F) -> (usize, ViewHandle<T>)
|
||||
@@ -410,11 +430,7 @@ impl TestAppContext {
|
||||
T: View,
|
||||
F: FnOnce(&mut ViewContext<T>) -> T,
|
||||
{
|
||||
let mut state = self.cx.borrow_mut();
|
||||
state.pending_flushes += 1;
|
||||
let handle = state.add_view(window_id, build_view);
|
||||
state.flush_effects();
|
||||
handle
|
||||
self.cx.borrow_mut().add_view(window_id, build_view)
|
||||
}
|
||||
|
||||
pub fn add_option_view<T, F>(
|
||||
@@ -426,11 +442,7 @@ impl TestAppContext {
|
||||
T: View,
|
||||
F: FnOnce(&mut ViewContext<T>) -> Option<T>,
|
||||
{
|
||||
let mut state = self.cx.borrow_mut();
|
||||
state.pending_flushes += 1;
|
||||
let handle = state.add_option_view(window_id, build_view);
|
||||
state.flush_effects();
|
||||
handle
|
||||
self.cx.borrow_mut().add_option_view(window_id, build_view)
|
||||
}
|
||||
|
||||
pub fn read<T, F: FnOnce(&AppContext) -> T>(&self, callback: F) -> T {
|
||||
@@ -509,11 +521,7 @@ impl AsyncAppContext {
|
||||
}
|
||||
|
||||
pub fn update<T, F: FnOnce(&mut MutableAppContext) -> T>(&mut self, callback: F) -> T {
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.pending_flushes += 1;
|
||||
let result = callback(&mut *state);
|
||||
state.flush_effects();
|
||||
result
|
||||
self.0.borrow_mut().update(callback)
|
||||
}
|
||||
|
||||
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
|
||||
@@ -543,11 +551,7 @@ impl UpdateModel for AsyncAppContext {
|
||||
handle: &ModelHandle<E>,
|
||||
update: &mut dyn FnMut(&mut E, &mut ModelContext<E>) -> O,
|
||||
) -> O {
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.pending_flushes += 1;
|
||||
let result = state.update_model(handle, update);
|
||||
state.flush_effects();
|
||||
result
|
||||
self.0.borrow_mut().update_model(handle, update)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,11 +585,7 @@ impl UpdateView for AsyncAppContext {
|
||||
where
|
||||
T: View,
|
||||
{
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.pending_flushes += 1;
|
||||
let result = state.update_view(handle, update);
|
||||
state.flush_effects();
|
||||
result
|
||||
self.0.borrow_mut().update_view(handle, update)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,11 +610,7 @@ impl UpdateModel for TestAppContext {
|
||||
handle: &ModelHandle<T>,
|
||||
update: &mut dyn FnMut(&mut T, &mut ModelContext<T>) -> O,
|
||||
) -> O {
|
||||
let mut state = self.cx.borrow_mut();
|
||||
state.pending_flushes += 1;
|
||||
let result = state.update_model(handle, update);
|
||||
state.flush_effects();
|
||||
result
|
||||
self.cx.borrow_mut().update_model(handle, update)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -639,11 +635,7 @@ impl UpdateView for TestAppContext {
|
||||
where
|
||||
T: View,
|
||||
{
|
||||
let mut state = self.cx.borrow_mut();
|
||||
state.pending_flushes += 1;
|
||||
let result = state.update_view(handle, update);
|
||||
state.flush_effects();
|
||||
result
|
||||
self.cx.borrow_mut().update_view(handle, update)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -675,7 +667,7 @@ pub struct MutableAppContext {
|
||||
assets: Arc<AssetCache>,
|
||||
cx: AppContext,
|
||||
actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
|
||||
global_actions: HashMap<TypeId, Vec<Box<GlobalActionCallback>>>,
|
||||
global_actions: HashMap<TypeId, Box<GlobalActionCallback>>,
|
||||
keystroke_matcher: keymap::Matcher,
|
||||
next_entity_id: usize,
|
||||
next_window_id: usize,
|
||||
@@ -701,6 +693,7 @@ impl MutableAppContext {
|
||||
foreground_platform: Rc<dyn platform::ForegroundPlatform>,
|
||||
font_cache: Arc<FontCache>,
|
||||
asset_source: impl AssetSource,
|
||||
// entity_drop_tx:
|
||||
) -> Self {
|
||||
Self {
|
||||
weak_self: None,
|
||||
@@ -739,6 +732,39 @@ impl MutableAppContext {
|
||||
App(self.weak_self.as_ref().unwrap().upgrade().unwrap())
|
||||
}
|
||||
|
||||
pub fn quit(&mut self) {
|
||||
let mut futures = Vec::new();
|
||||
for model_id in self.cx.models.keys().copied().collect::<Vec<_>>() {
|
||||
let mut model = self.cx.models.remove(&model_id).unwrap();
|
||||
futures.extend(model.app_will_quit(self));
|
||||
self.cx.models.insert(model_id, model);
|
||||
}
|
||||
|
||||
for view_id in self.cx.views.keys().copied().collect::<Vec<_>>() {
|
||||
let mut view = self.cx.views.remove(&view_id).unwrap();
|
||||
futures.extend(view.app_will_quit(self));
|
||||
self.cx.views.insert(view_id, view);
|
||||
}
|
||||
|
||||
self.remove_all_windows();
|
||||
|
||||
let futures = futures::future::join_all(futures);
|
||||
if self
|
||||
.background
|
||||
.block_with_timeout(Duration::from_millis(100), futures)
|
||||
.is_err()
|
||||
{
|
||||
log::error!("timed out waiting on app_will_quit");
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_all_windows(&mut self) {
|
||||
for (window_id, _) in self.cx.windows.drain() {
|
||||
self.presenters_and_platform_windows.remove(&window_id);
|
||||
}
|
||||
self.remove_dropped_entities();
|
||||
}
|
||||
|
||||
pub fn platform(&self) -> Arc<dyn platform::Platform> {
|
||||
self.cx.platform.clone()
|
||||
}
|
||||
@@ -812,10 +838,13 @@ impl MutableAppContext {
|
||||
handler(action, cx);
|
||||
});
|
||||
|
||||
self.global_actions
|
||||
.entry(TypeId::of::<A>())
|
||||
.or_default()
|
||||
.push(handler);
|
||||
if self
|
||||
.global_actions
|
||||
.insert(TypeId::of::<A>(), handler)
|
||||
.is_some()
|
||||
{
|
||||
panic!("registered multiple global handlers for the same action type");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn window_ids(&self) -> impl Iterator<Item = usize> + '_ {
|
||||
@@ -882,9 +911,9 @@ impl MutableAppContext {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn update<T, F: FnOnce() -> T>(&mut self, callback: F) -> T {
|
||||
pub fn update<T, F: FnOnce(&mut Self) -> T>(&mut self, callback: F) -> T {
|
||||
self.pending_flushes += 1;
|
||||
let result = callback();
|
||||
let result = callback(self);
|
||||
self.flush_effects();
|
||||
result
|
||||
}
|
||||
@@ -963,7 +992,7 @@ impl MutableAppContext {
|
||||
})
|
||||
}
|
||||
|
||||
fn observe<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
|
||||
pub fn observe<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
|
||||
where
|
||||
E: Entity,
|
||||
E::Event: 'static,
|
||||
@@ -1065,61 +1094,60 @@ impl MutableAppContext {
|
||||
path: &[usize],
|
||||
action: &dyn AnyAction,
|
||||
) -> bool {
|
||||
self.pending_flushes += 1;
|
||||
let mut halted_dispatch = false;
|
||||
self.update(|this| {
|
||||
let mut halted_dispatch = false;
|
||||
for view_id in path.iter().rev() {
|
||||
if let Some(mut view) = this.cx.views.remove(&(window_id, *view_id)) {
|
||||
let type_id = view.as_any().type_id();
|
||||
|
||||
for view_id in path.iter().rev() {
|
||||
if let Some(mut view) = self.cx.views.remove(&(window_id, *view_id)) {
|
||||
let type_id = view.as_any().type_id();
|
||||
|
||||
if let Some((name, mut handlers)) = self
|
||||
.actions
|
||||
.get_mut(&type_id)
|
||||
.and_then(|h| h.remove_entry(&action.id()))
|
||||
{
|
||||
for handler in handlers.iter_mut().rev() {
|
||||
let halt_dispatch =
|
||||
handler(view.as_mut(), action, self, window_id, *view_id);
|
||||
if halt_dispatch {
|
||||
halted_dispatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.actions
|
||||
if let Some((name, mut handlers)) = this
|
||||
.actions
|
||||
.get_mut(&type_id)
|
||||
.unwrap()
|
||||
.insert(name, handlers);
|
||||
}
|
||||
.and_then(|h| h.remove_entry(&action.id()))
|
||||
{
|
||||
for handler in handlers.iter_mut().rev() {
|
||||
let halt_dispatch =
|
||||
handler(view.as_mut(), action, this, window_id, *view_id);
|
||||
if halt_dispatch {
|
||||
halted_dispatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.actions
|
||||
.get_mut(&type_id)
|
||||
.unwrap()
|
||||
.insert(name, handlers);
|
||||
}
|
||||
|
||||
self.cx.views.insert((window_id, *view_id), view);
|
||||
this.cx.views.insert((window_id, *view_id), view);
|
||||
|
||||
if halted_dispatch {
|
||||
break;
|
||||
if halted_dispatch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !halted_dispatch {
|
||||
self.dispatch_global_action_any(action);
|
||||
}
|
||||
|
||||
self.flush_effects();
|
||||
halted_dispatch
|
||||
if !halted_dispatch {
|
||||
halted_dispatch = this.dispatch_global_action_any(action);
|
||||
}
|
||||
halted_dispatch
|
||||
})
|
||||
}
|
||||
|
||||
pub fn dispatch_global_action<A: Action>(&mut self, action: A) {
|
||||
self.dispatch_global_action_any(&action);
|
||||
}
|
||||
|
||||
fn dispatch_global_action_any(&mut self, action: &dyn AnyAction) {
|
||||
if let Some((name, mut handlers)) = self.global_actions.remove_entry(&action.id()) {
|
||||
self.pending_flushes += 1;
|
||||
for handler in handlers.iter_mut().rev() {
|
||||
handler(action, self);
|
||||
fn dispatch_global_action_any(&mut self, action: &dyn AnyAction) -> bool {
|
||||
self.update(|this| {
|
||||
if let Some((name, mut handler)) = this.global_actions.remove_entry(&action.id()) {
|
||||
handler(action, this);
|
||||
this.global_actions.insert(name, handler);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
self.global_actions.insert(name, handlers);
|
||||
self.flush_effects();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_bindings<T: IntoIterator<Item = keymap::Binding>>(&mut self, bindings: T) {
|
||||
@@ -1133,11 +1161,9 @@ impl MutableAppContext {
|
||||
keystroke: &Keystroke,
|
||||
) -> Result<bool> {
|
||||
let mut context_chain = Vec::new();
|
||||
let mut context = keymap::Context::default();
|
||||
for view_id in &responder_chain {
|
||||
if let Some(view) = self.cx.views.get(&(window_id, *view_id)) {
|
||||
context.extend(view.keymap_context(self.as_ref()));
|
||||
context_chain.push(context.clone());
|
||||
context_chain.push(view.keymap_context(self.as_ref()));
|
||||
} else {
|
||||
return Err(anyhow!(
|
||||
"View {} in responder chain does not exist",
|
||||
@@ -1157,6 +1183,7 @@ impl MutableAppContext {
|
||||
MatchResult::Action(action) => {
|
||||
if self.dispatch_action_any(window_id, &responder_chain[0..=i], action.as_ref())
|
||||
{
|
||||
self.keystroke_matcher.clear_pending();
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
@@ -1171,14 +1198,14 @@ impl MutableAppContext {
|
||||
T: Entity,
|
||||
F: FnOnce(&mut ModelContext<T>) -> T,
|
||||
{
|
||||
self.pending_flushes += 1;
|
||||
let model_id = post_inc(&mut self.next_entity_id);
|
||||
let handle = ModelHandle::new(model_id, &self.cx.ref_counts);
|
||||
let mut cx = ModelContext::new(self, model_id);
|
||||
let model = build_model(&mut cx);
|
||||
self.cx.models.insert(model_id, Box::new(model));
|
||||
self.flush_effects();
|
||||
handle
|
||||
self.update(|this| {
|
||||
let model_id = post_inc(&mut this.next_entity_id);
|
||||
let handle = ModelHandle::new(model_id, &this.cx.ref_counts);
|
||||
let mut cx = ModelContext::new(this, model_id);
|
||||
let model = build_model(&mut cx);
|
||||
this.cx.models.insert(model_id, Box::new(model));
|
||||
handle
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_window<T, F>(
|
||||
@@ -1190,26 +1217,26 @@ impl MutableAppContext {
|
||||
T: View,
|
||||
F: FnOnce(&mut ViewContext<T>) -> T,
|
||||
{
|
||||
self.pending_flushes += 1;
|
||||
let window_id = post_inc(&mut self.next_window_id);
|
||||
let root_view = self.add_view(window_id, build_root_view);
|
||||
self.update(|this| {
|
||||
let window_id = post_inc(&mut this.next_window_id);
|
||||
let root_view = this.add_view(window_id, build_root_view);
|
||||
|
||||
self.cx.windows.insert(
|
||||
window_id,
|
||||
Window {
|
||||
root_view: root_view.clone().into(),
|
||||
focused_view_id: root_view.id(),
|
||||
invalidation: None,
|
||||
},
|
||||
);
|
||||
self.open_platform_window(window_id, window_options);
|
||||
root_view.update(self, |view, cx| {
|
||||
view.on_focus(cx);
|
||||
cx.notify();
|
||||
});
|
||||
self.flush_effects();
|
||||
this.cx.windows.insert(
|
||||
window_id,
|
||||
Window {
|
||||
root_view: root_view.clone().into(),
|
||||
focused_view_id: root_view.id(),
|
||||
invalidation: None,
|
||||
},
|
||||
);
|
||||
this.open_platform_window(window_id, window_options);
|
||||
root_view.update(this, |view, cx| {
|
||||
view.on_focus(cx);
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
(window_id, root_view)
|
||||
(window_id, root_view)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remove_window(&mut self, window_id: usize) {
|
||||
@@ -1318,25 +1345,24 @@ impl MutableAppContext {
|
||||
T: View,
|
||||
F: FnOnce(&mut ViewContext<T>) -> Option<T>,
|
||||
{
|
||||
let view_id = post_inc(&mut self.next_entity_id);
|
||||
self.pending_flushes += 1;
|
||||
let handle = ViewHandle::new(window_id, view_id, &self.cx.ref_counts);
|
||||
let mut cx = ViewContext::new(self, window_id, view_id);
|
||||
let handle = if let Some(view) = build_view(&mut cx) {
|
||||
self.cx.views.insert((window_id, view_id), Box::new(view));
|
||||
if let Some(window) = self.cx.windows.get_mut(&window_id) {
|
||||
window
|
||||
.invalidation
|
||||
.get_or_insert_with(Default::default)
|
||||
.updated
|
||||
.insert(view_id);
|
||||
}
|
||||
Some(handle)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.flush_effects();
|
||||
handle
|
||||
self.update(|this| {
|
||||
let view_id = post_inc(&mut this.next_entity_id);
|
||||
let mut cx = ViewContext::new(this, window_id, view_id);
|
||||
let handle = if let Some(view) = build_view(&mut cx) {
|
||||
this.cx.views.insert((window_id, view_id), Box::new(view));
|
||||
if let Some(window) = this.cx.windows.get_mut(&window_id) {
|
||||
window
|
||||
.invalidation
|
||||
.get_or_insert_with(Default::default)
|
||||
.updated
|
||||
.insert(view_id);
|
||||
}
|
||||
Some(ViewHandle::new(window_id, view_id, &this.cx.ref_counts))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
handle
|
||||
})
|
||||
}
|
||||
|
||||
pub fn element_state<Tag: 'static, T: 'static + Default>(
|
||||
@@ -1588,27 +1614,25 @@ impl MutableAppContext {
|
||||
return;
|
||||
}
|
||||
|
||||
self.pending_flushes += 1;
|
||||
self.update(|this| {
|
||||
let blurred_id = this.cx.windows.get_mut(&window_id).map(|window| {
|
||||
let blurred_id = window.focused_view_id;
|
||||
window.focused_view_id = focused_id;
|
||||
blurred_id
|
||||
});
|
||||
|
||||
let blurred_id = self.cx.windows.get_mut(&window_id).map(|window| {
|
||||
let blurred_id = window.focused_view_id;
|
||||
window.focused_view_id = focused_id;
|
||||
blurred_id
|
||||
});
|
||||
|
||||
if let Some(blurred_id) = blurred_id {
|
||||
if let Some(mut blurred_view) = self.cx.views.remove(&(window_id, blurred_id)) {
|
||||
blurred_view.on_blur(self, window_id, blurred_id);
|
||||
self.cx.views.insert((window_id, blurred_id), blurred_view);
|
||||
if let Some(blurred_id) = blurred_id {
|
||||
if let Some(mut blurred_view) = this.cx.views.remove(&(window_id, blurred_id)) {
|
||||
blurred_view.on_blur(this, window_id, blurred_id);
|
||||
this.cx.views.insert((window_id, blurred_id), blurred_view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mut focused_view) = self.cx.views.remove(&(window_id, focused_id)) {
|
||||
focused_view.on_focus(self, window_id, focused_id);
|
||||
self.cx.views.insert((window_id, focused_id), focused_view);
|
||||
}
|
||||
|
||||
self.flush_effects();
|
||||
if let Some(mut focused_view) = this.cx.views.remove(&(window_id, focused_id)) {
|
||||
focused_view.on_focus(this, window_id, focused_id);
|
||||
this.cx.views.insert((window_id, focused_id), focused_view);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
|
||||
@@ -1654,18 +1678,18 @@ impl UpdateModel for MutableAppContext {
|
||||
update: &mut dyn FnMut(&mut T, &mut ModelContext<T>) -> V,
|
||||
) -> V {
|
||||
if let Some(mut model) = self.cx.models.remove(&handle.model_id) {
|
||||
self.pending_flushes += 1;
|
||||
let mut cx = ModelContext::new(self, handle.model_id);
|
||||
let result = update(
|
||||
model
|
||||
.as_any_mut()
|
||||
.downcast_mut()
|
||||
.expect("downcast is type safe"),
|
||||
&mut cx,
|
||||
);
|
||||
self.cx.models.insert(handle.model_id, model);
|
||||
self.flush_effects();
|
||||
result
|
||||
self.update(|this| {
|
||||
let mut cx = ModelContext::new(this, handle.model_id);
|
||||
let result = update(
|
||||
model
|
||||
.as_any_mut()
|
||||
.downcast_mut()
|
||||
.expect("downcast is type safe"),
|
||||
&mut cx,
|
||||
);
|
||||
this.cx.models.insert(handle.model_id, model);
|
||||
result
|
||||
})
|
||||
} else {
|
||||
panic!("circular model update");
|
||||
}
|
||||
@@ -1700,25 +1724,25 @@ impl UpdateView for MutableAppContext {
|
||||
where
|
||||
T: View,
|
||||
{
|
||||
self.pending_flushes += 1;
|
||||
let mut view = self
|
||||
.cx
|
||||
.views
|
||||
.remove(&(handle.window_id, handle.view_id))
|
||||
.expect("circular view update");
|
||||
self.update(|this| {
|
||||
let mut view = this
|
||||
.cx
|
||||
.views
|
||||
.remove(&(handle.window_id, handle.view_id))
|
||||
.expect("circular view update");
|
||||
|
||||
let mut cx = ViewContext::new(self, handle.window_id, handle.view_id);
|
||||
let result = update(
|
||||
view.as_any_mut()
|
||||
.downcast_mut()
|
||||
.expect("downcast is type safe"),
|
||||
&mut cx,
|
||||
);
|
||||
self.cx
|
||||
.views
|
||||
.insert((handle.window_id, handle.view_id), view);
|
||||
self.flush_effects();
|
||||
result
|
||||
let mut cx = ViewContext::new(this, handle.window_id, handle.view_id);
|
||||
let result = update(
|
||||
view.as_any_mut()
|
||||
.downcast_mut()
|
||||
.expect("downcast is type safe"),
|
||||
&mut cx,
|
||||
);
|
||||
this.cx
|
||||
.views
|
||||
.insert((handle.window_id, handle.view_id), view);
|
||||
result
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1879,6 +1903,10 @@ pub trait AnyModel {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
fn release(&mut self, cx: &mut MutableAppContext);
|
||||
fn app_will_quit(
|
||||
&mut self,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Option<Pin<Box<dyn 'static + Future<Output = ()>>>>;
|
||||
}
|
||||
|
||||
impl<T> AnyModel for T
|
||||
@@ -1896,12 +1924,23 @@ where
|
||||
fn release(&mut self, cx: &mut MutableAppContext) {
|
||||
self.release(cx);
|
||||
}
|
||||
|
||||
fn app_will_quit(
|
||||
&mut self,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Option<Pin<Box<dyn 'static + Future<Output = ()>>>> {
|
||||
self.app_will_quit(cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AnyView {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
fn release(&mut self, cx: &mut MutableAppContext);
|
||||
fn app_will_quit(
|
||||
&mut self,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Option<Pin<Box<dyn 'static + Future<Output = ()>>>>;
|
||||
fn ui_name(&self) -> &'static str;
|
||||
fn render<'a>(
|
||||
&mut self,
|
||||
@@ -1932,6 +1971,13 @@ where
|
||||
self.release(cx);
|
||||
}
|
||||
|
||||
fn app_will_quit(
|
||||
&mut self,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Option<Pin<Box<dyn 'static + Future<Output = ()>>>> {
|
||||
self.app_will_quit(cx)
|
||||
}
|
||||
|
||||
fn ui_name(&self) -> &'static str {
|
||||
T::ui_name()
|
||||
}
|
||||
@@ -2029,7 +2075,7 @@ impl<'a, T: Entity> ModelContext<'a, T> {
|
||||
S::Event: 'static,
|
||||
F: 'static + FnMut(&mut T, ModelHandle<S>, &S::Event, &mut ModelContext<T>),
|
||||
{
|
||||
let subscriber = self.handle().downgrade();
|
||||
let subscriber = self.weak_handle();
|
||||
self.app
|
||||
.subscribe_internal(handle, move |emitter, event, cx| {
|
||||
if let Some(subscriber) = subscriber.upgrade(cx) {
|
||||
@@ -2048,7 +2094,7 @@ impl<'a, T: Entity> ModelContext<'a, T> {
|
||||
S: Entity,
|
||||
F: 'static + FnMut(&mut T, ModelHandle<S>, &mut ModelContext<T>),
|
||||
{
|
||||
let observer = self.handle().downgrade();
|
||||
let observer = self.weak_handle();
|
||||
self.app.observe_internal(handle, move |observed, cx| {
|
||||
if let Some(observer) = observer.upgrade(cx) {
|
||||
observer.update(cx, |observer, cx| {
|
||||
@@ -2065,6 +2111,10 @@ impl<'a, T: Entity> ModelContext<'a, T> {
|
||||
ModelHandle::new(self.model_id, &self.app.cx.ref_counts)
|
||||
}
|
||||
|
||||
pub fn weak_handle(&self) -> WeakModelHandle<T> {
|
||||
WeakModelHandle::new(self.model_id)
|
||||
}
|
||||
|
||||
pub fn spawn<F, Fut, S>(&self, f: F) -> Task<S>
|
||||
where
|
||||
F: FnOnce(ModelHandle<T>, AsyncAppContext) -> Fut,
|
||||
@@ -2081,7 +2131,7 @@ impl<'a, T: Entity> ModelContext<'a, T> {
|
||||
Fut: 'static + Future<Output = S>,
|
||||
S: 'static,
|
||||
{
|
||||
let handle = self.handle().downgrade();
|
||||
let handle = self.weak_handle();
|
||||
self.app.spawn(|cx| f(handle, cx))
|
||||
}
|
||||
}
|
||||
@@ -2160,6 +2210,10 @@ impl<'a, T: View> ViewContext<'a, T> {
|
||||
ViewHandle::new(self.window_id, self.view_id, &self.app.cx.ref_counts)
|
||||
}
|
||||
|
||||
pub fn weak_handle(&self) -> WeakViewHandle<T> {
|
||||
WeakViewHandle::new(self.window_id, self.view_id)
|
||||
}
|
||||
|
||||
pub fn window_id(&self) -> usize {
|
||||
self.window_id
|
||||
}
|
||||
@@ -2255,7 +2309,7 @@ impl<'a, T: View> ViewContext<'a, T> {
|
||||
H: Handle<E>,
|
||||
F: 'static + FnMut(&mut T, H, &E::Event, &mut ViewContext<T>),
|
||||
{
|
||||
let subscriber = self.handle().downgrade();
|
||||
let subscriber = self.weak_handle();
|
||||
self.app
|
||||
.subscribe_internal(handle, move |emitter, event, cx| {
|
||||
if let Some(subscriber) = subscriber.upgrade(cx) {
|
||||
@@ -2275,7 +2329,7 @@ impl<'a, T: View> ViewContext<'a, T> {
|
||||
H: Handle<E>,
|
||||
F: 'static + FnMut(&mut T, H, &mut ViewContext<T>),
|
||||
{
|
||||
let observer = self.handle().downgrade();
|
||||
let observer = self.weak_handle();
|
||||
self.app.observe_internal(handle, move |observed, cx| {
|
||||
if let Some(observer) = observer.upgrade(cx) {
|
||||
observer.update(cx, |observer, cx| {
|
||||
@@ -2319,7 +2373,7 @@ impl<'a, T: View> ViewContext<'a, T> {
|
||||
Fut: 'static + Future<Output = S>,
|
||||
S: 'static,
|
||||
{
|
||||
let handle = self.handle().downgrade();
|
||||
let handle = self.weak_handle();
|
||||
self.app.spawn(|cx| f(handle, cx))
|
||||
}
|
||||
}
|
||||
@@ -2337,6 +2391,10 @@ impl<'a, T: View> RenderContext<'a, T> {
|
||||
pub fn handle(&self) -> WeakViewHandle<T> {
|
||||
WeakViewHandle::new(self.window_id, self.view_id)
|
||||
}
|
||||
|
||||
pub fn view_id(&self) -> usize {
|
||||
self.view_id
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<AppContext> for &AppContext {
|
||||
@@ -2375,6 +2433,12 @@ impl<V: View> UpdateModel for RenderContext<'_, V> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: View> ReadView for RenderContext<'_, V> {
|
||||
fn read_view<T: View>(&self, handle: &ViewHandle<T>) -> &T {
|
||||
self.app.read_view(handle)
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> AsRef<AppContext> for ViewContext<'_, M> {
|
||||
fn as_ref(&self) -> &AppContext {
|
||||
&self.app.cx
|
||||
@@ -2608,9 +2672,11 @@ impl<T: Entity> ModelHandle<T> {
|
||||
}
|
||||
}
|
||||
|
||||
cx.borrow().foreground().start_waiting();
|
||||
rx.recv()
|
||||
.await
|
||||
.expect("model dropped with pending condition");
|
||||
cx.borrow().foreground().finish_waiting();
|
||||
}
|
||||
})
|
||||
.await
|
||||
@@ -2707,6 +2773,10 @@ impl<T: Entity> WeakModelHandle<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> usize {
|
||||
self.model_id
|
||||
}
|
||||
|
||||
pub fn upgrade(self, cx: &impl UpgradeModelHandle) -> Option<ModelHandle<T>> {
|
||||
cx.upgrade_model_handle(self)
|
||||
}
|
||||
@@ -2800,6 +2870,28 @@ impl<T: View> ViewHandle<T> {
|
||||
.map_or(false, |focused_id| focused_id == self.view_id)
|
||||
}
|
||||
|
||||
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
|
||||
let (mut tx, mut rx) = mpsc::channel(1);
|
||||
let mut cx = cx.cx.borrow_mut();
|
||||
let subscription = cx.observe(self, move |_, _| {
|
||||
tx.blocking_send(()).ok();
|
||||
});
|
||||
|
||||
let duration = if std::env::var("CI").is_ok() {
|
||||
Duration::from_secs(5)
|
||||
} else {
|
||||
Duration::from_secs(1)
|
||||
};
|
||||
|
||||
async move {
|
||||
let notification = timeout(duration, rx.recv())
|
||||
.await
|
||||
.expect("next notification timed out");
|
||||
drop(subscription);
|
||||
notification.expect("model dropped while test was waiting for its next notification")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn condition(
|
||||
&self,
|
||||
cx: &TestAppContext,
|
||||
@@ -2850,9 +2942,11 @@ impl<T: View> ViewHandle<T> {
|
||||
}
|
||||
}
|
||||
|
||||
cx.borrow().foreground().start_waiting();
|
||||
rx.recv()
|
||||
.await
|
||||
.expect("view dropped with pending condition");
|
||||
cx.borrow().foreground().finish_waiting();
|
||||
}
|
||||
})
|
||||
.await
|
||||
@@ -3025,14 +3119,39 @@ impl Drop for AnyViewHandle {
|
||||
|
||||
pub struct AnyModelHandle {
|
||||
model_id: usize,
|
||||
model_type: TypeId,
|
||||
ref_counts: Arc<Mutex<RefCounts>>,
|
||||
}
|
||||
|
||||
impl AnyModelHandle {
|
||||
pub fn downcast<T: Entity>(self) -> Option<ModelHandle<T>> {
|
||||
if self.is::<T>() {
|
||||
let result = Some(ModelHandle {
|
||||
model_id: self.model_id,
|
||||
model_type: PhantomData,
|
||||
ref_counts: self.ref_counts.clone(),
|
||||
});
|
||||
unsafe {
|
||||
Arc::decrement_strong_count(&self.ref_counts);
|
||||
}
|
||||
std::mem::forget(self);
|
||||
result
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is<T: Entity>(&self) -> bool {
|
||||
self.model_type == TypeId::of::<T>()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Entity> From<ModelHandle<T>> for AnyModelHandle {
|
||||
fn from(handle: ModelHandle<T>) -> Self {
|
||||
handle.ref_counts.lock().inc_model(handle.model_id);
|
||||
Self {
|
||||
model_id: handle.model_id,
|
||||
model_type: TypeId::of::<T>(),
|
||||
ref_counts: handle.ref_counts.clone(),
|
||||
}
|
||||
}
|
||||
@@ -3237,7 +3356,9 @@ struct RefCounts {
|
||||
impl RefCounts {
|
||||
fn inc_model(&mut self, model_id: usize) {
|
||||
match self.entity_counts.entry(model_id) {
|
||||
Entry::Occupied(mut entry) => *entry.get_mut() += 1,
|
||||
Entry::Occupied(mut entry) => {
|
||||
*entry.get_mut() += 1;
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(1);
|
||||
self.dropped_models.remove(&model_id);
|
||||
@@ -3304,16 +3425,11 @@ impl RefCounts {
|
||||
HashSet<(usize, usize)>,
|
||||
HashSet<(TypeId, ElementStateId)>,
|
||||
) {
|
||||
let mut dropped_models = HashSet::new();
|
||||
let mut dropped_views = HashSet::new();
|
||||
let mut dropped_element_states = HashSet::new();
|
||||
std::mem::swap(&mut self.dropped_models, &mut dropped_models);
|
||||
std::mem::swap(&mut self.dropped_views, &mut dropped_views);
|
||||
std::mem::swap(
|
||||
&mut self.dropped_element_states,
|
||||
&mut dropped_element_states,
|
||||
);
|
||||
(dropped_models, dropped_views, dropped_element_states)
|
||||
(
|
||||
std::mem::take(&mut self.dropped_models),
|
||||
std::mem::take(&mut self.dropped_views),
|
||||
std::mem::take(&mut self.dropped_element_states),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3559,7 +3675,11 @@ mod tests {
|
||||
presenter.borrow_mut().dispatch_event(
|
||||
Event::LeftMouseDown {
|
||||
position: Default::default(),
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
shift: false,
|
||||
cmd: false,
|
||||
click_count: 1,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
@@ -3616,7 +3736,7 @@ mod tests {
|
||||
assert!(!*model_released.lock());
|
||||
assert!(!*view_released.lock());
|
||||
|
||||
cx.update(move || {
|
||||
cx.update(move |_| {
|
||||
drop(model);
|
||||
});
|
||||
assert!(*model_released.lock());
|
||||
@@ -3722,7 +3842,7 @@ mod tests {
|
||||
cx.subscribe(&observed_model, |_, _, _, _| {}).detach();
|
||||
});
|
||||
|
||||
cx.update(|| {
|
||||
cx.update(|_| {
|
||||
drop(observing_view);
|
||||
drop(observing_model);
|
||||
});
|
||||
@@ -3814,7 +3934,7 @@ mod tests {
|
||||
cx.observe(&observed_model, |_, _, _| {}).detach();
|
||||
});
|
||||
|
||||
cx.update(|| {
|
||||
cx.update(|_| {
|
||||
drop(observing_view);
|
||||
drop(observing_model);
|
||||
});
|
||||
@@ -3925,12 +4045,7 @@ mod tests {
|
||||
|
||||
let actions_clone = actions.clone();
|
||||
cx.add_global_action(move |_: &Action, _: &mut MutableAppContext| {
|
||||
actions_clone.borrow_mut().push("global a".to_string());
|
||||
});
|
||||
|
||||
let actions_clone = actions.clone();
|
||||
cx.add_global_action(move |_: &Action, _: &mut MutableAppContext| {
|
||||
actions_clone.borrow_mut().push("global b".to_string());
|
||||
actions_clone.borrow_mut().push("global".to_string());
|
||||
});
|
||||
|
||||
let actions_clone = actions.clone();
|
||||
@@ -3986,7 +4101,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
*actions.borrow(),
|
||||
vec!["4 d", "4 c", "3 b", "3 a", "2 d", "2 c", "global b", "global a"]
|
||||
vec!["4 d", "4 c", "3 b", "3 a", "2 d", "2 c", "global"]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4032,7 +4147,10 @@ mod tests {
|
||||
let mut view_2 = View::new(2);
|
||||
let mut view_3 = View::new(3);
|
||||
view_1.keymap_context.set.insert("a".into());
|
||||
view_2.keymap_context.set.insert("a".into());
|
||||
view_2.keymap_context.set.insert("b".into());
|
||||
view_3.keymap_context.set.insert("a".into());
|
||||
view_3.keymap_context.set.insert("b".into());
|
||||
view_3.keymap_context.set.insert("c".into());
|
||||
|
||||
let (window_id, view_1) = cx.add_window(Default::default(), |_| view_1);
|
||||
|
||||
@@ -4,6 +4,7 @@ mod constrained_box;
|
||||
mod container;
|
||||
mod empty;
|
||||
mod event_handler;
|
||||
mod expanded;
|
||||
mod flex;
|
||||
mod hook;
|
||||
mod image;
|
||||
@@ -16,6 +17,7 @@ mod svg;
|
||||
mod text;
|
||||
mod uniform_list;
|
||||
|
||||
use self::expanded::Expanded;
|
||||
pub use self::{
|
||||
align::*, canvas::*, constrained_box::*, container::*, empty::*, event_handler::*, flex::*,
|
||||
hook::*, image::*, label::*, list::*, mouse_event_handler::*, overlay::*, stack::*, svg::*,
|
||||
@@ -130,11 +132,18 @@ pub trait Element {
|
||||
Container::new(self.boxed())
|
||||
}
|
||||
|
||||
fn expanded(self, flex: f32) -> Expanded
|
||||
fn expanded(self) -> Expanded
|
||||
where
|
||||
Self: 'static + Sized,
|
||||
{
|
||||
Expanded::new(flex, self.boxed())
|
||||
Expanded::new(self.boxed())
|
||||
}
|
||||
|
||||
fn flexible(self, flex: f32, expanded: bool) -> Flexible
|
||||
where
|
||||
Self: 'static + Sized,
|
||||
{
|
||||
Flexible::new(flex, expanded, self.boxed())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,6 +310,10 @@ impl<T: Element> Default for Lifecycle<T> {
|
||||
}
|
||||
|
||||
impl ElementBox {
|
||||
pub fn name(&self) -> Option<&str> {
|
||||
self.0.name.as_deref()
|
||||
}
|
||||
|
||||
pub fn metadata<T: 'static>(&self) -> Option<&T> {
|
||||
let element = unsafe { &*self.0.element.as_ptr() };
|
||||
element.metadata().and_then(|m| m.downcast_ref())
|
||||
|
||||
@@ -25,6 +25,11 @@ impl Align {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bottom(mut self) -> Self {
|
||||
self.alignment.set_y(1.0);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn left(mut self) -> Self {
|
||||
self.alignment.set_x(-1.0);
|
||||
self
|
||||
|
||||
@@ -52,6 +52,11 @@ impl Container {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_margin_bottom(mut self, margin: f32) -> Self {
|
||||
self.style.margin.bottom = margin;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_margin_left(mut self, margin: f32) -> Self {
|
||||
self.style.margin.left = margin;
|
||||
self
|
||||
|
||||
90
crates/gpui/src/elements/expanded.rs
Normal file
90
crates/gpui/src/elements/expanded.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use crate::{
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
json, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
|
||||
SizeConstraint,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
pub struct Expanded {
|
||||
child: ElementBox,
|
||||
full_width: bool,
|
||||
full_height: bool,
|
||||
}
|
||||
|
||||
impl Expanded {
|
||||
pub fn new(child: ElementBox) -> Self {
|
||||
Self {
|
||||
child,
|
||||
full_width: true,
|
||||
full_height: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_full_width(mut self) -> Self {
|
||||
self.full_width = true;
|
||||
self.full_height = false;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn to_full_height(mut self) -> Self {
|
||||
self.full_width = false;
|
||||
self.full_height = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for Expanded {
|
||||
type LayoutState = ();
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
mut constraint: SizeConstraint,
|
||||
cx: &mut LayoutContext,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
if self.full_width {
|
||||
constraint.min.set_x(constraint.max.x());
|
||||
}
|
||||
if self.full_height {
|
||||
constraint.min.set_y(constraint.max.y());
|
||||
}
|
||||
let size = self.child.layout(constraint, cx);
|
||||
(size, ())
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: RectF,
|
||||
visible_bounds: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) -> Self::PaintState {
|
||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
_: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
_: &mut Self::PaintState,
|
||||
cx: &mut EventContext,
|
||||
) -> bool {
|
||||
self.child.dispatch_event(event, cx)
|
||||
}
|
||||
|
||||
fn debug(
|
||||
&self,
|
||||
_: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
cx: &DebugContext,
|
||||
) -> json::Value {
|
||||
json!({
|
||||
"type": "Expanded",
|
||||
"full_width": self.full_width,
|
||||
"full_height": self.full_height,
|
||||
"child": self.child.debug(cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -228,88 +228,15 @@ struct FlexParentData {
|
||||
expanded: bool,
|
||||
}
|
||||
|
||||
pub struct Expanded {
|
||||
metadata: FlexParentData,
|
||||
child: ElementBox,
|
||||
}
|
||||
|
||||
impl Expanded {
|
||||
pub fn new(flex: f32, child: ElementBox) -> Self {
|
||||
Expanded {
|
||||
metadata: FlexParentData {
|
||||
flex,
|
||||
expanded: true,
|
||||
},
|
||||
child,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for Expanded {
|
||||
type LayoutState = ();
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
cx: &mut LayoutContext,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let size = self.child.layout(constraint, cx);
|
||||
(size, ())
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: RectF,
|
||||
visible_bounds: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) -> Self::PaintState {
|
||||
self.child.paint(bounds.origin(), visible_bounds, cx)
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
_: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
_: &mut Self::PaintState,
|
||||
cx: &mut EventContext,
|
||||
) -> bool {
|
||||
self.child.dispatch_event(event, cx)
|
||||
}
|
||||
|
||||
fn metadata(&self) -> Option<&dyn Any> {
|
||||
Some(&self.metadata)
|
||||
}
|
||||
|
||||
fn debug(
|
||||
&self,
|
||||
_: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
cx: &DebugContext,
|
||||
) -> Value {
|
||||
json!({
|
||||
"type": "Expanded",
|
||||
"flex": self.metadata.flex,
|
||||
"child": self.child.debug(cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Flexible {
|
||||
metadata: FlexParentData,
|
||||
child: ElementBox,
|
||||
}
|
||||
|
||||
impl Flexible {
|
||||
pub fn new(flex: f32, child: ElementBox) -> Self {
|
||||
pub fn new(flex: f32, expanded: bool, child: ElementBox) -> Self {
|
||||
Flexible {
|
||||
metadata: FlexParentData {
|
||||
flex,
|
||||
expanded: false,
|
||||
},
|
||||
metadata: FlexParentData { flex, expanded },
|
||||
child,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ mod tests {
|
||||
"Menlo",
|
||||
12.,
|
||||
Default::default(),
|
||||
false,
|
||||
None,
|
||||
Color::black(),
|
||||
cx.font_cache(),
|
||||
)
|
||||
@@ -216,7 +216,7 @@ mod tests {
|
||||
"Menlo",
|
||||
12.,
|
||||
*FontProperties::new().weight(Weight::BOLD),
|
||||
false,
|
||||
None,
|
||||
Color::new(255, 0, 0, 255),
|
||||
cx.font_cache(),
|
||||
)
|
||||
|
||||
@@ -1,35 +1,56 @@
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
color::Color,
|
||||
fonts::TextStyle,
|
||||
fonts::{HighlightStyle, TextStyle},
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
json::{ToJson, Value},
|
||||
text_layout::{Line, ShapedBoundary},
|
||||
DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
|
||||
text_layout::{Line, RunStyle, ShapedBoundary},
|
||||
DebugContext, Element, Event, EventContext, FontCache, LayoutContext, PaintContext,
|
||||
SizeConstraint, TextLayoutCache,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
pub struct Text {
|
||||
text: String,
|
||||
style: TextStyle,
|
||||
soft_wrap: bool,
|
||||
highlights: Vec<(Range<usize>, HighlightStyle)>,
|
||||
}
|
||||
|
||||
pub struct LayoutState {
|
||||
lines: Vec<(Line, Vec<ShapedBoundary>)>,
|
||||
shaped_lines: Vec<Line>,
|
||||
wrap_boundaries: Vec<Vec<ShapedBoundary>>,
|
||||
line_height: f32,
|
||||
}
|
||||
|
||||
impl Text {
|
||||
pub fn new(text: String, style: TextStyle) -> Self {
|
||||
Self { text, style }
|
||||
Self {
|
||||
text,
|
||||
style,
|
||||
soft_wrap: true,
|
||||
highlights: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_default_color(mut self, color: Color) -> Self {
|
||||
self.style.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_highlights(mut self, runs: Vec<(Range<usize>, HighlightStyle)>) -> Self {
|
||||
self.highlights = runs;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_soft_wrap(mut self, soft_wrap: bool) -> Self {
|
||||
self.soft_wrap = soft_wrap;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for Text {
|
||||
@@ -41,28 +62,59 @@ impl Element for Text {
|
||||
constraint: SizeConstraint,
|
||||
cx: &mut LayoutContext,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let font_id = self.style.font_id;
|
||||
let line_height = cx.font_cache.line_height(font_id, self.style.font_size);
|
||||
// Convert the string and highlight ranges into an iterator of highlighted chunks.
|
||||
let mut offset = 0;
|
||||
let mut highlight_ranges = self.highlights.iter().peekable();
|
||||
let chunks = std::iter::from_fn(|| {
|
||||
let result;
|
||||
if let Some((range, highlight)) = highlight_ranges.peek() {
|
||||
if offset < range.start {
|
||||
result = Some((&self.text[offset..range.start], None));
|
||||
offset = range.start;
|
||||
} else {
|
||||
result = Some((&self.text[range.clone()], Some(*highlight)));
|
||||
highlight_ranges.next();
|
||||
offset = range.end;
|
||||
}
|
||||
} else if offset < self.text.len() {
|
||||
result = Some((&self.text[offset..], None));
|
||||
offset = self.text.len();
|
||||
} else {
|
||||
result = None;
|
||||
}
|
||||
result
|
||||
});
|
||||
|
||||
let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size);
|
||||
let mut lines = Vec::new();
|
||||
// Perform shaping on these highlighted chunks
|
||||
let shaped_lines = layout_highlighted_chunks(
|
||||
chunks,
|
||||
&self.style,
|
||||
cx.text_layout_cache,
|
||||
&cx.font_cache,
|
||||
usize::MAX,
|
||||
self.text.matches('\n').count() + 1,
|
||||
);
|
||||
|
||||
// If line wrapping is enabled, wrap each of the shaped lines.
|
||||
let font_id = self.style.font_id;
|
||||
let mut line_count = 0;
|
||||
let mut max_line_width = 0_f32;
|
||||
for line in self.text.lines() {
|
||||
let shaped_line = cx.text_layout_cache.layout_str(
|
||||
line,
|
||||
self.style.font_size,
|
||||
&[(line.len(), self.style.to_run())],
|
||||
);
|
||||
let wrap_boundaries = wrapper
|
||||
.wrap_shaped_line(line, &shaped_line, constraint.max.x())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut wrap_boundaries = Vec::new();
|
||||
let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size);
|
||||
for (line, shaped_line) in self.text.lines().zip(&shaped_lines) {
|
||||
if self.soft_wrap {
|
||||
let boundaries = wrapper
|
||||
.wrap_shaped_line(line, shaped_line, constraint.max.x())
|
||||
.collect::<Vec<_>>();
|
||||
line_count += boundaries.len() + 1;
|
||||
wrap_boundaries.push(boundaries);
|
||||
} else {
|
||||
line_count += 1;
|
||||
}
|
||||
max_line_width = max_line_width.max(shaped_line.width());
|
||||
line_count += wrap_boundaries.len() + 1;
|
||||
lines.push((shaped_line, wrap_boundaries));
|
||||
}
|
||||
|
||||
let line_height = cx.font_cache.line_height(font_id, self.style.font_size);
|
||||
let size = vec2f(
|
||||
max_line_width
|
||||
.ceil()
|
||||
@@ -70,7 +122,14 @@ impl Element for Text {
|
||||
.min(constraint.max.x()),
|
||||
(line_height * line_count as f32).ceil(),
|
||||
);
|
||||
(size, LayoutState { lines, line_height })
|
||||
(
|
||||
size,
|
||||
LayoutState {
|
||||
shaped_lines,
|
||||
wrap_boundaries,
|
||||
line_height,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
@@ -81,8 +140,10 @@ impl Element for Text {
|
||||
cx: &mut PaintContext,
|
||||
) -> Self::PaintState {
|
||||
let mut origin = bounds.origin();
|
||||
for (line, wrap_boundaries) in &layout.lines {
|
||||
let wrapped_line_boundaries = RectF::new(
|
||||
let empty = Vec::new();
|
||||
for (ix, line) in layout.shaped_lines.iter().enumerate() {
|
||||
let wrap_boundaries = layout.wrap_boundaries.get(ix).unwrap_or(&empty);
|
||||
let boundaries = RectF::new(
|
||||
origin,
|
||||
vec2f(
|
||||
bounds.width(),
|
||||
@@ -90,16 +151,20 @@ impl Element for Text {
|
||||
),
|
||||
);
|
||||
|
||||
if wrapped_line_boundaries.intersects(visible_bounds) {
|
||||
line.paint_wrapped(
|
||||
origin,
|
||||
visible_bounds,
|
||||
layout.line_height,
|
||||
wrap_boundaries.iter().copied(),
|
||||
cx,
|
||||
);
|
||||
if boundaries.intersects(visible_bounds) {
|
||||
if self.soft_wrap {
|
||||
line.paint_wrapped(
|
||||
origin,
|
||||
visible_bounds,
|
||||
layout.line_height,
|
||||
wrap_boundaries.iter().copied(),
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
line.paint(origin, visible_bounds, layout.line_height, cx);
|
||||
}
|
||||
}
|
||||
origin.set_y(wrapped_line_boundaries.max_y());
|
||||
origin.set_y(boundaries.max_y());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,3 +194,71 @@ impl Element for Text {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform text layout on a series of highlighted chunks of text.
|
||||
pub fn layout_highlighted_chunks<'a>(
|
||||
chunks: impl Iterator<Item = (&'a str, Option<HighlightStyle>)>,
|
||||
style: &'a TextStyle,
|
||||
text_layout_cache: &'a TextLayoutCache,
|
||||
font_cache: &'a Arc<FontCache>,
|
||||
max_line_len: usize,
|
||||
max_line_count: usize,
|
||||
) -> Vec<Line> {
|
||||
let mut layouts = Vec::with_capacity(max_line_count);
|
||||
let mut prev_font_properties = style.font_properties.clone();
|
||||
let mut prev_font_id = style.font_id;
|
||||
let mut line = String::new();
|
||||
let mut styles = Vec::new();
|
||||
let mut row = 0;
|
||||
let mut line_exceeded_max_len = false;
|
||||
for (chunk, highlight_style) in chunks.chain([("\n", None)]) {
|
||||
for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
|
||||
if ix > 0 {
|
||||
layouts.push(text_layout_cache.layout_str(&line, style.font_size, &styles));
|
||||
line.clear();
|
||||
styles.clear();
|
||||
row += 1;
|
||||
line_exceeded_max_len = false;
|
||||
if row == max_line_count {
|
||||
return layouts;
|
||||
}
|
||||
}
|
||||
|
||||
if !line_chunk.is_empty() && !line_exceeded_max_len {
|
||||
let highlight_style = highlight_style.unwrap_or(style.clone().into());
|
||||
|
||||
// Avoid a lookup if the font properties match the previous ones.
|
||||
let font_id = if highlight_style.font_properties == prev_font_properties {
|
||||
prev_font_id
|
||||
} else {
|
||||
font_cache
|
||||
.select_font(style.font_family_id, &highlight_style.font_properties)
|
||||
.unwrap_or(style.font_id)
|
||||
};
|
||||
|
||||
if line.len() + line_chunk.len() > max_line_len {
|
||||
let mut chunk_len = max_line_len - line.len();
|
||||
while !line_chunk.is_char_boundary(chunk_len) {
|
||||
chunk_len -= 1;
|
||||
}
|
||||
line_chunk = &line_chunk[..chunk_len];
|
||||
line_exceeded_max_len = true;
|
||||
}
|
||||
|
||||
line.push_str(line_chunk);
|
||||
styles.push((
|
||||
line_chunk.len(),
|
||||
RunStyle {
|
||||
font_id,
|
||||
color: highlight_style.color,
|
||||
underline: highlight_style.underline,
|
||||
},
|
||||
));
|
||||
prev_font_id = font_id;
|
||||
prev_font_properties = highlight_style.font_properties;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
|
||||
@@ -14,9 +14,15 @@ use std::{cmp, ops::Range, sync::Arc};
|
||||
#[derive(Clone, Default)]
|
||||
pub struct UniformListState(Arc<Mutex<StateInner>>);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ScrollTarget {
|
||||
Show(usize),
|
||||
Center(usize),
|
||||
}
|
||||
|
||||
impl UniformListState {
|
||||
pub fn scroll_to(&self, item_ix: usize) {
|
||||
self.0.lock().scroll_to = Some(item_ix);
|
||||
pub fn scroll_to(&self, scroll_to: ScrollTarget) {
|
||||
self.0.lock().scroll_to = Some(scroll_to);
|
||||
}
|
||||
|
||||
pub fn scroll_top(&self) -> f32 {
|
||||
@@ -27,7 +33,7 @@ impl UniformListState {
|
||||
#[derive(Default)]
|
||||
struct StateInner {
|
||||
scroll_top: f32,
|
||||
scroll_to: Option<usize>,
|
||||
scroll_to: Option<ScrollTarget>,
|
||||
}
|
||||
|
||||
pub struct LayoutState {
|
||||
@@ -93,20 +99,38 @@ where
|
||||
fn autoscroll(&mut self, scroll_max: f32, list_height: f32, item_height: f32) {
|
||||
let mut state = self.state.0.lock();
|
||||
|
||||
if state.scroll_top > scroll_max {
|
||||
state.scroll_top = scroll_max;
|
||||
}
|
||||
if let Some(scroll_to) = state.scroll_to.take() {
|
||||
let item_ix;
|
||||
let center;
|
||||
match scroll_to {
|
||||
ScrollTarget::Show(ix) => {
|
||||
item_ix = ix;
|
||||
center = false;
|
||||
}
|
||||
ScrollTarget::Center(ix) => {
|
||||
item_ix = ix;
|
||||
center = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(item_ix) = state.scroll_to.take() {
|
||||
let item_top = self.padding_top + item_ix as f32 * item_height;
|
||||
let item_bottom = item_top + item_height;
|
||||
|
||||
if item_top < state.scroll_top {
|
||||
state.scroll_top = item_top;
|
||||
} else if item_bottom > (state.scroll_top + list_height) {
|
||||
state.scroll_top = item_bottom - list_height;
|
||||
if center {
|
||||
let item_center = item_top + item_height / 2.;
|
||||
state.scroll_top = (item_center - list_height / 2.).max(0.);
|
||||
} else {
|
||||
let scroll_bottom = state.scroll_top + list_height;
|
||||
if item_top < state.scroll_top {
|
||||
state.scroll_top = item_top;
|
||||
} else if item_bottom > scroll_bottom {
|
||||
state.scroll_top = item_bottom - list_height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if state.scroll_top > scroll_max {
|
||||
state.scroll_top = scroll_max;
|
||||
}
|
||||
}
|
||||
|
||||
fn scroll_top(&self) -> f32 {
|
||||
|
||||
@@ -7,7 +7,7 @@ use rand::prelude::*;
|
||||
use smol::{channel, prelude::*, Executor, Timer};
|
||||
use std::{
|
||||
any::Any,
|
||||
fmt::{self, Debug},
|
||||
fmt::{self, Debug, Display},
|
||||
marker::PhantomData,
|
||||
mem,
|
||||
ops::RangeInclusive,
|
||||
@@ -25,7 +25,7 @@ use waker_fn::waker_fn;
|
||||
|
||||
use crate::{
|
||||
platform::{self, Dispatcher},
|
||||
util,
|
||||
util, MutableAppContext,
|
||||
};
|
||||
|
||||
pub enum Foreground {
|
||||
@@ -38,7 +38,9 @@ pub enum Foreground {
|
||||
}
|
||||
|
||||
pub enum Background {
|
||||
Deterministic(Arc<Deterministic>),
|
||||
Deterministic {
|
||||
executor: Arc<Deterministic>,
|
||||
},
|
||||
Production {
|
||||
executor: Arc<smol::Executor<'static>>,
|
||||
_stop: channel::Sender<()>,
|
||||
@@ -50,7 +52,9 @@ type AnyFuture = Pin<Box<dyn 'static + Send + Future<Output = Box<dyn Any + Send
|
||||
type AnyTask = async_task::Task<Box<dyn Any + Send + 'static>>;
|
||||
type AnyLocalTask = async_task::Task<Box<dyn Any + 'static>>;
|
||||
|
||||
#[must_use]
|
||||
pub enum Task<T> {
|
||||
Ready(Option<T>),
|
||||
Local {
|
||||
any_task: AnyLocalTask,
|
||||
result_type: PhantomData<T>,
|
||||
@@ -73,6 +77,7 @@ struct DeterministicState {
|
||||
block_on_ticks: RangeInclusive<usize>,
|
||||
now: Instant,
|
||||
pending_timers: Vec<(Instant, barrier::Sender)>,
|
||||
waiting_backtrace: Option<Backtrace>,
|
||||
}
|
||||
|
||||
pub struct Deterministic {
|
||||
@@ -93,6 +98,7 @@ impl Deterministic {
|
||||
block_on_ticks: 0..=1000,
|
||||
now: Instant::now(),
|
||||
pending_timers: Default::default(),
|
||||
waiting_backtrace: None,
|
||||
})),
|
||||
parker: Default::default(),
|
||||
}
|
||||
@@ -139,8 +145,8 @@ impl Deterministic {
|
||||
return result;
|
||||
}
|
||||
|
||||
if !woken.load(SeqCst) && self.state.lock().forbid_parking {
|
||||
panic!("deterministic executor parked after a call to forbid_parking");
|
||||
if !woken.load(SeqCst) {
|
||||
self.state.lock().will_park();
|
||||
}
|
||||
|
||||
woken.store(false, SeqCst);
|
||||
@@ -202,6 +208,7 @@ impl Deterministic {
|
||||
}
|
||||
|
||||
let state = self.state.lock();
|
||||
|
||||
if state.scheduled_from_foreground.is_empty()
|
||||
&& state.scheduled_from_background.is_empty()
|
||||
&& state.spawned_from_foreground.is_empty()
|
||||
@@ -240,11 +247,9 @@ impl Deterministic {
|
||||
if let Poll::Ready(result) = future.as_mut().poll(&mut cx) {
|
||||
return Some(result);
|
||||
}
|
||||
let state = self.state.lock();
|
||||
let mut state = self.state.lock();
|
||||
if state.scheduled_from_background.is_empty() {
|
||||
if state.forbid_parking {
|
||||
panic!("deterministic executor parked after a call to forbid_parking");
|
||||
}
|
||||
state.will_park();
|
||||
drop(state);
|
||||
self.parker.lock().park();
|
||||
}
|
||||
@@ -257,6 +262,26 @@ impl Deterministic {
|
||||
}
|
||||
}
|
||||
|
||||
impl DeterministicState {
|
||||
fn will_park(&mut self) {
|
||||
if self.forbid_parking {
|
||||
let mut backtrace_message = String::new();
|
||||
if let Some(backtrace) = self.waiting_backtrace.as_mut() {
|
||||
backtrace.resolve();
|
||||
backtrace_message = format!(
|
||||
"\nbacktrace of waiting future:\n{:?}",
|
||||
CwdBacktrace::new(backtrace)
|
||||
);
|
||||
}
|
||||
|
||||
panic!(
|
||||
"deterministic executor parked after a call to forbid_parking{}",
|
||||
backtrace_message
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Trace {
|
||||
executed: Vec<Backtrace>,
|
||||
@@ -302,32 +327,53 @@ impl Trace {
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Trace {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
struct FirstCwdFrameInBacktrace<'a>(&'a Backtrace);
|
||||
struct CwdBacktrace<'a> {
|
||||
backtrace: &'a Backtrace,
|
||||
first_frame_only: bool,
|
||||
}
|
||||
|
||||
impl<'a> Debug for FirstCwdFrameInBacktrace<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let cwd = std::env::current_dir().unwrap();
|
||||
let mut print_path = |fmt: &mut fmt::Formatter<'_>, path: BytesOrWideString<'_>| {
|
||||
fmt::Display::fmt(&path, fmt)
|
||||
};
|
||||
let mut fmt = BacktraceFmt::new(f, backtrace::PrintFmt::Full, &mut print_path);
|
||||
for frame in self.0.frames() {
|
||||
let mut formatted_frame = fmt.frame();
|
||||
if frame
|
||||
.symbols()
|
||||
.iter()
|
||||
.any(|s| s.filename().map_or(false, |f| f.starts_with(&cwd)))
|
||||
{
|
||||
formatted_frame.backtrace_frame(frame)?;
|
||||
break;
|
||||
}
|
||||
impl<'a> CwdBacktrace<'a> {
|
||||
fn new(backtrace: &'a Backtrace) -> Self {
|
||||
Self {
|
||||
backtrace,
|
||||
first_frame_only: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn first_frame(backtrace: &'a Backtrace) -> Self {
|
||||
Self {
|
||||
backtrace,
|
||||
first_frame_only: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Debug for CwdBacktrace<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let cwd = std::env::current_dir().unwrap();
|
||||
let mut print_path = |fmt: &mut fmt::Formatter<'_>, path: BytesOrWideString<'_>| {
|
||||
fmt::Display::fmt(&path, fmt)
|
||||
};
|
||||
let mut fmt = BacktraceFmt::new(f, backtrace::PrintFmt::Full, &mut print_path);
|
||||
for frame in self.backtrace.frames() {
|
||||
let mut formatted_frame = fmt.frame();
|
||||
if frame
|
||||
.symbols()
|
||||
.iter()
|
||||
.any(|s| s.filename().map_or(false, |f| f.starts_with(&cwd)))
|
||||
{
|
||||
formatted_frame.backtrace_frame(frame)?;
|
||||
if self.first_frame_only {
|
||||
break;
|
||||
}
|
||||
fmt.finish()
|
||||
}
|
||||
}
|
||||
fmt.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Trace {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for ((backtrace, scheduled), spawned_from_foreground) in self
|
||||
.executed
|
||||
.iter()
|
||||
@@ -336,7 +382,7 @@ impl Debug for Trace {
|
||||
{
|
||||
writeln!(f, "Scheduled")?;
|
||||
for backtrace in scheduled {
|
||||
writeln!(f, "- {:?}", FirstCwdFrameInBacktrace(backtrace))?;
|
||||
writeln!(f, "- {:?}", CwdBacktrace::first_frame(backtrace))?;
|
||||
}
|
||||
if scheduled.is_empty() {
|
||||
writeln!(f, "None")?;
|
||||
@@ -345,14 +391,14 @@ impl Debug for Trace {
|
||||
|
||||
writeln!(f, "Spawned from foreground")?;
|
||||
for backtrace in spawned_from_foreground {
|
||||
writeln!(f, "- {:?}", FirstCwdFrameInBacktrace(backtrace))?;
|
||||
writeln!(f, "- {:?}", CwdBacktrace::first_frame(backtrace))?;
|
||||
}
|
||||
if spawned_from_foreground.is_empty() {
|
||||
writeln!(f, "None")?;
|
||||
}
|
||||
writeln!(f, "==========")?;
|
||||
|
||||
writeln!(f, "Run: {:?}", FirstCwdFrameInBacktrace(backtrace))?;
|
||||
writeln!(f, "Run: {:?}", CwdBacktrace::first_frame(backtrace))?;
|
||||
writeln!(f, "+++++++++++++++++++")?;
|
||||
}
|
||||
|
||||
@@ -429,6 +475,31 @@ impl Foreground {
|
||||
*any_value.downcast().unwrap()
|
||||
}
|
||||
|
||||
pub fn parking_forbidden(&self) -> bool {
|
||||
match self {
|
||||
Self::Deterministic(executor) => executor.state.lock().forbid_parking,
|
||||
_ => panic!("this method can only be called on a deterministic executor"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_waiting(&self) {
|
||||
match self {
|
||||
Self::Deterministic(executor) => {
|
||||
executor.state.lock().waiting_backtrace = Some(Backtrace::new_unresolved());
|
||||
}
|
||||
_ => panic!("this method can only be called on a deterministic executor"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish_waiting(&self) {
|
||||
match self {
|
||||
Self::Deterministic(executor) => {
|
||||
executor.state.lock().waiting_backtrace.take();
|
||||
}
|
||||
_ => panic!("this method can only be called on a deterministic executor"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn forbid_parking(&self) {
|
||||
match self {
|
||||
Self::Deterministic(executor) => {
|
||||
@@ -515,7 +586,7 @@ impl Background {
|
||||
let future = any_future(future);
|
||||
let any_task = match self {
|
||||
Self::Production { executor, .. } => executor.spawn(future),
|
||||
Self::Deterministic(executor) => executor.spawn(future),
|
||||
Self::Deterministic { executor, .. } => executor.spawn(future),
|
||||
};
|
||||
Task::send(any_task)
|
||||
}
|
||||
@@ -533,7 +604,7 @@ impl Background {
|
||||
if !timeout.is_zero() {
|
||||
let output = match self {
|
||||
Self::Production { .. } => smol::block_on(util::timeout(timeout, &mut future)).ok(),
|
||||
Self::Deterministic(executor) => executor.block_on(&mut future),
|
||||
Self::Deterministic { executor, .. } => executor.block_on(&mut future),
|
||||
};
|
||||
if let Some(output) = output {
|
||||
return Ok(*output.downcast().unwrap());
|
||||
@@ -586,11 +657,15 @@ pub fn deterministic(seed: u64) -> (Rc<Foreground>, Arc<Background>) {
|
||||
let executor = Arc::new(Deterministic::new(seed));
|
||||
(
|
||||
Rc::new(Foreground::Deterministic(executor.clone())),
|
||||
Arc::new(Background::Deterministic(executor)),
|
||||
Arc::new(Background::Deterministic { executor }),
|
||||
)
|
||||
}
|
||||
|
||||
impl<T> Task<T> {
|
||||
pub fn ready(value: T) -> Self {
|
||||
Self::Ready(Some(value))
|
||||
}
|
||||
|
||||
fn local(any_task: AnyLocalTask) -> Self {
|
||||
Self::Local {
|
||||
any_task,
|
||||
@@ -600,12 +675,24 @@ impl<T> Task<T> {
|
||||
|
||||
pub fn detach(self) {
|
||||
match self {
|
||||
Task::Ready(_) => {}
|
||||
Task::Local { any_task, .. } => any_task.detach(),
|
||||
Task::Send { any_task, .. } => any_task.detach(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static, E: 'static + Display> Task<Result<T, E>> {
|
||||
pub fn detach_and_log_err(self, cx: &mut MutableAppContext) {
|
||||
cx.spawn(|_| async move {
|
||||
if let Err(err) = self.await {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send> Task<T> {
|
||||
fn send(any_task: AnyTask) -> Self {
|
||||
Self::Send {
|
||||
@@ -618,6 +705,7 @@ impl<T: Send> Task<T> {
|
||||
impl<T: fmt::Debug> fmt::Debug for Task<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Task::Ready(value) => value.fmt(f),
|
||||
Task::Local { any_task, .. } => any_task.fmt(f),
|
||||
Task::Send { any_task, .. } => any_task.fmt(f),
|
||||
}
|
||||
@@ -629,6 +717,7 @@ impl<T: 'static> Future for Task<T> {
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
match unsafe { self.get_unchecked_mut() } {
|
||||
Task::Ready(value) => Poll::Ready(value.take().unwrap()),
|
||||
Task::Local { any_task, .. } => {
|
||||
any_task.poll(cx).map(|value| *value.downcast().unwrap())
|
||||
}
|
||||
|
||||
@@ -157,6 +157,17 @@ impl FontCache {
|
||||
bounds.width() * self.em_scale(font_id, font_size)
|
||||
}
|
||||
|
||||
pub fn em_advance(&self, font_id: FontId, font_size: f32) -> f32 {
|
||||
let glyph_id;
|
||||
let advance;
|
||||
{
|
||||
let state = self.0.read();
|
||||
glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap();
|
||||
advance = state.fonts.advance(font_id, glyph_id).unwrap();
|
||||
}
|
||||
advance.x() * self.em_scale(font_id, font_size)
|
||||
}
|
||||
|
||||
pub fn line_height(&self, font_id: FontId, font_size: f32) -> f32 {
|
||||
let height = self.metric(font_id, |m| m.bounding_box.height());
|
||||
(height * self.em_scale(font_id, font_size)).ceil()
|
||||
|
||||
@@ -27,14 +27,14 @@ pub struct TextStyle {
|
||||
pub font_id: FontId,
|
||||
pub font_size: f32,
|
||||
pub font_properties: Properties,
|
||||
pub underline: bool,
|
||||
pub underline: Option<Color>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct HighlightStyle {
|
||||
pub color: Color,
|
||||
pub font_properties: Properties,
|
||||
pub underline: bool,
|
||||
pub underline: Option<Color>,
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
@@ -64,7 +64,7 @@ struct TextStyleJson {
|
||||
#[serde(default)]
|
||||
italic: bool,
|
||||
#[serde(default)]
|
||||
underline: bool,
|
||||
underline: UnderlineStyleJson,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -74,7 +74,14 @@ struct HighlightStyleJson {
|
||||
#[serde(default)]
|
||||
italic: bool,
|
||||
#[serde(default)]
|
||||
underline: bool,
|
||||
underline: UnderlineStyleJson,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum UnderlineStyleJson {
|
||||
Underlined(bool),
|
||||
UnderlinedWithColor(Color),
|
||||
}
|
||||
|
||||
impl TextStyle {
|
||||
@@ -82,7 +89,7 @@ impl TextStyle {
|
||||
font_family_name: impl Into<Arc<str>>,
|
||||
font_size: f32,
|
||||
font_properties: Properties,
|
||||
underline: bool,
|
||||
underline: Option<Color>,
|
||||
color: Color,
|
||||
font_cache: &FontCache,
|
||||
) -> anyhow::Result<Self> {
|
||||
@@ -116,7 +123,7 @@ impl TextStyle {
|
||||
json.family,
|
||||
json.size,
|
||||
font_properties,
|
||||
json.underline,
|
||||
underline_from_json(json.underline, json.color),
|
||||
json.color,
|
||||
font_cache,
|
||||
)
|
||||
@@ -144,6 +151,10 @@ impl TextStyle {
|
||||
font_cache.em_width(self.font_id, self.font_size)
|
||||
}
|
||||
|
||||
pub fn em_advance(&self, font_cache: &FontCache) -> f32 {
|
||||
font_cache.em_advance(self.font_id, self.font_size)
|
||||
}
|
||||
|
||||
pub fn descent(&self, font_cache: &FontCache) -> f32 {
|
||||
font_cache.metric(self.font_id, |m| m.descent) * self.em_scale(font_cache)
|
||||
}
|
||||
@@ -167,6 +178,12 @@ impl From<TextStyle> for HighlightStyle {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UnderlineStyleJson {
|
||||
fn default() -> Self {
|
||||
Self::Underlined(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TextStyle {
|
||||
fn default() -> Self {
|
||||
FONT_CACHE.with(|font_cache| {
|
||||
@@ -199,7 +216,7 @@ impl HighlightStyle {
|
||||
Self {
|
||||
color: json.color,
|
||||
font_properties,
|
||||
underline: json.underline,
|
||||
underline: underline_from_json(json.underline, json.color),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,7 +226,7 @@ impl From<Color> for HighlightStyle {
|
||||
Self {
|
||||
color,
|
||||
font_properties: Default::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -248,12 +265,20 @@ impl<'de> Deserialize<'de> for HighlightStyle {
|
||||
Ok(Self {
|
||||
color: serde_json::from_value(json).map_err(de::Error::custom)?,
|
||||
font_properties: Properties::new(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn underline_from_json(json: UnderlineStyleJson, text_color: Color) -> Option<Color> {
|
||||
match json {
|
||||
UnderlineStyleJson::Underlined(false) => None,
|
||||
UnderlineStyleJson::Underlined(true) => Some(text_color),
|
||||
UnderlineStyleJson::UnderlinedWithColor(color) => Some(color),
|
||||
}
|
||||
}
|
||||
|
||||
fn properties_from_json(weight: Option<WeightJson>, italic: bool) -> Properties {
|
||||
let weight = match weight.unwrap_or(WeightJson::normal) {
|
||||
WeightJson::thin => Weight::THIN,
|
||||
|
||||
@@ -23,6 +23,7 @@ struct Pending {
|
||||
context: Option<Context>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Keymap(Vec<Binding>);
|
||||
|
||||
pub struct Binding {
|
||||
@@ -93,6 +94,10 @@ impl Matcher {
|
||||
self.keymap.add_bindings(bindings);
|
||||
}
|
||||
|
||||
pub fn clear_pending(&mut self) {
|
||||
self.pending.clear();
|
||||
}
|
||||
|
||||
pub fn push_keystroke(
|
||||
&mut self,
|
||||
keystroke: Keystroke,
|
||||
@@ -149,24 +154,6 @@ impl Keymap {
|
||||
}
|
||||
}
|
||||
|
||||
pub mod menu {
|
||||
use crate::action;
|
||||
|
||||
action!(SelectPrev);
|
||||
action!(SelectNext);
|
||||
}
|
||||
|
||||
impl Default for Keymap {
|
||||
fn default() -> Self {
|
||||
Self(vec![
|
||||
Binding::new("up", menu::SelectPrev, Some("menu")),
|
||||
Binding::new("ctrl-p", menu::SelectPrev, Some("menu")),
|
||||
Binding::new("down", menu::SelectNext, Some("menu")),
|
||||
Binding::new("ctrl-n", menu::SelectNext, Some("menu")),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl Binding {
|
||||
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
|
||||
let context = if let Some(context) = context {
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
fonts::{FontId, GlyphId, Metrics as FontMetrics, Properties as FontProperties},
|
||||
geometry::{
|
||||
rect::{RectF, RectI},
|
||||
vector::{vec2f, Vector2F},
|
||||
vector::Vector2F,
|
||||
},
|
||||
text_layout::{LineLayout, RunStyle},
|
||||
AnyAction, ClipboardItem, Menu, Scene,
|
||||
@@ -53,11 +53,14 @@ pub trait Platform: Send + Sync {
|
||||
fn set_cursor_style(&self, style: CursorStyle);
|
||||
|
||||
fn local_timezone(&self) -> UtcOffset;
|
||||
|
||||
fn path_for_resource(&self, name: Option<&str>, extension: Option<&str>) -> Result<PathBuf>;
|
||||
}
|
||||
|
||||
pub(crate) trait ForegroundPlatform {
|
||||
fn on_become_active(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_resign_active(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_quit(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_event(&self, callback: Box<dyn FnMut(Event) -> bool>);
|
||||
fn on_open_files(&self, callback: Box<dyn FnMut(Vec<PathBuf>)>);
|
||||
fn run(&self, on_finish_launching: Box<dyn FnOnce() -> ()>);
|
||||
@@ -102,13 +105,20 @@ pub trait WindowContext {
|
||||
fn present_scene(&mut self, scene: Scene);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WindowOptions<'a> {
|
||||
pub bounds: RectF,
|
||||
pub bounds: WindowBounds,
|
||||
pub title: Option<&'a str>,
|
||||
pub titlebar_appears_transparent: bool,
|
||||
pub traffic_light_position: Option<Vector2F>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum WindowBounds {
|
||||
Maximized,
|
||||
Fixed(RectF),
|
||||
}
|
||||
|
||||
pub struct PathPromptOptions {
|
||||
pub files: bool,
|
||||
pub directories: bool,
|
||||
@@ -138,6 +148,7 @@ pub trait FontSystem: Send + Sync {
|
||||
) -> anyhow::Result<FontId>;
|
||||
fn font_metrics(&self, font_id: FontId) -> FontMetrics;
|
||||
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<RectF>;
|
||||
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<Vector2F>;
|
||||
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
|
||||
fn rasterize_glyph(
|
||||
&self,
|
||||
@@ -154,7 +165,7 @@ pub trait FontSystem: Send + Sync {
|
||||
impl<'a> Default for WindowOptions<'a> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bounds: RectF::new(Default::default(), vec2f(1024.0, 768.0)),
|
||||
bounds: WindowBounds::Maximized,
|
||||
title: Default::default(),
|
||||
titlebar_appears_transparent: Default::default(),
|
||||
traffic_light_position: Default::default(),
|
||||
|
||||
@@ -14,7 +14,11 @@ pub enum Event {
|
||||
},
|
||||
LeftMouseDown {
|
||||
position: Vector2F,
|
||||
ctrl: bool,
|
||||
alt: bool,
|
||||
shift: bool,
|
||||
cmd: bool,
|
||||
click_count: usize,
|
||||
},
|
||||
LeftMouseUp {
|
||||
position: Vector2F,
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
use crate::{geometry::vector::vec2f, keymap::Keystroke, platform::Event};
|
||||
use cocoa::appkit::{
|
||||
NSDeleteFunctionKey as DELETE_KEY, NSDownArrowFunctionKey as ARROW_DOWN_KEY,
|
||||
NSLeftArrowFunctionKey as ARROW_LEFT_KEY, NSPageDownFunctionKey as PAGE_DOWN_KEY,
|
||||
NSPageUpFunctionKey as PAGE_UP_KEY, NSRightArrowFunctionKey as ARROW_RIGHT_KEY,
|
||||
NSUpArrowFunctionKey as ARROW_UP_KEY,
|
||||
};
|
||||
use cocoa::{
|
||||
appkit::{NSEvent, NSEventModifierFlags, NSEventType},
|
||||
base::{id, nil, YES},
|
||||
@@ -12,11 +6,6 @@ use cocoa::{
|
||||
};
|
||||
use std::{ffi::CStr, os::raw::c_char};
|
||||
|
||||
const BACKSPACE_KEY: u16 = 0x7f;
|
||||
const ENTER_KEY: u16 = 0x0d;
|
||||
const ESCAPE_KEY: u16 = 0x1b;
|
||||
const TAB_KEY: u16 = 0x09;
|
||||
|
||||
impl Event {
|
||||
pub unsafe fn from_native(native_event: id, window_height: Option<f32>) -> Option<Self> {
|
||||
let event_type = native_event.eventType();
|
||||
@@ -39,18 +28,41 @@ impl Event {
|
||||
.unwrap();
|
||||
|
||||
let unmodified_chars = if let Some(first_char) = unmodified_chars.chars().next() {
|
||||
use cocoa::appkit::*;
|
||||
const BACKSPACE_KEY: u16 = 0x7f;
|
||||
const ENTER_KEY: u16 = 0x0d;
|
||||
const ESCAPE_KEY: u16 = 0x1b;
|
||||
const TAB_KEY: u16 = 0x09;
|
||||
const SHIFT_TAB_KEY: u16 = 0x19;
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
match first_char as u16 {
|
||||
ARROW_UP_KEY => "up",
|
||||
ARROW_DOWN_KEY => "down",
|
||||
ARROW_LEFT_KEY => "left",
|
||||
ARROW_RIGHT_KEY => "right",
|
||||
PAGE_UP_KEY => "pageup",
|
||||
PAGE_DOWN_KEY => "pagedown",
|
||||
BACKSPACE_KEY => "backspace",
|
||||
ENTER_KEY => "enter",
|
||||
DELETE_KEY => "delete",
|
||||
ESCAPE_KEY => "escape",
|
||||
TAB_KEY => "tab",
|
||||
SHIFT_TAB_KEY => "tab",
|
||||
|
||||
NSUpArrowFunctionKey => "up",
|
||||
NSDownArrowFunctionKey => "down",
|
||||
NSLeftArrowFunctionKey => "left",
|
||||
NSRightArrowFunctionKey => "right",
|
||||
NSPageUpFunctionKey => "pageup",
|
||||
NSPageDownFunctionKey => "pagedown",
|
||||
NSDeleteFunctionKey => "delete",
|
||||
NSF1FunctionKey => "f1",
|
||||
NSF2FunctionKey => "f2",
|
||||
NSF3FunctionKey => "f3",
|
||||
NSF4FunctionKey => "f4",
|
||||
NSF5FunctionKey => "f5",
|
||||
NSF6FunctionKey => "f6",
|
||||
NSF7FunctionKey => "f7",
|
||||
NSF8FunctionKey => "f8",
|
||||
NSF9FunctionKey => "f9",
|
||||
NSF10FunctionKey => "f10",
|
||||
NSF11FunctionKey => "f11",
|
||||
NSF12FunctionKey => "f12",
|
||||
|
||||
_ => unmodified_chars,
|
||||
}
|
||||
} else {
|
||||
@@ -76,14 +88,17 @@ impl Event {
|
||||
})
|
||||
}
|
||||
NSEventType::NSLeftMouseDown => {
|
||||
let modifiers = native_event.modifierFlags();
|
||||
window_height.map(|window_height| Self::LeftMouseDown {
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
),
|
||||
cmd: native_event
|
||||
.modifierFlags()
|
||||
.contains(NSEventModifierFlags::NSCommandKeyMask),
|
||||
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
|
||||
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
|
||||
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
|
||||
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
|
||||
click_count: native_event.clickCount() as usize,
|
||||
})
|
||||
}
|
||||
NSEventType::NSLeftMouseUp => window_height.map(|window_height| Self::LeftMouseUp {
|
||||
|
||||
@@ -69,6 +69,10 @@ impl platform::FontSystem for FontSystem {
|
||||
self.0.read().typographic_bounds(font_id, glyph_id)
|
||||
}
|
||||
|
||||
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<Vector2F> {
|
||||
self.0.read().advance(font_id, glyph_id)
|
||||
}
|
||||
|
||||
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
|
||||
self.0.read().glyph_for_char(font_id, ch)
|
||||
}
|
||||
@@ -137,6 +141,10 @@ impl FontSystemState {
|
||||
Ok(self.fonts[font_id.0].typographic_bounds(glyph_id)?)
|
||||
}
|
||||
|
||||
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<Vector2F> {
|
||||
Ok(self.fonts[font_id.0].advance(glyph_id)?)
|
||||
}
|
||||
|
||||
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
|
||||
self.fonts[font_id.0].glyph_for_char(ch)
|
||||
}
|
||||
@@ -417,21 +425,21 @@ mod tests {
|
||||
let menlo_regular = RunStyle {
|
||||
font_id: fonts.select_font(&menlo, &Properties::new()).unwrap(),
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
};
|
||||
let menlo_italic = RunStyle {
|
||||
font_id: fonts
|
||||
.select_font(&menlo, &Properties::new().style(Style::Italic))
|
||||
.unwrap(),
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
};
|
||||
let menlo_bold = RunStyle {
|
||||
font_id: fonts
|
||||
.select_font(&menlo, &Properties::new().weight(Weight::BOLD))
|
||||
.unwrap(),
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
};
|
||||
assert_ne!(menlo_regular, menlo_italic);
|
||||
assert_ne!(menlo_regular, menlo_bold);
|
||||
@@ -458,13 +466,13 @@ mod tests {
|
||||
let zapfino_regular = RunStyle {
|
||||
font_id: fonts.select_font(&zapfino, &Properties::new())?,
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
};
|
||||
let menlo = fonts.load_family("Menlo")?;
|
||||
let menlo_regular = RunStyle {
|
||||
font_id: fonts.select_font(&menlo, &Properties::new())?,
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
};
|
||||
|
||||
let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈";
|
||||
@@ -543,7 +551,7 @@ mod tests {
|
||||
let style = RunStyle {
|
||||
font_id: fonts.select_font(&font_ids, &Default::default()).unwrap(),
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
};
|
||||
|
||||
let line = "\u{feff}";
|
||||
|
||||
@@ -14,7 +14,9 @@ use cocoa::{
|
||||
NSPasteboardTypeString, NSSavePanel, NSWindow,
|
||||
},
|
||||
base::{id, nil, selector, YES},
|
||||
foundation::{NSArray, NSAutoreleasePool, NSData, NSInteger, NSString, NSURL},
|
||||
foundation::{
|
||||
NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSString, NSUInteger, NSURL,
|
||||
},
|
||||
};
|
||||
use core_foundation::{
|
||||
base::{CFType, CFTypeRef, OSStatus, TCFType as _},
|
||||
@@ -45,6 +47,9 @@ use std::{
|
||||
};
|
||||
use time::UtcOffset;
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSUTF8StringEncoding: NSUInteger = 4;
|
||||
|
||||
const MAC_PLATFORM_IVAR: &'static str = "platform";
|
||||
static mut APP_CLASS: *const Class = ptr::null();
|
||||
static mut APP_DELEGATE_CLASS: *const Class = ptr::null();
|
||||
@@ -76,6 +81,10 @@ unsafe fn build_classes() {
|
||||
sel!(applicationDidResignActive:),
|
||||
did_resign_active as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(applicationWillTerminate:),
|
||||
will_terminate as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(handleGPUIMenuItem:),
|
||||
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
|
||||
@@ -95,6 +104,7 @@ pub struct MacForegroundPlatform(RefCell<MacForegroundPlatformState>);
|
||||
pub struct MacForegroundPlatformState {
|
||||
become_active: Option<Box<dyn FnMut()>>,
|
||||
resign_active: Option<Box<dyn FnMut()>>,
|
||||
quit: Option<Box<dyn FnMut()>>,
|
||||
event: Option<Box<dyn FnMut(crate::Event) -> bool>>,
|
||||
menu_command: Option<Box<dyn FnMut(&dyn AnyAction)>>,
|
||||
open_files: Option<Box<dyn FnMut(Vec<PathBuf>)>>,
|
||||
@@ -191,6 +201,10 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
|
||||
self.0.borrow_mut().resign_active = Some(callback);
|
||||
}
|
||||
|
||||
fn on_quit(&self, callback: Box<dyn FnMut()>) {
|
||||
self.0.borrow_mut().quit = Some(callback);
|
||||
}
|
||||
|
||||
fn on_event(&self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
|
||||
self.0.borrow_mut().event = Some(callback);
|
||||
}
|
||||
@@ -588,6 +602,27 @@ impl platform::Platform for MacPlatform {
|
||||
UtcOffset::from_whole_seconds(seconds_from_gmt.try_into().unwrap()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn path_for_resource(&self, name: Option<&str>, extension: Option<&str>) -> Result<PathBuf> {
|
||||
unsafe {
|
||||
let bundle: id = NSBundle::mainBundle();
|
||||
if bundle.is_null() {
|
||||
Err(anyhow!("app is not running inside a bundle"))
|
||||
} else {
|
||||
let name = name.map_or(nil, |name| ns_string(name));
|
||||
let extension = extension.map_or(nil, |extension| ns_string(extension));
|
||||
let path: id = msg_send![bundle, pathForResource: name ofType: extension];
|
||||
if path.is_null() {
|
||||
Err(anyhow!("resource could not be found"))
|
||||
} else {
|
||||
let len = msg_send![path, lengthOfBytesUsingEncoding: NSUTF8StringEncoding];
|
||||
let bytes = path.UTF8String() as *const u8;
|
||||
let path = str::from_utf8(slice::from_raw_parts(bytes, len)).unwrap();
|
||||
Ok(PathBuf::from(path))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn get_foreground_platform(object: &mut Object) -> &MacForegroundPlatform {
|
||||
@@ -638,6 +673,13 @@ extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) {
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
|
||||
let platform = unsafe { get_foreground_platform(this) };
|
||||
if let Some(callback) = platform.0.borrow_mut().quit.as_mut() {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn open_files(this: &mut Object, _: Sel, _: id, paths: id) {
|
||||
let paths = unsafe {
|
||||
(0..paths.count())
|
||||
|
||||
@@ -40,6 +40,7 @@ impl Renderer {
|
||||
pub fn new(
|
||||
device: metal::Device,
|
||||
pixel_format: metal::MTLPixelFormat,
|
||||
scale_factor: f32,
|
||||
fonts: Arc<dyn platform::FontSystem>,
|
||||
) -> Self {
|
||||
let library = device
|
||||
@@ -64,7 +65,7 @@ impl Renderer {
|
||||
MTLResourceOptions::StorageModeManaged,
|
||||
);
|
||||
|
||||
let sprite_cache = SpriteCache::new(device.clone(), vec2i(1024, 768), fonts);
|
||||
let sprite_cache = SpriteCache::new(device.clone(), vec2i(1024, 768), scale_factor, fonts);
|
||||
let image_cache = ImageCache::new(device.clone(), vec2i(1024, 768));
|
||||
let path_atlases =
|
||||
AtlasAllocator::new(device.clone(), build_path_atlas_texture_descriptor());
|
||||
@@ -106,7 +107,7 @@ impl Renderer {
|
||||
"path_atlas",
|
||||
"path_atlas_vertex",
|
||||
"path_atlas_fragment",
|
||||
MTLPixelFormat::R8Unorm,
|
||||
MTLPixelFormat::R16Float,
|
||||
);
|
||||
Self {
|
||||
sprite_cache,
|
||||
@@ -522,6 +523,8 @@ impl Renderer {
|
||||
return;
|
||||
}
|
||||
|
||||
self.sprite_cache.set_scale_factor(scale_factor);
|
||||
|
||||
let mut sprites_by_atlas = HashMap::new();
|
||||
|
||||
for glyph in glyphs {
|
||||
@@ -530,7 +533,6 @@ impl Renderer {
|
||||
glyph.font_size,
|
||||
glyph.id,
|
||||
glyph.origin,
|
||||
scale_factor,
|
||||
) {
|
||||
// Snap sprite to pixel grid.
|
||||
let origin = (glyph.origin * scale_factor).floor() + sprite.offset.to_f32();
|
||||
@@ -825,7 +827,7 @@ fn build_path_atlas_texture_descriptor() -> metal::TextureDescriptor {
|
||||
let texture_descriptor = metal::TextureDescriptor::new();
|
||||
texture_descriptor.set_width(2048);
|
||||
texture_descriptor.set_height(2048);
|
||||
texture_descriptor.set_pixel_format(MTLPixelFormat::R8Unorm);
|
||||
texture_descriptor.set_pixel_format(MTLPixelFormat::R16Float);
|
||||
texture_descriptor
|
||||
.set_usage(metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead);
|
||||
texture_descriptor.set_storage_mode(metal::MTLStorageMode::Private);
|
||||
|
||||
@@ -205,8 +205,6 @@ vertex SpriteFragmentInput sprite_vertex(
|
||||
};
|
||||
}
|
||||
|
||||
#define MAX_WINDINGS 32.
|
||||
|
||||
fragment float4 sprite_fragment(
|
||||
SpriteFragmentInput input [[stage_in]],
|
||||
texture2d<float> atlas [[ texture(GPUISpriteFragmentInputIndexAtlas) ]]
|
||||
@@ -216,7 +214,7 @@ fragment float4 sprite_fragment(
|
||||
float4 sample = atlas.sample(atlas_sampler, input.atlas_position);
|
||||
float mask;
|
||||
if (input.compute_winding) {
|
||||
mask = 1. - abs(1. - fmod(sample.r * MAX_WINDINGS, 2.));
|
||||
mask = 1. - abs(1. - fmod(sample.r, 2.));
|
||||
} else {
|
||||
mask = sample.a;
|
||||
}
|
||||
@@ -303,6 +301,6 @@ fragment float4 path_atlas_fragment(
|
||||
);
|
||||
float f = (input.st_position.x * input.st_position.x) - input.st_position.y;
|
||||
float distance = f / length(gradient);
|
||||
float alpha = saturate(0.5 - distance) / MAX_WINDINGS;
|
||||
float alpha = saturate(0.5 - distance);
|
||||
return float4(alpha, 0., 0., 1.);
|
||||
}
|
||||
|
||||
@@ -43,12 +43,14 @@ pub struct SpriteCache {
|
||||
atlases: AtlasAllocator,
|
||||
glyphs: HashMap<GlyphDescriptor, Option<GlyphSprite>>,
|
||||
icons: HashMap<IconDescriptor, IconSprite>,
|
||||
scale_factor: f32,
|
||||
}
|
||||
|
||||
impl SpriteCache {
|
||||
pub fn new(
|
||||
device: metal::Device,
|
||||
size: Vector2I,
|
||||
scale_factor: f32,
|
||||
fonts: Arc<dyn platform::FontSystem>,
|
||||
) -> Self {
|
||||
let descriptor = TextureDescriptor::new();
|
||||
@@ -60,19 +62,29 @@ impl SpriteCache {
|
||||
atlases: AtlasAllocator::new(device, descriptor),
|
||||
glyphs: Default::default(),
|
||||
icons: Default::default(),
|
||||
scale_factor,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_scale_factor(&mut self, scale_factor: f32) {
|
||||
if scale_factor != self.scale_factor {
|
||||
self.icons.clear();
|
||||
self.glyphs.clear();
|
||||
self.atlases.clear();
|
||||
}
|
||||
self.scale_factor = scale_factor;
|
||||
}
|
||||
|
||||
pub fn render_glyph(
|
||||
&mut self,
|
||||
font_id: FontId,
|
||||
font_size: f32,
|
||||
glyph_id: GlyphId,
|
||||
target_position: Vector2F,
|
||||
scale_factor: f32,
|
||||
) -> Option<GlyphSprite> {
|
||||
const SUBPIXEL_VARIANTS: u8 = 4;
|
||||
|
||||
let scale_factor = self.scale_factor;
|
||||
let target_position = target_position * scale_factor;
|
||||
let fonts = &self.fonts;
|
||||
let atlases = &mut self.atlases;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use crate::{
|
||||
executor,
|
||||
geometry::vector::Vector2F,
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
keymap::Keystroke,
|
||||
platform::{self, Event, WindowContext},
|
||||
platform::{self, Event, WindowBounds, WindowContext},
|
||||
Scene,
|
||||
};
|
||||
use block::ConcreteBlock;
|
||||
@@ -25,7 +28,6 @@ use objc::{
|
||||
runtime::{Class, Object, Protocol, Sel, BOOL, NO, YES},
|
||||
sel, sel_impl,
|
||||
};
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use smol::Timer;
|
||||
use std::{
|
||||
any::Any,
|
||||
@@ -158,7 +160,11 @@ impl Window {
|
||||
unsafe {
|
||||
let pool = NSAutoreleasePool::new(nil);
|
||||
|
||||
let frame = options.bounds.to_ns_rect();
|
||||
let frame = match options.bounds {
|
||||
WindowBounds::Maximized => RectF::new(Default::default(), vec2f(1024., 768.)),
|
||||
WindowBounds::Fixed(rect) => rect,
|
||||
}
|
||||
.to_ns_rect();
|
||||
let mut style_mask = NSWindowStyleMask::NSClosableWindowMask
|
||||
| NSWindowStyleMask::NSMiniaturizableWindowMask
|
||||
| NSWindowStyleMask::NSResizableWindowMask
|
||||
@@ -177,6 +183,11 @@ impl Window {
|
||||
);
|
||||
assert!(!native_window.is_null());
|
||||
|
||||
if matches!(options.bounds, WindowBounds::Maximized) {
|
||||
let screen = native_window.screen();
|
||||
native_window.setFrame_display_(screen.visibleFrame(), YES);
|
||||
}
|
||||
|
||||
let device =
|
||||
metal::Device::system_default().expect("could not find default metal device");
|
||||
|
||||
@@ -205,7 +216,12 @@ impl Window {
|
||||
synthetic_drag_counter: 0,
|
||||
executor,
|
||||
scene_to_render: Default::default(),
|
||||
renderer: Renderer::new(device.clone(), PIXEL_FORMAT, fonts),
|
||||
renderer: Renderer::new(
|
||||
device.clone(),
|
||||
PIXEL_FORMAT,
|
||||
get_scale_factor(native_window),
|
||||
fonts,
|
||||
),
|
||||
command_queue: device.new_command_queue(),
|
||||
last_fresh_keydown: None,
|
||||
layer,
|
||||
@@ -405,10 +421,7 @@ impl platform::WindowContext for WindowState {
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> f32 {
|
||||
unsafe {
|
||||
let screen: id = msg_send![self.native_window, screen];
|
||||
NSScreen::backingScaleFactor(screen) as f32
|
||||
}
|
||||
get_scale_factor(self.native_window)
|
||||
}
|
||||
|
||||
fn titlebar_height(&self) -> f32 {
|
||||
@@ -427,6 +440,13 @@ impl platform::WindowContext for WindowState {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_scale_factor(native_window: id) -> f32 {
|
||||
unsafe {
|
||||
let screen: id = msg_send![native_window, screen];
|
||||
NSScreen::backingScaleFactor(screen) as f32
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn get_window_state(object: &Object) -> Rc<RefCell<WindowState>> {
|
||||
let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR);
|
||||
let rc1 = Rc::from_raw(raw as *mut RefCell<WindowState>);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use super::CursorStyle;
|
||||
use crate::{AnyAction, ClipboardItem};
|
||||
use anyhow::Result;
|
||||
use super::{CursorStyle, WindowBounds};
|
||||
use crate::{
|
||||
geometry::vector::{vec2f, Vector2F},
|
||||
AnyAction, ClipboardItem,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use parking_lot::Mutex;
|
||||
use pathfinder_geometry::vector::Vector2F;
|
||||
use std::{
|
||||
any::Any,
|
||||
cell::RefCell,
|
||||
@@ -58,6 +60,8 @@ impl super::ForegroundPlatform for ForegroundPlatform {
|
||||
|
||||
fn on_resign_active(&self, _: Box<dyn FnMut()>) {}
|
||||
|
||||
fn on_quit(&self, _: Box<dyn FnMut()>) {}
|
||||
|
||||
fn on_event(&self, _: Box<dyn FnMut(crate::Event) -> bool>) {}
|
||||
|
||||
fn on_open_files(&self, _: Box<dyn FnMut(Vec<std::path::PathBuf>)>) {}
|
||||
@@ -110,7 +114,10 @@ impl super::Platform for Platform {
|
||||
options: super::WindowOptions,
|
||||
_executor: Rc<super::executor::Foreground>,
|
||||
) -> Box<dyn super::Window> {
|
||||
Box::new(Window::new(options.bounds.size()))
|
||||
Box::new(Window::new(match options.bounds {
|
||||
WindowBounds::Maximized => vec2f(1024., 768.),
|
||||
WindowBounds::Fixed(rect) => rect.size(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn key_window_id(&self) -> Option<usize> {
|
||||
@@ -148,6 +155,10 @@ impl super::Platform for Platform {
|
||||
fn local_timezone(&self) -> UtcOffset {
|
||||
UtcOffset::UTC
|
||||
}
|
||||
|
||||
fn path_for_resource(&self, _name: Option<&str>, _extension: Option<&str>) -> Result<PathBuf> {
|
||||
Err(anyhow!("app not running inside a bundle"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Window {
|
||||
|
||||
@@ -7,7 +7,13 @@ use std::{
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{executor, platform, FontCache, MutableAppContext, Platform, TestAppContext};
|
||||
use futures::StreamExt;
|
||||
use smol::channel;
|
||||
|
||||
use crate::{
|
||||
executor, platform, Entity, FontCache, Handle, MutableAppContext, Platform, Subscription,
|
||||
TestAppContext,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
@@ -87,3 +93,47 @@ pub fn run_test(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Observation<T> {
|
||||
rx: channel::Receiver<T>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl<T> futures::Stream for Observation<T> {
|
||||
type Item = T;
|
||||
|
||||
fn poll_next(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Option<Self::Item>> {
|
||||
self.rx.poll_next_unpin(cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn observe<T: Entity>(entity: &impl Handle<T>, cx: &mut TestAppContext) -> Observation<()> {
|
||||
let (tx, rx) = smol::channel::unbounded();
|
||||
let _subscription = cx.update(|cx| {
|
||||
cx.observe(entity, move |_, _| {
|
||||
let _ = smol::block_on(tx.send(()));
|
||||
})
|
||||
});
|
||||
|
||||
Observation { rx, _subscription }
|
||||
}
|
||||
|
||||
pub fn subscribe<T: Entity>(
|
||||
entity: &impl Handle<T>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Observation<T::Event>
|
||||
where
|
||||
T::Event: Clone,
|
||||
{
|
||||
let (tx, rx) = smol::channel::unbounded();
|
||||
let _subscription = cx.update(|cx| {
|
||||
cx.subscribe(entity, move |_, event, _| {
|
||||
let _ = smol::block_on(tx.send(event.clone()));
|
||||
})
|
||||
});
|
||||
|
||||
Observation { rx, _subscription }
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ pub struct TextLayoutCache {
|
||||
pub struct RunStyle {
|
||||
pub color: Color,
|
||||
pub font_id: FontId,
|
||||
pub underline: bool,
|
||||
pub underline: Option<Color>,
|
||||
}
|
||||
|
||||
impl TextLayoutCache {
|
||||
@@ -167,7 +167,7 @@ impl<'a> Hash for CacheKeyRef<'a> {
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Line {
|
||||
layout: Arc<LineLayout>,
|
||||
style_runs: SmallVec<[(u32, Color, bool); 32]>,
|
||||
style_runs: SmallVec<[(u32, Color, Option<Color>); 32]>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
@@ -249,7 +249,7 @@ impl Line {
|
||||
let mut style_runs = self.style_runs.iter();
|
||||
let mut run_end = 0;
|
||||
let mut color = Color::black();
|
||||
let mut underline_start = None;
|
||||
let mut underline = None;
|
||||
|
||||
for run in &self.layout.runs {
|
||||
let max_glyph_width = cx
|
||||
@@ -259,33 +259,20 @@ impl Line {
|
||||
|
||||
for glyph in &run.glyphs {
|
||||
let glyph_origin = origin + baseline_offset + glyph.position;
|
||||
|
||||
if glyph_origin.x() + max_glyph_width < visible_bounds.origin().x() {
|
||||
continue;
|
||||
}
|
||||
if glyph_origin.x() > visible_bounds.upper_right().x() {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut finished_underline = None;
|
||||
if glyph.index >= run_end {
|
||||
if let Some((run_len, run_color, run_underlined)) = style_runs.next() {
|
||||
if let Some(underline_origin) = underline_start {
|
||||
if !*run_underlined || *run_color != color {
|
||||
cx.scene.push_underline(scene::Quad {
|
||||
bounds: RectF::from_points(
|
||||
underline_origin,
|
||||
glyph_origin + vec2f(0., 1.),
|
||||
),
|
||||
background: Some(color),
|
||||
border: Default::default(),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
underline_start = None;
|
||||
if let Some((run_len, run_color, run_underline_color)) = style_runs.next() {
|
||||
if let Some((_, underline_color)) = underline {
|
||||
if *run_underline_color != Some(underline_color) {
|
||||
finished_underline = underline.take();
|
||||
}
|
||||
}
|
||||
|
||||
if *run_underlined {
|
||||
underline_start.get_or_insert(glyph_origin);
|
||||
if let Some(run_underline_color) = run_underline_color {
|
||||
underline.get_or_insert((glyph_origin, *run_underline_color));
|
||||
}
|
||||
|
||||
run_end += *run_len as usize;
|
||||
@@ -293,20 +280,23 @@ impl Line {
|
||||
} else {
|
||||
run_end = self.layout.len;
|
||||
color = Color::black();
|
||||
if let Some(underline_origin) = underline_start.take() {
|
||||
cx.scene.push_underline(scene::Quad {
|
||||
bounds: RectF::from_points(
|
||||
underline_origin,
|
||||
glyph_origin + vec2f(0., 1.),
|
||||
),
|
||||
background: Some(color),
|
||||
border: Default::default(),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
}
|
||||
finished_underline = underline.take();
|
||||
}
|
||||
}
|
||||
|
||||
if glyph_origin.x() + max_glyph_width < visible_bounds.origin().x() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some((underline_origin, underline_color)) = finished_underline {
|
||||
cx.scene.push_underline(scene::Quad {
|
||||
bounds: RectF::from_points(underline_origin, glyph_origin + vec2f(0., 1.)),
|
||||
background: Some(underline_color),
|
||||
border: Default::default(),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
}
|
||||
|
||||
cx.scene.push_glyph(scene::Glyph {
|
||||
font_id: run.font_id,
|
||||
font_size: self.layout.font_size,
|
||||
@@ -317,12 +307,12 @@ impl Line {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(underline_start) = underline_start.take() {
|
||||
if let Some((underline_start, underline_color)) = underline.take() {
|
||||
let line_end = origin + baseline_offset + vec2f(self.layout.width, 0.);
|
||||
|
||||
cx.scene.push_underline(scene::Quad {
|
||||
bounds: RectF::from_points(underline_start, line_end + vec2f(0., 1.)),
|
||||
background: Some(color),
|
||||
background: Some(underline_color),
|
||||
border: Default::default(),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
@@ -597,7 +587,7 @@ impl LineWrapper {
|
||||
RunStyle {
|
||||
font_id: self.font_id,
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
},
|
||||
)],
|
||||
)
|
||||
@@ -681,7 +671,7 @@ mod tests {
|
||||
let normal = RunStyle {
|
||||
font_id,
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
};
|
||||
let bold = RunStyle {
|
||||
font_id: font_cache
|
||||
@@ -694,7 +684,7 @@ mod tests {
|
||||
)
|
||||
.unwrap(),
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
};
|
||||
|
||||
let text = "aa bbb cccc ddddd eeee";
|
||||
|
||||
@@ -42,7 +42,7 @@ impl Select {
|
||||
render_item: F,
|
||||
) -> Self {
|
||||
Self {
|
||||
handle: cx.handle().downgrade(),
|
||||
handle: cx.weak_handle(),
|
||||
render_item: Box::new(render_item),
|
||||
selected_item_ix: 0,
|
||||
item_count,
|
||||
|
||||
@@ -4,6 +4,7 @@ version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
path = "src/gpui_macros.rs"
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
|
||||
16
crates/journal/Cargo.toml
Normal file
16
crates/journal/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "journal"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/journal.rs"
|
||||
|
||||
[dependencies]
|
||||
editor = { path = "../editor" }
|
||||
gpui = { path = "../gpui" }
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
chrono = "0.4"
|
||||
dirs = "4.0"
|
||||
log = "0.4"
|
||||
75
crates/journal/src/journal.rs
Normal file
75
crates/journal/src/journal.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use chrono::{Datelike, Local, Timelike};
|
||||
use editor::{Autoscroll, Editor};
|
||||
use gpui::{action, keymap::Binding, MutableAppContext};
|
||||
use std::{fs::OpenOptions, sync::Arc};
|
||||
use util::TryFutureExt as _;
|
||||
use workspace::AppState;
|
||||
|
||||
action!(NewJournalEntry);
|
||||
|
||||
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
cx.add_bindings(vec![Binding::new("ctrl-alt-cmd-j", NewJournalEntry, None)]);
|
||||
cx.add_global_action(move |_: &NewJournalEntry, cx| new_journal_entry(app_state.clone(), cx));
|
||||
}
|
||||
|
||||
pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
let now = Local::now();
|
||||
let home_dir = match dirs::home_dir() {
|
||||
Some(home_dir) => home_dir,
|
||||
None => {
|
||||
log::error!("can't determine home directory");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let journal_dir = home_dir.join("journal");
|
||||
let month_dir = journal_dir
|
||||
.join(format!("{:02}", now.year()))
|
||||
.join(format!("{:02}", now.month()));
|
||||
let entry_path = month_dir.join(format!("{:02}.md", now.day()));
|
||||
let now = now.time();
|
||||
let (pm, hour) = now.hour12();
|
||||
let am_or_pm = if pm { "PM" } else { "AM" };
|
||||
let entry_heading = format!("# {}:{:02} {}\n\n", hour, now.minute(), am_or_pm);
|
||||
|
||||
let create_entry = cx.background().spawn(async move {
|
||||
std::fs::create_dir_all(month_dir)?;
|
||||
OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.open(&entry_path)?;
|
||||
Ok::<_, std::io::Error>((journal_dir, entry_path))
|
||||
});
|
||||
|
||||
cx.spawn(|mut cx| {
|
||||
async move {
|
||||
let (journal_dir, entry_path) = create_entry.await?;
|
||||
let workspace = cx
|
||||
.update(|cx| workspace::open_paths(&[journal_dir], &app_state, cx))
|
||||
.await;
|
||||
|
||||
let opened = workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.open_paths(&[entry_path], cx)
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Some(Some(Ok(item))) = opened.first() {
|
||||
if let Some(editor) = item.to_any().downcast::<Editor>() {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
let len = editor.buffer().read(cx).read(cx).len();
|
||||
editor.select_ranges([len..len], Some(Autoscroll::Center), cx);
|
||||
if len > 0 {
|
||||
editor.insert("\n\n", cx);
|
||||
}
|
||||
editor.insert(&entry_heading, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
55
crates/language/Cargo.toml
Normal file
55
crates/language/Cargo.toml
Normal file
@@ -0,0 +1,55 @@
|
||||
[package]
|
||||
name = "language"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/language.rs"
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"rand",
|
||||
"collections/test-support",
|
||||
"lsp/test-support",
|
||||
"text/test-support",
|
||||
"tree-sitter-rust",
|
||||
"util/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
gpui = { path = "../gpui" }
|
||||
lsp = { path = "../lsp" }
|
||||
rpc = { path = "../rpc" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
text = { path = "../text" }
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
anyhow = "1.0.38"
|
||||
async-trait = "0.1"
|
||||
futures = "0.3"
|
||||
lazy_static = "1.4"
|
||||
log = "0.4"
|
||||
parking_lot = "0.11.1"
|
||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||
rand = { version = "0.8.3", optional = true }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
similar = "1.3"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "1.2"
|
||||
tree-sitter = "0.20.0"
|
||||
tree-sitter-rust = { version = "0.20.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
collections = { path = "../collections", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
text = { path = "../text", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
ctor = "0.1"
|
||||
env_logger = "0.8"
|
||||
rand = "0.8.3"
|
||||
tree-sitter-rust = "0.20.0"
|
||||
unindent = "0.1.7"
|
||||
5
crates/language/build.rs
Normal file
5
crates/language/build.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
fn main() {
|
||||
if let Ok(bundled) = std::env::var("ZED_BUNDLE") {
|
||||
println!("cargo:rustc-env=ZED_BUNDLE={}", bundled);
|
||||
}
|
||||
}
|
||||
2331
crates/language/src/buffer.rs
Normal file
2331
crates/language/src/buffer.rs
Normal file
File diff suppressed because it is too large
Load Diff
211
crates/language/src/diagnostic_set.rs
Normal file
211
crates/language/src/diagnostic_set.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
use crate::Diagnostic;
|
||||
use collections::HashMap;
|
||||
use std::{
|
||||
cmp::{Ordering, Reverse},
|
||||
iter,
|
||||
ops::Range,
|
||||
};
|
||||
use sum_tree::{self, Bias, SumTree};
|
||||
use text::{Anchor, FromAnchor, Point, ToOffset};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DiagnosticSet {
|
||||
diagnostics: SumTree<DiagnosticEntry<Anchor>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DiagnosticEntry<T> {
|
||||
pub range: Range<T>,
|
||||
pub diagnostic: Diagnostic,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DiagnosticGroup<T> {
|
||||
pub entries: Vec<DiagnosticEntry<T>>,
|
||||
pub primary_ix: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Summary {
|
||||
start: Anchor,
|
||||
end: Anchor,
|
||||
min_start: Anchor,
|
||||
max_end: Anchor,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl DiagnosticSet {
|
||||
pub fn from_sorted_entries<I>(iter: I, buffer: &text::BufferSnapshot) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = DiagnosticEntry<Anchor>>,
|
||||
{
|
||||
Self {
|
||||
diagnostics: SumTree::from_iter(iter, buffer),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new<I>(iter: I, buffer: &text::BufferSnapshot) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = DiagnosticEntry<Point>>,
|
||||
{
|
||||
let mut entries = iter.into_iter().collect::<Vec<_>>();
|
||||
entries.sort_unstable_by_key(|entry| (entry.range.start, Reverse(entry.range.end)));
|
||||
Self {
|
||||
diagnostics: SumTree::from_iter(
|
||||
entries.into_iter().map(|entry| DiagnosticEntry {
|
||||
range: buffer.anchor_before(entry.range.start)
|
||||
..buffer.anchor_after(entry.range.end),
|
||||
diagnostic: entry.diagnostic,
|
||||
}),
|
||||
buffer,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &DiagnosticEntry<Anchor>> {
|
||||
self.diagnostics.iter()
|
||||
}
|
||||
|
||||
pub fn range<'a, T, O>(
|
||||
&'a self,
|
||||
range: Range<T>,
|
||||
buffer: &'a text::BufferSnapshot,
|
||||
inclusive: bool,
|
||||
) -> impl 'a + Iterator<Item = DiagnosticEntry<O>>
|
||||
where
|
||||
T: 'a + ToOffset,
|
||||
O: FromAnchor,
|
||||
{
|
||||
let end_bias = if inclusive { Bias::Right } else { Bias::Left };
|
||||
let range = buffer.anchor_before(range.start)..buffer.anchor_at(range.end, end_bias);
|
||||
let mut cursor = self.diagnostics.filter::<_, ()>(
|
||||
{
|
||||
move |summary: &Summary| {
|
||||
let start_cmp = range.start.cmp(&summary.max_end, buffer).unwrap();
|
||||
let end_cmp = range.end.cmp(&summary.min_start, buffer).unwrap();
|
||||
if inclusive {
|
||||
start_cmp <= Ordering::Equal && end_cmp >= Ordering::Equal
|
||||
} else {
|
||||
start_cmp == Ordering::Less && end_cmp == Ordering::Greater
|
||||
}
|
||||
}
|
||||
},
|
||||
buffer,
|
||||
);
|
||||
|
||||
iter::from_fn({
|
||||
move || {
|
||||
if let Some(diagnostic) = cursor.item() {
|
||||
cursor.next(buffer);
|
||||
Some(diagnostic.resolve(buffer))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn groups(&self, output: &mut Vec<DiagnosticGroup<Anchor>>, buffer: &text::BufferSnapshot) {
|
||||
let mut groups = HashMap::default();
|
||||
for entry in self.diagnostics.iter() {
|
||||
groups
|
||||
.entry(entry.diagnostic.group_id)
|
||||
.or_insert(Vec::new())
|
||||
.push(entry.clone());
|
||||
}
|
||||
|
||||
let start_ix = output.len();
|
||||
output.extend(groups.into_values().filter_map(|mut entries| {
|
||||
entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start, buffer).unwrap());
|
||||
entries
|
||||
.iter()
|
||||
.position(|entry| entry.diagnostic.is_primary)
|
||||
.map(|primary_ix| DiagnosticGroup {
|
||||
entries,
|
||||
primary_ix,
|
||||
})
|
||||
}));
|
||||
output[start_ix..].sort_unstable_by(|a, b| {
|
||||
a.entries[a.primary_ix]
|
||||
.range
|
||||
.start
|
||||
.cmp(&b.entries[b.primary_ix].range.start, buffer)
|
||||
.unwrap()
|
||||
});
|
||||
}
|
||||
|
||||
pub fn group<'a, O: FromAnchor>(
|
||||
&'a self,
|
||||
group_id: usize,
|
||||
buffer: &'a text::BufferSnapshot,
|
||||
) -> impl 'a + Iterator<Item = DiagnosticEntry<O>> {
|
||||
self.iter()
|
||||
.filter(move |entry| entry.diagnostic.group_id == group_id)
|
||||
.map(|entry| entry.resolve(buffer))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DiagnosticSet {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
diagnostics: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Item for DiagnosticEntry<Anchor> {
|
||||
type Summary = Summary;
|
||||
|
||||
fn summary(&self) -> Self::Summary {
|
||||
Summary {
|
||||
start: self.range.start.clone(),
|
||||
end: self.range.end.clone(),
|
||||
min_start: self.range.start.clone(),
|
||||
max_end: self.range.end.clone(),
|
||||
count: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DiagnosticEntry<Anchor> {
|
||||
pub fn resolve<O: FromAnchor>(&self, buffer: &text::BufferSnapshot) -> DiagnosticEntry<O> {
|
||||
DiagnosticEntry {
|
||||
range: O::from_anchor(&self.range.start, buffer)
|
||||
..O::from_anchor(&self.range.end, buffer),
|
||||
diagnostic: self.diagnostic.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Summary {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
start: Anchor::min(),
|
||||
end: Anchor::max(),
|
||||
min_start: Anchor::max(),
|
||||
max_end: Anchor::min(),
|
||||
count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Summary for Summary {
|
||||
type Context = text::BufferSnapshot;
|
||||
|
||||
fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
|
||||
if other
|
||||
.min_start
|
||||
.cmp(&self.min_start, buffer)
|
||||
.unwrap()
|
||||
.is_lt()
|
||||
{
|
||||
self.min_start = other.min_start.clone();
|
||||
}
|
||||
if other.max_end.cmp(&self.max_end, buffer).unwrap().is_gt() {
|
||||
self.max_end = other.max_end.clone();
|
||||
}
|
||||
self.start = other.start.clone();
|
||||
self.end = other.end.clone();
|
||||
self.count += other.count;
|
||||
}
|
||||
}
|
||||
276
crates/language/src/language.rs
Normal file
276
crates/language/src/language.rs
Normal file
@@ -0,0 +1,276 @@
|
||||
mod buffer;
|
||||
mod diagnostic_set;
|
||||
mod highlight_map;
|
||||
mod outline;
|
||||
pub mod proto;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
pub use buffer::Operation;
|
||||
pub use buffer::*;
|
||||
use collections::HashSet;
|
||||
pub use diagnostic_set::DiagnosticEntry;
|
||||
use gpui::AppContext;
|
||||
use highlight_map::HighlightMap;
|
||||
use lazy_static::lazy_static;
|
||||
pub use outline::{Outline, OutlineItem};
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use std::{ops::Range, path::Path, str, sync::Arc};
|
||||
use theme::SyntaxTheme;
|
||||
use tree_sitter::{self, Query};
|
||||
pub use tree_sitter::{Parser, Tree};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref PLAIN_TEXT: Arc<Language> = Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Plain Text".to_string(),
|
||||
path_suffixes: Default::default(),
|
||||
brackets: Default::default(),
|
||||
line_comment: None,
|
||||
language_server: None,
|
||||
},
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
pub trait ToPointUtf16 {
|
||||
fn to_point_utf16(self) -> PointUtf16;
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
pub struct LanguageConfig {
|
||||
pub name: String,
|
||||
pub path_suffixes: Vec<String>,
|
||||
pub brackets: Vec<BracketPair>,
|
||||
pub line_comment: Option<String>,
|
||||
pub language_server: Option<LanguageServerConfig>,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
pub struct LanguageServerConfig {
|
||||
pub binary: String,
|
||||
pub disk_based_diagnostic_sources: HashSet<String>,
|
||||
pub disk_based_diagnostics_progress_token: Option<String>,
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[serde(skip)]
|
||||
pub fake_server: Option<(Arc<lsp::LanguageServer>, Arc<std::sync::atomic::AtomicBool>)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct BracketPair {
|
||||
pub start: String,
|
||||
pub end: String,
|
||||
pub close: bool,
|
||||
pub newline: bool,
|
||||
}
|
||||
|
||||
pub struct Language {
|
||||
pub(crate) config: LanguageConfig,
|
||||
pub(crate) grammar: Option<Arc<Grammar>>,
|
||||
}
|
||||
|
||||
pub struct Grammar {
|
||||
pub(crate) ts_language: tree_sitter::Language,
|
||||
pub(crate) highlights_query: Query,
|
||||
pub(crate) brackets_query: Query,
|
||||
pub(crate) indents_query: Query,
|
||||
pub(crate) outline_query: Query,
|
||||
pub(crate) highlight_map: Mutex<HighlightMap>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct LanguageRegistry {
|
||||
languages: Vec<Arc<Language>>,
|
||||
}
|
||||
|
||||
impl LanguageRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn add(&mut self, language: Arc<Language>) {
|
||||
self.languages.push(language);
|
||||
}
|
||||
|
||||
pub fn set_theme(&self, theme: &SyntaxTheme) {
|
||||
for language in &self.languages {
|
||||
language.set_theme(theme);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_language(&self, name: &str) -> Option<&Arc<Language>> {
|
||||
self.languages
|
||||
.iter()
|
||||
.find(|language| language.name() == name)
|
||||
}
|
||||
|
||||
pub fn select_language(&self, path: impl AsRef<Path>) -> Option<&Arc<Language>> {
|
||||
let path = path.as_ref();
|
||||
let filename = path.file_name().and_then(|name| name.to_str());
|
||||
let extension = path.extension().and_then(|name| name.to_str());
|
||||
let path_suffixes = [extension, filename];
|
||||
self.languages.iter().find(|language| {
|
||||
language
|
||||
.config
|
||||
.path_suffixes
|
||||
.iter()
|
||||
.any(|suffix| path_suffixes.contains(&Some(suffix.as_str())))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Language {
|
||||
pub fn new(config: LanguageConfig, ts_language: Option<tree_sitter::Language>) -> Self {
|
||||
Self {
|
||||
config,
|
||||
grammar: ts_language.map(|ts_language| {
|
||||
Arc::new(Grammar {
|
||||
brackets_query: Query::new(ts_language, "").unwrap(),
|
||||
highlights_query: Query::new(ts_language, "").unwrap(),
|
||||
indents_query: Query::new(ts_language, "").unwrap(),
|
||||
outline_query: Query::new(ts_language, "").unwrap(),
|
||||
ts_language,
|
||||
highlight_map: Default::default(),
|
||||
})
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_highlights_query(mut self, source: &str) -> Result<Self> {
|
||||
let grammar = self
|
||||
.grammar
|
||||
.as_mut()
|
||||
.and_then(Arc::get_mut)
|
||||
.ok_or_else(|| anyhow!("grammar does not exist or is already being used"))?;
|
||||
grammar.highlights_query = Query::new(grammar.ts_language, source)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn with_brackets_query(mut self, source: &str) -> Result<Self> {
|
||||
let grammar = self
|
||||
.grammar
|
||||
.as_mut()
|
||||
.and_then(Arc::get_mut)
|
||||
.ok_or_else(|| anyhow!("grammar does not exist or is already being used"))?;
|
||||
grammar.brackets_query = Query::new(grammar.ts_language, source)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn with_indents_query(mut self, source: &str) -> Result<Self> {
|
||||
let grammar = self
|
||||
.grammar
|
||||
.as_mut()
|
||||
.and_then(Arc::get_mut)
|
||||
.ok_or_else(|| anyhow!("grammar does not exist or is already being used"))?;
|
||||
grammar.indents_query = Query::new(grammar.ts_language, source)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn with_outline_query(mut self, source: &str) -> Result<Self> {
|
||||
let grammar = self
|
||||
.grammar
|
||||
.as_mut()
|
||||
.and_then(Arc::get_mut)
|
||||
.ok_or_else(|| anyhow!("grammar does not exist or is already being used"))?;
|
||||
grammar.outline_query = Query::new(grammar.ts_language, source)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
self.config.name.as_str()
|
||||
}
|
||||
|
||||
pub fn line_comment_prefix(&self) -> Option<&str> {
|
||||
self.config.line_comment.as_deref()
|
||||
}
|
||||
|
||||
pub fn start_server(
|
||||
&self,
|
||||
root_path: &Path,
|
||||
cx: &AppContext,
|
||||
) -> Result<Option<Arc<lsp::LanguageServer>>> {
|
||||
if let Some(config) = &self.config.language_server {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
if let Some((server, started)) = &config.fake_server {
|
||||
started.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
return Ok(Some(server.clone()));
|
||||
}
|
||||
|
||||
const ZED_BUNDLE: Option<&'static str> = option_env!("ZED_BUNDLE");
|
||||
let binary_path = if ZED_BUNDLE.map_or(Ok(false), |b| b.parse())? {
|
||||
cx.platform()
|
||||
.path_for_resource(Some(&config.binary), None)?
|
||||
} else {
|
||||
Path::new(&config.binary).to_path_buf()
|
||||
};
|
||||
lsp::LanguageServer::new(&binary_path, root_path, cx.background().clone()).map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disk_based_diagnostic_sources(&self) -> Option<&HashSet<String>> {
|
||||
self.config
|
||||
.language_server
|
||||
.as_ref()
|
||||
.map(|config| &config.disk_based_diagnostic_sources)
|
||||
}
|
||||
|
||||
pub fn disk_based_diagnostics_progress_token(&self) -> Option<&String> {
|
||||
self.config
|
||||
.language_server
|
||||
.as_ref()
|
||||
.and_then(|config| config.disk_based_diagnostics_progress_token.as_ref())
|
||||
}
|
||||
|
||||
pub fn brackets(&self) -> &[BracketPair] {
|
||||
&self.config.brackets
|
||||
}
|
||||
|
||||
pub fn set_theme(&self, theme: &SyntaxTheme) {
|
||||
if let Some(grammar) = self.grammar.as_ref() {
|
||||
*grammar.highlight_map.lock() =
|
||||
HighlightMap::new(grammar.highlights_query.capture_names(), theme);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Grammar {
|
||||
pub fn highlight_map(&self) -> HighlightMap {
|
||||
self.highlight_map.lock().clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl LanguageServerConfig {
|
||||
pub async fn fake(
|
||||
executor: Arc<gpui::executor::Background>,
|
||||
) -> (Self, lsp::FakeLanguageServer) {
|
||||
let (server, fake) = lsp::LanguageServer::fake(executor).await;
|
||||
fake.started
|
||||
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
let started = fake.started.clone();
|
||||
(
|
||||
Self {
|
||||
fake_server: Some((server, started)),
|
||||
disk_based_diagnostics_progress_token: Some("fakeServer/check".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
fake,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPointUtf16 for lsp::Position {
|
||||
fn to_point_utf16(self) -> PointUtf16 {
|
||||
PointUtf16::new(self.line, self.character)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn range_from_lsp(range: lsp::Range) -> Range<PointUtf16> {
|
||||
let start = PointUtf16::new(range.start.line, range.start.character);
|
||||
let end = PointUtf16::new(range.end.line, range.end.character);
|
||||
start..end
|
||||
}
|
||||
146
crates/language/src/outline.rs
Normal file
146
crates/language/src/outline.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{executor::Background, fonts::HighlightStyle};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Outline<T> {
|
||||
pub items: Vec<OutlineItem<T>>,
|
||||
candidates: Vec<StringMatchCandidate>,
|
||||
path_candidates: Vec<StringMatchCandidate>,
|
||||
path_candidate_prefixes: Vec<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OutlineItem<T> {
|
||||
pub depth: usize,
|
||||
pub range: Range<T>,
|
||||
pub text: String,
|
||||
pub highlight_ranges: Vec<(Range<usize>, HighlightStyle)>,
|
||||
pub name_ranges: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
impl<T> Outline<T> {
|
||||
pub fn new(items: Vec<OutlineItem<T>>) -> Self {
|
||||
let mut candidates = Vec::new();
|
||||
let mut path_candidates = Vec::new();
|
||||
let mut path_candidate_prefixes = Vec::new();
|
||||
let mut path_text = String::new();
|
||||
let mut path_stack = Vec::new();
|
||||
|
||||
for (id, item) in items.iter().enumerate() {
|
||||
if item.depth < path_stack.len() {
|
||||
path_stack.truncate(item.depth);
|
||||
path_text.truncate(path_stack.last().copied().unwrap_or(0));
|
||||
}
|
||||
if !path_text.is_empty() {
|
||||
path_text.push(' ');
|
||||
}
|
||||
path_candidate_prefixes.push(path_text.len());
|
||||
path_text.push_str(&item.text);
|
||||
path_stack.push(path_text.len());
|
||||
|
||||
let candidate_text = item
|
||||
.name_ranges
|
||||
.iter()
|
||||
.map(|range| &item.text[range.start as usize..range.end as usize])
|
||||
.collect::<String>();
|
||||
|
||||
path_candidates.push(StringMatchCandidate {
|
||||
id,
|
||||
char_bag: path_text.as_str().into(),
|
||||
string: path_text.clone(),
|
||||
});
|
||||
candidates.push(StringMatchCandidate {
|
||||
id,
|
||||
char_bag: candidate_text.as_str().into(),
|
||||
string: candidate_text,
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
candidates,
|
||||
path_candidates,
|
||||
path_candidate_prefixes,
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn search(&self, query: &str, executor: Arc<Background>) -> Vec<StringMatch> {
|
||||
let query = query.trim_start();
|
||||
let is_path_query = query.contains(' ');
|
||||
let smart_case = query.chars().any(|c| c.is_uppercase());
|
||||
let mut matches = fuzzy::match_strings(
|
||||
if is_path_query {
|
||||
&self.path_candidates
|
||||
} else {
|
||||
&self.candidates
|
||||
},
|
||||
query,
|
||||
smart_case,
|
||||
100,
|
||||
&Default::default(),
|
||||
executor.clone(),
|
||||
)
|
||||
.await;
|
||||
matches.sort_unstable_by_key(|m| m.candidate_id);
|
||||
|
||||
let mut tree_matches = Vec::new();
|
||||
|
||||
let mut prev_item_ix = 0;
|
||||
for mut string_match in matches {
|
||||
let outline_match = &self.items[string_match.candidate_id];
|
||||
|
||||
if is_path_query {
|
||||
let prefix_len = self.path_candidate_prefixes[string_match.candidate_id];
|
||||
string_match
|
||||
.positions
|
||||
.retain(|position| *position >= prefix_len);
|
||||
for position in &mut string_match.positions {
|
||||
*position -= prefix_len;
|
||||
}
|
||||
} else {
|
||||
let mut name_ranges = outline_match.name_ranges.iter();
|
||||
let mut name_range = name_ranges.next().unwrap();
|
||||
let mut preceding_ranges_len = 0;
|
||||
for position in &mut string_match.positions {
|
||||
while *position >= preceding_ranges_len + name_range.len() as usize {
|
||||
preceding_ranges_len += name_range.len();
|
||||
name_range = name_ranges.next().unwrap();
|
||||
}
|
||||
*position = name_range.start as usize + (*position - preceding_ranges_len);
|
||||
}
|
||||
}
|
||||
|
||||
let insertion_ix = tree_matches.len();
|
||||
let mut cur_depth = outline_match.depth;
|
||||
for (ix, item) in self.items[prev_item_ix..string_match.candidate_id]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
{
|
||||
if cur_depth == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let candidate_index = ix + prev_item_ix;
|
||||
if item.depth == cur_depth - 1 {
|
||||
tree_matches.insert(
|
||||
insertion_ix,
|
||||
StringMatch {
|
||||
candidate_id: candidate_index,
|
||||
score: Default::default(),
|
||||
positions: Default::default(),
|
||||
string: Default::default(),
|
||||
},
|
||||
);
|
||||
cur_depth -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
prev_item_ix = string_match.candidate_id + 1;
|
||||
tree_matches.push(string_match);
|
||||
}
|
||||
|
||||
tree_matches
|
||||
}
|
||||
}
|
||||
367
crates/language/src/proto.rs
Normal file
367
crates/language/src/proto.rs
Normal file
@@ -0,0 +1,367 @@
|
||||
use crate::{diagnostic_set::DiagnosticEntry, Diagnostic, Operation};
|
||||
use anyhow::{anyhow, Result};
|
||||
use clock::ReplicaId;
|
||||
use collections::HashSet;
|
||||
use lsp::DiagnosticSeverity;
|
||||
use rpc::proto;
|
||||
use std::sync::Arc;
|
||||
use text::*;
|
||||
|
||||
pub use proto::{Buffer, SelectionSet};
|
||||
|
||||
pub fn serialize_operation(operation: &Operation) -> proto::Operation {
|
||||
proto::Operation {
|
||||
variant: Some(match operation {
|
||||
Operation::Buffer(text::Operation::Edit(edit)) => {
|
||||
proto::operation::Variant::Edit(serialize_edit_operation(edit))
|
||||
}
|
||||
Operation::Buffer(text::Operation::Undo {
|
||||
undo,
|
||||
lamport_timestamp,
|
||||
}) => proto::operation::Variant::Undo(proto::operation::Undo {
|
||||
replica_id: undo.id.replica_id as u32,
|
||||
local_timestamp: undo.id.value,
|
||||
lamport_timestamp: lamport_timestamp.value,
|
||||
ranges: undo
|
||||
.ranges
|
||||
.iter()
|
||||
.map(|r| proto::Range {
|
||||
start: r.start.0 as u64,
|
||||
end: r.end.0 as u64,
|
||||
})
|
||||
.collect(),
|
||||
counts: undo
|
||||
.counts
|
||||
.iter()
|
||||
.map(|(edit_id, count)| proto::UndoCount {
|
||||
replica_id: edit_id.replica_id as u32,
|
||||
local_timestamp: edit_id.value,
|
||||
count: *count,
|
||||
})
|
||||
.collect(),
|
||||
version: From::from(&undo.version),
|
||||
}),
|
||||
Operation::UpdateSelections {
|
||||
replica_id,
|
||||
selections,
|
||||
lamport_timestamp,
|
||||
} => proto::operation::Variant::UpdateSelections(proto::operation::UpdateSelections {
|
||||
replica_id: *replica_id as u32,
|
||||
lamport_timestamp: lamport_timestamp.value,
|
||||
selections: serialize_selections(selections),
|
||||
}),
|
||||
Operation::UpdateDiagnostics {
|
||||
diagnostics,
|
||||
lamport_timestamp,
|
||||
} => proto::operation::Variant::UpdateDiagnostics(proto::UpdateDiagnostics {
|
||||
replica_id: lamport_timestamp.replica_id as u32,
|
||||
lamport_timestamp: lamport_timestamp.value,
|
||||
diagnostics: serialize_diagnostics(diagnostics.iter()),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation::Edit {
|
||||
let ranges = operation
|
||||
.ranges
|
||||
.iter()
|
||||
.map(|range| proto::Range {
|
||||
start: range.start.0 as u64,
|
||||
end: range.end.0 as u64,
|
||||
})
|
||||
.collect();
|
||||
proto::operation::Edit {
|
||||
replica_id: operation.timestamp.replica_id as u32,
|
||||
local_timestamp: operation.timestamp.local,
|
||||
lamport_timestamp: operation.timestamp.lamport,
|
||||
version: From::from(&operation.version),
|
||||
ranges,
|
||||
new_text: operation.new_text.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_undo_map_entry(
|
||||
(edit_id, counts): (&clock::Local, &[(clock::Local, u32)]),
|
||||
) -> proto::UndoMapEntry {
|
||||
proto::UndoMapEntry {
|
||||
replica_id: edit_id.replica_id as u32,
|
||||
local_timestamp: edit_id.value,
|
||||
counts: counts
|
||||
.iter()
|
||||
.map(|(undo_id, count)| proto::UndoCount {
|
||||
replica_id: undo_id.replica_id as u32,
|
||||
local_timestamp: undo_id.value,
|
||||
count: *count,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_buffer_fragment(fragment: &text::Fragment) -> proto::BufferFragment {
|
||||
proto::BufferFragment {
|
||||
replica_id: fragment.insertion_timestamp.replica_id as u32,
|
||||
local_timestamp: fragment.insertion_timestamp.local,
|
||||
lamport_timestamp: fragment.insertion_timestamp.lamport,
|
||||
insertion_offset: fragment.insertion_offset as u32,
|
||||
len: fragment.len as u32,
|
||||
visible: fragment.visible,
|
||||
deletions: fragment
|
||||
.deletions
|
||||
.iter()
|
||||
.map(|clock| proto::VectorClockEntry {
|
||||
replica_id: clock.replica_id as u32,
|
||||
timestamp: clock.value,
|
||||
})
|
||||
.collect(),
|
||||
max_undos: From::from(&fragment.max_undos),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_selections(selections: &Arc<[Selection<Anchor>]>) -> Vec<proto::Selection> {
|
||||
selections
|
||||
.iter()
|
||||
.map(|selection| proto::Selection {
|
||||
id: selection.id as u64,
|
||||
start: Some(serialize_anchor(&selection.start)),
|
||||
end: Some(serialize_anchor(&selection.end)),
|
||||
reversed: selection.reversed,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn serialize_diagnostics<'a>(
|
||||
diagnostics: impl IntoIterator<Item = &'a DiagnosticEntry<Anchor>>,
|
||||
) -> Vec<proto::Diagnostic> {
|
||||
diagnostics
|
||||
.into_iter()
|
||||
.map(|entry| proto::Diagnostic {
|
||||
start: Some(serialize_anchor(&entry.range.start)),
|
||||
end: Some(serialize_anchor(&entry.range.end)),
|
||||
message: entry.diagnostic.message.clone(),
|
||||
severity: match entry.diagnostic.severity {
|
||||
DiagnosticSeverity::ERROR => proto::diagnostic::Severity::Error,
|
||||
DiagnosticSeverity::WARNING => proto::diagnostic::Severity::Warning,
|
||||
DiagnosticSeverity::INFORMATION => proto::diagnostic::Severity::Information,
|
||||
DiagnosticSeverity::HINT => proto::diagnostic::Severity::Hint,
|
||||
_ => proto::diagnostic::Severity::None,
|
||||
} as i32,
|
||||
group_id: entry.diagnostic.group_id as u64,
|
||||
is_primary: entry.diagnostic.is_primary,
|
||||
is_valid: entry.diagnostic.is_valid,
|
||||
code: entry.diagnostic.code.clone(),
|
||||
is_disk_based: entry.diagnostic.is_disk_based,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn serialize_anchor(anchor: &Anchor) -> proto::Anchor {
|
||||
proto::Anchor {
|
||||
replica_id: anchor.timestamp.replica_id as u32,
|
||||
local_timestamp: anchor.timestamp.value,
|
||||
offset: anchor.offset as u64,
|
||||
bias: match anchor.bias {
|
||||
Bias::Left => proto::Bias::Left as i32,
|
||||
Bias::Right => proto::Bias::Right as i32,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
|
||||
Ok(
|
||||
match message
|
||||
.variant
|
||||
.ok_or_else(|| anyhow!("missing operation variant"))?
|
||||
{
|
||||
proto::operation::Variant::Edit(edit) => {
|
||||
Operation::Buffer(text::Operation::Edit(deserialize_edit_operation(edit)))
|
||||
}
|
||||
proto::operation::Variant::Undo(undo) => Operation::Buffer(text::Operation::Undo {
|
||||
lamport_timestamp: clock::Lamport {
|
||||
replica_id: undo.replica_id as ReplicaId,
|
||||
value: undo.lamport_timestamp,
|
||||
},
|
||||
undo: UndoOperation {
|
||||
id: clock::Local {
|
||||
replica_id: undo.replica_id as ReplicaId,
|
||||
value: undo.local_timestamp,
|
||||
},
|
||||
counts: undo
|
||||
.counts
|
||||
.into_iter()
|
||||
.map(|c| {
|
||||
(
|
||||
clock::Local {
|
||||
replica_id: c.replica_id as ReplicaId,
|
||||
value: c.local_timestamp,
|
||||
},
|
||||
c.count,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
ranges: undo
|
||||
.ranges
|
||||
.into_iter()
|
||||
.map(|r| FullOffset(r.start as usize)..FullOffset(r.end as usize))
|
||||
.collect(),
|
||||
version: undo.version.into(),
|
||||
},
|
||||
}),
|
||||
proto::operation::Variant::UpdateSelections(message) => {
|
||||
let selections = message
|
||||
.selections
|
||||
.into_iter()
|
||||
.filter_map(|selection| {
|
||||
Some(Selection {
|
||||
id: selection.id as usize,
|
||||
start: deserialize_anchor(selection.start?)?,
|
||||
end: deserialize_anchor(selection.end?)?,
|
||||
reversed: selection.reversed,
|
||||
goal: SelectionGoal::None,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Operation::UpdateSelections {
|
||||
replica_id: message.replica_id as ReplicaId,
|
||||
lamport_timestamp: clock::Lamport {
|
||||
replica_id: message.replica_id as ReplicaId,
|
||||
value: message.lamport_timestamp,
|
||||
},
|
||||
selections: Arc::from(selections),
|
||||
}
|
||||
}
|
||||
proto::operation::Variant::UpdateDiagnostics(message) => Operation::UpdateDiagnostics {
|
||||
diagnostics: deserialize_diagnostics(message.diagnostics),
|
||||
lamport_timestamp: clock::Lamport {
|
||||
replica_id: message.replica_id as ReplicaId,
|
||||
value: message.lamport_timestamp,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn deserialize_edit_operation(edit: proto::operation::Edit) -> EditOperation {
|
||||
let ranges = edit
|
||||
.ranges
|
||||
.into_iter()
|
||||
.map(|range| FullOffset(range.start as usize)..FullOffset(range.end as usize))
|
||||
.collect();
|
||||
EditOperation {
|
||||
timestamp: InsertionTimestamp {
|
||||
replica_id: edit.replica_id as ReplicaId,
|
||||
local: edit.local_timestamp,
|
||||
lamport: edit.lamport_timestamp,
|
||||
},
|
||||
version: edit.version.into(),
|
||||
ranges,
|
||||
new_text: edit.new_text,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_undo_map_entry(
|
||||
entry: proto::UndoMapEntry,
|
||||
) -> (clock::Local, Vec<(clock::Local, u32)>) {
|
||||
(
|
||||
clock::Local {
|
||||
replica_id: entry.replica_id as u16,
|
||||
value: entry.local_timestamp,
|
||||
},
|
||||
entry
|
||||
.counts
|
||||
.into_iter()
|
||||
.map(|undo_count| {
|
||||
(
|
||||
clock::Local {
|
||||
replica_id: undo_count.replica_id as u16,
|
||||
value: undo_count.local_timestamp,
|
||||
},
|
||||
undo_count.count,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn deserialize_buffer_fragment(
|
||||
message: proto::BufferFragment,
|
||||
ix: usize,
|
||||
count: usize,
|
||||
) -> Fragment {
|
||||
Fragment {
|
||||
id: locator::Locator::from_index(ix, count),
|
||||
insertion_timestamp: InsertionTimestamp {
|
||||
replica_id: message.replica_id as ReplicaId,
|
||||
local: message.local_timestamp,
|
||||
lamport: message.lamport_timestamp,
|
||||
},
|
||||
insertion_offset: message.insertion_offset as usize,
|
||||
len: message.len as usize,
|
||||
visible: message.visible,
|
||||
deletions: HashSet::from_iter(message.deletions.into_iter().map(|entry| clock::Local {
|
||||
replica_id: entry.replica_id as ReplicaId,
|
||||
value: entry.timestamp,
|
||||
})),
|
||||
max_undos: From::from(message.max_undos),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_selections(selections: Vec<proto::Selection>) -> Arc<[Selection<Anchor>]> {
|
||||
Arc::from(
|
||||
selections
|
||||
.into_iter()
|
||||
.filter_map(|selection| {
|
||||
Some(Selection {
|
||||
id: selection.id as usize,
|
||||
start: deserialize_anchor(selection.start?)?,
|
||||
end: deserialize_anchor(selection.end?)?,
|
||||
reversed: selection.reversed,
|
||||
goal: SelectionGoal::None,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn deserialize_diagnostics(
|
||||
diagnostics: Vec<proto::Diagnostic>,
|
||||
) -> Arc<[DiagnosticEntry<Anchor>]> {
|
||||
diagnostics
|
||||
.into_iter()
|
||||
.filter_map(|diagnostic| {
|
||||
Some(DiagnosticEntry {
|
||||
range: deserialize_anchor(diagnostic.start?)?..deserialize_anchor(diagnostic.end?)?,
|
||||
diagnostic: Diagnostic {
|
||||
severity: match proto::diagnostic::Severity::from_i32(diagnostic.severity)? {
|
||||
proto::diagnostic::Severity::Error => DiagnosticSeverity::ERROR,
|
||||
proto::diagnostic::Severity::Warning => DiagnosticSeverity::WARNING,
|
||||
proto::diagnostic::Severity::Information => DiagnosticSeverity::INFORMATION,
|
||||
proto::diagnostic::Severity::Hint => DiagnosticSeverity::HINT,
|
||||
proto::diagnostic::Severity::None => return None,
|
||||
},
|
||||
message: diagnostic.message,
|
||||
group_id: diagnostic.group_id as usize,
|
||||
code: diagnostic.code,
|
||||
is_valid: diagnostic.is_valid,
|
||||
is_primary: diagnostic.is_primary,
|
||||
is_disk_based: diagnostic.is_disk_based,
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn deserialize_anchor(anchor: proto::Anchor) -> Option<Anchor> {
|
||||
Some(Anchor {
|
||||
timestamp: clock::Local {
|
||||
replica_id: anchor.replica_id as ReplicaId,
|
||||
value: anchor.local_timestamp,
|
||||
},
|
||||
offset: anchor.offset as usize,
|
||||
bias: match proto::Bias::from_i32(anchor.bias)? {
|
||||
proto::Bias::Left => Bias::Left,
|
||||
proto::Bias::Right => Bias::Right,
|
||||
},
|
||||
})
|
||||
}
|
||||
1170
crates/language/src/tests.rs
Normal file
1170
crates/language/src/tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
31
crates/lsp/Cargo.toml
Normal file
31
crates/lsp/Cargo.toml
Normal file
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "lsp"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
path = "src/lsp.rs"
|
||||
|
||||
[features]
|
||||
test-support = ["async-pipe"]
|
||||
|
||||
[dependencies]
|
||||
gpui = { path = "../gpui" }
|
||||
util = { path = "../util" }
|
||||
anyhow = "1.0"
|
||||
async-pipe = { git = "https://github.com/routerify/async-pipe-rs", rev = "feeb77e83142a9ff837d0767652ae41bfc5d8e47", optional = true }
|
||||
futures = "0.3"
|
||||
log = "0.4"
|
||||
lsp-types = "0.91"
|
||||
parking_lot = "0.11"
|
||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0", features = ["raw_value"] }
|
||||
smol = "1.2"
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
async-pipe = { git = "https://github.com/routerify/async-pipe-rs", rev = "feeb77e83142a9ff837d0767652ae41bfc5d8e47" }
|
||||
simplelog = "0.9"
|
||||
unindent = "0.1.7"
|
||||
738
crates/lsp/src/lsp.rs
Normal file
738
crates/lsp/src/lsp.rs
Normal file
@@ -0,0 +1,738 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use futures::{io::BufWriter, AsyncRead, AsyncWrite};
|
||||
use gpui::{executor, Task};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use postage::{barrier, oneshot, prelude::Stream, sink::Sink};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, value::RawValue, Value};
|
||||
use smol::{
|
||||
channel,
|
||||
io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
|
||||
process::Command,
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
future::Future,
|
||||
io::Write,
|
||||
str::FromStr,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use std::{path::Path, process::Stdio};
|
||||
use util::TryFutureExt;
|
||||
|
||||
pub use lsp_types::*;
|
||||
|
||||
const JSON_RPC_VERSION: &'static str = "2.0";
|
||||
const CONTENT_LEN_HEADER: &'static str = "Content-Length: ";
|
||||
|
||||
type NotificationHandler = Box<dyn Send + Sync + FnMut(&str)>;
|
||||
type ResponseHandler = Box<dyn Send + FnOnce(Result<&str, Error>)>;
|
||||
|
||||
pub struct LanguageServer {
|
||||
next_id: AtomicUsize,
|
||||
outbound_tx: RwLock<Option<channel::Sender<Vec<u8>>>>,
|
||||
notification_handlers: Arc<RwLock<HashMap<&'static str, NotificationHandler>>>,
|
||||
response_handlers: Arc<Mutex<HashMap<usize, ResponseHandler>>>,
|
||||
executor: Arc<executor::Background>,
|
||||
io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
|
||||
initialized: barrier::Receiver,
|
||||
output_done_rx: Mutex<Option<barrier::Receiver>>,
|
||||
}
|
||||
|
||||
pub struct Subscription {
|
||||
method: &'static str,
|
||||
notification_handlers: Arc<RwLock<HashMap<&'static str, NotificationHandler>>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Request<'a, T> {
|
||||
jsonrpc: &'a str,
|
||||
id: usize,
|
||||
method: &'a str,
|
||||
params: T,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct AnyResponse<'a> {
|
||||
id: usize,
|
||||
#[serde(default)]
|
||||
error: Option<Error>,
|
||||
#[serde(borrow)]
|
||||
result: Option<&'a RawValue>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Notification<'a, T> {
|
||||
#[serde(borrow)]
|
||||
jsonrpc: &'a str,
|
||||
#[serde(borrow)]
|
||||
method: &'a str,
|
||||
params: T,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AnyNotification<'a> {
|
||||
#[serde(borrow)]
|
||||
method: &'a str,
|
||||
#[serde(borrow)]
|
||||
params: &'a RawValue,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Error {
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl LanguageServer {
|
||||
pub fn new(
|
||||
binary_path: &Path,
|
||||
root_path: &Path,
|
||||
background: Arc<executor::Background>,
|
||||
) -> Result<Arc<Self>> {
|
||||
let mut server = Command::new(binary_path)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()?;
|
||||
let stdin = server.stdin.take().unwrap();
|
||||
let stdout = server.stdout.take().unwrap();
|
||||
Self::new_internal(stdin, stdout, root_path, background)
|
||||
}
|
||||
|
||||
fn new_internal<Stdin, Stdout>(
|
||||
stdin: Stdin,
|
||||
stdout: Stdout,
|
||||
root_path: &Path,
|
||||
executor: Arc<executor::Background>,
|
||||
) -> Result<Arc<Self>>
|
||||
where
|
||||
Stdin: AsyncWrite + Unpin + Send + 'static,
|
||||
Stdout: AsyncRead + Unpin + Send + 'static,
|
||||
{
|
||||
let mut stdin = BufWriter::new(stdin);
|
||||
let mut stdout = BufReader::new(stdout);
|
||||
let (outbound_tx, outbound_rx) = channel::unbounded::<Vec<u8>>();
|
||||
let notification_handlers = Arc::new(RwLock::new(HashMap::<_, NotificationHandler>::new()));
|
||||
let response_handlers = Arc::new(Mutex::new(HashMap::<_, ResponseHandler>::new()));
|
||||
let input_task = executor.spawn(
|
||||
{
|
||||
let notification_handlers = notification_handlers.clone();
|
||||
let response_handlers = response_handlers.clone();
|
||||
async move {
|
||||
let mut buffer = Vec::new();
|
||||
loop {
|
||||
buffer.clear();
|
||||
stdout.read_until(b'\n', &mut buffer).await?;
|
||||
stdout.read_until(b'\n', &mut buffer).await?;
|
||||
let message_len: usize = std::str::from_utf8(&buffer)?
|
||||
.strip_prefix(CONTENT_LEN_HEADER)
|
||||
.ok_or_else(|| anyhow!("invalid header"))?
|
||||
.trim_end()
|
||||
.parse()?;
|
||||
|
||||
buffer.resize(message_len, 0);
|
||||
stdout.read_exact(&mut buffer).await?;
|
||||
|
||||
if let Ok(AnyNotification { method, params }) =
|
||||
serde_json::from_slice(&buffer)
|
||||
{
|
||||
if let Some(handler) = notification_handlers.write().get_mut(method) {
|
||||
handler(params.get());
|
||||
} else {
|
||||
log::info!(
|
||||
"unhandled notification {}:\n{}",
|
||||
method,
|
||||
serde_json::to_string_pretty(
|
||||
&Value::from_str(params.get()).unwrap()
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
} else if let Ok(AnyResponse { id, error, result }) =
|
||||
serde_json::from_slice(&buffer)
|
||||
{
|
||||
if let Some(handler) = response_handlers.lock().remove(&id) {
|
||||
if let Some(error) = error {
|
||||
handler(Err(error));
|
||||
} else if let Some(result) = result {
|
||||
handler(Ok(result.get()));
|
||||
} else {
|
||||
handler(Ok("null"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow!(
|
||||
"failed to deserialize message:\n{}",
|
||||
std::str::from_utf8(&buffer)?
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.log_err(),
|
||||
);
|
||||
let (output_done_tx, output_done_rx) = barrier::channel();
|
||||
let output_task = executor.spawn(
|
||||
async move {
|
||||
let mut content_len_buffer = Vec::new();
|
||||
while let Ok(message) = outbound_rx.recv().await {
|
||||
content_len_buffer.clear();
|
||||
write!(content_len_buffer, "{}", message.len()).unwrap();
|
||||
stdin.write_all(CONTENT_LEN_HEADER.as_bytes()).await?;
|
||||
stdin.write_all(&content_len_buffer).await?;
|
||||
stdin.write_all("\r\n\r\n".as_bytes()).await?;
|
||||
stdin.write_all(&message).await?;
|
||||
stdin.flush().await?;
|
||||
}
|
||||
drop(output_done_tx);
|
||||
Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
);
|
||||
|
||||
let (initialized_tx, initialized_rx) = barrier::channel();
|
||||
let this = Arc::new(Self {
|
||||
notification_handlers,
|
||||
response_handlers,
|
||||
next_id: Default::default(),
|
||||
outbound_tx: RwLock::new(Some(outbound_tx)),
|
||||
executor: executor.clone(),
|
||||
io_tasks: Mutex::new(Some((input_task, output_task))),
|
||||
initialized: initialized_rx,
|
||||
output_done_rx: Mutex::new(Some(output_done_rx)),
|
||||
});
|
||||
|
||||
let root_uri =
|
||||
lsp_types::Url::from_file_path(root_path).map_err(|_| anyhow!("invalid root path"))?;
|
||||
executor
|
||||
.spawn({
|
||||
let this = this.clone();
|
||||
async move {
|
||||
this.init(root_uri).log_err().await;
|
||||
drop(initialized_tx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
async fn init(self: Arc<Self>, root_uri: lsp_types::Url) -> Result<()> {
|
||||
#[allow(deprecated)]
|
||||
let params = lsp_types::InitializeParams {
|
||||
process_id: Default::default(),
|
||||
root_path: Default::default(),
|
||||
root_uri: Some(root_uri),
|
||||
initialization_options: Default::default(),
|
||||
capabilities: lsp_types::ClientCapabilities {
|
||||
experimental: Some(json!({
|
||||
"serverStatusNotification": true,
|
||||
})),
|
||||
window: Some(lsp_types::WindowClientCapabilities {
|
||||
work_done_progress: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
trace: Default::default(),
|
||||
workspace_folders: Default::default(),
|
||||
client_info: Default::default(),
|
||||
locale: Default::default(),
|
||||
};
|
||||
|
||||
let this = self.clone();
|
||||
let request = Self::request_internal::<lsp_types::request::Initialize>(
|
||||
&this.next_id,
|
||||
&this.response_handlers,
|
||||
this.outbound_tx.read().as_ref(),
|
||||
params,
|
||||
);
|
||||
request.await?;
|
||||
Self::notify_internal::<lsp_types::notification::Initialized>(
|
||||
this.outbound_tx.read().as_ref(),
|
||||
lsp_types::InitializedParams {},
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn shutdown(&self) -> Option<impl 'static + Send + Future<Output = Result<()>>> {
|
||||
if let Some(tasks) = self.io_tasks.lock().take() {
|
||||
let response_handlers = self.response_handlers.clone();
|
||||
let outbound_tx = self.outbound_tx.write().take();
|
||||
let next_id = AtomicUsize::new(self.next_id.load(SeqCst));
|
||||
let mut output_done = self.output_done_rx.lock().take().unwrap();
|
||||
Some(async move {
|
||||
Self::request_internal::<lsp_types::request::Shutdown>(
|
||||
&next_id,
|
||||
&response_handlers,
|
||||
outbound_tx.as_ref(),
|
||||
(),
|
||||
)
|
||||
.await?;
|
||||
Self::notify_internal::<lsp_types::notification::Exit>(outbound_tx.as_ref(), ())?;
|
||||
drop(outbound_tx);
|
||||
output_done.recv().await;
|
||||
drop(tasks);
|
||||
Ok(())
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_notification<T, F>(&self, mut f: F) -> Subscription
|
||||
where
|
||||
T: lsp_types::notification::Notification,
|
||||
F: 'static + Send + Sync + FnMut(T::Params),
|
||||
{
|
||||
let prev_handler = self.notification_handlers.write().insert(
|
||||
T::METHOD,
|
||||
Box::new(
|
||||
move |notification| match serde_json::from_str(notification) {
|
||||
Ok(notification) => f(notification),
|
||||
Err(err) => log::error!("error parsing notification {}: {}", T::METHOD, err),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
assert!(
|
||||
prev_handler.is_none(),
|
||||
"registered multiple handlers for the same notification"
|
||||
);
|
||||
|
||||
Subscription {
|
||||
method: T::METHOD,
|
||||
notification_handlers: self.notification_handlers.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request<T: lsp_types::request::Request>(
|
||||
self: Arc<Self>,
|
||||
params: T::Params,
|
||||
) -> impl Future<Output = Result<T::Result>>
|
||||
where
|
||||
T::Result: 'static + Send,
|
||||
{
|
||||
let this = self.clone();
|
||||
async move {
|
||||
this.initialized.clone().recv().await;
|
||||
Self::request_internal::<T>(
|
||||
&this.next_id,
|
||||
&this.response_handlers,
|
||||
this.outbound_tx.read().as_ref(),
|
||||
params,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
fn request_internal<T: lsp_types::request::Request>(
|
||||
next_id: &AtomicUsize,
|
||||
response_handlers: &Mutex<HashMap<usize, ResponseHandler>>,
|
||||
outbound_tx: Option<&channel::Sender<Vec<u8>>>,
|
||||
params: T::Params,
|
||||
) -> impl 'static + Future<Output = Result<T::Result>>
|
||||
where
|
||||
T::Result: 'static + Send,
|
||||
{
|
||||
let id = next_id.fetch_add(1, SeqCst);
|
||||
let message = serde_json::to_vec(&Request {
|
||||
jsonrpc: JSON_RPC_VERSION,
|
||||
id,
|
||||
method: T::METHOD,
|
||||
params,
|
||||
})
|
||||
.unwrap();
|
||||
let mut response_handlers = response_handlers.lock();
|
||||
let (mut tx, mut rx) = oneshot::channel();
|
||||
response_handlers.insert(
|
||||
id,
|
||||
Box::new(move |result| {
|
||||
let response = match result {
|
||||
Ok(response) => {
|
||||
serde_json::from_str(response).context("failed to deserialize response")
|
||||
}
|
||||
Err(error) => Err(anyhow!("{}", error.message)),
|
||||
};
|
||||
let _ = tx.try_send(response);
|
||||
}),
|
||||
);
|
||||
|
||||
let send = outbound_tx
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
anyhow!("tried to send a request to a language server that has been shut down")
|
||||
})
|
||||
.and_then(|outbound_tx| {
|
||||
outbound_tx.try_send(message)?;
|
||||
Ok(())
|
||||
});
|
||||
async move {
|
||||
send?;
|
||||
rx.recv().await.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notify<T: lsp_types::notification::Notification>(
|
||||
self: &Arc<Self>,
|
||||
params: T::Params,
|
||||
) -> impl Future<Output = Result<()>> {
|
||||
let this = self.clone();
|
||||
async move {
|
||||
this.initialized.clone().recv().await;
|
||||
Self::notify_internal::<T>(this.outbound_tx.read().as_ref(), params)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_internal<T: lsp_types::notification::Notification>(
|
||||
outbound_tx: Option<&channel::Sender<Vec<u8>>>,
|
||||
params: T::Params,
|
||||
) -> Result<()> {
|
||||
let message = serde_json::to_vec(&Notification {
|
||||
jsonrpc: JSON_RPC_VERSION,
|
||||
method: T::METHOD,
|
||||
params,
|
||||
})
|
||||
.unwrap();
|
||||
let outbound_tx = outbound_tx
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("tried to notify a language server that has been shut down"))?;
|
||||
outbound_tx.try_send(message)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for LanguageServer {
|
||||
fn drop(&mut self) {
|
||||
if let Some(shutdown) = self.shutdown() {
|
||||
self.executor.spawn(shutdown).detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Subscription {
|
||||
pub fn detach(mut self) {
|
||||
self.method = "";
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Subscription {
|
||||
fn drop(&mut self) {
|
||||
self.notification_handlers.write().remove(self.method);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct FakeLanguageServer {
|
||||
buffer: Vec<u8>,
|
||||
stdin: smol::io::BufReader<async_pipe::PipeReader>,
|
||||
stdout: smol::io::BufWriter<async_pipe::PipeWriter>,
|
||||
pub started: Arc<std::sync::atomic::AtomicBool>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct RequestId<T> {
|
||||
id: usize,
|
||||
_type: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl LanguageServer {
|
||||
pub async fn fake(executor: Arc<executor::Background>) -> (Arc<Self>, FakeLanguageServer) {
|
||||
let stdin = async_pipe::pipe();
|
||||
let stdout = async_pipe::pipe();
|
||||
let mut fake = FakeLanguageServer {
|
||||
stdin: smol::io::BufReader::new(stdin.1),
|
||||
stdout: smol::io::BufWriter::new(stdout.0),
|
||||
buffer: Vec::new(),
|
||||
started: Arc::new(std::sync::atomic::AtomicBool::new(true)),
|
||||
};
|
||||
|
||||
let server = Self::new_internal(stdin.0, stdout.1, Path::new("/"), executor).unwrap();
|
||||
|
||||
let (init_id, _) = fake.receive_request::<request::Initialize>().await;
|
||||
fake.respond(init_id, InitializeResult::default()).await;
|
||||
fake.receive_notification::<notification::Initialized>()
|
||||
.await;
|
||||
|
||||
(server, fake)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl FakeLanguageServer {
|
||||
pub async fn notify<T: notification::Notification>(&mut self, params: T::Params) {
|
||||
if !self.started.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
panic!("can't simulate an LSP notification before the server has been started");
|
||||
}
|
||||
let message = serde_json::to_vec(&Notification {
|
||||
jsonrpc: JSON_RPC_VERSION,
|
||||
method: T::METHOD,
|
||||
params,
|
||||
})
|
||||
.unwrap();
|
||||
self.send(message).await;
|
||||
}
|
||||
|
||||
pub async fn respond<'a, T: request::Request>(
|
||||
&mut self,
|
||||
request_id: RequestId<T>,
|
||||
result: T::Result,
|
||||
) {
|
||||
let result = serde_json::to_string(&result).unwrap();
|
||||
let message = serde_json::to_vec(&AnyResponse {
|
||||
id: request_id.id,
|
||||
error: None,
|
||||
result: Some(&RawValue::from_string(result).unwrap()),
|
||||
})
|
||||
.unwrap();
|
||||
self.send(message).await;
|
||||
}
|
||||
|
||||
pub async fn receive_request<T: request::Request>(&mut self) -> (RequestId<T>, T::Params) {
|
||||
loop {
|
||||
self.receive().await;
|
||||
if let Ok(request) = serde_json::from_slice::<Request<T::Params>>(&self.buffer) {
|
||||
assert_eq!(request.method, T::METHOD);
|
||||
assert_eq!(request.jsonrpc, JSON_RPC_VERSION);
|
||||
return (
|
||||
RequestId {
|
||||
id: request.id,
|
||||
_type: std::marker::PhantomData,
|
||||
},
|
||||
request.params,
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"skipping message in fake language server {:?}",
|
||||
std::str::from_utf8(&self.buffer)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn receive_notification<T: notification::Notification>(&mut self) -> T::Params {
|
||||
self.receive().await;
|
||||
let notification = serde_json::from_slice::<Notification<T::Params>>(&self.buffer).unwrap();
|
||||
assert_eq!(notification.method, T::METHOD);
|
||||
notification.params
|
||||
}
|
||||
|
||||
pub async fn start_progress(&mut self, token: impl Into<String>) {
|
||||
self.notify::<notification::Progress>(ProgressParams {
|
||||
token: NumberOrString::String(token.into()),
|
||||
value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(Default::default())),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn end_progress(&mut self, token: impl Into<String>) {
|
||||
self.notify::<notification::Progress>(ProgressParams {
|
||||
token: NumberOrString::String(token.into()),
|
||||
value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(Default::default())),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn send(&mut self, message: Vec<u8>) {
|
||||
self.stdout
|
||||
.write_all(CONTENT_LEN_HEADER.as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
self.stdout
|
||||
.write_all((format!("{}", message.len())).as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
self.stdout.write_all("\r\n\r\n".as_bytes()).await.unwrap();
|
||||
self.stdout.write_all(&message).await.unwrap();
|
||||
self.stdout.flush().await.unwrap();
|
||||
}
|
||||
|
||||
async fn receive(&mut self) {
|
||||
self.buffer.clear();
|
||||
self.stdin
|
||||
.read_until(b'\n', &mut self.buffer)
|
||||
.await
|
||||
.unwrap();
|
||||
self.stdin
|
||||
.read_until(b'\n', &mut self.buffer)
|
||||
.await
|
||||
.unwrap();
|
||||
let message_len: usize = std::str::from_utf8(&self.buffer)
|
||||
.unwrap()
|
||||
.strip_prefix(CONTENT_LEN_HEADER)
|
||||
.unwrap()
|
||||
.trim_end()
|
||||
.parse()
|
||||
.unwrap();
|
||||
self.buffer.resize(message_len, 0);
|
||||
self.stdin.read_exact(&mut self.buffer).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use simplelog::SimpleLogger;
|
||||
use unindent::Unindent;
|
||||
use util::test::temp_tree;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_basic(cx: TestAppContext) {
|
||||
let lib_source = r#"
|
||||
fn fun() {
|
||||
let hello = "world";
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
let root_dir = temp_tree(json!({
|
||||
"Cargo.toml": r#"
|
||||
[package]
|
||||
name = "temp"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
"#.unindent(),
|
||||
"src": {
|
||||
"lib.rs": &lib_source
|
||||
}
|
||||
}));
|
||||
let lib_file_uri =
|
||||
lsp_types::Url::from_file_path(root_dir.path().join("src/lib.rs")).unwrap();
|
||||
|
||||
let server = cx.read(|cx| {
|
||||
LanguageServer::new(
|
||||
Path::new("rust-analyzer"),
|
||||
root_dir.path(),
|
||||
cx.background().clone(),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
server.next_idle_notification().await;
|
||||
|
||||
server
|
||||
.notify::<lsp_types::notification::DidOpenTextDocument>(
|
||||
lsp_types::DidOpenTextDocumentParams {
|
||||
text_document: lsp_types::TextDocumentItem::new(
|
||||
lib_file_uri.clone(),
|
||||
"rust".to_string(),
|
||||
0,
|
||||
lib_source,
|
||||
),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let hover = server
|
||||
.request::<lsp_types::request::HoverRequest>(lsp_types::HoverParams {
|
||||
text_document_position_params: lsp_types::TextDocumentPositionParams {
|
||||
text_document: lsp_types::TextDocumentIdentifier::new(lib_file_uri),
|
||||
position: lsp_types::Position::new(1, 21),
|
||||
},
|
||||
work_done_progress_params: Default::default(),
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
hover.contents,
|
||||
lsp_types::HoverContents::Markup(lsp_types::MarkupContent {
|
||||
kind: lsp_types::MarkupKind::Markdown,
|
||||
value: "&str".to_string()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_fake(cx: TestAppContext) {
|
||||
SimpleLogger::init(log::LevelFilter::Info, Default::default()).unwrap();
|
||||
|
||||
let (server, mut fake) = LanguageServer::fake(cx.background()).await;
|
||||
|
||||
let (message_tx, message_rx) = channel::unbounded();
|
||||
let (diagnostics_tx, diagnostics_rx) = channel::unbounded();
|
||||
server
|
||||
.on_notification::<notification::ShowMessage, _>(move |params| {
|
||||
message_tx.try_send(params).unwrap()
|
||||
})
|
||||
.detach();
|
||||
server
|
||||
.on_notification::<notification::PublishDiagnostics, _>(move |params| {
|
||||
diagnostics_tx.try_send(params).unwrap()
|
||||
})
|
||||
.detach();
|
||||
|
||||
server
|
||||
.notify::<notification::DidOpenTextDocument>(DidOpenTextDocumentParams {
|
||||
text_document: TextDocumentItem::new(
|
||||
Url::from_str("file://a/b").unwrap(),
|
||||
"rust".to_string(),
|
||||
0,
|
||||
"".to_string(),
|
||||
),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
fake.receive_notification::<notification::DidOpenTextDocument>()
|
||||
.await
|
||||
.text_document
|
||||
.uri
|
||||
.as_str(),
|
||||
"file://a/b"
|
||||
);
|
||||
|
||||
fake.notify::<notification::ShowMessage>(ShowMessageParams {
|
||||
typ: MessageType::ERROR,
|
||||
message: "ok".to_string(),
|
||||
})
|
||||
.await;
|
||||
fake.notify::<notification::PublishDiagnostics>(PublishDiagnosticsParams {
|
||||
uri: Url::from_str("file://b/c").unwrap(),
|
||||
version: Some(5),
|
||||
diagnostics: vec![],
|
||||
})
|
||||
.await;
|
||||
assert_eq!(message_rx.recv().await.unwrap().message, "ok");
|
||||
assert_eq!(
|
||||
diagnostics_rx.recv().await.unwrap().uri.as_str(),
|
||||
"file://b/c"
|
||||
);
|
||||
|
||||
drop(server);
|
||||
let (shutdown_request, _) = fake.receive_request::<lsp_types::request::Shutdown>().await;
|
||||
fake.respond(shutdown_request, ()).await;
|
||||
fake.receive_notification::<lsp_types::notification::Exit>()
|
||||
.await;
|
||||
}
|
||||
|
||||
impl LanguageServer {
|
||||
async fn next_idle_notification(self: &Arc<Self>) {
|
||||
let (tx, rx) = channel::unbounded();
|
||||
let _subscription =
|
||||
self.on_notification::<ServerStatusNotification, _>(move |params| {
|
||||
if params.quiescent {
|
||||
tx.try_send(()).unwrap();
|
||||
}
|
||||
});
|
||||
let _ = rx.recv().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ServerStatusNotification {}
|
||||
|
||||
impl lsp_types::notification::Notification for ServerStatusNotification {
|
||||
type Params = ServerStatusParams;
|
||||
const METHOD: &'static str = "experimental/serverStatus";
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, PartialEq, Eq, Clone)]
|
||||
pub struct ServerStatusParams {
|
||||
pub quiescent: bool,
|
||||
}
|
||||
}
|
||||
18
crates/outline/Cargo.toml
Normal file
18
crates/outline/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "outline"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/outline.rs"
|
||||
|
||||
[dependencies]
|
||||
editor = { path = "../editor" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
text = { path = "../text" }
|
||||
workspace = { path = "../workspace" }
|
||||
ordered-float = "2.1.1"
|
||||
postage = { version = "0.4", features = ["futures-traits"] }
|
||||
smol = "1.2"
|
||||
540
crates/outline/src/outline.rs
Normal file
540
crates/outline/src/outline.rs
Normal file
@@ -0,0 +1,540 @@
|
||||
use editor::{
|
||||
display_map::ToDisplayPoint, Anchor, AnchorRangeExt, Autoscroll, Editor, EditorSettings,
|
||||
ToPoint,
|
||||
};
|
||||
use fuzzy::StringMatch;
|
||||
use gpui::{
|
||||
action,
|
||||
elements::*,
|
||||
fonts::{self, HighlightStyle},
|
||||
geometry::vector::Vector2F,
|
||||
keymap::{self, Binding},
|
||||
AppContext, Axis, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
|
||||
WeakViewHandle,
|
||||
};
|
||||
use language::{Outline, Selection};
|
||||
use ordered_float::OrderedFloat;
|
||||
use postage::watch;
|
||||
use std::{
|
||||
cmp::{self, Reverse},
|
||||
ops::Range,
|
||||
sync::Arc,
|
||||
};
|
||||
use workspace::{
|
||||
menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev},
|
||||
Settings, Workspace,
|
||||
};
|
||||
|
||||
action!(Toggle);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_bindings([
|
||||
Binding::new("cmd-shift-O", Toggle, Some("Editor")),
|
||||
Binding::new("escape", Toggle, Some("OutlineView")),
|
||||
]);
|
||||
cx.add_action(OutlineView::toggle);
|
||||
cx.add_action(OutlineView::confirm);
|
||||
cx.add_action(OutlineView::select_prev);
|
||||
cx.add_action(OutlineView::select_next);
|
||||
cx.add_action(OutlineView::select_first);
|
||||
cx.add_action(OutlineView::select_last);
|
||||
}
|
||||
|
||||
struct OutlineView {
|
||||
handle: WeakViewHandle<Self>,
|
||||
active_editor: ViewHandle<Editor>,
|
||||
outline: Outline<Anchor>,
|
||||
selected_match_index: usize,
|
||||
restore_state: Option<RestoreState>,
|
||||
symbol_selection_id: Option<usize>,
|
||||
matches: Vec<StringMatch>,
|
||||
query_editor: ViewHandle<Editor>,
|
||||
list_state: UniformListState,
|
||||
settings: watch::Receiver<Settings>,
|
||||
}
|
||||
|
||||
struct RestoreState {
|
||||
scroll_position: Vector2F,
|
||||
selections: Vec<Selection<usize>>,
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
Dismissed,
|
||||
}
|
||||
|
||||
impl Entity for OutlineView {
|
||||
type Event = Event;
|
||||
|
||||
fn release(&mut self, cx: &mut MutableAppContext) {
|
||||
self.restore_active_editor(cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl View for OutlineView {
|
||||
fn ui_name() -> &'static str {
|
||||
"OutlineView"
|
||||
}
|
||||
|
||||
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
|
||||
let mut cx = Self::default_keymap_context();
|
||||
cx.set.insert("menu".into());
|
||||
cx
|
||||
}
|
||||
|
||||
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
||||
let settings = self.settings.borrow();
|
||||
|
||||
Flex::new(Axis::Vertical)
|
||||
.with_child(
|
||||
Container::new(ChildView::new(self.query_editor.id()).boxed())
|
||||
.with_style(settings.theme.selector.input_editor.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(Flexible::new(1.0, false, self.render_matches()).boxed())
|
||||
.contained()
|
||||
.with_style(settings.theme.selector.container)
|
||||
.constrained()
|
||||
.with_max_width(800.0)
|
||||
.with_max_height(1200.0)
|
||||
.aligned()
|
||||
.top()
|
||||
.named("outline view")
|
||||
}
|
||||
|
||||
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.focus(&self.query_editor);
|
||||
}
|
||||
}
|
||||
|
||||
impl OutlineView {
|
||||
fn new(
|
||||
outline: Outline<Anchor>,
|
||||
editor: ViewHandle<Editor>,
|
||||
settings: watch::Receiver<Settings>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let query_editor = cx.add_view(|cx| {
|
||||
Editor::single_line(
|
||||
{
|
||||
let settings = settings.clone();
|
||||
Arc::new(move |_| {
|
||||
let settings = settings.borrow();
|
||||
EditorSettings {
|
||||
style: settings.theme.selector.input_editor.as_editor(),
|
||||
tab_size: settings.tab_size,
|
||||
soft_wrap: editor::SoftWrap::None,
|
||||
}
|
||||
})
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.subscribe(&query_editor, Self::on_query_editor_event)
|
||||
.detach();
|
||||
|
||||
let restore_state = editor.update(cx, |editor, cx| {
|
||||
Some(RestoreState {
|
||||
scroll_position: editor.scroll_position(cx),
|
||||
selections: editor.local_selections::<usize>(cx),
|
||||
})
|
||||
});
|
||||
|
||||
let mut this = Self {
|
||||
handle: cx.weak_handle(),
|
||||
active_editor: editor,
|
||||
matches: Default::default(),
|
||||
selected_match_index: 0,
|
||||
restore_state,
|
||||
symbol_selection_id: None,
|
||||
outline,
|
||||
query_editor,
|
||||
list_state: Default::default(),
|
||||
settings,
|
||||
};
|
||||
this.update_matches(cx);
|
||||
this
|
||||
}
|
||||
|
||||
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||
if let Some(editor) = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.to_any().downcast::<Editor>())
|
||||
{
|
||||
let settings = workspace.settings();
|
||||
let buffer = editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.read(cx)
|
||||
.outline(Some(settings.borrow().theme.editor.syntax.as_ref()));
|
||||
if let Some(outline) = buffer {
|
||||
workspace.toggle_modal(cx, |cx, _| {
|
||||
let view = cx.add_view(|cx| OutlineView::new(outline, editor, settings, cx));
|
||||
cx.subscribe(&view, Self::on_event).detach();
|
||||
view
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
|
||||
if self.selected_match_index > 0 {
|
||||
self.select(self.selected_match_index - 1, true, false, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
|
||||
if self.selected_match_index + 1 < self.matches.len() {
|
||||
self.select(self.selected_match_index + 1, true, false, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
|
||||
self.select(0, true, false, cx);
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
|
||||
self.select(self.matches.len().saturating_sub(1), true, false, cx);
|
||||
}
|
||||
|
||||
fn select(&mut self, index: usize, navigate: bool, center: bool, cx: &mut ViewContext<Self>) {
|
||||
self.selected_match_index = index;
|
||||
self.list_state.scroll_to(if center {
|
||||
ScrollTarget::Center(index)
|
||||
} else {
|
||||
ScrollTarget::Show(index)
|
||||
});
|
||||
if navigate {
|
||||
let selected_match = &self.matches[self.selected_match_index];
|
||||
let outline_item = &self.outline.items[selected_match.candidate_id];
|
||||
self.symbol_selection_id = self.active_editor.update(cx, |active_editor, cx| {
|
||||
let snapshot = active_editor.snapshot(cx).display_snapshot;
|
||||
let buffer_snapshot = &snapshot.buffer_snapshot;
|
||||
let start = outline_item.range.start.to_point(&buffer_snapshot);
|
||||
let end = outline_item.range.end.to_point(&buffer_snapshot);
|
||||
let display_rows = start.to_display_point(&snapshot).row()
|
||||
..end.to_display_point(&snapshot).row() + 1;
|
||||
active_editor.select_ranges([start..start], Some(Autoscroll::Center), cx);
|
||||
active_editor.set_highlighted_rows(Some(display_rows));
|
||||
Some(active_editor.newest_selection::<usize>(&buffer_snapshot).id)
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||
self.restore_state.take();
|
||||
cx.emit(Event::Dismissed);
|
||||
}
|
||||
|
||||
fn restore_active_editor(&mut self, cx: &mut MutableAppContext) {
|
||||
let symbol_selection_id = self.symbol_selection_id.take();
|
||||
self.active_editor.update(cx, |editor, cx| {
|
||||
editor.set_highlighted_rows(None);
|
||||
if let Some((symbol_selection_id, restore_state)) =
|
||||
symbol_selection_id.zip(self.restore_state.as_ref())
|
||||
{
|
||||
let newest_selection =
|
||||
editor.newest_selection::<usize>(&editor.buffer().read(cx).read(cx));
|
||||
if symbol_selection_id == newest_selection.id {
|
||||
editor.set_scroll_position(restore_state.scroll_position, cx);
|
||||
editor.update_selections(restore_state.selections.clone(), None, cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
workspace: &mut Workspace,
|
||||
_: ViewHandle<Self>,
|
||||
event: &Event,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
match event {
|
||||
Event::Dismissed => workspace.dismiss_modal(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_query_editor_event(
|
||||
&mut self,
|
||||
_: ViewHandle<Editor>,
|
||||
event: &editor::Event,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
||||
editor::Event::Edited => self.update_matches(cx),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let selected_index;
|
||||
let navigate_to_selected_index;
|
||||
let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
|
||||
if query.is_empty() {
|
||||
self.restore_active_editor(cx);
|
||||
self.matches = self
|
||||
.outline
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, _)| StringMatch {
|
||||
candidate_id: index,
|
||||
score: Default::default(),
|
||||
positions: Default::default(),
|
||||
string: Default::default(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let editor = self.active_editor.read(cx);
|
||||
let buffer = editor.buffer().read(cx).read(cx);
|
||||
let cursor_offset = editor.newest_selection::<usize>(&buffer).head();
|
||||
selected_index = self
|
||||
.outline
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, item)| {
|
||||
let range = item.range.to_offset(&buffer);
|
||||
let distance_to_closest_endpoint = cmp::min(
|
||||
(range.start as isize - cursor_offset as isize).abs() as usize,
|
||||
(range.end as isize - cursor_offset as isize).abs() as usize,
|
||||
);
|
||||
let depth = if range.contains(&cursor_offset) {
|
||||
Some(item.depth)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(ix, depth, distance_to_closest_endpoint)
|
||||
})
|
||||
.max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance)))
|
||||
.unwrap()
|
||||
.0;
|
||||
navigate_to_selected_index = false;
|
||||
} else {
|
||||
self.matches = smol::block_on(self.outline.search(&query, cx.background().clone()));
|
||||
selected_index = self
|
||||
.matches
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by_key(|(_, m)| OrderedFloat(m.score))
|
||||
.map(|(ix, _)| ix)
|
||||
.unwrap_or(0);
|
||||
navigate_to_selected_index = !self.matches.is_empty();
|
||||
}
|
||||
self.select(selected_index, navigate_to_selected_index, true, cx);
|
||||
}
|
||||
|
||||
fn render_matches(&self) -> ElementBox {
|
||||
if self.matches.is_empty() {
|
||||
let settings = self.settings.borrow();
|
||||
return Container::new(
|
||||
Label::new(
|
||||
"No matches".into(),
|
||||
settings.theme.selector.empty.label.clone(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(settings.theme.selector.empty.container)
|
||||
.named("empty matches");
|
||||
}
|
||||
|
||||
let handle = self.handle.clone();
|
||||
let list = UniformList::new(
|
||||
self.list_state.clone(),
|
||||
self.matches.len(),
|
||||
move |mut range, items, cx| {
|
||||
let cx = cx.as_ref();
|
||||
let view = handle.upgrade(cx).unwrap();
|
||||
let view = view.read(cx);
|
||||
let start = range.start;
|
||||
range.end = cmp::min(range.end, view.matches.len());
|
||||
items.extend(
|
||||
view.matches[range]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(move |(ix, m)| view.render_match(m, start + ix)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Container::new(list.boxed())
|
||||
.with_margin_top(6.0)
|
||||
.named("matches")
|
||||
}
|
||||
|
||||
fn render_match(&self, string_match: &StringMatch, index: usize) -> ElementBox {
|
||||
let settings = self.settings.borrow();
|
||||
let style = if index == self.selected_match_index {
|
||||
&settings.theme.selector.active_item
|
||||
} else {
|
||||
&settings.theme.selector.item
|
||||
};
|
||||
let outline_item = &self.outline.items[string_match.candidate_id];
|
||||
|
||||
Text::new(outline_item.text.clone(), style.label.text.clone())
|
||||
.with_soft_wrap(false)
|
||||
.with_highlights(combine_syntax_and_fuzzy_match_highlights(
|
||||
&outline_item.text,
|
||||
style.label.text.clone().into(),
|
||||
&outline_item.highlight_ranges,
|
||||
&string_match.positions,
|
||||
))
|
||||
.contained()
|
||||
.with_padding_left(20. * outline_item.depth as f32)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
fn combine_syntax_and_fuzzy_match_highlights(
|
||||
text: &str,
|
||||
default_style: HighlightStyle,
|
||||
syntax_ranges: &[(Range<usize>, HighlightStyle)],
|
||||
match_indices: &[usize],
|
||||
) -> Vec<(Range<usize>, HighlightStyle)> {
|
||||
let mut result = Vec::new();
|
||||
let mut match_indices = match_indices.iter().copied().peekable();
|
||||
|
||||
for (range, mut syntax_highlight) in syntax_ranges
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain([(usize::MAX..0, Default::default())])
|
||||
{
|
||||
syntax_highlight.font_properties.weight(Default::default());
|
||||
|
||||
// Add highlights for any fuzzy match characters before the next
|
||||
// syntax highlight range.
|
||||
while let Some(&match_index) = match_indices.peek() {
|
||||
if match_index >= range.start {
|
||||
break;
|
||||
}
|
||||
match_indices.next();
|
||||
let end_index = char_ix_after(match_index, text);
|
||||
let mut match_style = default_style;
|
||||
match_style.font_properties.weight(fonts::Weight::BOLD);
|
||||
result.push((match_index..end_index, match_style));
|
||||
}
|
||||
|
||||
if range.start == usize::MAX {
|
||||
break;
|
||||
}
|
||||
|
||||
// Add highlights for any fuzzy match characters within the
|
||||
// syntax highlight range.
|
||||
let mut offset = range.start;
|
||||
while let Some(&match_index) = match_indices.peek() {
|
||||
if match_index >= range.end {
|
||||
break;
|
||||
}
|
||||
|
||||
match_indices.next();
|
||||
if match_index > offset {
|
||||
result.push((offset..match_index, syntax_highlight));
|
||||
}
|
||||
|
||||
let mut end_index = char_ix_after(match_index, text);
|
||||
while let Some(&next_match_index) = match_indices.peek() {
|
||||
if next_match_index == end_index && next_match_index < range.end {
|
||||
end_index = char_ix_after(next_match_index, text);
|
||||
match_indices.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut match_style = syntax_highlight;
|
||||
match_style.font_properties.weight(fonts::Weight::BOLD);
|
||||
result.push((match_index..end_index, match_style));
|
||||
offset = end_index;
|
||||
}
|
||||
|
||||
if offset < range.end {
|
||||
result.push((offset..range.end, syntax_highlight));
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn char_ix_after(ix: usize, text: &str) -> usize {
|
||||
ix + text[ix..].chars().next().unwrap().len_utf8()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::{color::Color, fonts::HighlightStyle};
|
||||
|
||||
#[test]
|
||||
fn test_combine_syntax_and_fuzzy_match_highlights() {
|
||||
let string = "abcdefghijklmnop";
|
||||
let default = HighlightStyle::default();
|
||||
let syntax_ranges = [
|
||||
(
|
||||
0..3,
|
||||
HighlightStyle {
|
||||
color: Color::red(),
|
||||
..default
|
||||
},
|
||||
),
|
||||
(
|
||||
4..8,
|
||||
HighlightStyle {
|
||||
color: Color::green(),
|
||||
..default
|
||||
},
|
||||
),
|
||||
];
|
||||
let match_indices = [4, 6, 7, 8];
|
||||
assert_eq!(
|
||||
combine_syntax_and_fuzzy_match_highlights(
|
||||
&string,
|
||||
default,
|
||||
&syntax_ranges,
|
||||
&match_indices,
|
||||
),
|
||||
&[
|
||||
(
|
||||
0..3,
|
||||
HighlightStyle {
|
||||
color: Color::red(),
|
||||
..default
|
||||
},
|
||||
),
|
||||
(
|
||||
4..5,
|
||||
HighlightStyle {
|
||||
color: Color::green(),
|
||||
font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD),
|
||||
..default
|
||||
},
|
||||
),
|
||||
(
|
||||
5..6,
|
||||
HighlightStyle {
|
||||
color: Color::green(),
|
||||
..default
|
||||
},
|
||||
),
|
||||
(
|
||||
6..8,
|
||||
HighlightStyle {
|
||||
color: Color::green(),
|
||||
font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD),
|
||||
..default
|
||||
},
|
||||
),
|
||||
(
|
||||
8..9,
|
||||
HighlightStyle {
|
||||
font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD),
|
||||
..default
|
||||
},
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user