Compare commits
553 Commits
v0.115.3-p
...
list-scrol
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fb1e6846d | ||
|
|
188d727d31 | ||
|
|
6b58eb202d | ||
|
|
4adafb1148 | ||
|
|
f3b764b7d2 | ||
|
|
e4bc346723 | ||
|
|
049314e84e | ||
|
|
d03e29d55d | ||
|
|
8feb11ccee | ||
|
|
008c56a09b | ||
|
|
cf42d95af6 | ||
|
|
60e4311294 | ||
|
|
cbbcc3502a | ||
|
|
44890d6966 | ||
|
|
2517b1017b | ||
|
|
1fecd3c327 | ||
|
|
be636348c4 | ||
|
|
13dad89a85 | ||
|
|
324aeaa0de | ||
|
|
ad05f22256 | ||
|
|
2ef3954700 | ||
|
|
650ca1f39b | ||
|
|
fce0c1f4d0 | ||
|
|
e9730e0b5c | ||
|
|
cd93ac1d2a | ||
|
|
c739906413 | ||
|
|
c7d8169cab | ||
|
|
213ed2028c | ||
|
|
6955579f19 | ||
|
|
113c7287df | ||
|
|
4353bdb9d5 | ||
|
|
40b219f861 | ||
|
|
f67d4cea31 | ||
|
|
0dc02b8354 | ||
|
|
133b1a8ad2 | ||
|
|
f0cc54a0b5 | ||
|
|
27501d2929 | ||
|
|
77c8108f9b | ||
|
|
1c850f495c | ||
|
|
1d35a815a6 | ||
|
|
79e6dedb7a | ||
|
|
c653a02be3 | ||
|
|
f4c93abad5 | ||
|
|
62155f3a88 | ||
|
|
fdde76c1a5 | ||
|
|
ab5b76e943 | ||
|
|
be6c909587 | ||
|
|
a5a0ad8b5c | ||
|
|
726d761646 | ||
|
|
9ac9532d3d | ||
|
|
63cc9e5068 | ||
|
|
0cab3de0ae | ||
|
|
21476ec51d | ||
|
|
c51cedef14 | ||
|
|
7964b35607 | ||
|
|
8987b2205c | ||
|
|
970c7b8987 | ||
|
|
a283cbaf8f | ||
|
|
0b2aa052ea | ||
|
|
52e4c577d2 | ||
|
|
4596e7a68a | ||
|
|
b66e1d2d58 | ||
|
|
6f064cfc36 | ||
|
|
260a753005 | ||
|
|
6634a5e9f6 | ||
|
|
1b6721170a | ||
|
|
63ce7cd407 | ||
|
|
40a95221ea | ||
|
|
a8a5b9524d | ||
|
|
53d77b192a | ||
|
|
53ff5ff724 | ||
|
|
d0a673ec62 | ||
|
|
7a9f764aa0 | ||
|
|
5b96ffbbd1 | ||
|
|
efb4ff816a | ||
|
|
1f51f74670 | ||
|
|
a88372dc99 | ||
|
|
f272881a6b | ||
|
|
89f62a1a69 | ||
|
|
f9d569f1b8 | ||
|
|
808a0626c0 | ||
|
|
c7e19c0bcd | ||
|
|
0b78a401de | ||
|
|
58d8c51255 | ||
|
|
5e3d0a6d03 | ||
|
|
aa34637705 | ||
|
|
901bd6ac87 | ||
|
|
445f0da4d8 | ||
|
|
d1b47b4059 | ||
|
|
f3cbce7e86 | ||
|
|
04d019ef66 | ||
|
|
d23bba5b18 | ||
|
|
282e4398a0 | ||
|
|
fa223e0c6f | ||
|
|
6a164db700 | ||
|
|
8ed04fe798 | ||
|
|
853daf953b | ||
|
|
8b9b19195d | ||
|
|
5c3b8a1af1 | ||
|
|
0ee4ad6ba0 | ||
|
|
ef4bc5e20b | ||
|
|
439df24526 | ||
|
|
1fcd006b2a | ||
|
|
7c19650a40 | ||
|
|
393be3cedf | ||
|
|
c91207b450 | ||
|
|
7ad6d489a4 | ||
|
|
794b795800 | ||
|
|
2461902086 | ||
|
|
08c4e1abdc | ||
|
|
5e5eb25aab | ||
|
|
49d30b4f67 | ||
|
|
b9a904e552 | ||
|
|
6c653b9c05 | ||
|
|
b692b4c9c2 | ||
|
|
f829120f51 | ||
|
|
2a82dff2fe | ||
|
|
646b74e0a7 | ||
|
|
75324abbb5 | ||
|
|
ea708c50f0 | ||
|
|
8839bfa1df | ||
|
|
2c2c633104 | ||
|
|
18393d1fac | ||
|
|
7842fdeb32 | ||
|
|
45f7ab876f | ||
|
|
cbd1ce64f7 | ||
|
|
9a916d71b0 | ||
|
|
a40515dbb3 | ||
|
|
edb2f60d9d | ||
|
|
02bd4fb1f1 | ||
|
|
e96197d63b | ||
|
|
d02ff42608 | ||
|
|
f798b193d0 | ||
|
|
197f355729 | ||
|
|
7a1eb54106 | ||
|
|
42a3da4ba3 | ||
|
|
84aaeb4360 | ||
|
|
da3ba35d1c | ||
|
|
2a68a6171a | ||
|
|
fffe4f51fb | ||
|
|
27e1787ce3 | ||
|
|
9e6103f863 | ||
|
|
d2362d7f12 | ||
|
|
79e0d8ce3b | ||
|
|
ee45db8a78 | ||
|
|
8fc15c05c5 | ||
|
|
256f0308ae | ||
|
|
d62b151bc0 | ||
|
|
b2a0c32148 | ||
|
|
668f5accab | ||
|
|
66b93212fe | ||
|
|
57a9f024e7 | ||
|
|
22cd62213b | ||
|
|
bbe9986f38 | ||
|
|
1abc9800af | ||
|
|
034c11cc96 | ||
|
|
0885fa67fc | ||
|
|
c8cb1140b9 | ||
|
|
c092cfbfb3 | ||
|
|
a4b271e063 | ||
|
|
110612bf26 | ||
|
|
fd31e429f5 | ||
|
|
12e7f61f62 | ||
|
|
1a747c8457 | ||
|
|
2d18b949ad | ||
|
|
46c998ca8d | ||
|
|
a8d96790cf | ||
|
|
b72c54fc31 | ||
|
|
89c8a7c242 | ||
|
|
41fc30f62e | ||
|
|
f4c7b13397 | ||
|
|
f7c995c4a0 | ||
|
|
cd818f580c | ||
|
|
dba94c5122 | ||
|
|
dbb501d7eb | ||
|
|
d711087529 | ||
|
|
ebddb612c8 | ||
|
|
62c41e226b | ||
|
|
b94c335605 | ||
|
|
6bbb1642b8 | ||
|
|
e9dcca7712 | ||
|
|
1ec81e02da | ||
|
|
bcdefb8ec8 | ||
|
|
5644815c4c | ||
|
|
9e1d797445 | ||
|
|
ec798e6574 | ||
|
|
d7473ad6e7 | ||
|
|
5ebf1d9e4f | ||
|
|
f76e1cfd91 | ||
|
|
489c25ac6a | ||
|
|
ac07e230fa | ||
|
|
147c99f1a7 | ||
|
|
02a6a2e1a3 | ||
|
|
4863c9ac25 | ||
|
|
2c350a71fe | ||
|
|
fdd64832e7 | ||
|
|
eff925cb6a | ||
|
|
7e2ff63270 | ||
|
|
cc9eff89f5 | ||
|
|
e4884f1d76 | ||
|
|
02e7a06cbb | ||
|
|
5ae2060783 | ||
|
|
14def2a1a3 | ||
|
|
7c9e2f6b7d | ||
|
|
f6a7a6c4d4 | ||
|
|
2aee3e3192 | ||
|
|
5e558e2a58 | ||
|
|
80f315106d | ||
|
|
80c8fd1f4c | ||
|
|
886ec79d58 | ||
|
|
c8ddc95caa | ||
|
|
8f1c74b8bc | ||
|
|
0ced310f93 | ||
|
|
74e3b12a26 | ||
|
|
cf971f706c | ||
|
|
b5c790ed29 | ||
|
|
6549a9a091 | ||
|
|
3b8c566f31 | ||
|
|
f833cd7c16 | ||
|
|
e9002ab10a | ||
|
|
a1c8f01ff3 | ||
|
|
54ea7257ff | ||
|
|
d09dfe01f5 | ||
|
|
3c1bf0260c | ||
|
|
cb0880cf3c | ||
|
|
a58f393458 | ||
|
|
0ef97edd6e | ||
|
|
1f538c5fdd | ||
|
|
5f172a52a4 | ||
|
|
1b0ec82caa | ||
|
|
7998e8281c | ||
|
|
3f9fe58c48 | ||
|
|
9eb98122ec | ||
|
|
1de02cf6e5 | ||
|
|
b29cea287b | ||
|
|
735f2029e9 | ||
|
|
5660c8f655 | ||
|
|
ed31d36ac1 | ||
|
|
d8757845a9 | ||
|
|
2ee0ecb677 | ||
|
|
d2fe9f8f9b | ||
|
|
c9dd6b879c | ||
|
|
65bb05af4c | ||
|
|
82534b6612 | ||
|
|
79567d1c87 | ||
|
|
5b6fd3ac1d | ||
|
|
a27db35ff7 | ||
|
|
02e507b973 | ||
|
|
f2faa70f73 | ||
|
|
863222edc5 | ||
|
|
fdc2d7ce42 | ||
|
|
dec4cfa0b4 | ||
|
|
78e1c0f9c3 | ||
|
|
631e264e3c | ||
|
|
ab140ee4c2 | ||
|
|
fc9b662042 | ||
|
|
8141f4fd86 | ||
|
|
e0ca7f844a | ||
|
|
4c4b235b13 | ||
|
|
dc7e4a4b17 | ||
|
|
27703a3279 | ||
|
|
7b4b068230 | ||
|
|
ceb5d0c69e | ||
|
|
38d41acf9b | ||
|
|
d86da04584 | ||
|
|
5e79807f6f | ||
|
|
a80b145d0d | ||
|
|
ce8bdb354b | ||
|
|
dccdcd3221 | ||
|
|
8d57d6ca6f | ||
|
|
e534c5fdcd | ||
|
|
eed5a698cf | ||
|
|
412c6157b1 | ||
|
|
dffe0ea058 | ||
|
|
2299730538 | ||
|
|
ede86d9187 | ||
|
|
c9b50c8bab | ||
|
|
7c5df51d2e | ||
|
|
30725d6a17 | ||
|
|
09db455db2 | ||
|
|
c2d3e74ffb | ||
|
|
62bfa3b452 | ||
|
|
16b5d4b35c | ||
|
|
20352c51c1 | ||
|
|
f9efaebddf | ||
|
|
af72772a72 | ||
|
|
b73ccc8180 | ||
|
|
d433da1e70 | ||
|
|
001ce47a0c | ||
|
|
e5616bce98 | ||
|
|
92fbdb429c | ||
|
|
b43dc480a9 | ||
|
|
2c2c14a360 | ||
|
|
dffe323e73 | ||
|
|
f0ca7141b8 | ||
|
|
1f6c69c7dc | ||
|
|
590238bcca | ||
|
|
caa5fccbc4 | ||
|
|
1dd6625dd4 | ||
|
|
ee695bbb34 | ||
|
|
9695ea1017 | ||
|
|
e0ec5032e9 | ||
|
|
948c065f86 | ||
|
|
ae6ddceb67 | ||
|
|
eff3a72fb5 | ||
|
|
591dc9d82a | ||
|
|
71a1125e88 | ||
|
|
959b2961ff | ||
|
|
45992b0d63 | ||
|
|
9162f299a7 | ||
|
|
2c5603032d | ||
|
|
63667ecf6f | ||
|
|
13bb16577c | ||
|
|
1c9b984738 | ||
|
|
7db0a9e105 | ||
|
|
fc16e4509a | ||
|
|
c82fea375d | ||
|
|
5bdaf0e074 | ||
|
|
c10d8a8110 | ||
|
|
79773178c8 | ||
|
|
2c2e5144c9 | ||
|
|
059959a4ab | ||
|
|
3627ff87f0 | ||
|
|
9a2fd18425 | ||
|
|
4cb4033a36 | ||
|
|
b212aab00d | ||
|
|
584a3a7627 | ||
|
|
63213b5a57 | ||
|
|
4a5f703c32 | ||
|
|
a3e4559b0e | ||
|
|
26c797c358 | ||
|
|
fb352af35c | ||
|
|
4f4029524f | ||
|
|
e928ed44a0 | ||
|
|
23626aa902 | ||
|
|
5ab6874ae9 | ||
|
|
4ee4e4e8d8 | ||
|
|
7a0aa1e5df | ||
|
|
b6ed3b258c | ||
|
|
50fe683305 | ||
|
|
b5924d6b11 | ||
|
|
80ae640060 | ||
|
|
0af0c5549c | ||
|
|
68d309e79c | ||
|
|
fd2f1c2594 | ||
|
|
2108ddf621 | ||
|
|
ee23fa43dc | ||
|
|
118c9b5fe8 | ||
|
|
b9a917f42a | ||
|
|
7dc22fef24 | ||
|
|
24b08921fe | ||
|
|
dfe4fc4d0a | ||
|
|
11c16258eb | ||
|
|
ff734d494f | ||
|
|
3b1a0652ae | ||
|
|
b3e741b397 | ||
|
|
1a5f6f604b | ||
|
|
0f7fc8c1a0 | ||
|
|
9ffe78d264 | ||
|
|
45230dcaf3 | ||
|
|
aa3c9b8568 | ||
|
|
859f2d2862 | ||
|
|
63b65b2b2e | ||
|
|
1da18ebe9a | ||
|
|
6426997abb | ||
|
|
6a5b5f022d | ||
|
|
2de6758443 | ||
|
|
fe83919746 | ||
|
|
6730ff3624 | ||
|
|
b0f9144167 | ||
|
|
0edd89a92f | ||
|
|
6f7995c150 | ||
|
|
379ba620b2 | ||
|
|
2bf48872b6 | ||
|
|
03ebf0a5a9 | ||
|
|
26ae31b503 | ||
|
|
6b0457c52f | ||
|
|
d81fb3680e | ||
|
|
d788bf3f1e | ||
|
|
c07455efa7 | ||
|
|
d85db44552 | ||
|
|
89aa6a3726 | ||
|
|
f5679f98d6 | ||
|
|
be509a5ce0 | ||
|
|
5dd9db3607 | ||
|
|
aace0d816d | ||
|
|
4479628342 | ||
|
|
0a1765b01f | ||
|
|
c3e7732eab | ||
|
|
a985b7aab4 | ||
|
|
20ae58eddd | ||
|
|
9408eecb6e | ||
|
|
2ab84b81da | ||
|
|
e821e1fc35 | ||
|
|
237efc841e | ||
|
|
4b23c5c658 | ||
|
|
1c52b936bc | ||
|
|
4f507756d6 | ||
|
|
12b58f5b60 | ||
|
|
f09d9ef723 | ||
|
|
be5eb3e3b5 | ||
|
|
53f3f960d2 | ||
|
|
6f9e08ceea | ||
|
|
85d72f63c1 | ||
|
|
4c4ec221af | ||
|
|
910aeb4f56 | ||
|
|
0d33f01fa4 | ||
|
|
e0ccaa60ff | ||
|
|
a40a5fb212 | ||
|
|
aed11ee8cb | ||
|
|
13f4cc563c | ||
|
|
ab75dbe7af | ||
|
|
88188b0c30 | ||
|
|
ce16e5b54a | ||
|
|
134f3eb79f | ||
|
|
5fefc121bf | ||
|
|
9fedb4c7ee | ||
|
|
f0c205be5d | ||
|
|
f0bc4a04bd | ||
|
|
1abc876c15 | ||
|
|
8e4f2fb25a | ||
|
|
faa896343b | ||
|
|
a8a5785ec5 | ||
|
|
c9be637b6b | ||
|
|
0b7e324d86 | ||
|
|
8c59a2d2c0 | ||
|
|
21d3ae603d | ||
|
|
1b07fd6a70 | ||
|
|
a75f1a4a95 | ||
|
|
eef6c3729e | ||
|
|
9849a0a6d8 | ||
|
|
6d62e6c562 | ||
|
|
0441b4805a | ||
|
|
e1c8369b3d | ||
|
|
4f74d65d0c | ||
|
|
ebbbeca9a6 | ||
|
|
cb11c1282c | ||
|
|
a003a91212 | ||
|
|
bac39e85a8 | ||
|
|
e228422f14 | ||
|
|
8b7be8f614 | ||
|
|
e5a5b1e84c | ||
|
|
5098fafa02 | ||
|
|
f987a640fd | ||
|
|
c63ca09eed | ||
|
|
164084c61c | ||
|
|
180ba42456 | ||
|
|
bd6fa66a7c | ||
|
|
16dc978bb4 | ||
|
|
f922ad9f7f | ||
|
|
39887be7d0 | ||
|
|
d516ae0d8a | ||
|
|
3ffcb97da2 | ||
|
|
e78538e162 | ||
|
|
865baaa1a1 | ||
|
|
5fdfdb046c | ||
|
|
5eb89781e3 | ||
|
|
b34b197d89 | ||
|
|
679851e349 | ||
|
|
8d4652a4db | ||
|
|
21dfe58ad9 | ||
|
|
75afb65b26 | ||
|
|
0a8a84f656 | ||
|
|
0e1597d385 | ||
|
|
428c517693 | ||
|
|
81098312ef | ||
|
|
5243401afa | ||
|
|
591299b4c7 | ||
|
|
d9b8c07b1e | ||
|
|
e20309f560 | ||
|
|
daf6201deb | ||
|
|
b64fe04803 | ||
|
|
82b3efa16c | ||
|
|
cdae8ebd06 | ||
|
|
1b39916438 | ||
|
|
091d93533d | ||
|
|
69bfd47cf9 | ||
|
|
7b76db4b50 | ||
|
|
74a5ae5633 | ||
|
|
fac029b808 | ||
|
|
f052329a6b | ||
|
|
d2c1897385 | ||
|
|
14993e0876 | ||
|
|
624ea58f2e | ||
|
|
cc0bc444b1 | ||
|
|
f9cd45269a | ||
|
|
02174084ca | ||
|
|
90b8acfb34 | ||
|
|
4fbbb28da2 | ||
|
|
54cfcef0aa | ||
|
|
a37f86f294 | ||
|
|
92d2048aa4 | ||
|
|
8638fc4149 | ||
|
|
012543052b | ||
|
|
1a1451a943 | ||
|
|
b4a5701e12 | ||
|
|
e49325080c | ||
|
|
c95a7c7387 | ||
|
|
bda1dc7721 | ||
|
|
9d53287341 | ||
|
|
481e42ade9 | ||
|
|
27a6761afc | ||
|
|
4c27f4453c | ||
|
|
04bbd107c1 | ||
|
|
b357ae4dc3 | ||
|
|
cd60d466b1 | ||
|
|
0eef4d03ed | ||
|
|
0b8f9067ff | ||
|
|
4f040208c6 | ||
|
|
9451bd605d | ||
|
|
3bdd51cb2a | ||
|
|
4ebae4d8bd | ||
|
|
df5de47a78 | ||
|
|
2bd428102b | ||
|
|
fc2251e7ed | ||
|
|
fb377aed73 | ||
|
|
d8ed7c0721 | ||
|
|
6ab3825ab1 | ||
|
|
87cf0cf5ac | ||
|
|
0d31722589 | ||
|
|
3c05d57e6a | ||
|
|
509504d4d2 | ||
|
|
139708d486 | ||
|
|
7e5aa5ce77 | ||
|
|
e77846d2dc | ||
|
|
e377bd805b | ||
|
|
6f5cc0af94 | ||
|
|
e99fa77bda | ||
|
|
41e7653906 | ||
|
|
c7b79c9aef | ||
|
|
390b0d8d56 | ||
|
|
ee260a5e24 | ||
|
|
960ef7116d | ||
|
|
3e2c517dd1 | ||
|
|
dbfc7d3555 | ||
|
|
c23a610d52 | ||
|
|
233aac5573 | ||
|
|
043cef89eb | ||
|
|
ac34229118 | ||
|
|
912c30c05b | ||
|
|
97e6fd295a | ||
|
|
a8bf0834e6 | ||
|
|
a4035f15af | ||
|
|
35481e2c79 | ||
|
|
5d59108b97 | ||
|
|
f735f5287e | ||
|
|
5fbc60d8da | ||
|
|
77acba9e4c | ||
|
|
6bf7ad71eb | ||
|
|
bcf449d3fe | ||
|
|
3ac545088a | ||
|
|
600b564bbf | ||
|
|
d551b41aae |
263
.github/workflows/ci.yml
vendored
263
.github/workflows/ci.yml
vendored
@@ -1,144 +1,149 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "v[0-9]+.[0-9]+.x"
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "v[0-9]+.[0-9]+.x"
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
|
||||
concurrency:
|
||||
# Allow only one workflow per any non-`main` branch.
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
name: Check formatting
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
rustfmt:
|
||||
name: Check formatting
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Set up default .cargo/config.toml
|
||||
run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml
|
||||
- name: Set up default .cargo/config.toml
|
||||
run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml
|
||||
|
||||
- name: Run rustfmt
|
||||
uses: ./.github/actions/check_formatting
|
||||
- name: Run rustfmt
|
||||
uses: ./.github/actions/check_formatting
|
||||
|
||||
tests:
|
||||
name: Run tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: rustfmt
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
tests:
|
||||
name: Run tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: rustfmt
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
- name: Build collab
|
||||
run: cargo build -p collab
|
||||
- name: Build collab
|
||||
run: cargo build -p collab
|
||||
|
||||
- name: Build other binaries
|
||||
run: cargo build --workspace --bins --all-features
|
||||
- name: Build other binaries
|
||||
run: cargo build --workspace --bins --all-features
|
||||
|
||||
bundle:
|
||||
name: Bundle app
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
needs: tests
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add wasm32-wasi
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- name: Determine version and release channel
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
version=$(script/get-crate-version zed)
|
||||
channel=$(cat crates/zed/RELEASE_CHANNEL)
|
||||
echo "Publishing version: ${version} on release channel ${channel}"
|
||||
echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
|
||||
|
||||
expected_tag_name=""
|
||||
case ${channel} in
|
||||
stable)
|
||||
expected_tag_name="v${version}";;
|
||||
preview)
|
||||
expected_tag_name="v${version}-pre";;
|
||||
nightly)
|
||||
expected_tag_name="v${version}-nightly";;
|
||||
*)
|
||||
echo "can't publish a release on channel ${channel}"
|
||||
exit 1;;
|
||||
esac
|
||||
if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
|
||||
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle
|
||||
|
||||
- name: Upload app bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v3
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
|
||||
path: target/release/Zed.dmg
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
name: Upload app bundle to release
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
files: target/release/Zed.dmg
|
||||
body: ""
|
||||
bundle:
|
||||
name: Bundle app
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
needs: tests
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add wasm32-wasi
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- name: Determine version and release channel
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
version=$(script/get-crate-version zed)
|
||||
channel=$(cat crates/zed/RELEASE_CHANNEL)
|
||||
echo "Publishing version: ${version} on release channel ${channel}"
|
||||
echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
|
||||
|
||||
expected_tag_name=""
|
||||
case ${channel} in
|
||||
stable)
|
||||
expected_tag_name="v${version}";;
|
||||
preview)
|
||||
expected_tag_name="v${version}-pre";;
|
||||
nightly)
|
||||
expected_tag_name="v${version}-nightly";;
|
||||
*)
|
||||
echo "can't publish a release on channel ${channel}"
|
||||
exit 1;;
|
||||
esac
|
||||
if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
|
||||
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle
|
||||
|
||||
- name: Upload app bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v3
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
|
||||
path: target/release/Zed.dmg
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
name: Upload app bundle to release
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
files: target/release/Zed.dmg
|
||||
body: ""
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
160
.github/workflows/release_nightly.yml
vendored
160
.github/workflows/release_nightly.yml
vendored
@@ -1,98 +1,98 @@
|
||||
name: Release Nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Fire every night at 1:00am
|
||||
- cron: "0 1 * * *"
|
||||
push:
|
||||
tags:
|
||||
- "nightly"
|
||||
schedule:
|
||||
# Fire every night at 1:00am
|
||||
- cron: "0 1 * * *"
|
||||
push:
|
||||
tags:
|
||||
- "nightly"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
name: Check formatting
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
rustfmt:
|
||||
name: Check formatting
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Run rustfmt
|
||||
uses: ./.github/actions/check_formatting
|
||||
- name: Run rustfmt
|
||||
uses: ./.github/actions/check_formatting
|
||||
|
||||
tests:
|
||||
name: Run tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: rustfmt
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
tests:
|
||||
name: Run tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: rustfmt
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
bundle:
|
||||
name: Bundle app
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
needs: tests
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add wasm32-wasi
|
||||
bundle:
|
||||
name: Bundle app
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
needs: tests
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add wasm32-wasi
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- name: Set release channel to nightly
|
||||
run: |
|
||||
set -eu
|
||||
version=$(git rev-parse --short HEAD)
|
||||
echo "Publishing version: ${version} on release channel nightly"
|
||||
echo "nightly" > crates/zed/RELEASE_CHANNEL
|
||||
- name: Set release channel to nightly
|
||||
run: |
|
||||
set -eu
|
||||
version=$(git rev-parse --short HEAD)
|
||||
echo "Publishing version: ${version} on release channel nightly"
|
||||
echo "nightly" > crates/zed/RELEASE_CHANNEL
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle -2
|
||||
- name: Create app bundle
|
||||
run: script/bundle -2
|
||||
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly
|
||||
|
||||
346
Cargo.lock
generated
346
Cargo.lock
generated
@@ -19,6 +19,25 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "activity_indicator2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"auto_update2",
|
||||
"editor2",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"project2",
|
||||
"settings2",
|
||||
"smallvec",
|
||||
"theme2",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.17.0"
|
||||
@@ -293,6 +312,15 @@ version = "1.0.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.7"
|
||||
@@ -357,6 +385,47 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "assistant2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ai2",
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"client2",
|
||||
"collections",
|
||||
"ctor",
|
||||
"editor2",
|
||||
"env_logger 0.9.3",
|
||||
"fs2",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"indoc",
|
||||
"isahc",
|
||||
"language2",
|
||||
"log",
|
||||
"menu2",
|
||||
"multi_buffer2",
|
||||
"ordered-float 2.10.0",
|
||||
"parking_lot 0.11.2",
|
||||
"project2",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"schemars",
|
||||
"search2",
|
||||
"semantic_index2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings2",
|
||||
"smol",
|
||||
"theme2",
|
||||
"tiktoken-rs",
|
||||
"ui2",
|
||||
"util",
|
||||
"uuid 1.4.1",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.4.1"
|
||||
@@ -394,9 +463,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.3.15"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a"
|
||||
checksum = "bc2d0cfb2a7388d34f590e76686704c494ed7aaceed62ee1ba35cbf363abc2a5"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"futures-core",
|
||||
@@ -1076,6 +1145,24 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "breadcrumbs2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"collections",
|
||||
"editor2",
|
||||
"gpui2",
|
||||
"itertools 0.10.5",
|
||||
"language2",
|
||||
"outline2",
|
||||
"project2",
|
||||
"search2",
|
||||
"settings2",
|
||||
"theme2",
|
||||
"ui2",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bromberg_sl2"
|
||||
version = "0.6.0"
|
||||
@@ -1186,7 +1273,6 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
"async-trait",
|
||||
"audio2",
|
||||
"client2",
|
||||
"collections",
|
||||
@@ -1206,9 +1292,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings2",
|
||||
"smallvec",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1304,10 +1388,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.84"
|
||||
version = "1.0.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856"
|
||||
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -1669,7 +1754,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.29.0"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1756,7 +1841,7 @@ dependencies = [
|
||||
"clap 3.2.25",
|
||||
"client2",
|
||||
"clock",
|
||||
"collab_ui",
|
||||
"collab_ui2",
|
||||
"collections",
|
||||
"ctor",
|
||||
"dashmap",
|
||||
@@ -1885,6 +1970,7 @@ dependencies = [
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"project2",
|
||||
"recent_projects2",
|
||||
"rich_text2",
|
||||
"rpc2",
|
||||
"schemars",
|
||||
@@ -2079,12 +2165,13 @@ dependencies = [
|
||||
"lsp2",
|
||||
"node_runtime",
|
||||
"parking_lot 0.11.2",
|
||||
"rpc",
|
||||
"rpc2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"settings2",
|
||||
"smol",
|
||||
"theme2",
|
||||
"ui2",
|
||||
"util",
|
||||
]
|
||||
|
||||
@@ -2107,6 +2194,25 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "copilot_button2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"copilot2",
|
||||
"editor2",
|
||||
"fs2",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"settings2",
|
||||
"smol",
|
||||
"theme2",
|
||||
"util",
|
||||
"workspace2",
|
||||
"zed_actions2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.3"
|
||||
@@ -3067,6 +3173,12 @@ dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fast-srgb8"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.9.0"
|
||||
@@ -3128,6 +3240,39 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "feedback2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client2",
|
||||
"db2",
|
||||
"editor2",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"human_bytes",
|
||||
"isahc",
|
||||
"language2",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"menu2",
|
||||
"postage",
|
||||
"project2",
|
||||
"regex",
|
||||
"search2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"settings2",
|
||||
"smallvec",
|
||||
"sysinfo",
|
||||
"theme2",
|
||||
"tree-sitter-markdown",
|
||||
"ui2",
|
||||
"urlencoding",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "file-per-thread-logger"
|
||||
version = "0.1.6"
|
||||
@@ -4526,6 +4671,15 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "journal"
|
||||
version = "0.1.0"
|
||||
@@ -4755,6 +4909,24 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "language_selector2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"editor2",
|
||||
"fuzzy2",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"picker2",
|
||||
"project2",
|
||||
"settings2",
|
||||
"theme2",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "language_tools"
|
||||
version = "0.1.0"
|
||||
@@ -4883,18 +5055,18 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "linkme"
|
||||
version = "0.3.17"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91ed2ee9464ff9707af8e9ad834cffa4802f072caad90639c583dd3c62e6e608"
|
||||
checksum = "b1e6b0bb9ca88d3c5ae88240beb9683821f903b824ee8381ef9ab4e8522fbfa9"
|
||||
dependencies = [
|
||||
"linkme-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linkme-impl"
|
||||
version = "0.3.17"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba125974b109d512fccbc6c0244e7580143e460895dfd6ea7f8bbb692fd94396"
|
||||
checksum = "b3b3f61e557a617ec6ba36c79431e1f3b5e100d67cfbdb61ed6ef384298af016"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -6091,12 +6263,54 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "outline2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"editor2",
|
||||
"fuzzy2",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"ordered-float 2.10.0",
|
||||
"picker2",
|
||||
"postage",
|
||||
"settings2",
|
||||
"smol",
|
||||
"text2",
|
||||
"theme2",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "palette"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2e2f34147767aa758aa649415b50a69eeb46a67f9dc7db8011eeb3d84b351dc"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"fast-srgb8",
|
||||
"palette_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "palette_derive"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7db010ec5ff3d4385e4f133916faacd9dad0f6a09394c92d825b3aed310fa0a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parity-tokio-ipc"
|
||||
version = "0.9.0"
|
||||
@@ -6981,6 +7195,18 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick_action_bar2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"assistant2",
|
||||
"editor2",
|
||||
"gpui2",
|
||||
"search2",
|
||||
"ui2",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.33"
|
||||
@@ -7163,6 +7389,28 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "recent_projects2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"db",
|
||||
"editor2",
|
||||
"futures 0.3.28",
|
||||
"fuzzy2",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"ordered-float 2.10.0",
|
||||
"picker2",
|
||||
"postage",
|
||||
"settings2",
|
||||
"smol",
|
||||
"text2",
|
||||
"theme2",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
@@ -8142,6 +8390,57 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semantic_index2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ai2",
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"client2",
|
||||
"collections",
|
||||
"ctor",
|
||||
"env_logger 0.9.3",
|
||||
"futures 0.3.28",
|
||||
"globset",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"ndarray",
|
||||
"node_runtime",
|
||||
"ordered-float 2.10.0",
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"project2",
|
||||
"rand 0.8.5",
|
||||
"rpc2",
|
||||
"rusqlite",
|
||||
"rust-embed",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings2",
|
||||
"sha1",
|
||||
"smol",
|
||||
"tempdir",
|
||||
"tiktoken-rs",
|
||||
"tree-sitter",
|
||||
"tree-sitter-cpp",
|
||||
"tree-sitter-elixir",
|
||||
"tree-sitter-json 0.20.0",
|
||||
"tree-sitter-lua",
|
||||
"tree-sitter-php",
|
||||
"tree-sitter-ruby",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-toml",
|
||||
"tree-sitter-typescript",
|
||||
"unindent",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.18"
|
||||
@@ -9337,6 +9636,7 @@ dependencies = [
|
||||
"terminal2",
|
||||
"theme2",
|
||||
"thiserror",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
@@ -9443,6 +9743,7 @@ dependencies = [
|
||||
"indexmap 1.9.3",
|
||||
"json_comments",
|
||||
"log",
|
||||
"palette",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"simplelog",
|
||||
@@ -10211,7 +10512,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tree-sitter-vue"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/zed-industries/tree-sitter-vue?rev=9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58#9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"
|
||||
source = "git+https://github.com/zed-industries/tree-sitter-vue?rev=6608d9d60c386f19d80af7d8132322fa11199c42#6608d9d60c386f19d80af7d8132322fa11199c42"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
@@ -11441,7 +11742,6 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion 1.0.5",
|
||||
"async-trait",
|
||||
"bincode",
|
||||
"call2",
|
||||
"client2",
|
||||
@@ -11554,7 +11854,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.115.3"
|
||||
version = "0.117.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"ai",
|
||||
@@ -11695,10 +11995,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed2"
|
||||
version = "0.109.0"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"activity_indicator2",
|
||||
"ai2",
|
||||
"anyhow",
|
||||
"assistant2",
|
||||
"async-compression",
|
||||
"async-recursion 0.3.2",
|
||||
"async-tar",
|
||||
@@ -11706,6 +12008,7 @@ dependencies = [
|
||||
"audio2",
|
||||
"auto_update2",
|
||||
"backtrace",
|
||||
"breadcrumbs2",
|
||||
"call2",
|
||||
"channel2",
|
||||
"chrono",
|
||||
@@ -11715,12 +12018,14 @@ dependencies = [
|
||||
"collections",
|
||||
"command_palette2",
|
||||
"copilot2",
|
||||
"copilot_button2",
|
||||
"ctor",
|
||||
"db2",
|
||||
"diagnostics2",
|
||||
"editor2",
|
||||
"env_logger 0.9.3",
|
||||
"feature_flags2",
|
||||
"feedback2",
|
||||
"file_finder2",
|
||||
"fs2",
|
||||
"fsevent",
|
||||
@@ -11734,6 +12039,7 @@ dependencies = [
|
||||
"isahc",
|
||||
"journal2",
|
||||
"language2",
|
||||
"language_selector2",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -11741,11 +12047,14 @@ dependencies = [
|
||||
"menu2",
|
||||
"node_runtime",
|
||||
"num_cpus",
|
||||
"outline2",
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"project2",
|
||||
"project_panel2",
|
||||
"quick_action_bar2",
|
||||
"rand 0.8.5",
|
||||
"recent_projects2",
|
||||
"regex",
|
||||
"rope2",
|
||||
"rpc2",
|
||||
@@ -11753,6 +12062,7 @@ dependencies = [
|
||||
"rust-embed",
|
||||
"schemars",
|
||||
"search2",
|
||||
"semantic_index2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
|
||||
13
Cargo.toml
13
Cargo.toml
@@ -1,13 +1,16 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/activity_indicator2",
|
||||
"crates/ai",
|
||||
"crates/assistant",
|
||||
"crates/assistant2",
|
||||
"crates/audio",
|
||||
"crates/audio2",
|
||||
"crates/auto_update",
|
||||
"crates/auto_update2",
|
||||
"crates/breadcrumbs",
|
||||
"crates/breadcrumbs2",
|
||||
"crates/call",
|
||||
"crates/call2",
|
||||
"crates/channel",
|
||||
@@ -59,6 +62,7 @@ members = [
|
||||
"crates/language",
|
||||
"crates/language2",
|
||||
"crates/language_selector",
|
||||
"crates/language_selector2",
|
||||
"crates/language_tools",
|
||||
"crates/live_kit_client",
|
||||
"crates/live_kit_server",
|
||||
@@ -73,6 +77,7 @@ members = [
|
||||
"crates/notifications",
|
||||
"crates/notifications2",
|
||||
"crates/outline",
|
||||
"crates/outline2",
|
||||
"crates/picker",
|
||||
"crates/picker2",
|
||||
"crates/plugin",
|
||||
@@ -85,12 +90,16 @@ members = [
|
||||
"crates/project_panel",
|
||||
"crates/project_panel2",
|
||||
"crates/project_symbols",
|
||||
"crates/quick_action_bar2",
|
||||
"crates/recent_projects",
|
||||
"crates/recent_projects2",
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
"crates/rpc2",
|
||||
"crates/search",
|
||||
"crates/search2",
|
||||
"crates/semantic_index",
|
||||
"crates/semantic_index2",
|
||||
"crates/settings",
|
||||
"crates/settings2",
|
||||
"crates/snippet",
|
||||
@@ -110,7 +119,6 @@ members = [
|
||||
"crates/theme_selector2",
|
||||
"crates/ui2",
|
||||
"crates/util",
|
||||
"crates/semantic_index",
|
||||
"crates/story",
|
||||
"crates/vim",
|
||||
"crates/vcs_menu",
|
||||
@@ -129,6 +137,7 @@ resolver = "2"
|
||||
[workspace.dependencies]
|
||||
anyhow = { version = "1.0.57" }
|
||||
async-trait = { version = "0.1" }
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
# TODO: Switch back to the published version of `ctor` once:
|
||||
# 1. A new version of `ctor` is published with this change: https://github.com/mmastrac/rust-ctor/pull/295
|
||||
# 2. We've confirmed it's fine to update to the latest version of `ctor` (we're currently on v0.1.20).
|
||||
@@ -196,7 +205,7 @@ tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml",
|
||||
tree-sitter-lua = "0.0.14"
|
||||
tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
|
||||
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"}
|
||||
tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"}
|
||||
tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "6608d9d60c386f19d80af7d8132322fa11199c42"}
|
||||
tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"}
|
||||
|
||||
[patch.crates-io]
|
||||
|
||||
1
assets/icons/copy.svg
Normal file
1
assets/icons/copy.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
|
||||
|
After Width: | Height: | Size: 338 B |
@@ -17,18 +17,9 @@
|
||||
"cmd-enter": "menu::SecondaryConfirm",
|
||||
"escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"cmd-{": "pane::ActivatePrevItem",
|
||||
"cmd-}": "pane::ActivateNextItem",
|
||||
"alt-cmd-left": "pane::ActivatePrevItem",
|
||||
"alt-cmd-right": "pane::ActivateNextItem",
|
||||
"cmd-w": "pane::CloseActiveItem",
|
||||
"alt-cmd-t": "pane::CloseInactiveItems",
|
||||
"ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes",
|
||||
"cmd-k u": "pane::CloseCleanItems",
|
||||
"cmd-k cmd-w": "pane::CloseAllItems",
|
||||
"cmd-shift-w": "workspace::CloseWindow",
|
||||
"cmd-s": "workspace::Save",
|
||||
"cmd-shift-s": "workspace::SaveAs",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"cmd-o": "workspace::Open",
|
||||
"cmd-=": "zed::IncreaseBufferFontSize",
|
||||
"cmd-+": "zed::IncreaseBufferFontSize",
|
||||
"cmd--": "zed::DecreaseBufferFontSize",
|
||||
@@ -38,15 +29,7 @@
|
||||
"cmd-h": "zed::Hide",
|
||||
"alt-cmd-h": "zed::HideOthers",
|
||||
"cmd-m": "zed::Minimize",
|
||||
"ctrl-cmd-f": "zed::ToggleFullScreen",
|
||||
"cmd-n": "workspace::NewFile",
|
||||
"cmd-shift-n": "workspace::NewWindow",
|
||||
"cmd-o": "workspace::Open",
|
||||
"alt-cmd-o": "projects::OpenRecent",
|
||||
"alt-cmd-b": "branches::OpenRecent",
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"shift-escape": "workspace::ToggleZoom"
|
||||
"ctrl-cmd-f": "zed::ToggleFullScreen"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -284,6 +267,15 @@
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"cmd-{": "pane::ActivatePrevItem",
|
||||
"cmd-}": "pane::ActivateNextItem",
|
||||
"alt-cmd-left": "pane::ActivatePrevItem",
|
||||
"alt-cmd-right": "pane::ActivateNextItem",
|
||||
"cmd-w": "pane::CloseActiveItem",
|
||||
"alt-cmd-t": "pane::CloseInactiveItems",
|
||||
"ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes",
|
||||
"cmd-k u": "pane::CloseCleanItems",
|
||||
"cmd-k cmd-w": "pane::CloseAllItems",
|
||||
"cmd-f": "project_search::ToggleFocus",
|
||||
"cmd-g": "search::SelectNextMatch",
|
||||
"cmd-shift-g": "search::SelectPrevMatch",
|
||||
@@ -389,6 +381,14 @@
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"alt-cmd-o": "projects::OpenRecent",
|
||||
"alt-cmd-b": "branches::OpenRecent",
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
"cmd-s": "workspace::Save",
|
||||
"cmd-shift-s": "workspace::SaveAs",
|
||||
"cmd-n": "workspace::NewFile",
|
||||
"cmd-shift-n": "workspace::NewWindow",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"cmd-1": ["workspace::ActivatePane", 0],
|
||||
"cmd-2": ["workspace::ActivatePane", 1],
|
||||
"cmd-3": ["workspace::ActivatePane", 2],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -17,5 +17,8 @@
|
||||
"file_name": "rose-pine-dawn.json",
|
||||
"appearance": "light"
|
||||
}
|
||||
]
|
||||
],
|
||||
"syntax": {
|
||||
"function": ["entity.name"]
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
28
crates/activity_indicator2/Cargo.toml
Normal file
28
crates/activity_indicator2/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "activity_indicator2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/activity_indicator.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
auto_update = { path = "../auto_update2", package = "auto_update2" }
|
||||
editor = { path = "../editor2", package = "editor2" }
|
||||
language = { path = "../language2", package = "language2" }
|
||||
gpui = { path = "../gpui2", package = "gpui2" }
|
||||
project = { path = "../project2", package = "project2" }
|
||||
settings = { path = "../settings2", package = "settings2" }
|
||||
ui = { path = "../ui2", package = "ui2" }
|
||||
util = { path = "../util" }
|
||||
theme = { path = "../theme2", package = "theme2" }
|
||||
workspace = { path = "../workspace2", package = "workspace2" }
|
||||
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
smallvec.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor2", package = "editor2", features = ["test-support"] }
|
||||
333
crates/activity_indicator2/src/activity_indicator.rs
Normal file
333
crates/activity_indicator2/src/activity_indicator.rs
Normal file
@@ -0,0 +1,333 @@
|
||||
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
|
||||
use editor::Editor;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
actions, svg, AppContext, CursorStyle, Div, EventEmitter, InteractiveElement as _, Model,
|
||||
ParentElement as _, Render, SharedString, Stateful, StatefulInteractiveElement, Styled, View,
|
||||
ViewContext, VisualContext as _,
|
||||
};
|
||||
use language::{LanguageRegistry, LanguageServerBinaryStatus};
|
||||
use project::{LanguageServerProgress, Project};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cmp::Reverse, fmt::Write, sync::Arc};
|
||||
use ui::h_stack;
|
||||
use util::ResultExt;
|
||||
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
||||
|
||||
actions!(ShowErrorMessage);
|
||||
|
||||
const DOWNLOAD_ICON: &str = "icons/download.svg";
|
||||
const WARNING_ICON: &str = "icons/warning.svg";
|
||||
|
||||
pub enum Event {
|
||||
ShowError { lsp_name: Arc<str>, error: String },
|
||||
}
|
||||
|
||||
pub struct ActivityIndicator {
|
||||
statuses: Vec<LspStatus>,
|
||||
project: Model<Project>,
|
||||
auto_updater: Option<Model<AutoUpdater>>,
|
||||
}
|
||||
|
||||
struct LspStatus {
|
||||
name: Arc<str>,
|
||||
status: LanguageServerBinaryStatus,
|
||||
}
|
||||
|
||||
struct PendingWork<'a> {
|
||||
language_server_name: &'a str,
|
||||
progress_token: &'a str,
|
||||
progress: &'a LanguageServerProgress,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Content {
|
||||
icon: Option<&'static str>,
|
||||
message: String,
|
||||
on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
|
||||
}
|
||||
|
||||
impl ActivityIndicator {
|
||||
pub fn new(
|
||||
workspace: &mut Workspace,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> View<ActivityIndicator> {
|
||||
let project = workspace.project().clone();
|
||||
let auto_updater = AutoUpdater::get(cx);
|
||||
let this = cx.build_view(|cx: &mut ViewContext<Self>| {
|
||||
let mut status_events = languages.language_server_binary_statuses();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
while let Some((language, event)) = status_events.next().await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.statuses.retain(|s| s.name != language.name());
|
||||
this.statuses.push(LspStatus {
|
||||
name: language.name(),
|
||||
status: event,
|
||||
});
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
cx.observe(&project, |_, _, cx| cx.notify()).detach();
|
||||
|
||||
if let Some(auto_updater) = auto_updater.as_ref() {
|
||||
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
|
||||
}
|
||||
|
||||
// cx.observe_active_labeled_tasks(|_, cx| cx.notify())
|
||||
// .detach();
|
||||
|
||||
Self {
|
||||
statuses: Default::default(),
|
||||
project: project.clone(),
|
||||
auto_updater,
|
||||
}
|
||||
});
|
||||
|
||||
cx.subscribe(&this, move |workspace, _, event, cx| match event {
|
||||
Event::ShowError { lsp_name, error } => {
|
||||
if let Some(buffer) = project
|
||||
.update(cx, |project, cx| project.create_buffer(error, None, cx))
|
||||
.log_err()
|
||||
{
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
[(0..0, format!("Language server error: {}\n\n", lsp_name))],
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
workspace.add_item(
|
||||
Box::new(cx.build_view(|cx| {
|
||||
Editor::for_buffer(buffer, Some(project.clone()), cx)
|
||||
})),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
this
|
||||
}
|
||||
|
||||
fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext<Self>) {
|
||||
self.statuses.retain(|status| {
|
||||
if let LanguageServerBinaryStatus::Failed { error } = &status.status {
|
||||
cx.emit(Event::ShowError {
|
||||
lsp_name: status.name.clone(),
|
||||
error: error.clone(),
|
||||
});
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
|
||||
if let Some(updater) = &self.auto_updater {
|
||||
updater.update(cx, |updater, cx| {
|
||||
updater.dismiss_error(cx);
|
||||
});
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn pending_language_server_work<'a>(
|
||||
&self,
|
||||
cx: &'a AppContext,
|
||||
) -> impl Iterator<Item = PendingWork<'a>> {
|
||||
self.project
|
||||
.read(cx)
|
||||
.language_server_statuses()
|
||||
.rev()
|
||||
.filter_map(|status| {
|
||||
if status.pending_work.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let mut pending_work = status
|
||||
.pending_work
|
||||
.iter()
|
||||
.map(|(token, progress)| PendingWork {
|
||||
language_server_name: status.name.as_str(),
|
||||
progress_token: token.as_str(),
|
||||
progress,
|
||||
})
|
||||
.collect::<SmallVec<[_; 4]>>();
|
||||
pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
|
||||
Some(pending_work)
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Content {
|
||||
// Show any language server has pending activity.
|
||||
let mut pending_work = self.pending_language_server_work(cx);
|
||||
if let Some(PendingWork {
|
||||
language_server_name,
|
||||
progress_token,
|
||||
progress,
|
||||
}) = pending_work.next()
|
||||
{
|
||||
let mut message = language_server_name.to_string();
|
||||
|
||||
message.push_str(": ");
|
||||
if let Some(progress_message) = progress.message.as_ref() {
|
||||
message.push_str(progress_message);
|
||||
} else {
|
||||
message.push_str(progress_token);
|
||||
}
|
||||
|
||||
if let Some(percentage) = progress.percentage {
|
||||
write!(&mut message, " ({}%)", percentage).unwrap();
|
||||
}
|
||||
|
||||
let additional_work_count = pending_work.count();
|
||||
if additional_work_count > 0 {
|
||||
write!(&mut message, " + {} more", additional_work_count).unwrap();
|
||||
}
|
||||
|
||||
return Content {
|
||||
icon: None,
|
||||
message,
|
||||
on_click: None,
|
||||
};
|
||||
}
|
||||
|
||||
// Show any language server installation info.
|
||||
let mut downloading = SmallVec::<[_; 3]>::new();
|
||||
let mut checking_for_update = SmallVec::<[_; 3]>::new();
|
||||
let mut failed = SmallVec::<[_; 3]>::new();
|
||||
for status in &self.statuses {
|
||||
let name = status.name.clone();
|
||||
match status.status {
|
||||
LanguageServerBinaryStatus::CheckingForUpdate => checking_for_update.push(name),
|
||||
LanguageServerBinaryStatus::Downloading => downloading.push(name),
|
||||
LanguageServerBinaryStatus::Failed { .. } => failed.push(name),
|
||||
LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {}
|
||||
}
|
||||
}
|
||||
|
||||
if !downloading.is_empty() {
|
||||
return Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: format!(
|
||||
"Downloading {} language server{}...",
|
||||
downloading.join(", "),
|
||||
if downloading.len() > 1 { "s" } else { "" }
|
||||
),
|
||||
on_click: None,
|
||||
};
|
||||
} else if !checking_for_update.is_empty() {
|
||||
return Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: format!(
|
||||
"Checking for updates to {} language server{}...",
|
||||
checking_for_update.join(", "),
|
||||
if checking_for_update.len() > 1 {
|
||||
"s"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
),
|
||||
on_click: None,
|
||||
};
|
||||
} else if !failed.is_empty() {
|
||||
return Content {
|
||||
icon: Some(WARNING_ICON),
|
||||
message: format!(
|
||||
"Failed to download {} language server{}. Click to show error.",
|
||||
failed.join(", "),
|
||||
if failed.len() > 1 { "s" } else { "" }
|
||||
),
|
||||
on_click: Some(Arc::new(|this, cx| {
|
||||
this.show_error_message(&Default::default(), cx)
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// Show any application auto-update info.
|
||||
if let Some(updater) = &self.auto_updater {
|
||||
return match &updater.read(cx).status() {
|
||||
AutoUpdateStatus::Checking => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Checking for Zed updates…".to_string(),
|
||||
on_click: None,
|
||||
},
|
||||
AutoUpdateStatus::Downloading => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Downloading Zed update…".to_string(),
|
||||
on_click: None,
|
||||
},
|
||||
AutoUpdateStatus::Installing => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Installing Zed update…".to_string(),
|
||||
on_click: None,
|
||||
},
|
||||
AutoUpdateStatus::Updated => Content {
|
||||
icon: None,
|
||||
message: "Click to restart and update Zed".to_string(),
|
||||
on_click: Some(Arc::new(|_, cx| {
|
||||
workspace::restart(&Default::default(), cx)
|
||||
})),
|
||||
},
|
||||
AutoUpdateStatus::Errored => Content {
|
||||
icon: Some(WARNING_ICON),
|
||||
message: "Auto update failed".to_string(),
|
||||
on_click: Some(Arc::new(|this, cx| {
|
||||
this.dismiss_error_message(&Default::default(), cx)
|
||||
})),
|
||||
},
|
||||
AutoUpdateStatus::Idle => Default::default(),
|
||||
};
|
||||
}
|
||||
|
||||
// todo!(show active tasks)
|
||||
// if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
|
||||
// return Content {
|
||||
// icon: None,
|
||||
// message: most_recent_active_task.to_string(),
|
||||
// on_click: None,
|
||||
// };
|
||||
// }
|
||||
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for ActivityIndicator {}
|
||||
|
||||
impl Render for ActivityIndicator {
|
||||
type Element = Stateful<Div>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let content = self.content_to_render(cx);
|
||||
|
||||
let mut result = h_stack()
|
||||
.id("activity-indicator")
|
||||
.on_action(cx.listener(Self::show_error_message))
|
||||
.on_action(cx.listener(Self::dismiss_error_message));
|
||||
|
||||
if let Some(on_click) = content.on_click {
|
||||
result = result
|
||||
.cursor(CursorStyle::PointingHand)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
on_click(this, cx);
|
||||
}))
|
||||
}
|
||||
|
||||
result
|
||||
.children(content.icon.map(|icon| svg().path(icon)))
|
||||
.child(SharedString::from(content.message))
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for ActivityIndicator {
|
||||
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ pub enum ProviderCredential {
|
||||
NotNeeded,
|
||||
}
|
||||
|
||||
pub trait CredentialProvider {
|
||||
pub trait CredentialProvider: Send + Sync {
|
||||
fn has_credentials(&self) -> bool;
|
||||
fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential;
|
||||
fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential);
|
||||
|
||||
@@ -104,7 +104,7 @@ pub struct OpenAIResponseStreamEvent {
|
||||
|
||||
pub async fn stream_completion(
|
||||
credential: ProviderCredential,
|
||||
executor: Arc<BackgroundExecutor>,
|
||||
executor: BackgroundExecutor,
|
||||
request: Box<dyn CompletionRequest>,
|
||||
) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
|
||||
let api_key = match credential {
|
||||
@@ -197,11 +197,11 @@ pub async fn stream_completion(
|
||||
pub struct OpenAICompletionProvider {
|
||||
model: OpenAILanguageModel,
|
||||
credential: Arc<RwLock<ProviderCredential>>,
|
||||
executor: Arc<BackgroundExecutor>,
|
||||
executor: BackgroundExecutor,
|
||||
}
|
||||
|
||||
impl OpenAICompletionProvider {
|
||||
pub fn new(model_name: &str, executor: Arc<BackgroundExecutor>) -> Self {
|
||||
pub fn new(model_name: &str, executor: BackgroundExecutor) -> Self {
|
||||
let model = OpenAILanguageModel::load(model_name);
|
||||
let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials));
|
||||
Self {
|
||||
|
||||
@@ -35,7 +35,7 @@ pub struct OpenAIEmbeddingProvider {
|
||||
model: OpenAILanguageModel,
|
||||
credential: Arc<RwLock<ProviderCredential>>,
|
||||
pub client: Arc<dyn HttpClient>,
|
||||
pub executor: Arc<BackgroundExecutor>,
|
||||
pub executor: BackgroundExecutor,
|
||||
rate_limit_count_rx: watch::Receiver<Option<Instant>>,
|
||||
rate_limit_count_tx: Arc<Mutex<watch::Sender<Option<Instant>>>>,
|
||||
}
|
||||
@@ -66,7 +66,7 @@ struct OpenAIEmbeddingUsage {
|
||||
}
|
||||
|
||||
impl OpenAIEmbeddingProvider {
|
||||
pub fn new(client: Arc<dyn HttpClient>, executor: Arc<BackgroundExecutor>) -> Self {
|
||||
pub fn new(client: Arc<dyn HttpClient>, executor: BackgroundExecutor) -> Self {
|
||||
let (rate_limit_count_tx, rate_limit_count_rx) = watch::channel_with(None);
|
||||
let rate_limit_count_tx = Arc::new(Mutex::new(rate_limit_count_tx));
|
||||
|
||||
|
||||
@@ -1218,6 +1218,31 @@ impl View for AssistantPanel {
|
||||
let style = &theme.assistant;
|
||||
if let Some(api_key_editor) = self.api_key_editor.as_ref() {
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Text::new(
|
||||
"To use the assistant panel or inline assistant, you need to add your OpenAI api key.",
|
||||
style.api_key_prompt.text.clone(),
|
||||
),
|
||||
)
|
||||
.with_child(
|
||||
Text::new(
|
||||
" - Having a subscription for another service like GitHub Copilot won't work.",
|
||||
style.api_key_prompt.text.clone(),
|
||||
),
|
||||
)
|
||||
.with_child(
|
||||
Text::new(
|
||||
" - You can create a api key at: platform.openai.com/api-keys",
|
||||
style.api_key_prompt.text.clone(),
|
||||
),
|
||||
)
|
||||
.with_child(
|
||||
Text::new(
|
||||
" ",
|
||||
style.api_key_prompt.text.clone(),
|
||||
)
|
||||
.aligned(),
|
||||
)
|
||||
.with_child(
|
||||
Text::new(
|
||||
"Paste your OpenAI API key and press Enter to use the assistant",
|
||||
@@ -1231,6 +1256,20 @@ impl View for AssistantPanel {
|
||||
.with_style(style.api_key_editor.container)
|
||||
.aligned(),
|
||||
)
|
||||
.with_child(
|
||||
Text::new(
|
||||
" ",
|
||||
style.api_key_prompt.text.clone(),
|
||||
)
|
||||
.aligned(),
|
||||
)
|
||||
.with_child(
|
||||
Text::new(
|
||||
"Click on the Z button in the status bar to close this panel.",
|
||||
style.api_key_prompt.text.clone(),
|
||||
)
|
||||
.aligned(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.api_key_prompt.container)
|
||||
.aligned()
|
||||
|
||||
54
crates/assistant2/Cargo.toml
Normal file
54
crates/assistant2/Cargo.toml
Normal file
@@ -0,0 +1,54 @@
|
||||
[package]
|
||||
name = "assistant2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/assistant.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
ai = { package = "ai2", path = "../ai2" }
|
||||
client = { package = "client2", path = "../client2" }
|
||||
collections = { path = "../collections"}
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
fs = { package = "fs2", path = "../fs2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
menu = { package = "menu2", path = "../menu2" }
|
||||
multi_buffer = { package = "multi_buffer2", path = "../multi_buffer2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
search = { package = "search2", path = "../search2" }
|
||||
semantic_index = { package = "semantic_index2", path = "../semantic_index2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
util = { path = "../util" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
|
||||
uuid.workspace = true
|
||||
log.workspace = true
|
||||
anyhow.workspace = true
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
futures.workspace = true
|
||||
indoc.workspace = true
|
||||
isahc.workspace = true
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
regex.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
tiktoken-rs.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ai = { package = "ai2", path = "../ai2", features = ["test-support"]}
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
project = { package = "project2", path = "../project2", features = ["test-support"] }
|
||||
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
log.workspace = true
|
||||
rand.workspace = true
|
||||
63
crates/assistant2/README.zmd
Normal file
63
crates/assistant2/README.zmd
Normal file
@@ -0,0 +1,63 @@
|
||||
> Hello, I'm Nathan Sobo.
|
||||
I'm currently your sole creator and the leader of Zed Industries.
|
||||
Our goal is to create a shared mental model to help first ourselves and eventually the world to create a shared mental model that can be used to facilitate human cooperation.
|
||||
|
||||
< Hello, Nathan! Welcome back to Zed. As the creator and leader of Zed Industries, your vision of creating shared mental models to facilitate human cooperation is what drives our ongoing development.
|
||||
|
||||
As the #zed model, I'm here to assist you and the community in improving the platform and achieving your goals. Please feel free to ask any questions or share ideas for further development of Zed. Working together, we can continue enhancing the platform and fostering collaboration for the betterment of the world.
|
||||
|
||||
> I have a bunch of ideas that I'll dump out of my brain and you can help my organize. Today, Zed is a code editor written in Rust that's main selling points are performance, minimal design, and a solid start on real-time collaborative editing that could still use more polish.
|
||||
|
||||
We've decided that our next milestone will be open-sourcing Zed on the Zed platform.
|
||||
|
||||
< Nathan, it's exciting to hear that you have more ideas to enhance Zed! Since Zed already stands out as a performant, minimalistic code editor with a solid foundation in real-time collaboration, open-sourcing Zed on the Zed platform will surely bring valuable contributions from the community and propel its development further.
|
||||
|
||||
Let's organize your ideas and create a plan for your next milestone. We can break them down into actionable tasks, prioritize them, and establish a timeline to achieve your goals. Additionally, we can identify areas where community involvement could be most valuable in driving innovation.
|
||||
|
||||
Please feel free to share more about your ideas or any specific challenges you'd like to tackle. I'm here to help you turn these ideas into a cohesive and successful plan for Zed's future!
|
||||
|
||||
@nathan> #zed, can you describe the conversation UI as you currently understand it.
|
||||
|
||||
< Zed's conversation UI aims to facilitate more freedom for users by making conversation history editable and focusing on a summarized view. Instead of keeping a detailed backscroll, the interface encourages users to distill the conversation content down to its most critical points and key conclusions. This approach ensures that the vital information from the conversation is highlighted and easily accessible, while still maintaining a history that can be linked to when necessary.
|
||||
|
||||
Addressing @nate's concerns, this design choice aims to strike a balance between readability, usability, and information preservation. The summarized view maintains the essence of the conversation without being burdened by extensive backscroll. Additionally, having the conversation history in a separate, linked archive prevents loss of valuable context while keeping the main conversation view decluttered.
|
||||
|
||||
@nathan> What details around the conversation view are worth exploring?
|
||||
|
||||
< @nathan, there are several aspects of the conversation view that are worth exploring and refining:
|
||||
|
||||
1. Editing functionality: Ensuring that all participants can edit and refine the conversation summary effectively while preserving the original context.
|
||||
2. Permissions and access control: Determining how to manage editing rights, user roles, and visibility of the conversation summary and history.
|
||||
3. Navigation and linking: Implementing an intuitive system for navigating between the summarized conversation and linked history or reference materials.
|
||||
4. Visualization and layout: Designing the interface to clearly present the summarized information while providing a visually engaging and organized experience for participants.
|
||||
5. Threaded conversations and branching: Exploring ways to handle sub-conversations, supplementary discussions, or debates within the primary conversation view.
|
||||
|
||||
Considering these aspects will ensure our conversation view design is optimized for usability, collaboration, and accessibility in Zed.
|
||||
|
||||
@nathan> @nate, which of these would you like to explore?
|
||||
|
||||
@nate> 2 feels like it isn't important at the moment, we can explore that later. Let's start with 4, which I think will lead us to discussion 3 and 5.
|
||||
|
||||
#zed share your thoughts on the points we need to consider to design a layout and visualization for a conversation view between you (#zed) and multuple peoople, or between multiple people and multiple bots (you and other bots).
|
||||
|
||||
@nathan> Agreed. I'm interested in threading I think more than anything. Or 4 yeah. I think we need to scope the threading conversation. Also, asking #zed to propose the solution... not sure it will be that effective but it's worth a try...
|
||||
|
||||
#zed describe a hypothetical conversation UI that meets the description.
|
||||
|
||||
< @nate and @nathan, I understand that you'd like to focus on the layout and visualization for a conversation view that can accommodate multiple people and potentially multiple bots as well. Here's a proposal for a conversation UI that meets the requirements:
|
||||
|
||||
1. Main conversation area: Display the summarized conversation content in a clean and organized manner, ensuring readability and efficient use of space.
|
||||
|
||||
2. Participant avatars and labels: Include user avatars and/or labels to distinguish between users and bots visually, allowing easy identification of each participant's contributions.
|
||||
|
||||
3. Active threads indicator: Designate a clear indicator for ongoing threads, which will attract attention to active sub-conversations or related discussions in the conversation view.
|
||||
|
||||
4. Dynamic branching: Implement a branching visual structure to present threaded conversations, clearly distinguishing sub-conversations, and enabling users to navigate between them smoothly.
|
||||
|
||||
5. Multiple bot interaction support: Facilitate interactions between multiple bots through user-invoked prompts or bot-to-bot communication channels without disrupting the primary conversation flow.
|
||||
|
||||
6. Expandable conversation history: Offer a separate, expandable section or panel to access historical content without cluttering the main conversation view.
|
||||
|
||||
7. Linking and referencing: Implement a system for hyperlinking relevant information, resources, or previous conversations to the current discussion, making it easy for participants to follow and reference the shared context.
|
||||
|
||||
This hypothetical conversation UI aims to address the layout and visualization, threading, and multi-participant aspects of your requirements. Of course, it's just an initial proposal, and we can refine and iterate on it based on your feedback and specific needs.
|
||||
3
crates/assistant2/features.zmd
Normal file
3
crates/assistant2/features.zmd
Normal file
@@ -0,0 +1,3 @@
|
||||
Push content to a deeper layer.
|
||||
A context can have multiple sublayers.
|
||||
You can enable or disable arbitrary sublayers at arbitrary nesting depths when viewing the document.
|
||||
126
crates/assistant2/src/assistant.rs
Normal file
126
crates/assistant2/src/assistant.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
pub mod assistant_panel;
|
||||
mod assistant_settings;
|
||||
mod codegen;
|
||||
mod prompts;
|
||||
mod streaming_diff;
|
||||
|
||||
use ai::providers::open_ai::Role;
|
||||
use anyhow::Result;
|
||||
pub use assistant_panel::AssistantPanel;
|
||||
use assistant_settings::OpenAIModel;
|
||||
use chrono::{DateTime, Local};
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::{actions, AppContext, SharedString};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc};
|
||||
use util::paths::CONVERSATIONS_DIR;
|
||||
|
||||
actions!(
|
||||
NewConversation,
|
||||
Assist,
|
||||
Split,
|
||||
CycleMessageRole,
|
||||
QuoteSelection,
|
||||
ToggleFocus,
|
||||
ResetKey,
|
||||
InlineAssist,
|
||||
ToggleIncludeConversation,
|
||||
ToggleRetrieveContext,
|
||||
);
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
struct MessageId(usize);
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct MessageMetadata {
|
||||
role: Role,
|
||||
sent_at: DateTime<Local>,
|
||||
status: MessageStatus,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
enum MessageStatus {
|
||||
Pending,
|
||||
Done,
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedMessage {
|
||||
id: MessageId,
|
||||
start: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedConversation {
|
||||
id: Option<String>,
|
||||
zed: String,
|
||||
version: String,
|
||||
text: String,
|
||||
messages: Vec<SavedMessage>,
|
||||
message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
summary: String,
|
||||
model: OpenAIModel,
|
||||
}
|
||||
|
||||
impl SavedConversation {
|
||||
const VERSION: &'static str = "0.1.0";
|
||||
}
|
||||
|
||||
struct SavedConversationMetadata {
|
||||
title: String,
|
||||
path: PathBuf,
|
||||
mtime: chrono::DateTime<chrono::Local>,
|
||||
}
|
||||
|
||||
impl SavedConversationMetadata {
|
||||
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
|
||||
fs.create_dir(&CONVERSATIONS_DIR).await?;
|
||||
|
||||
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
|
||||
let mut conversations = Vec::<SavedConversationMetadata>::new();
|
||||
while let Some(path) = paths.next().await {
|
||||
let path = path?;
|
||||
if path.extension() != Some(OsStr::new("json")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pattern = r" - \d+.zed.json$";
|
||||
let re = Regex::new(pattern).unwrap();
|
||||
|
||||
let metadata = fs.metadata(&path).await?;
|
||||
if let Some((file_name, metadata)) = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.zip(metadata)
|
||||
{
|
||||
let title = re.replace(file_name, "");
|
||||
conversations.push(Self {
|
||||
title: title.into_owned(),
|
||||
path,
|
||||
mtime: metadata.mtime.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
|
||||
|
||||
Ok(conversations)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
assistant_panel::init(cx);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::init();
|
||||
}
|
||||
}
|
||||
3480
crates/assistant2/src/assistant_panel.rs
Normal file
3480
crates/assistant2/src/assistant_panel.rs
Normal file
File diff suppressed because it is too large
Load Diff
80
crates/assistant2/src/assistant_settings.rs
Normal file
80
crates/assistant2/src/assistant_settings.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use anyhow;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub enum OpenAIModel {
|
||||
#[serde(rename = "gpt-3.5-turbo-0613")]
|
||||
ThreePointFiveTurbo,
|
||||
#[serde(rename = "gpt-4-0613")]
|
||||
Four,
|
||||
#[serde(rename = "gpt-4-1106-preview")]
|
||||
FourTurbo,
|
||||
}
|
||||
|
||||
impl OpenAIModel {
|
||||
pub fn full_name(&self) -> &'static str {
|
||||
match self {
|
||||
OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo-0613",
|
||||
OpenAIModel::Four => "gpt-4-0613",
|
||||
OpenAIModel::FourTurbo => "gpt-4-1106-preview",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn short_name(&self) -> &'static str {
|
||||
match self {
|
||||
OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo",
|
||||
OpenAIModel::Four => "gpt-4",
|
||||
OpenAIModel::FourTurbo => "gpt-4-turbo",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cycle(&self) -> Self {
|
||||
match self {
|
||||
OpenAIModel::ThreePointFiveTurbo => OpenAIModel::Four,
|
||||
OpenAIModel::Four => OpenAIModel::FourTurbo,
|
||||
OpenAIModel::FourTurbo => OpenAIModel::ThreePointFiveTurbo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AssistantDockPosition {
|
||||
Left,
|
||||
Right,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct AssistantSettings {
|
||||
pub button: bool,
|
||||
pub dock: AssistantDockPosition,
|
||||
pub default_width: f32,
|
||||
pub default_height: f32,
|
||||
pub default_open_ai_model: OpenAIModel,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AssistantSettingsContent {
|
||||
pub button: Option<bool>,
|
||||
pub dock: Option<AssistantDockPosition>,
|
||||
pub default_width: Option<f32>,
|
||||
pub default_height: Option<f32>,
|
||||
pub default_open_ai_model: Option<OpenAIModel>,
|
||||
}
|
||||
|
||||
impl Settings for AssistantSettings {
|
||||
const KEY: Option<&'static str> = Some("assistant");
|
||||
|
||||
type FileContent = AssistantSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
}
|
||||
}
|
||||
688
crates/assistant2/src/codegen.rs
Normal file
688
crates/assistant2/src/codegen.rs
Normal file
@@ -0,0 +1,688 @@
|
||||
use crate::streaming_diff::{Hunk, StreamingDiff};
|
||||
use ai::completion::{CompletionProvider, CompletionRequest};
|
||||
use anyhow::Result;
|
||||
use editor::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
|
||||
use gpui::{EventEmitter, Model, ModelContext, Task};
|
||||
use language::{Rope, TransactionId};
|
||||
use multi_buffer;
|
||||
use std::{cmp, future, ops::Range, sync::Arc};
|
||||
|
||||
pub enum Event {
|
||||
Finished,
|
||||
Undone,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum CodegenKind {
|
||||
Transform { range: Range<Anchor> },
|
||||
Generate { position: Anchor },
|
||||
}
|
||||
|
||||
pub struct Codegen {
|
||||
provider: Arc<dyn CompletionProvider>,
|
||||
buffer: Model<MultiBuffer>,
|
||||
snapshot: MultiBufferSnapshot,
|
||||
kind: CodegenKind,
|
||||
last_equal_ranges: Vec<Range<Anchor>>,
|
||||
transaction_id: Option<TransactionId>,
|
||||
error: Option<anyhow::Error>,
|
||||
generation: Task<()>,
|
||||
idle: bool,
|
||||
_subscription: gpui::Subscription,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for Codegen {}
|
||||
|
||||
impl Codegen {
|
||||
pub fn new(
|
||||
buffer: Model<MultiBuffer>,
|
||||
kind: CodegenKind,
|
||||
provider: Arc<dyn CompletionProvider>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
Self {
|
||||
provider,
|
||||
buffer: buffer.clone(),
|
||||
snapshot,
|
||||
kind,
|
||||
last_equal_ranges: Default::default(),
|
||||
transaction_id: Default::default(),
|
||||
error: Default::default(),
|
||||
idle: true,
|
||||
generation: Task::ready(()),
|
||||
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_buffer_event(
|
||||
&mut self,
|
||||
_buffer: Model<MultiBuffer>,
|
||||
event: &multi_buffer::Event,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let multi_buffer::Event::TransactionUndone { transaction_id } = event {
|
||||
if self.transaction_id == Some(*transaction_id) {
|
||||
self.transaction_id = None;
|
||||
self.generation = Task::ready(());
|
||||
cx.emit(Event::Undone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn range(&self) -> Range<Anchor> {
|
||||
match &self.kind {
|
||||
CodegenKind::Transform { range } => range.clone(),
|
||||
CodegenKind::Generate { position } => position.bias_left(&self.snapshot)..*position,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> &CodegenKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
pub fn last_equal_ranges(&self) -> &[Range<Anchor>] {
|
||||
&self.last_equal_ranges
|
||||
}
|
||||
|
||||
pub fn idle(&self) -> bool {
|
||||
self.idle
|
||||
}
|
||||
|
||||
pub fn error(&self) -> Option<&anyhow::Error> {
|
||||
self.error.as_ref()
|
||||
}
|
||||
|
||||
pub fn start(&mut self, prompt: Box<dyn CompletionRequest>, cx: &mut ModelContext<Self>) {
|
||||
let range = self.range();
|
||||
let snapshot = self.snapshot.clone();
|
||||
let selected_text = snapshot
|
||||
.text_for_range(range.start..range.end)
|
||||
.collect::<Rope>();
|
||||
|
||||
let selection_start = range.start.to_point(&snapshot);
|
||||
let suggested_line_indent = snapshot
|
||||
.suggested_indents(selection_start.row..selection_start.row + 1, cx)
|
||||
.into_values()
|
||||
.next()
|
||||
.unwrap_or_else(|| snapshot.indent_size_for_line(selection_start.row));
|
||||
|
||||
let response = self.provider.complete(prompt);
|
||||
self.generation = cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let generate = async {
|
||||
let mut edit_start = range.start.to_offset(&snapshot);
|
||||
|
||||
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
|
||||
let diff = cx.background_executor().spawn(async move {
|
||||
let chunks = strip_invalid_spans_from_codeblock(response.await?);
|
||||
futures::pin_mut!(chunks);
|
||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||
|
||||
let mut new_text = String::new();
|
||||
let mut base_indent = None;
|
||||
let mut line_indent = None;
|
||||
let mut first_line = true;
|
||||
|
||||
while let Some(chunk) = chunks.next().await {
|
||||
let chunk = chunk?;
|
||||
|
||||
let mut lines = chunk.split('\n').peekable();
|
||||
while let Some(line) = lines.next() {
|
||||
new_text.push_str(line);
|
||||
if line_indent.is_none() {
|
||||
if let Some(non_whitespace_ch_ix) =
|
||||
new_text.find(|ch: char| !ch.is_whitespace())
|
||||
{
|
||||
line_indent = Some(non_whitespace_ch_ix);
|
||||
base_indent = base_indent.or(line_indent);
|
||||
|
||||
let line_indent = line_indent.unwrap();
|
||||
let base_indent = base_indent.unwrap();
|
||||
let indent_delta = line_indent as i32 - base_indent as i32;
|
||||
let mut corrected_indent_len = cmp::max(
|
||||
0,
|
||||
suggested_line_indent.len as i32 + indent_delta,
|
||||
)
|
||||
as usize;
|
||||
if first_line {
|
||||
corrected_indent_len = corrected_indent_len
|
||||
.saturating_sub(selection_start.column as usize);
|
||||
}
|
||||
|
||||
let indent_char = suggested_line_indent.char();
|
||||
let mut indent_buffer = [0; 4];
|
||||
let indent_str =
|
||||
indent_char.encode_utf8(&mut indent_buffer);
|
||||
new_text.replace_range(
|
||||
..line_indent,
|
||||
&indent_str.repeat(corrected_indent_len),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if line_indent.is_some() {
|
||||
hunks_tx.send(diff.push_new(&new_text)).await?;
|
||||
new_text.clear();
|
||||
}
|
||||
|
||||
if lines.peek().is_some() {
|
||||
hunks_tx.send(diff.push_new("\n")).await?;
|
||||
line_indent = None;
|
||||
first_line = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
hunks_tx.send(diff.push_new(&new_text)).await?;
|
||||
hunks_tx.send(diff.finish()).await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
while let Some(hunks) = hunks_rx.next().await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.last_equal_ranges.clear();
|
||||
|
||||
let transaction = this.buffer.update(cx, |buffer, cx| {
|
||||
// Avoid grouping assistant edits with user edits.
|
||||
buffer.finalize_last_transaction(cx);
|
||||
|
||||
buffer.start_transaction(cx);
|
||||
buffer.edit(
|
||||
hunks.into_iter().filter_map(|hunk| match hunk {
|
||||
Hunk::Insert { text } => {
|
||||
let edit_start = snapshot.anchor_after(edit_start);
|
||||
Some((edit_start..edit_start, text))
|
||||
}
|
||||
Hunk::Remove { len } => {
|
||||
let edit_end = edit_start + len;
|
||||
let edit_range = snapshot.anchor_after(edit_start)
|
||||
..snapshot.anchor_before(edit_end);
|
||||
edit_start = edit_end;
|
||||
Some((edit_range, String::new()))
|
||||
}
|
||||
Hunk::Keep { len } => {
|
||||
let edit_end = edit_start + len;
|
||||
let edit_range = snapshot.anchor_after(edit_start)
|
||||
..snapshot.anchor_before(edit_end);
|
||||
edit_start = edit_end;
|
||||
this.last_equal_ranges.push(edit_range);
|
||||
None
|
||||
}
|
||||
}),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
|
||||
buffer.end_transaction(cx)
|
||||
});
|
||||
|
||||
if let Some(transaction) = transaction {
|
||||
if let Some(first_transaction) = this.transaction_id {
|
||||
// Group all assistant edits into the first transaction.
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.merge_transactions(
|
||||
transaction,
|
||||
first_transaction,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
} else {
|
||||
this.transaction_id = Some(transaction);
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction(cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
|
||||
diff.await?;
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
let result = generate.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.last_equal_ranges.clear();
|
||||
this.idle = true;
|
||||
if let Err(error) = result {
|
||||
this.error = Some(error);
|
||||
}
|
||||
cx.emit(Event::Finished);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
self.error.take();
|
||||
self.idle = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if let Some(transaction_id) = self.transaction_id {
|
||||
self.buffer
|
||||
.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_invalid_spans_from_codeblock(
|
||||
stream: impl Stream<Item = Result<String>>,
|
||||
) -> impl Stream<Item = Result<String>> {
|
||||
let mut first_line = true;
|
||||
let mut buffer = String::new();
|
||||
let mut starts_with_markdown_codeblock = false;
|
||||
let mut includes_start_or_end_span = false;
|
||||
stream.filter_map(move |chunk| {
|
||||
let chunk = match chunk {
|
||||
Ok(chunk) => chunk,
|
||||
Err(err) => return future::ready(Some(Err(err))),
|
||||
};
|
||||
buffer.push_str(&chunk);
|
||||
|
||||
if buffer.len() > "<|S|".len() && buffer.starts_with("<|S|") {
|
||||
includes_start_or_end_span = true;
|
||||
|
||||
buffer = buffer
|
||||
.strip_prefix("<|S|>")
|
||||
.or_else(|| buffer.strip_prefix("<|S|"))
|
||||
.unwrap_or(&buffer)
|
||||
.to_string();
|
||||
} else if buffer.ends_with("|E|>") {
|
||||
includes_start_or_end_span = true;
|
||||
} else if buffer.starts_with("<|")
|
||||
|| buffer.starts_with("<|S")
|
||||
|| buffer.starts_with("<|S|")
|
||||
|| buffer.ends_with("|")
|
||||
|| buffer.ends_with("|E")
|
||||
|| buffer.ends_with("|E|")
|
||||
{
|
||||
return future::ready(None);
|
||||
}
|
||||
|
||||
if first_line {
|
||||
if buffer == "" || buffer == "`" || buffer == "``" {
|
||||
return future::ready(None);
|
||||
} else if buffer.starts_with("```") {
|
||||
starts_with_markdown_codeblock = true;
|
||||
if let Some(newline_ix) = buffer.find('\n') {
|
||||
buffer.replace_range(..newline_ix + 1, "");
|
||||
first_line = false;
|
||||
} else {
|
||||
return future::ready(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut text = buffer.to_string();
|
||||
if starts_with_markdown_codeblock {
|
||||
text = text
|
||||
.strip_suffix("\n```\n")
|
||||
.or_else(|| text.strip_suffix("\n```"))
|
||||
.or_else(|| text.strip_suffix("\n``"))
|
||||
.or_else(|| text.strip_suffix("\n`"))
|
||||
.or_else(|| text.strip_suffix('\n'))
|
||||
.unwrap_or(&text)
|
||||
.to_string();
|
||||
}
|
||||
|
||||
if includes_start_or_end_span {
|
||||
text = text
|
||||
.strip_suffix("|E|>")
|
||||
.or_else(|| text.strip_suffix("E|>"))
|
||||
.or_else(|| text.strip_prefix("|>"))
|
||||
.or_else(|| text.strip_prefix(">"))
|
||||
.unwrap_or(&text)
|
||||
.to_string();
|
||||
};
|
||||
|
||||
if text.contains('\n') {
|
||||
first_line = false;
|
||||
}
|
||||
|
||||
let remainder = buffer.split_off(text.len());
|
||||
let result = if buffer.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Ok(buffer.clone()))
|
||||
};
|
||||
|
||||
buffer = remainder;
|
||||
future::ready(result)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use ai::test::FakeCompletionProvider;
|
||||
use futures::stream::{self};
|
||||
use gpui::{Context, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point};
|
||||
use rand::prelude::*;
|
||||
use serde::Serialize;
|
||||
use settings::SettingsStore;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DummyCompletionRequest {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl CompletionRequest for DummyCompletionRequest {
|
||||
fn data(&self) -> serde_json::Result<String> {
|
||||
serde_json::to_string(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
fn main() {
|
||||
let x = 0;
|
||||
for _ in 0..10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let range = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
|
||||
});
|
||||
let provider = Arc::new(FakeCompletionProvider::new());
|
||||
let codegen = cx.build_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
CodegenKind::Transform { range },
|
||||
provider.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let request = Box::new(DummyCompletionRequest {
|
||||
name: "test".to_string(),
|
||||
});
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
" let mut x = 0;\n",
|
||||
" while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
" }",
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
println!("CHUNK: {:?}", &chunk);
|
||||
provider.send_completion(chunk);
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_autoindent_when_generating_past_indentation(
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
fn main() {
|
||||
le
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let position = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 6))
|
||||
});
|
||||
let provider = Arc::new(FakeCompletionProvider::new());
|
||||
let codegen = cx.build_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
CodegenKind::Generate { position },
|
||||
provider.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let request = Box::new(DummyCompletionRequest {
|
||||
name: "test".to_string(),
|
||||
});
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
"t mut x = 0;\n",
|
||||
"while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
"}", //
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
provider.send_completion(chunk);
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_autoindent_when_generating_before_indentation(
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = concat!(
|
||||
"fn main() {\n",
|
||||
" \n",
|
||||
"}\n" //
|
||||
);
|
||||
let buffer =
|
||||
cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let position = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 2))
|
||||
});
|
||||
let provider = Arc::new(FakeCompletionProvider::new());
|
||||
let codegen = cx.build_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
CodegenKind::Generate { position },
|
||||
provider.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let request = Box::new(DummyCompletionRequest {
|
||||
name: "test".to_string(),
|
||||
});
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
"let mut x = 0;\n",
|
||||
"while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
"}", //
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
println!("{:?}", &chunk);
|
||||
provider.send_completion(chunk);
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_strip_invalid_spans_from_codeblock() {
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("Lorem ipsum dolor", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks(
|
||||
"```html\n```js\nLorem ipsum dolor\n```\n```",
|
||||
2
|
||||
))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"```js\nLorem ipsum dolor\n```"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("``\nLorem ipsum dolor\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"``\nLorem ipsum dolor\n```"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("<|S|Lorem ipsum|E|>", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("<|S|>Lorem ipsum", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\n<|S|>Lorem ipsum\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\n<|S|Lorem ipsum|E|>\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
fn chunks(text: &str, size: usize) -> impl Stream<Item = Result<String>> {
|
||||
stream::iter(
|
||||
text.chars()
|
||||
.collect::<Vec<_>>()
|
||||
.chunks(size)
|
||||
.map(|chunk| Ok(chunk.iter().collect::<String>()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)
|
||||
.with_indents_query(
|
||||
r#"
|
||||
(call_expression) @indent
|
||||
(field_expression) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
389
crates/assistant2/src/prompts.rs
Normal file
389
crates/assistant2/src/prompts.rs
Normal file
@@ -0,0 +1,389 @@
|
||||
use ai::models::LanguageModel;
|
||||
use ai::prompts::base::{PromptArguments, PromptChain, PromptPriority, PromptTemplate};
|
||||
use ai::prompts::file_context::FileContext;
|
||||
use ai::prompts::generate::GenerateInlineContent;
|
||||
use ai::prompts::preamble::EngineerPreamble;
|
||||
use ai::prompts::repository_context::{PromptCodeSnippet, RepositoryContext};
|
||||
use ai::providers::open_ai::OpenAILanguageModel;
|
||||
use language::{BufferSnapshot, OffsetRangeExt, ToOffset};
|
||||
use std::cmp::{self, Reverse};
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> String {
|
||||
#[derive(Debug)]
|
||||
struct Match {
|
||||
collapse: Range<usize>,
|
||||
keep: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
let selected_range = selected_range.to_offset(buffer);
|
||||
let mut ts_matches = buffer.matches(0..buffer.len(), |grammar| {
|
||||
Some(&grammar.embedding_config.as_ref()?.query)
|
||||
});
|
||||
let configs = ts_matches
|
||||
.grammars()
|
||||
.iter()
|
||||
.map(|g| g.embedding_config.as_ref().unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let mut matches = Vec::new();
|
||||
while let Some(mat) = ts_matches.peek() {
|
||||
let config = &configs[mat.grammar_index];
|
||||
if let Some(collapse) = mat.captures.iter().find_map(|cap| {
|
||||
if Some(cap.index) == config.collapse_capture_ix {
|
||||
Some(cap.node.byte_range())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
let mut keep = Vec::new();
|
||||
for capture in mat.captures.iter() {
|
||||
if Some(capture.index) == config.keep_capture_ix {
|
||||
keep.push(capture.node.byte_range());
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ts_matches.advance();
|
||||
matches.push(Match { collapse, keep });
|
||||
} else {
|
||||
ts_matches.advance();
|
||||
}
|
||||
}
|
||||
matches.sort_unstable_by_key(|mat| (mat.collapse.start, Reverse(mat.collapse.end)));
|
||||
let mut matches = matches.into_iter().peekable();
|
||||
|
||||
let mut summary = String::new();
|
||||
let mut offset = 0;
|
||||
let mut flushed_selection = false;
|
||||
while let Some(mat) = matches.next() {
|
||||
// Keep extending the collapsed range if the next match surrounds
|
||||
// the current one.
|
||||
while let Some(next_mat) = matches.peek() {
|
||||
if mat.collapse.start <= next_mat.collapse.start
|
||||
&& mat.collapse.end >= next_mat.collapse.end
|
||||
{
|
||||
matches.next().unwrap();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if offset > mat.collapse.start {
|
||||
// Skip collapsed nodes that have already been summarized.
|
||||
offset = cmp::max(offset, mat.collapse.end);
|
||||
continue;
|
||||
}
|
||||
|
||||
if offset <= selected_range.start && selected_range.start <= mat.collapse.end {
|
||||
if !flushed_selection {
|
||||
// The collapsed node ends after the selection starts, so we'll flush the selection first.
|
||||
summary.extend(buffer.text_for_range(offset..selected_range.start));
|
||||
summary.push_str("<|S|");
|
||||
if selected_range.end == selected_range.start {
|
||||
summary.push_str(">");
|
||||
} else {
|
||||
summary.extend(buffer.text_for_range(selected_range.clone()));
|
||||
summary.push_str("|E|>");
|
||||
}
|
||||
offset = selected_range.end;
|
||||
flushed_selection = true;
|
||||
}
|
||||
|
||||
// If the selection intersects the collapsed node, we won't collapse it.
|
||||
if selected_range.end >= mat.collapse.start {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
summary.extend(buffer.text_for_range(offset..mat.collapse.start));
|
||||
for keep in mat.keep {
|
||||
summary.extend(buffer.text_for_range(keep));
|
||||
}
|
||||
offset = mat.collapse.end;
|
||||
}
|
||||
|
||||
// Flush selection if we haven't already done so.
|
||||
if !flushed_selection && offset <= selected_range.start {
|
||||
summary.extend(buffer.text_for_range(offset..selected_range.start));
|
||||
summary.push_str("<|S|");
|
||||
if selected_range.end == selected_range.start {
|
||||
summary.push_str(">");
|
||||
} else {
|
||||
summary.extend(buffer.text_for_range(selected_range.clone()));
|
||||
summary.push_str("|E|>");
|
||||
}
|
||||
offset = selected_range.end;
|
||||
}
|
||||
|
||||
summary.extend(buffer.text_for_range(offset..buffer.len()));
|
||||
summary
|
||||
}
|
||||
|
||||
pub fn generate_content_prompt(
|
||||
user_prompt: String,
|
||||
language_name: Option<&str>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
search_results: Vec<PromptCodeSnippet>,
|
||||
model: &str,
|
||||
project_name: Option<String>,
|
||||
) -> anyhow::Result<String> {
|
||||
// Using new Prompt Templates
|
||||
let openai_model: Arc<dyn LanguageModel> = Arc::new(OpenAILanguageModel::load(model));
|
||||
let lang_name = if let Some(language_name) = language_name {
|
||||
Some(language_name.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let args = PromptArguments {
|
||||
model: openai_model,
|
||||
language_name: lang_name.clone(),
|
||||
project_name,
|
||||
snippets: search_results.clone(),
|
||||
reserved_tokens: 1000,
|
||||
buffer: Some(buffer),
|
||||
selected_range: Some(range),
|
||||
user_prompt: Some(user_prompt.clone()),
|
||||
};
|
||||
|
||||
let templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)> = vec![
|
||||
(PromptPriority::Mandatory, Box::new(EngineerPreamble {})),
|
||||
(
|
||||
PromptPriority::Ordered { order: 1 },
|
||||
Box::new(RepositoryContext {}),
|
||||
),
|
||||
(
|
||||
PromptPriority::Ordered { order: 0 },
|
||||
Box::new(FileContext {}),
|
||||
),
|
||||
(
|
||||
PromptPriority::Mandatory,
|
||||
Box::new(GenerateInlineContent {}),
|
||||
),
|
||||
];
|
||||
let chain = PromptChain::new(args, templates);
|
||||
let (prompt, _) = chain.generate(true)?;
|
||||
|
||||
anyhow::Ok(prompt)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{AppContext, Context};
|
||||
use indoc::indoc;
|
||||
use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point};
|
||||
use settings::SettingsStore;
|
||||
|
||||
pub(crate) fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)
|
||||
.with_embedding_query(
|
||||
r#"
|
||||
(
|
||||
[(line_comment) (attribute_item)]* @context
|
||||
.
|
||||
[
|
||||
(struct_item
|
||||
name: (_) @name)
|
||||
|
||||
(enum_item
|
||||
name: (_) @name)
|
||||
|
||||
(impl_item
|
||||
trait: (_)? @name
|
||||
"for"? @name
|
||||
type: (_) @name)
|
||||
|
||||
(trait_item
|
||||
name: (_) @name)
|
||||
|
||||
(function_item
|
||||
name: (_) @name
|
||||
body: (block
|
||||
"{" @keep
|
||||
"}" @keep) @collapse)
|
||||
|
||||
(macro_definition
|
||||
name: (_) @name)
|
||||
] @item
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_outline_for_prompt(cx: &mut AppContext) {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language_settings::init(cx);
|
||||
let text = indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
Self { a, b }
|
||||
}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {
|
||||
self.a
|
||||
}
|
||||
|
||||
pub fn b(&self) -> usize {
|
||||
self.b
|
||||
}
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(1, 4)..Point::new(1, 4)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
<|S|>a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(8, 12)..Point::new(8, 14)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {
|
||||
let <|S|a |E|>= 1;
|
||||
let b = 2;
|
||||
Self { a, b }
|
||||
}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(6, 0)..Point::new(6, 0)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
<|S|>
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(21, 0)..Point::new(21, 0)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
<|S|>"}
|
||||
);
|
||||
|
||||
// Ensure nested functions get collapsed properly.
|
||||
let text = indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
Self { a, b }
|
||||
}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {
|
||||
let a = 30;
|
||||
fn nested() -> usize {
|
||||
3
|
||||
}
|
||||
self.a + nested()
|
||||
}
|
||||
|
||||
pub fn b(&self) -> usize {
|
||||
self.b
|
||||
}
|
||||
}
|
||||
"};
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(0, 0)..Point::new(0, 0)),
|
||||
indoc! {"
|
||||
<|S|>struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
}
|
||||
293
crates/assistant2/src/streaming_diff.rs
Normal file
293
crates/assistant2/src/streaming_diff.rs
Normal file
@@ -0,0 +1,293 @@
|
||||
use collections::HashMap;
|
||||
use ordered_float::OrderedFloat;
|
||||
use std::{
|
||||
cmp,
|
||||
fmt::{self, Debug},
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
struct Matrix {
|
||||
cells: Vec<f64>,
|
||||
rows: usize,
|
||||
cols: usize,
|
||||
}
|
||||
|
||||
impl Matrix {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
cells: Vec::new(),
|
||||
rows: 0,
|
||||
cols: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn resize(&mut self, rows: usize, cols: usize) {
|
||||
self.cells.resize(rows * cols, 0.);
|
||||
self.rows = rows;
|
||||
self.cols = cols;
|
||||
}
|
||||
|
||||
fn get(&self, row: usize, col: usize) -> f64 {
|
||||
if row >= self.rows {
|
||||
panic!("row out of bounds")
|
||||
}
|
||||
|
||||
if col >= self.cols {
|
||||
panic!("col out of bounds")
|
||||
}
|
||||
self.cells[col * self.rows + row]
|
||||
}
|
||||
|
||||
fn set(&mut self, row: usize, col: usize, value: f64) {
|
||||
if row >= self.rows {
|
||||
panic!("row out of bounds")
|
||||
}
|
||||
|
||||
if col >= self.cols {
|
||||
panic!("col out of bounds")
|
||||
}
|
||||
|
||||
self.cells[col * self.rows + row] = value;
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Matrix {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(f)?;
|
||||
for i in 0..self.rows {
|
||||
for j in 0..self.cols {
|
||||
write!(f, "{:5}", self.get(i, j))?;
|
||||
}
|
||||
writeln!(f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Hunk {
|
||||
Insert { text: String },
|
||||
Remove { len: usize },
|
||||
Keep { len: usize },
|
||||
}
|
||||
|
||||
pub struct StreamingDiff {
|
||||
old: Vec<char>,
|
||||
new: Vec<char>,
|
||||
scores: Matrix,
|
||||
old_text_ix: usize,
|
||||
new_text_ix: usize,
|
||||
equal_runs: HashMap<(usize, usize), u32>,
|
||||
}
|
||||
|
||||
impl StreamingDiff {
|
||||
const INSERTION_SCORE: f64 = -1.;
|
||||
const DELETION_SCORE: f64 = -20.;
|
||||
const EQUALITY_BASE: f64 = 1.8;
|
||||
const MAX_EQUALITY_EXPONENT: i32 = 16;
|
||||
|
||||
pub fn new(old: String) -> Self {
|
||||
let old = old.chars().collect::<Vec<_>>();
|
||||
let mut scores = Matrix::new();
|
||||
scores.resize(old.len() + 1, 1);
|
||||
for i in 0..=old.len() {
|
||||
scores.set(i, 0, i as f64 * Self::DELETION_SCORE);
|
||||
}
|
||||
Self {
|
||||
old,
|
||||
new: Vec::new(),
|
||||
scores,
|
||||
old_text_ix: 0,
|
||||
new_text_ix: 0,
|
||||
equal_runs: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_new(&mut self, text: &str) -> Vec<Hunk> {
|
||||
self.new.extend(text.chars());
|
||||
self.scores.resize(self.old.len() + 1, self.new.len() + 1);
|
||||
|
||||
for j in self.new_text_ix + 1..=self.new.len() {
|
||||
self.scores.set(0, j, j as f64 * Self::INSERTION_SCORE);
|
||||
for i in 1..=self.old.len() {
|
||||
let insertion_score = self.scores.get(i, j - 1) + Self::INSERTION_SCORE;
|
||||
let deletion_score = self.scores.get(i - 1, j) + Self::DELETION_SCORE;
|
||||
let equality_score = if self.old[i - 1] == self.new[j - 1] {
|
||||
let mut equal_run = self.equal_runs.get(&(i - 1, j - 1)).copied().unwrap_or(0);
|
||||
equal_run += 1;
|
||||
self.equal_runs.insert((i, j), equal_run);
|
||||
|
||||
let exponent = cmp::min(equal_run as i32 / 4, Self::MAX_EQUALITY_EXPONENT);
|
||||
self.scores.get(i - 1, j - 1) + Self::EQUALITY_BASE.powi(exponent)
|
||||
} else {
|
||||
f64::NEG_INFINITY
|
||||
};
|
||||
|
||||
let score = insertion_score.max(deletion_score).max(equality_score);
|
||||
self.scores.set(i, j, score);
|
||||
}
|
||||
}
|
||||
|
||||
let mut max_score = f64::NEG_INFINITY;
|
||||
let mut next_old_text_ix = self.old_text_ix;
|
||||
let next_new_text_ix = self.new.len();
|
||||
for i in self.old_text_ix..=self.old.len() {
|
||||
let score = self.scores.get(i, next_new_text_ix);
|
||||
if score > max_score {
|
||||
max_score = score;
|
||||
next_old_text_ix = i;
|
||||
}
|
||||
}
|
||||
|
||||
let hunks = self.backtrack(next_old_text_ix, next_new_text_ix);
|
||||
self.old_text_ix = next_old_text_ix;
|
||||
self.new_text_ix = next_new_text_ix;
|
||||
hunks
|
||||
}
|
||||
|
||||
fn backtrack(&self, old_text_ix: usize, new_text_ix: usize) -> Vec<Hunk> {
|
||||
let mut pending_insert: Option<Range<usize>> = None;
|
||||
let mut hunks = Vec::new();
|
||||
let mut i = old_text_ix;
|
||||
let mut j = new_text_ix;
|
||||
while (i, j) != (self.old_text_ix, self.new_text_ix) {
|
||||
let insertion_score = if j > self.new_text_ix {
|
||||
Some((i, j - 1))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let deletion_score = if i > self.old_text_ix {
|
||||
Some((i - 1, j))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let equality_score = if i > self.old_text_ix && j > self.new_text_ix {
|
||||
if self.old[i - 1] == self.new[j - 1] {
|
||||
Some((i - 1, j - 1))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (prev_i, prev_j) = [insertion_score, deletion_score, equality_score]
|
||||
.iter()
|
||||
.max_by_key(|cell| cell.map(|(i, j)| OrderedFloat(self.scores.get(i, j))))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
if prev_i == i && prev_j == j - 1 {
|
||||
if let Some(pending_insert) = pending_insert.as_mut() {
|
||||
pending_insert.start = prev_j;
|
||||
} else {
|
||||
pending_insert = Some(prev_j..j);
|
||||
}
|
||||
} else {
|
||||
if let Some(range) = pending_insert.take() {
|
||||
hunks.push(Hunk::Insert {
|
||||
text: self.new[range].iter().collect(),
|
||||
});
|
||||
}
|
||||
|
||||
let char_len = self.old[i - 1].len_utf8();
|
||||
if prev_i == i - 1 && prev_j == j {
|
||||
if let Some(Hunk::Remove { len }) = hunks.last_mut() {
|
||||
*len += char_len;
|
||||
} else {
|
||||
hunks.push(Hunk::Remove { len: char_len })
|
||||
}
|
||||
} else {
|
||||
if let Some(Hunk::Keep { len }) = hunks.last_mut() {
|
||||
*len += char_len;
|
||||
} else {
|
||||
hunks.push(Hunk::Keep { len: char_len })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i = prev_i;
|
||||
j = prev_j;
|
||||
}
|
||||
|
||||
if let Some(range) = pending_insert.take() {
|
||||
hunks.push(Hunk::Insert {
|
||||
text: self.new[range].iter().collect(),
|
||||
});
|
||||
}
|
||||
|
||||
hunks.reverse();
|
||||
hunks
|
||||
}
|
||||
|
||||
pub fn finish(self) -> Vec<Hunk> {
|
||||
self.backtrack(self.old.len(), self.new.len())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::env;
|
||||
|
||||
use super::*;
|
||||
use rand::prelude::*;
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_diffs(mut rng: StdRng) {
|
||||
let old_text_len = env::var("OLD_TEXT_LEN")
|
||||
.map(|i| i.parse().expect("invalid `OLD_TEXT_LEN` variable"))
|
||||
.unwrap_or(10);
|
||||
let new_text_len = env::var("NEW_TEXT_LEN")
|
||||
.map(|i| i.parse().expect("invalid `NEW_TEXT_LEN` variable"))
|
||||
.unwrap_or(10);
|
||||
|
||||
let old = util::RandomCharIter::new(&mut rng)
|
||||
.take(old_text_len)
|
||||
.collect::<String>();
|
||||
log::info!("old text: {:?}", old);
|
||||
|
||||
let mut diff = StreamingDiff::new(old.clone());
|
||||
let mut hunks = Vec::new();
|
||||
let mut new_len = 0;
|
||||
let mut new = String::new();
|
||||
while new_len < new_text_len {
|
||||
let new_chunk_len = rng.gen_range(1..=new_text_len - new_len);
|
||||
let new_chunk = util::RandomCharIter::new(&mut rng)
|
||||
.take(new_len)
|
||||
.collect::<String>();
|
||||
log::info!("new chunk: {:?}", new_chunk);
|
||||
new_len += new_chunk_len;
|
||||
new.push_str(&new_chunk);
|
||||
let new_hunks = diff.push_new(&new_chunk);
|
||||
log::info!("hunks: {:?}", new_hunks);
|
||||
hunks.extend(new_hunks);
|
||||
}
|
||||
let final_hunks = diff.finish();
|
||||
log::info!("final hunks: {:?}", final_hunks);
|
||||
hunks.extend(final_hunks);
|
||||
|
||||
log::info!("new text: {:?}", new);
|
||||
let mut old_ix = 0;
|
||||
let mut new_ix = 0;
|
||||
let mut patched = String::new();
|
||||
for hunk in hunks {
|
||||
match hunk {
|
||||
Hunk::Keep { len } => {
|
||||
assert_eq!(&old[old_ix..old_ix + len], &new[new_ix..new_ix + len]);
|
||||
patched.push_str(&old[old_ix..old_ix + len]);
|
||||
old_ix += len;
|
||||
new_ix += len;
|
||||
}
|
||||
Hunk::Remove { len } => {
|
||||
old_ix += len;
|
||||
}
|
||||
Hunk::Insert { text } => {
|
||||
assert_eq!(text, &new[new_ix..new_ix + text.len()]);
|
||||
patched.push_str(&text);
|
||||
new_ix += text.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(patched, new);
|
||||
}
|
||||
}
|
||||
@@ -85,15 +85,7 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
|
||||
AutoUpdateSetting::register(cx);
|
||||
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
|
||||
workspace
|
||||
.register_action(|_, action: &Check, cx| check(action, cx))
|
||||
.register_action(|_, _action: &CheckThatAutoUpdaterWorks, cx| {
|
||||
let prompt = cx.prompt(gpui::PromptLevel::Info, "It does!", &["Ok"]);
|
||||
cx.spawn(|_, _cx| async move {
|
||||
prompt.await.ok();
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
workspace.register_action(|_, action: &Check, cx| check(action, cx));
|
||||
|
||||
// @nate - code to trigger update notification on launch
|
||||
// workspace.show_notification(0, _cx, |cx| {
|
||||
@@ -102,7 +94,7 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
|
||||
})
|
||||
.detach();
|
||||
|
||||
if let Some(version) = *ZED_APP_VERSION {
|
||||
if let Some(version) = ZED_APP_VERSION.or_else(|| cx.app_metadata().app_version) {
|
||||
let auto_updater = cx.build_model(|cx| {
|
||||
let updater = AutoUpdater::new(version, http_client, server_url);
|
||||
|
||||
@@ -130,9 +122,15 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check(_: &Check, cx: &mut AppContext) {
|
||||
pub fn check(_: &Check, cx: &mut ViewContext<Workspace>) {
|
||||
if let Some(updater) = AutoUpdater::get(cx) {
|
||||
updater.update(cx, |updater, cx| updater.poll(cx));
|
||||
} else {
|
||||
drop(cx.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
"Auto-updates disabled for non-bundled app.",
|
||||
&["Ok"],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,6 @@ impl UpdateNotification {
|
||||
}
|
||||
|
||||
pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent::Dismiss);
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
28
crates/breadcrumbs2/Cargo.toml
Normal file
28
crates/breadcrumbs2/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "breadcrumbs2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/breadcrumbs.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
collections = { path = "../collections" }
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
search = { package = "search2", path = "../search2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
outline = { package = "outline2", path = "../outline2" }
|
||||
itertools = "0.10"
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
||||
140
crates/breadcrumbs2/src/breadcrumbs.rs
Normal file
140
crates/breadcrumbs2/src/breadcrumbs.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
use gpui::{
|
||||
Div, Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription,
|
||||
ViewContext, WeakView,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use theme::ActiveTheme;
|
||||
use ui::{prelude::*, ButtonLike, ButtonStyle, Label, Tooltip};
|
||||
use workspace::{
|
||||
item::{ItemEvent, ItemHandle},
|
||||
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
};
|
||||
|
||||
pub enum Event {
|
||||
UpdateLocation,
|
||||
}
|
||||
|
||||
pub struct Breadcrumbs {
|
||||
pane_focused: bool,
|
||||
active_item: Option<Box<dyn ItemHandle>>,
|
||||
subscription: Option<Subscription>,
|
||||
workspace: WeakView<Workspace>,
|
||||
}
|
||||
|
||||
impl Breadcrumbs {
|
||||
pub fn new(workspace: &Workspace) -> Self {
|
||||
Self {
|
||||
pane_focused: false,
|
||||
active_item: Default::default(),
|
||||
subscription: Default::default(),
|
||||
workspace: workspace.weak_handle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for Breadcrumbs {}
|
||||
impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
|
||||
|
||||
impl Render for Breadcrumbs {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let element = h_stack().text_ui();
|
||||
|
||||
let Some(active_item) = &self
|
||||
.active_item
|
||||
.as_ref()
|
||||
.filter(|item| item.downcast::<editor::Editor>().is_some())
|
||||
else {
|
||||
return element;
|
||||
};
|
||||
|
||||
let Some(segments) = active_item.breadcrumbs(cx.theme(), cx) else {
|
||||
return element;
|
||||
};
|
||||
|
||||
let highlighted_segments = segments.into_iter().map(|segment| {
|
||||
StyledText::new(segment.text)
|
||||
.with_highlights(&cx.text_style(), segment.highlights.unwrap_or_default())
|
||||
.into_any()
|
||||
});
|
||||
let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
|
||||
Label::new("›").into_any_element()
|
||||
});
|
||||
|
||||
element.child(
|
||||
ButtonLike::new("toggle outline view")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(h_stack().gap_1().children(breadcrumbs))
|
||||
// We disable the button when the containing pane is not focused:
|
||||
// Because right now all the breadcrumb does is open the outline view, which is an
|
||||
// action which operates on the active editor, clicking the breadcrumbs of another
|
||||
// editor could cause weirdness. I remember that at one point it actually caused a
|
||||
// panic weirdly.
|
||||
//
|
||||
// It might be possible that with changes around how focus is managed that we
|
||||
// might be able to update the active editor to the one with the breadcrumbs
|
||||
// clicked on? That or we could just add a code path for being able to open the
|
||||
// outline for a specific editor. Long term we'd like for it to be an actual
|
||||
// breadcrumb bar so that problem goes away
|
||||
//
|
||||
// — Julia (https://github.com/zed-industries/zed/pull/3505#pullrequestreview-1766198050)
|
||||
.disabled(!self.pane_focused)
|
||||
.on_click(cx.listener(|breadcrumbs, _, cx| {
|
||||
if let Some(workspace) = breadcrumbs.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
outline::toggle(workspace, &outline::Toggle, cx)
|
||||
})
|
||||
}
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::for_action("Show symbol outline", &outline::Toggle, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolbarItemView for Breadcrumbs {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> ToolbarItemLocation {
|
||||
cx.notify();
|
||||
self.active_item = None;
|
||||
if let Some(item) = active_pane_item {
|
||||
let this = cx.view().downgrade();
|
||||
self.subscription = Some(item.subscribe_to_item_events(
|
||||
cx,
|
||||
Box::new(move |event, cx| {
|
||||
if let ItemEvent::UpdateBreadcrumbs = event {
|
||||
this.update(cx, |_, cx| {
|
||||
cx.emit(Event::UpdateLocation);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
));
|
||||
self.active_item = Some(item.boxed_clone());
|
||||
item.breadcrumb_location(cx)
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
|
||||
// fn location_for_event(
|
||||
// &self,
|
||||
// _: &Event,
|
||||
// current_location: ToolbarItemLocation,
|
||||
// cx: &AppContext,
|
||||
// ) -> ToolbarItemLocation {
|
||||
// if let Some(active_item) = self.active_item.as_ref() {
|
||||
// active_item.breadcrumb_location(cx)
|
||||
// } else {
|
||||
// current_location
|
||||
// }
|
||||
// }
|
||||
|
||||
fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
|
||||
self.pane_focused = pane_focused;
|
||||
}
|
||||
}
|
||||
@@ -31,9 +31,7 @@ media = { path = "../media" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
util = { path = "../util" }
|
||||
ui = {package = "ui2", path = "../ui2"}
|
||||
workspace = {package = "workspace2", path = "../workspace2"}
|
||||
async-trait.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
async-broadcast = "0.4"
|
||||
futures.workspace = true
|
||||
|
||||
@@ -1,32 +1,25 @@
|
||||
pub mod call_settings;
|
||||
pub mod participant;
|
||||
pub mod room;
|
||||
mod shared_screen;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use audio::Audio;
|
||||
use call_settings::CallSettings;
|
||||
use client::{
|
||||
proto::{self, PeerId},
|
||||
Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE,
|
||||
};
|
||||
use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
|
||||
use collections::HashSet;
|
||||
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, PromptLevel,
|
||||
Subscription, Task, View, ViewContext, VisualContext, WeakModel, WeakView, WindowHandle,
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task,
|
||||
WeakModel,
|
||||
};
|
||||
pub use participant::ParticipantLocation;
|
||||
use postage::watch;
|
||||
use project::Project;
|
||||
use room::Event;
|
||||
pub use room::Room;
|
||||
use settings::Settings;
|
||||
use shared_screen::SharedScreen;
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
use workspace::{item::ItemHandle, CallHandler, Pane, Workspace};
|
||||
|
||||
pub use participant::ParticipantLocation;
|
||||
pub use room::Room;
|
||||
|
||||
pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
|
||||
CallSettings::register(cx);
|
||||
@@ -334,55 +327,12 @@ impl ActiveCall {
|
||||
pub fn join_channel(
|
||||
&mut self,
|
||||
channel_id: u64,
|
||||
requesting_window: Option<WindowHandle<Workspace>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Option<Model<Room>>>> {
|
||||
if let Some(room) = self.room().cloned() {
|
||||
if room.read(cx).channel_id() == Some(channel_id) {
|
||||
return cx.spawn(|_, _| async move {
|
||||
todo!();
|
||||
// let future = room.update(&mut cx, |room, cx| {
|
||||
// room.most_active_project(cx).map(|(host, project)| {
|
||||
// room.join_project(project, host, app_state.clone(), cx)
|
||||
// })
|
||||
// })
|
||||
|
||||
// if let Some(future) = future {
|
||||
// future.await?;
|
||||
// }
|
||||
|
||||
// Ok(Some(room))
|
||||
});
|
||||
}
|
||||
|
||||
let should_prompt = room.update(cx, |room, _| {
|
||||
room.channel_id().is_some()
|
||||
&& room.is_sharing_project()
|
||||
&& room.remote_participants().len() > 0
|
||||
});
|
||||
if should_prompt && requesting_window.is_some() {
|
||||
return cx.spawn(|this, mut cx| async move {
|
||||
let answer = requesting_window.unwrap().update(&mut cx, |_, cx| {
|
||||
cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
"Leaving this call will unshare your current project.\nDo you want to switch channels?",
|
||||
&["Yes, Join Channel", "Cancel"],
|
||||
)
|
||||
})?;
|
||||
if answer.await? == 1 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
room.update(&mut cx, |room, cx| room.clear_state(cx))?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.join_channel(channel_id, requesting_window, cx)
|
||||
})?
|
||||
.await
|
||||
});
|
||||
}
|
||||
|
||||
if room.read(cx).channel_id().is_some() {
|
||||
return Task::ready(Ok(Some(room)));
|
||||
} else {
|
||||
room.update(cx, |room, cx| room.clear_state(cx));
|
||||
}
|
||||
}
|
||||
@@ -555,208 +505,6 @@ pub fn report_call_event_for_channel(
|
||||
)
|
||||
}
|
||||
|
||||
pub struct Call {
|
||||
active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
|
||||
parent_workspace: WeakView<Workspace>,
|
||||
}
|
||||
|
||||
impl Call {
|
||||
pub fn new(
|
||||
parent_workspace: WeakView<Workspace>,
|
||||
cx: &mut ViewContext<'_, Workspace>,
|
||||
) -> Box<dyn CallHandler> {
|
||||
let mut active_call = None;
|
||||
if cx.has_global::<Model<ActiveCall>>() {
|
||||
let call = cx.global::<Model<ActiveCall>>().clone();
|
||||
let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)];
|
||||
active_call = Some((call, subscriptions));
|
||||
}
|
||||
Box::new(Self {
|
||||
active_call,
|
||||
parent_workspace,
|
||||
})
|
||||
}
|
||||
fn on_active_call_event(
|
||||
workspace: &mut Workspace,
|
||||
_: Model<ActiveCall>,
|
||||
event: &room::Event,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
match event {
|
||||
room::Event::ParticipantLocationChanged { participant_id }
|
||||
| room::Event::RemoteVideoTracksChanged { participant_id } => {
|
||||
workspace.leader_updated(*participant_id, cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl CallHandler for Call {
|
||||
fn peer_state(
|
||||
&mut self,
|
||||
leader_id: PeerId,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Option<(bool, bool)> {
|
||||
let (call, _) = self.active_call.as_ref()?;
|
||||
let room = call.read(cx).room()?.read(cx);
|
||||
let participant = room.remote_participant_for_peer_id(leader_id)?;
|
||||
|
||||
let leader_in_this_app;
|
||||
let leader_in_this_project;
|
||||
match participant.location {
|
||||
ParticipantLocation::SharedProject { project_id } => {
|
||||
leader_in_this_app = true;
|
||||
leader_in_this_project = Some(project_id)
|
||||
== self
|
||||
.parent_workspace
|
||||
.update(cx, |this, cx| this.project().read(cx).remote_id())
|
||||
.log_err()
|
||||
.flatten();
|
||||
}
|
||||
ParticipantLocation::UnsharedProject => {
|
||||
leader_in_this_app = true;
|
||||
leader_in_this_project = false;
|
||||
}
|
||||
ParticipantLocation::External => {
|
||||
leader_in_this_app = false;
|
||||
leader_in_this_project = false;
|
||||
}
|
||||
};
|
||||
|
||||
Some((leader_in_this_project, leader_in_this_app))
|
||||
}
|
||||
|
||||
fn shared_screen_for_peer(
|
||||
&self,
|
||||
peer_id: PeerId,
|
||||
pane: &View<Pane>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Option<Box<dyn ItemHandle>> {
|
||||
let (call, _) = self.active_call.as_ref()?;
|
||||
let room = call.read(cx).room()?.read(cx);
|
||||
let participant = room.remote_participant_for_peer_id(peer_id)?;
|
||||
let track = participant.video_tracks.values().next()?.clone();
|
||||
let user = participant.user.clone();
|
||||
for item in pane.read(cx).items_of_type::<SharedScreen>() {
|
||||
if item.read(cx).peer_id == peer_id {
|
||||
return Some(Box::new(item));
|
||||
}
|
||||
}
|
||||
|
||||
Some(Box::new(cx.build_view(|cx| {
|
||||
SharedScreen::new(&track, peer_id, user.clone(), cx)
|
||||
})))
|
||||
}
|
||||
fn room_id(&self, cx: &AppContext) -> Option<u64> {
|
||||
Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id())
|
||||
}
|
||||
fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
let Some((call, _)) = self.active_call.as_ref() else {
|
||||
return Task::ready(Err(anyhow!("Cannot exit a call; not in a call")));
|
||||
};
|
||||
|
||||
call.update(cx, |this, cx| this.hang_up(cx))
|
||||
}
|
||||
fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>> {
|
||||
ActiveCall::global(cx).read(cx).location().cloned()
|
||||
}
|
||||
fn invite(
|
||||
&mut self,
|
||||
called_user_id: u64,
|
||||
initial_project: Option<Model<Project>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
ActiveCall::global(cx).update(cx, |this, cx| {
|
||||
this.invite(called_user_id, initial_project, cx)
|
||||
})
|
||||
}
|
||||
fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, PeerId)>> {
|
||||
self.active_call
|
||||
.as_ref()
|
||||
.map(|call| {
|
||||
call.0.read(cx).room().map(|room| {
|
||||
room.read(cx)
|
||||
.remote_participants()
|
||||
.iter()
|
||||
.map(|participant| {
|
||||
(participant.1.user.clone(), participant.1.peer_id.clone())
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
fn is_muted(&self, cx: &AppContext) -> Option<bool> {
|
||||
self.active_call
|
||||
.as_ref()
|
||||
.map(|call| {
|
||||
call.0
|
||||
.read(cx)
|
||||
.room()
|
||||
.map(|room| room.read(cx).is_muted(cx))
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
fn toggle_mute(&self, cx: &mut AppContext) {
|
||||
self.active_call.as_ref().map(|call| {
|
||||
call.0.update(cx, |this, cx| {
|
||||
this.room().map(|room| {
|
||||
let room = room.clone();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
room.update(&mut cx, |this, cx| this.toggle_mute(cx))??
|
||||
.await
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
fn toggle_screen_share(&self, cx: &mut AppContext) {
|
||||
self.active_call.as_ref().map(|call| {
|
||||
call.0.update(cx, |this, cx| {
|
||||
this.room().map(|room| {
|
||||
room.update(cx, |this, cx| {
|
||||
if this.is_screen_sharing() {
|
||||
this.unshare_screen(cx).log_err();
|
||||
} else {
|
||||
let t = this.share_screen(cx);
|
||||
cx.spawn(move |_, _| async move {
|
||||
t.await.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
fn toggle_deafen(&self, cx: &mut AppContext) {
|
||||
self.active_call.as_ref().map(|call| {
|
||||
call.0.update(cx, |this, cx| {
|
||||
this.room().map(|room| {
|
||||
room.update(cx, |this, cx| {
|
||||
this.toggle_deafen(cx).log_err();
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
fn is_deafened(&self, cx: &AppContext) -> Option<bool> {
|
||||
self.active_call
|
||||
.as_ref()
|
||||
.map(|call| {
|
||||
call.0
|
||||
.read(cx)
|
||||
.room()
|
||||
.map(|room| room.read(cx).is_deafened())
|
||||
})
|
||||
.flatten()
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use gpui::TestAppContext;
|
||||
|
||||
@@ -4,7 +4,7 @@ use client::{proto, User};
|
||||
use collections::HashMap;
|
||||
use gpui::WeakModel;
|
||||
pub use live_kit_client::Frame;
|
||||
pub(crate) use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
|
||||
pub use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
|
||||
use project::Project;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
use crate::participant::{Frame, RemoteVideoTrack};
|
||||
use anyhow::Result;
|
||||
use client::{proto::PeerId, User};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
div, AppContext, Div, Element, EventEmitter, FocusHandle, FocusableView, ParentElement, Render,
|
||||
SharedString, Task, View, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use std::sync::{Arc, Weak};
|
||||
use workspace::{item::Item, ItemNavHistory, WorkspaceId};
|
||||
|
||||
pub enum Event {
|
||||
Close,
|
||||
}
|
||||
|
||||
pub struct SharedScreen {
|
||||
track: Weak<RemoteVideoTrack>,
|
||||
frame: Option<Frame>,
|
||||
// temporary addition just to render something interactive.
|
||||
current_frame_id: usize,
|
||||
pub peer_id: PeerId,
|
||||
user: Arc<User>,
|
||||
nav_history: Option<ItemNavHistory>,
|
||||
_maintain_frame: Task<Result<()>>,
|
||||
focus: FocusHandle,
|
||||
}
|
||||
|
||||
impl SharedScreen {
|
||||
pub fn new(
|
||||
track: &Arc<RemoteVideoTrack>,
|
||||
peer_id: PeerId,
|
||||
user: Arc<User>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
cx.focus_handle();
|
||||
let mut frames = track.frames();
|
||||
Self {
|
||||
track: Arc::downgrade(track),
|
||||
frame: None,
|
||||
peer_id,
|
||||
user,
|
||||
nav_history: Default::default(),
|
||||
_maintain_frame: cx.spawn(|this, mut cx| async move {
|
||||
while let Some(frame) = frames.next().await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.frame = Some(frame);
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
this.update(&mut cx, |_, cx| cx.emit(Event::Close))?;
|
||||
Ok(())
|
||||
}),
|
||||
focus: cx.focus_handle(),
|
||||
current_frame_id: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for SharedScreen {}
|
||||
impl EventEmitter<workspace::item::ItemEvent> for SharedScreen {}
|
||||
|
||||
impl FocusableView for SharedScreen {
|
||||
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
|
||||
self.focus.clone()
|
||||
}
|
||||
}
|
||||
impl Render for SharedScreen {
|
||||
type Element = Div;
|
||||
fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element {
|
||||
let frame = self.frame.clone();
|
||||
let frame_id = self.current_frame_id;
|
||||
self.current_frame_id = self.current_frame_id.wrapping_add(1);
|
||||
div().children(frame.map(|_| {
|
||||
ui::Label::new(frame_id.to_string()).color(ui::Color::Error)
|
||||
// img().data(Arc::new(ImageData::new(image::ImageBuffer::new(
|
||||
// frame.width() as u32,
|
||||
// frame.height() as u32,
|
||||
// ))))
|
||||
}))
|
||||
}
|
||||
}
|
||||
// impl View for SharedScreen {
|
||||
// fn ui_name() -> &'static str {
|
||||
// "SharedScreen"
|
||||
// }
|
||||
|
||||
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
// enum Focus {}
|
||||
|
||||
// let frame = self.frame.clone();
|
||||
// MouseEventHandler::new::<Focus, _>(0, cx, |_, cx| {
|
||||
// Canvas::new(move |bounds, _, _, cx| {
|
||||
// if let Some(frame) = frame.clone() {
|
||||
// let size = constrain_size_preserving_aspect_ratio(
|
||||
// bounds.size(),
|
||||
// vec2f(frame.width() as f32, frame.height() as f32),
|
||||
// );
|
||||
// let origin = bounds.origin() + (bounds.size() / 2.) - size / 2.;
|
||||
// cx.scene().push_surface(gpui::platform::mac::Surface {
|
||||
// bounds: RectF::new(origin, size),
|
||||
// image_buffer: frame.image(),
|
||||
// });
|
||||
// }
|
||||
// })
|
||||
// .contained()
|
||||
// .with_style(theme::current(cx).shared_screen)
|
||||
// })
|
||||
// .on_down(MouseButton::Left, |_, _, cx| cx.focus_parent())
|
||||
// .into_any()
|
||||
// }
|
||||
// }
|
||||
|
||||
impl Item for SharedScreen {
|
||||
fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
|
||||
Some(format!("{}'s screen", self.user.github_login).into())
|
||||
}
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(nav_history) = self.nav_history.as_mut() {
|
||||
nav_history.push::<()>(None, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_content(&self, _: Option<usize>, _: &WindowContext<'_>) -> gpui::AnyElement {
|
||||
div().child("Shared screen").into_any()
|
||||
// Flex::row()
|
||||
// .with_child(
|
||||
// Svg::new("icons/desktop.svg")
|
||||
// .with_color(style.label.text.color)
|
||||
// .constrained()
|
||||
// .with_width(style.type_icon_width)
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_margin_right(style.spacing),
|
||||
// )
|
||||
// .with_child(
|
||||
// Label::new(
|
||||
// format!("{}'s screen", self.user.github_login),
|
||||
// style.label.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// )
|
||||
// .into_any()
|
||||
}
|
||||
|
||||
fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
|
||||
self.nav_history = Some(history);
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: WorkspaceId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<View<Self>> {
|
||||
let track = self.track.upgrade()?;
|
||||
Some(cx.build_view(|cx| Self::new(&track, self.peer_id, self.user.clone(), cx)))
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,8 @@ use collections::{hash_map, HashMap, HashSet};
|
||||
use db::RELEASE_CHANNEL;
|
||||
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SharedString, Task,
|
||||
WeakModel,
|
||||
};
|
||||
use rpc::{
|
||||
proto::{self, ChannelVisibility},
|
||||
@@ -46,7 +47,7 @@ pub struct ChannelStore {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Channel {
|
||||
pub id: ChannelId,
|
||||
pub name: String,
|
||||
pub name: SharedString,
|
||||
pub visibility: proto::ChannelVisibility,
|
||||
pub role: proto::ChannelRole,
|
||||
pub unseen_note_version: Option<(u64, clock::Global)>,
|
||||
@@ -895,14 +896,16 @@ impl ChannelStore {
|
||||
.channel_invitations
|
||||
.binary_search_by_key(&channel.id, |c| c.id)
|
||||
{
|
||||
Ok(ix) => Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name,
|
||||
Ok(ix) => {
|
||||
Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name.into()
|
||||
}
|
||||
Err(ix) => self.channel_invitations.insert(
|
||||
ix,
|
||||
Arc::new(Channel {
|
||||
id: channel.id,
|
||||
visibility: channel.visibility(),
|
||||
role: channel.role(),
|
||||
name: channel.name,
|
||||
name: channel.name.into(),
|
||||
unseen_note_version: None,
|
||||
unseen_message_id: None,
|
||||
parent_path: channel.parent_path,
|
||||
|
||||
@@ -104,7 +104,7 @@ impl<'a> ChannelPathsInsertGuard<'a> {
|
||||
|
||||
existing_channel.visibility = channel_proto.visibility();
|
||||
existing_channel.role = channel_proto.role();
|
||||
existing_channel.name = channel_proto.name;
|
||||
existing_channel.name = channel_proto.name.into();
|
||||
} else {
|
||||
self.channels_by_id.insert(
|
||||
channel_proto.id,
|
||||
@@ -112,7 +112,7 @@ impl<'a> ChannelPathsInsertGuard<'a> {
|
||||
id: channel_proto.id,
|
||||
visibility: channel_proto.visibility(),
|
||||
role: channel_proto.role(),
|
||||
name: channel_proto.name,
|
||||
name: channel_proto.name.into(),
|
||||
unseen_note_version: None,
|
||||
unseen_message_id: None,
|
||||
parent_path: channel_proto.parent_path,
|
||||
@@ -146,11 +146,11 @@ fn channel_path_sorting_key<'a>(
|
||||
let (parent_path, name) = channels_by_id
|
||||
.get(&id)
|
||||
.map_or((&[] as &[_], None), |channel| {
|
||||
(channel.parent_path.as_slice(), Some(channel.name.as_str()))
|
||||
(channel.parent_path.as_slice(), Some(channel.name.as_ref()))
|
||||
});
|
||||
parent_path
|
||||
.iter()
|
||||
.filter_map(|id| Some(channels_by_id.get(id)?.name.as_str()))
|
||||
.filter_map(|id| Some(channels_by_id.get(id)?.name.as_ref()))
|
||||
.chain(name)
|
||||
}
|
||||
|
||||
|
||||
@@ -346,7 +346,7 @@ impl<T: Entity> Drop for PendingEntitySubscription<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct TelemetrySettings {
|
||||
pub diagnostics: bool,
|
||||
pub metrics: bool,
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.29.0"
|
||||
version = "0.30.1"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
|
||||
@@ -116,12 +116,13 @@ struct CreateUserResponse {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Panic {
|
||||
version: String,
|
||||
release_channel: String,
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[instrument(skip(panic))]
|
||||
async fn trace_panic(panic: Json<Panic>) -> Result<()> {
|
||||
tracing::error!(version = %panic.version, text = %panic.text, "panic report");
|
||||
tracing::error!(version = %panic.version, release_channel = %panic.release_channel, text = %panic.text, "panic report");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1220,6 +1220,13 @@ impl Database {
|
||||
self.check_user_is_channel_admin(&new_parent, admin_id, &*tx)
|
||||
.await?;
|
||||
|
||||
if new_parent
|
||||
.ancestors_including_self()
|
||||
.any(|id| id == channel.id)
|
||||
{
|
||||
Err(anyhow!("cannot move a channel into one of its descendants"))?;
|
||||
}
|
||||
|
||||
new_parent_path = new_parent.path();
|
||||
new_parent_channel = Some(new_parent);
|
||||
} else {
|
||||
|
||||
@@ -450,6 +450,20 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||
(livestreaming_id, &[projects_id]),
|
||||
],
|
||||
);
|
||||
|
||||
// Can't move a channel into its ancestor
|
||||
db.move_channel(projects_id, Some(livestreaming_id), user_id)
|
||||
.await
|
||||
.unwrap_err();
|
||||
let result = db.get_channels_for_user(user_id).await.unwrap();
|
||||
assert_channel_tree(
|
||||
result.channels,
|
||||
&[
|
||||
(zed_id, &[]),
|
||||
(projects_id, &[]),
|
||||
(livestreaming_id, &[projects_id]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
|
||||
@@ -4,8 +4,10 @@ use collab_ui::notifications::project_shared_notification::ProjectSharedNotifica
|
||||
use editor::{Editor, ExcerptRange, MultiBuffer};
|
||||
use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle};
|
||||
use live_kit_client::MacOSDisplay;
|
||||
use project::project_settings::ProjectSettings;
|
||||
use rpc::proto::PeerId;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
use workspace::{
|
||||
dock::{test::TestPanel, DockPosition},
|
||||
@@ -1602,6 +1604,141 @@ async fn test_following_across_workspaces(
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_following_into_excluded_file(
|
||||
deterministic: Arc<Deterministic>,
|
||||
mut cx_a: &mut TestAppContext,
|
||||
mut cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
for cx in [&mut cx_a, &mut cx_b] {
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||
project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
cx_a.update(editor::init);
|
||||
cx_b.update(editor::init);
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
".git": {
|
||||
"COMMIT_EDITMSG": "write your commit message here",
|
||||
},
|
||||
"1.txt": "one\none\none",
|
||||
"2.txt": "two\ntwo\ntwo",
|
||||
"3.txt": "three\nthree\nthree",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let window_a = client_a.build_workspace(&project_a, cx_a);
|
||||
let workspace_a = window_a.root(cx_a);
|
||||
let peer_id_a = client_a.peer_id().unwrap();
|
||||
let window_b = client_b.build_workspace(&project_b, cx_b);
|
||||
let workspace_b = window_b.root(cx_b);
|
||||
|
||||
// Client A opens editors for a regular file and an excluded file.
|
||||
let editor_for_regular = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
let editor_for_excluded_a = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
// Client A updates their selections in those editors
|
||||
editor_for_regular.update(cx_a, |editor, cx| {
|
||||
editor.handle_input("a", cx);
|
||||
editor.handle_input("b", cx);
|
||||
editor.handle_input("c", cx);
|
||||
editor.select_left(&Default::default(), cx);
|
||||
assert_eq!(editor.selections.ranges(cx), vec![3..2]);
|
||||
});
|
||||
editor_for_excluded_a.update(cx_a, |editor, cx| {
|
||||
editor.select_all(&Default::default(), cx);
|
||||
editor.handle_input("new commit message", cx);
|
||||
editor.select_left(&Default::default(), cx);
|
||||
assert_eq!(editor.selections.ranges(cx), vec![18..17]);
|
||||
});
|
||||
|
||||
// When client B starts following client A, currently visible file is replicated
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.follow(peer_id_a, cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let editor_for_excluded_b = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap()
|
||||
});
|
||||
assert_eq!(
|
||||
cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
|
||||
Some((worktree_id, ".git/COMMIT_EDITMSG").into())
|
||||
);
|
||||
assert_eq!(
|
||||
editor_for_excluded_b.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
vec![18..17]
|
||||
);
|
||||
|
||||
// Changes from B to the excluded file are replicated in A's editor
|
||||
editor_for_excluded_b.update(cx_b, |editor, cx| {
|
||||
editor.handle_input("\nCo-Authored-By: B <b@b.b>", cx);
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
editor_for_excluded_a.update(cx_a, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"new commit messag\nCo-Authored-By: B <b@b.b>"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn visible_push_notifications(
|
||||
cx: &mut TestAppContext,
|
||||
) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
|
||||
|
||||
@@ -2981,11 +2981,10 @@ async fn test_fs_operations(
|
||||
|
||||
let entry = project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "c.txt"), false, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "c.txt"), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
assert_eq!(
|
||||
@@ -3010,7 +3009,6 @@ async fn test_fs_operations(
|
||||
.update(cx_b, |project, cx| {
|
||||
project.rename_entry(entry.id, Path::new("d.txt"), cx)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
@@ -3034,11 +3032,10 @@ async fn test_fs_operations(
|
||||
|
||||
let dir_entry = project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR"), true, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
assert_eq!(
|
||||
@@ -3061,25 +3058,19 @@ async fn test_fs_operations(
|
||||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR/e.txt"), false, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR/e.txt"), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -3120,9 +3111,7 @@ async fn test_fs_operations(
|
||||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.copy_entry(entry.id, Path::new("f.txt"), cx)
|
||||
.unwrap()
|
||||
project.copy_entry(entry.id, Path::new("f.txt"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -665,7 +665,6 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
ensure_project_shared(&project, client, cx).await;
|
||||
project
|
||||
.update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
|
||||
.unwrap()
|
||||
.await?;
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ settings = { package = "settings2", path = "../settings2", features = ["test-sup
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
||||
|
||||
collab_ui = { path = "../collab_ui", features = ["test-support"] }
|
||||
collab_ui = { path = "../collab_ui2", package = "collab_ui2", features = ["test-support"] }
|
||||
|
||||
async-trait.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
|
||||
@@ -1220,6 +1220,13 @@ impl Database {
|
||||
self.check_user_is_channel_admin(&new_parent, admin_id, &*tx)
|
||||
.await?;
|
||||
|
||||
if new_parent
|
||||
.ancestors_including_self()
|
||||
.any(|id| id == channel.id)
|
||||
{
|
||||
Err(anyhow!("cannot move a channel into one of its descendants"))?;
|
||||
}
|
||||
|
||||
new_parent_path = new_parent.path();
|
||||
new_parent_channel = Some(new_parent);
|
||||
} else {
|
||||
|
||||
@@ -420,8 +420,6 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Dag is: zed - projects - livestreaming
|
||||
|
||||
// Move to same parent should be a no-op
|
||||
assert!(db
|
||||
.move_channel(projects_id, Some(zed_id), user_id)
|
||||
@@ -450,6 +448,20 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||
(livestreaming_id, &[projects_id]),
|
||||
],
|
||||
);
|
||||
|
||||
// Can't move a channel into its ancestor
|
||||
db.move_channel(projects_id, Some(livestreaming_id), user_id)
|
||||
.await
|
||||
.unwrap_err();
|
||||
let result = db.get_channels_for_user(user_id).await.unwrap();
|
||||
assert_channel_tree(
|
||||
result.channels,
|
||||
&[
|
||||
(zed_id, &[]),
|
||||
(projects_id, &[]),
|
||||
(livestreaming_id, &[projects_id]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ use call::ActiveCall;
|
||||
use channel::{ChannelId, ChannelMembership, ChannelStore};
|
||||
use client::User;
|
||||
use futures::future::try_join_all;
|
||||
use gpui::{BackgroundExecutor, Model, TestAppContext};
|
||||
use gpui::{BackgroundExecutor, Model, SharedString, TestAppContext};
|
||||
use rpc::{
|
||||
proto::{self, ChannelRole},
|
||||
RECEIVE_TIMEOUT,
|
||||
@@ -46,13 +46,13 @@ async fn test_core_channels(
|
||||
&[
|
||||
ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".to_string(),
|
||||
name: "channel-b".into(),
|
||||
depth: 1,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
@@ -92,7 +92,7 @@ async fn test_core_channels(
|
||||
cx_b,
|
||||
&[ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
@@ -140,13 +140,13 @@ async fn test_core_channels(
|
||||
&[
|
||||
ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 0,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".to_string(),
|
||||
name: "channel-b".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 1,
|
||||
},
|
||||
@@ -168,19 +168,19 @@ async fn test_core_channels(
|
||||
&[
|
||||
ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 0,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".to_string(),
|
||||
name: "channel-b".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 1,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_c_id,
|
||||
name: "channel-c".to_string(),
|
||||
name: "channel-c".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 2,
|
||||
},
|
||||
@@ -211,19 +211,19 @@ async fn test_core_channels(
|
||||
&[
|
||||
ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".to_string(),
|
||||
name: "channel-b".into(),
|
||||
depth: 1,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_c_id,
|
||||
name: "channel-c".to_string(),
|
||||
name: "channel-c".into(),
|
||||
depth: 2,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
@@ -245,7 +245,7 @@ async fn test_core_channels(
|
||||
cx_a,
|
||||
&[ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
@@ -255,7 +255,7 @@ async fn test_core_channels(
|
||||
cx_b,
|
||||
&[ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
@@ -278,7 +278,7 @@ async fn test_core_channels(
|
||||
cx_a,
|
||||
&[ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
@@ -309,7 +309,7 @@ async fn test_core_channels(
|
||||
cx_a,
|
||||
&[ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a-renamed".to_string(),
|
||||
name: "channel-a-renamed".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
@@ -364,8 +364,7 @@ async fn test_joining_channel_ancestor_member(
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
assert!(active_call_b
|
||||
.update(cx_b, |active_call, cx| active_call
|
||||
.join_channel(sub_id, None, cx))
|
||||
.update(cx_b, |active_call, cx| active_call.join_channel(sub_id, cx))
|
||||
.await
|
||||
.is_ok());
|
||||
}
|
||||
@@ -395,9 +394,7 @@ async fn test_channel_room(
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
active_call_a
|
||||
.update(cx_a, |active_call, cx| {
|
||||
active_call.join_channel(zed_id, None, cx)
|
||||
})
|
||||
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -421,7 +418,7 @@ async fn test_channel_room(
|
||||
cx_b,
|
||||
&[ExpectedChannel {
|
||||
id: zed_id,
|
||||
name: "zed".to_string(),
|
||||
name: "zed".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
@@ -445,9 +442,7 @@ async fn test_channel_room(
|
||||
});
|
||||
|
||||
active_call_b
|
||||
.update(cx_b, |active_call, cx| {
|
||||
active_call.join_channel(zed_id, None, cx)
|
||||
})
|
||||
.update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -564,16 +559,12 @@ async fn test_channel_room(
|
||||
});
|
||||
|
||||
active_call_a
|
||||
.update(cx_a, |active_call, cx| {
|
||||
active_call.join_channel(zed_id, None, cx)
|
||||
})
|
||||
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
active_call_b
|
||||
.update(cx_b, |active_call, cx| {
|
||||
active_call.join_channel(zed_id, None, cx)
|
||||
})
|
||||
.update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -617,9 +608,7 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
active_call_a
|
||||
.update(cx_a, |active_call, cx| {
|
||||
active_call.join_channel(zed_id, None, cx)
|
||||
})
|
||||
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -638,7 +627,7 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo
|
||||
|
||||
active_call_a
|
||||
.update(cx_a, |active_call, cx| {
|
||||
active_call.join_channel(rust_id, None, cx)
|
||||
active_call.join_channel(rust_id, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -691,7 +680,7 @@ async fn test_permissions_update_while_invited(
|
||||
&[ExpectedChannel {
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust".to_string(),
|
||||
name: "rust".into(),
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
@@ -719,7 +708,7 @@ async fn test_permissions_update_while_invited(
|
||||
&[ExpectedChannel {
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust".to_string(),
|
||||
name: "rust".into(),
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
@@ -758,7 +747,7 @@ async fn test_channel_rename(
|
||||
&[ExpectedChannel {
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust-archive".to_string(),
|
||||
name: "rust-archive".into(),
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
);
|
||||
@@ -770,7 +759,7 @@ async fn test_channel_rename(
|
||||
&[ExpectedChannel {
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust-archive".to_string(),
|
||||
name: "rust-archive".into(),
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
@@ -804,7 +793,7 @@ async fn test_call_from_channel(
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.join_channel(channel_id, None, cx))
|
||||
.update(cx_a, |call, cx| call.join_channel(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -899,7 +888,7 @@ async fn test_lost_channel_creation(
|
||||
&[ExpectedChannel {
|
||||
depth: 0,
|
||||
id: channel_id,
|
||||
name: "x".to_string(),
|
||||
name: "x".into(),
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
@@ -923,13 +912,13 @@ async fn test_lost_channel_creation(
|
||||
ExpectedChannel {
|
||||
depth: 0,
|
||||
id: channel_id,
|
||||
name: "x".to_string(),
|
||||
name: "x".into(),
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
ExpectedChannel {
|
||||
depth: 1,
|
||||
id: subchannel_id,
|
||||
name: "subchannel".to_string(),
|
||||
name: "subchannel".into(),
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
],
|
||||
@@ -954,13 +943,13 @@ async fn test_lost_channel_creation(
|
||||
ExpectedChannel {
|
||||
depth: 0,
|
||||
id: channel_id,
|
||||
name: "x".to_string(),
|
||||
name: "x".into(),
|
||||
role: ChannelRole::Member,
|
||||
},
|
||||
ExpectedChannel {
|
||||
depth: 1,
|
||||
id: subchannel_id,
|
||||
name: "subchannel".to_string(),
|
||||
name: "subchannel".into(),
|
||||
role: ChannelRole::Member,
|
||||
},
|
||||
],
|
||||
@@ -1232,13 +1221,13 @@ async fn test_channel_membership_notifications(
|
||||
ExpectedChannel {
|
||||
depth: 0,
|
||||
id: zed_channel,
|
||||
name: "zed".to_string(),
|
||||
name: "zed".into(),
|
||||
role: ChannelRole::Guest,
|
||||
},
|
||||
ExpectedChannel {
|
||||
depth: 1,
|
||||
id: vim_channel,
|
||||
name: "vim".to_string(),
|
||||
name: "vim".into(),
|
||||
role: ChannelRole::Member,
|
||||
},
|
||||
],
|
||||
@@ -1261,13 +1250,13 @@ async fn test_channel_membership_notifications(
|
||||
ExpectedChannel {
|
||||
depth: 0,
|
||||
id: zed_channel,
|
||||
name: "zed".to_string(),
|
||||
name: "zed".into(),
|
||||
role: ChannelRole::Guest,
|
||||
},
|
||||
ExpectedChannel {
|
||||
depth: 1,
|
||||
id: vim_channel,
|
||||
name: "vim".to_string(),
|
||||
name: "vim".into(),
|
||||
role: ChannelRole::Guest,
|
||||
},
|
||||
],
|
||||
@@ -1297,7 +1286,7 @@ async fn test_guest_access(
|
||||
|
||||
// Non-members should not be allowed to join
|
||||
assert!(active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_a, None, cx))
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_a, cx))
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
@@ -1319,7 +1308,7 @@ async fn test_guest_access(
|
||||
|
||||
// Client B joins channel A as a guest
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_a, None, cx))
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_a, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1352,7 +1341,7 @@ async fn test_guest_access(
|
||||
assert_channels_list_shape(client_b.channel_store(), cx_b, &[]);
|
||||
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_b, None, cx))
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_b, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1383,7 +1372,7 @@ async fn test_invite_access(
|
||||
|
||||
// should not be allowed to join
|
||||
assert!(active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx))
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
@@ -1401,7 +1390,7 @@ async fn test_invite_access(
|
||||
.unwrap();
|
||||
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx))
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1487,7 +1476,7 @@ async fn test_channel_moving(
|
||||
struct ExpectedChannel {
|
||||
depth: usize,
|
||||
id: ChannelId,
|
||||
name: String,
|
||||
name: SharedString,
|
||||
role: ChannelRole,
|
||||
}
|
||||
|
||||
@@ -1526,7 +1515,7 @@ fn assert_channels(
|
||||
.ordered_channels()
|
||||
.map(|(depth, channel)| ExpectedChannel {
|
||||
depth,
|
||||
name: channel.name.clone(),
|
||||
name: channel.name.clone().into(),
|
||||
id: channel.id,
|
||||
role: channel.role,
|
||||
})
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
// use call::ActiveCall;
|
||||
// use collab_ui::notifications::project_shared_notification::ProjectSharedNotification;
|
||||
// use editor::{Editor, ExcerptRange, MultiBuffer};
|
||||
// use gpui::{BackgroundExecutor, TestAppContext, View};
|
||||
// use gpui::{point, BackgroundExecutor, TestAppContext, View, VisualTestContext, WindowContext};
|
||||
// use live_kit_client::MacOSDisplay;
|
||||
// use project::project_settings::ProjectSettings;
|
||||
// use rpc::proto::PeerId;
|
||||
// use serde_json::json;
|
||||
// use settings::SettingsStore;
|
||||
// use std::borrow::Cow;
|
||||
// use workspace::{
|
||||
// dock::{test::TestPanel, DockPosition},
|
||||
@@ -24,7 +26,7 @@
|
||||
// cx_c: &mut TestAppContext,
|
||||
// cx_d: &mut TestAppContext,
|
||||
// ) {
|
||||
// let mut server = TestServer::start(&executor).await;
|
||||
// let mut server = TestServer::start(executor.clone()).await;
|
||||
// let client_a = server.create_client(cx_a, "user_a").await;
|
||||
// let client_b = server.create_client(cx_b, "user_b").await;
|
||||
// let client_c = server.create_client(cx_c, "user_c").await;
|
||||
@@ -71,12 +73,22 @@
|
||||
// .unwrap();
|
||||
|
||||
// let window_a = client_a.build_workspace(&project_a, cx_a);
|
||||
// let workspace_a = window_a.root(cx_a);
|
||||
// let workspace_a = window_a.root(cx_a).unwrap();
|
||||
// let window_b = client_b.build_workspace(&project_b, cx_b);
|
||||
// let workspace_b = window_b.root(cx_b);
|
||||
// let workspace_b = window_b.root(cx_b).unwrap();
|
||||
|
||||
// todo!("could be wrong")
|
||||
// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
|
||||
// let cx_a = &mut cx_a;
|
||||
// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
|
||||
// let cx_b = &mut cx_b;
|
||||
// let mut cx_c = VisualTestContext::from_window(*window_c, cx_c);
|
||||
// let cx_c = &mut cx_c;
|
||||
// let mut cx_d = VisualTestContext::from_window(*window_d, cx_d);
|
||||
// let cx_d = &mut cx_d;
|
||||
|
||||
// // Client A opens some editors.
|
||||
// let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
// let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
// let editor_a1 = workspace_a
|
||||
// .update(cx_a, |workspace, cx| {
|
||||
// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
@@ -132,8 +144,8 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// cx_c.foreground().run_until_parked();
|
||||
// let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
// cx_c.executor().run_until_parked();
|
||||
// let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
|
||||
// workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
@@ -145,19 +157,19 @@
|
||||
// Some((worktree_id, "2.txt").into())
|
||||
// );
|
||||
// assert_eq!(
|
||||
// editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
// editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
// vec![2..1]
|
||||
// );
|
||||
// assert_eq!(
|
||||
// editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
// editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
// vec![3..2]
|
||||
// );
|
||||
|
||||
// cx_c.foreground().run_until_parked();
|
||||
// cx_c.executor().run_until_parked();
|
||||
// let active_call_c = cx_c.read(ActiveCall::global);
|
||||
// let project_c = client_c.build_remote_project(project_id, cx_c).await;
|
||||
// let window_c = client_c.build_workspace(&project_c, cx_c);
|
||||
// let workspace_c = window_c.root(cx_c);
|
||||
// let workspace_c = window_c.root(cx_c).unwrap();
|
||||
// active_call_c
|
||||
// .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
|
||||
// .await
|
||||
@@ -172,10 +184,13 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// cx_d.foreground().run_until_parked();
|
||||
// cx_d.executor().run_until_parked();
|
||||
// let active_call_d = cx_d.read(ActiveCall::global);
|
||||
// let project_d = client_d.build_remote_project(project_id, cx_d).await;
|
||||
// let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d);
|
||||
// let workspace_d = client_d
|
||||
// .build_workspace(&project_d, cx_d)
|
||||
// .root(cx_d)
|
||||
// .unwrap();
|
||||
// active_call_d
|
||||
// .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
|
||||
// .await
|
||||
@@ -183,7 +198,7 @@
|
||||
// drop(project_d);
|
||||
|
||||
// // All clients see that clients B and C are following client A.
|
||||
// cx_c.foreground().run_until_parked();
|
||||
// cx_c.executor().run_until_parked();
|
||||
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
// assert_eq!(
|
||||
// followers_by_leader(project_id, cx),
|
||||
@@ -198,7 +213,7 @@
|
||||
// });
|
||||
|
||||
// // All clients see that clients B is following client A.
|
||||
// cx_c.foreground().run_until_parked();
|
||||
// cx_c.executor().run_until_parked();
|
||||
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
// assert_eq!(
|
||||
// followers_by_leader(project_id, cx),
|
||||
@@ -216,7 +231,7 @@
|
||||
// .unwrap();
|
||||
|
||||
// // All clients see that clients B and C are following client A.
|
||||
// cx_c.foreground().run_until_parked();
|
||||
// cx_c.executor().run_until_parked();
|
||||
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
// assert_eq!(
|
||||
// followers_by_leader(project_id, cx),
|
||||
@@ -240,7 +255,7 @@
|
||||
// .unwrap();
|
||||
|
||||
// // All clients see that D is following C
|
||||
// cx_d.foreground().run_until_parked();
|
||||
// cx_d.executor().run_until_parked();
|
||||
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
// assert_eq!(
|
||||
// followers_by_leader(project_id, cx),
|
||||
@@ -257,7 +272,7 @@
|
||||
// cx_c.drop_last(workspace_c);
|
||||
|
||||
// // Clients A and B see that client B is following A, and client C is not present in the followers.
|
||||
// cx_c.foreground().run_until_parked();
|
||||
// cx_c.executor().run_until_parked();
|
||||
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
// assert_eq!(
|
||||
// followers_by_leader(project_id, cx),
|
||||
@@ -271,12 +286,15 @@
|
||||
// workspace.activate_item(&editor_a1, cx)
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
|
||||
// workspace_b.update(cx_b, |workspace, cx| {
|
||||
// assert_eq!(
|
||||
// workspace.active_item(cx).unwrap().item_id(),
|
||||
// editor_b1.item_id()
|
||||
// );
|
||||
// });
|
||||
|
||||
// // When client A opens a multibuffer, client B does so as well.
|
||||
// let multibuffer_a = cx_a.add_model(|cx| {
|
||||
// let multibuffer_a = cx_a.build_model(|cx| {
|
||||
// let buffer_a1 = project_a.update(cx, |project, cx| {
|
||||
// project
|
||||
// .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
|
||||
@@ -308,12 +326,12 @@
|
||||
// });
|
||||
// let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
|
||||
// let editor =
|
||||
// cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
|
||||
// cx.build_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
|
||||
// workspace.add_item(Box::new(editor.clone()), cx);
|
||||
// editor
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
// let multibuffer_editor_b = workspace_b.update(cx_b, |workspace, cx| {
|
||||
// workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
@@ -321,8 +339,8 @@
|
||||
// .unwrap()
|
||||
// });
|
||||
// assert_eq!(
|
||||
// multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
|
||||
// multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
|
||||
// multibuffer_editor_a.update(cx_a, |editor, cx| editor.text(cx)),
|
||||
// multibuffer_editor_b.update(cx_b, |editor, cx| editor.text(cx)),
|
||||
// );
|
||||
|
||||
// // When client A navigates back and forth, client B does so as well.
|
||||
@@ -333,8 +351,11 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// executor.run_until_parked();
|
||||
// workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
|
||||
// workspace_b.update(cx_b, |workspace, cx| {
|
||||
// assert_eq!(
|
||||
// workspace.active_item(cx).unwrap().item_id(),
|
||||
// editor_b1.item_id()
|
||||
// );
|
||||
// });
|
||||
|
||||
// workspace_a
|
||||
@@ -344,8 +365,11 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// executor.run_until_parked();
|
||||
// workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
|
||||
// workspace_b.update(cx_b, |workspace, cx| {
|
||||
// assert_eq!(
|
||||
// workspace.active_item(cx).unwrap().item_id(),
|
||||
// editor_b2.item_id()
|
||||
// );
|
||||
// });
|
||||
|
||||
// workspace_a
|
||||
@@ -355,8 +379,11 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// executor.run_until_parked();
|
||||
// workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
|
||||
// workspace_b.update(cx_b, |workspace, cx| {
|
||||
// assert_eq!(
|
||||
// workspace.active_item(cx).unwrap().item_id(),
|
||||
// editor_b1.item_id()
|
||||
// );
|
||||
// });
|
||||
|
||||
// // Changes to client A's editor are reflected on client B.
|
||||
@@ -364,20 +391,20 @@
|
||||
// editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// editor_b1.read_with(cx_b, |editor, cx| {
|
||||
// editor_b1.update(cx_b, |editor, cx| {
|
||||
// assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
|
||||
// });
|
||||
|
||||
// editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
|
||||
// executor.run_until_parked();
|
||||
// editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
|
||||
// editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
|
||||
|
||||
// editor_a1.update(cx_a, |editor, cx| {
|
||||
// editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
|
||||
// editor.set_scroll_position(vec2f(0., 100.), cx);
|
||||
// editor.set_scroll_position(point(0., 100.), cx);
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// editor_b1.read_with(cx_b, |editor, cx| {
|
||||
// editor_b1.update(cx_b, |editor, cx| {
|
||||
// assert_eq!(editor.selections.ranges(cx), &[3..3]);
|
||||
// });
|
||||
|
||||
@@ -390,11 +417,11 @@
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, cx| workspace
|
||||
// workspace_b.update(cx_b, |workspace, cx| workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
// .id()),
|
||||
// editor_b1.id()
|
||||
// .item_id()),
|
||||
// editor_b1.item_id()
|
||||
// );
|
||||
|
||||
// // Client A starts following client B.
|
||||
@@ -405,15 +432,15 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||
// workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||
// Some(peer_id_b)
|
||||
// );
|
||||
// assert_eq!(
|
||||
// workspace_a.read_with(cx_a, |workspace, cx| workspace
|
||||
// workspace_a.update(cx_a, |workspace, cx| workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
// .id()),
|
||||
// editor_a1.id()
|
||||
// .item_id()),
|
||||
// editor_a1.item_id()
|
||||
// );
|
||||
|
||||
// // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
|
||||
@@ -432,7 +459,7 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// executor.run_until_parked();
|
||||
// let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
|
||||
// let shared_screen = workspace_a.update(cx_a, |workspace, cx| {
|
||||
// workspace
|
||||
// .active_item(cx)
|
||||
// .expect("no active item")
|
||||
@@ -446,8 +473,11 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// executor.run_until_parked();
|
||||
// workspace_a.read_with(cx_a, |workspace, cx| {
|
||||
// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
|
||||
// workspace_a.update(cx_a, |workspace, cx| {
|
||||
// assert_eq!(
|
||||
// workspace.active_item(cx).unwrap().item_id(),
|
||||
// editor_a1.item_id()
|
||||
// )
|
||||
// });
|
||||
|
||||
// // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
|
||||
@@ -455,26 +485,26 @@
|
||||
// workspace.activate_item(&multibuffer_editor_b, cx)
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// workspace_a.read_with(cx_a, |workspace, cx| {
|
||||
// workspace_a.update(cx_a, |workspace, cx| {
|
||||
// assert_eq!(
|
||||
// workspace.active_item(cx).unwrap().id(),
|
||||
// multibuffer_editor_a.id()
|
||||
// workspace.active_item(cx).unwrap().item_id(),
|
||||
// multibuffer_editor_a.item_id()
|
||||
// )
|
||||
// });
|
||||
|
||||
// // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
|
||||
// let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left));
|
||||
// let panel = window_b.build_view(cx_b, |_| TestPanel::new(DockPosition::Left));
|
||||
// workspace_b.update(cx_b, |workspace, cx| {
|
||||
// workspace.add_panel(panel, cx);
|
||||
// workspace.toggle_panel_focus::<TestPanel>(cx);
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// assert_eq!(
|
||||
// workspace_a.read_with(cx_a, |workspace, cx| workspace
|
||||
// workspace_a.update(cx_a, |workspace, cx| workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
// .id()),
|
||||
// shared_screen.id()
|
||||
// .item_id()),
|
||||
// shared_screen.item_id()
|
||||
// );
|
||||
|
||||
// // Toggling the focus back to the pane causes client A to return to the multibuffer.
|
||||
@@ -482,16 +512,16 @@
|
||||
// workspace.toggle_panel_focus::<TestPanel>(cx);
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// workspace_a.read_with(cx_a, |workspace, cx| {
|
||||
// workspace_a.update(cx_a, |workspace, cx| {
|
||||
// assert_eq!(
|
||||
// workspace.active_item(cx).unwrap().id(),
|
||||
// multibuffer_editor_a.id()
|
||||
// workspace.active_item(cx).unwrap().item_id(),
|
||||
// multibuffer_editor_a.item_id()
|
||||
// )
|
||||
// });
|
||||
|
||||
// // Client B activates an item that doesn't implement following,
|
||||
// // so the previously-opened screen-sharing item gets activated.
|
||||
// let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new());
|
||||
// let unfollowable_item = window_b.build_view(cx_b, |_| TestItem::new());
|
||||
// workspace_b.update(cx_b, |workspace, cx| {
|
||||
// workspace.active_pane().update(cx, |pane, cx| {
|
||||
// pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
|
||||
@@ -499,18 +529,18 @@
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// assert_eq!(
|
||||
// workspace_a.read_with(cx_a, |workspace, cx| workspace
|
||||
// workspace_a.update(cx_a, |workspace, cx| workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
// .id()),
|
||||
// shared_screen.id()
|
||||
// .item_id()),
|
||||
// shared_screen.item_id()
|
||||
// );
|
||||
|
||||
// // Following interrupts when client B disconnects.
|
||||
// client_b.disconnect(&cx_b.to_async());
|
||||
// executor.advance_clock(RECONNECT_TIMEOUT);
|
||||
// assert_eq!(
|
||||
// workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||
// workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||
// None
|
||||
// );
|
||||
// }
|
||||
@@ -521,7 +551,7 @@
|
||||
// cx_a: &mut TestAppContext,
|
||||
// cx_b: &mut TestAppContext,
|
||||
// ) {
|
||||
// let mut server = TestServer::start(&executor).await;
|
||||
// let mut server = TestServer::start(executor.clone()).await;
|
||||
// let client_a = server.create_client(cx_a, "user_a").await;
|
||||
// let client_b = server.create_client(cx_b, "user_b").await;
|
||||
// server
|
||||
@@ -560,13 +590,19 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
|
||||
// let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
// let workspace_a = client_a
|
||||
// .build_workspace(&project_a, cx_a)
|
||||
// .root(cx_a)
|
||||
// .unwrap();
|
||||
// let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||
// let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
|
||||
// let workspace_b = client_b
|
||||
// .build_workspace(&project_b, cx_b)
|
||||
// .root(cx_b)
|
||||
// .unwrap();
|
||||
// let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
// let client_b_id = project_a.read_with(cx_a, |project, _| {
|
||||
// let client_b_id = project_a.update(cx_a, |project, _| {
|
||||
// project.collaborators().values().next().unwrap().peer_id
|
||||
// });
|
||||
|
||||
@@ -584,7 +620,7 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
|
||||
// let pane_paths = |pane: &View<workspace::Pane>, cx: &mut TestAppContext| {
|
||||
// pane.update(cx, |pane, cx| {
|
||||
// pane.items()
|
||||
// .map(|item| {
|
||||
@@ -642,7 +678,7 @@
|
||||
// cx_a: &mut TestAppContext,
|
||||
// cx_b: &mut TestAppContext,
|
||||
// ) {
|
||||
// let mut server = TestServer::start(&executor).await;
|
||||
// let mut server = TestServer::start(executor.clone()).await;
|
||||
// let client_a = server.create_client(cx_a, "user_a").await;
|
||||
// let client_b = server.create_client(cx_b, "user_b").await;
|
||||
// server
|
||||
@@ -685,7 +721,10 @@
|
||||
// .unwrap();
|
||||
|
||||
// // Client A opens a file.
|
||||
// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
|
||||
// let workspace_a = client_a
|
||||
// .build_workspace(&project_a, cx_a)
|
||||
// .root(cx_a)
|
||||
// .unwrap();
|
||||
// workspace_a
|
||||
// .update(cx_a, |workspace, cx| {
|
||||
// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
@@ -696,7 +735,10 @@
|
||||
// .unwrap();
|
||||
|
||||
// // Client B opens a different file.
|
||||
// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||
// let workspace_b = client_b
|
||||
// .build_workspace(&project_b, cx_b)
|
||||
// .root(cx_b)
|
||||
// .unwrap();
|
||||
// workspace_b
|
||||
// .update(cx_b, |workspace, cx| {
|
||||
// workspace.open_path((worktree_id, "2.txt"), None, true, cx)
|
||||
@@ -1167,7 +1209,7 @@
|
||||
// cx_b: &mut TestAppContext,
|
||||
// ) {
|
||||
// // 2 clients connect to a server.
|
||||
// let mut server = TestServer::start(&executor).await;
|
||||
// let mut server = TestServer::start(executor.clone()).await;
|
||||
// let client_a = server.create_client(cx_a, "user_a").await;
|
||||
// let client_b = server.create_client(cx_b, "user_b").await;
|
||||
// server
|
||||
@@ -1207,8 +1249,17 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// todo!("could be wrong")
|
||||
// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
|
||||
// let cx_a = &mut cx_a;
|
||||
// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
|
||||
// let cx_b = &mut cx_b;
|
||||
|
||||
// // Client A opens some editors.
|
||||
// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
|
||||
// let workspace_a = client_a
|
||||
// .build_workspace(&project_a, cx_a)
|
||||
// .root(cx_a)
|
||||
// .unwrap();
|
||||
// let _editor_a1 = workspace_a
|
||||
// .update(cx_a, |workspace, cx| {
|
||||
// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
@@ -1219,9 +1270,12 @@
|
||||
// .unwrap();
|
||||
|
||||
// // Client B starts following client A.
|
||||
// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||
// let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
|
||||
// let leader_id = project_b.read_with(cx_b, |project, _| {
|
||||
// let workspace_b = client_b
|
||||
// .build_workspace(&project_b, cx_b)
|
||||
// .root(cx_b)
|
||||
// .unwrap();
|
||||
// let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
|
||||
// let leader_id = project_b.update(cx_b, |project, _| {
|
||||
// project.collaborators().values().next().unwrap().peer_id
|
||||
// });
|
||||
// workspace_b
|
||||
@@ -1231,10 +1285,10 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// Some(leader_id)
|
||||
// );
|
||||
// let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
// let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
|
||||
// workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
@@ -1245,7 +1299,7 @@
|
||||
// // When client B moves, it automatically stops following client A.
|
||||
// editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// None
|
||||
// );
|
||||
|
||||
@@ -1256,14 +1310,14 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// Some(leader_id)
|
||||
// );
|
||||
|
||||
// // When client B edits, it automatically stops following client A.
|
||||
// editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// None
|
||||
// );
|
||||
|
||||
@@ -1274,16 +1328,16 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// Some(leader_id)
|
||||
// );
|
||||
|
||||
// // When client B scrolls, it automatically stops following client A.
|
||||
// editor_b2.update(cx_b, |editor, cx| {
|
||||
// editor.set_scroll_position(vec2f(0., 3.), cx)
|
||||
// editor.set_scroll_position(point(0., 3.), cx)
|
||||
// });
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// None
|
||||
// );
|
||||
|
||||
@@ -1294,7 +1348,7 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// Some(leader_id)
|
||||
// );
|
||||
|
||||
@@ -1303,13 +1357,13 @@
|
||||
// workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx)
|
||||
// });
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// Some(leader_id)
|
||||
// );
|
||||
|
||||
// workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// Some(leader_id)
|
||||
// );
|
||||
|
||||
@@ -1321,7 +1375,7 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// None
|
||||
// );
|
||||
// }
|
||||
@@ -1332,7 +1386,7 @@
|
||||
// cx_a: &mut TestAppContext,
|
||||
// cx_b: &mut TestAppContext,
|
||||
// ) {
|
||||
// let mut server = TestServer::start(&executor).await;
|
||||
// let mut server = TestServer::start(executor.clone()).await;
|
||||
// let client_a = server.create_client(cx_a, "user_a").await;
|
||||
// let client_b = server.create_client(cx_b, "user_b").await;
|
||||
// server
|
||||
@@ -1345,20 +1399,26 @@
|
||||
|
||||
// client_a.fs().insert_tree("/a", json!({})).await;
|
||||
// let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
|
||||
// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
|
||||
// let workspace_a = client_a
|
||||
// .build_workspace(&project_a, cx_a)
|
||||
// .root(cx_a)
|
||||
// .unwrap();
|
||||
// let project_id = active_call_a
|
||||
// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||
// let workspace_b = client_b
|
||||
// .build_workspace(&project_b, cx_b)
|
||||
// .root(cx_b)
|
||||
// .unwrap();
|
||||
|
||||
// executor.run_until_parked();
|
||||
// let client_a_id = project_b.read_with(cx_b, |project, _| {
|
||||
// let client_a_id = project_b.update(cx_b, |project, _| {
|
||||
// project.collaborators().values().next().unwrap().peer_id
|
||||
// });
|
||||
// let client_b_id = project_a.read_with(cx_a, |project, _| {
|
||||
// let client_b_id = project_a.update(cx_a, |project, _| {
|
||||
// project.collaborators().values().next().unwrap().peer_id
|
||||
// });
|
||||
|
||||
@@ -1370,13 +1430,13 @@
|
||||
// });
|
||||
|
||||
// futures::try_join!(a_follow_b, b_follow_a).unwrap();
|
||||
// workspace_a.read_with(cx_a, |workspace, _| {
|
||||
// workspace_a.update(cx_a, |workspace, _| {
|
||||
// assert_eq!(
|
||||
// workspace.leader_for_pane(workspace.active_pane()),
|
||||
// Some(client_b_id)
|
||||
// );
|
||||
// });
|
||||
// workspace_b.read_with(cx_b, |workspace, _| {
|
||||
// workspace_b.update(cx_b, |workspace, _| {
|
||||
// assert_eq!(
|
||||
// workspace.leader_for_pane(workspace.active_pane()),
|
||||
// Some(client_a_id)
|
||||
@@ -1398,7 +1458,7 @@
|
||||
// // b opens a different file in project 2, a follows b
|
||||
// // b opens a different file in project 1, a cannot follow b
|
||||
// // b shares the project, a joins the project and follows b
|
||||
// let mut server = TestServer::start(&executor).await;
|
||||
// let mut server = TestServer::start(executor.clone()).await;
|
||||
// let client_a = server.create_client(cx_a, "user_a").await;
|
||||
// let client_b = server.create_client(cx_b, "user_b").await;
|
||||
// cx_a.update(editor::init);
|
||||
@@ -1435,8 +1495,14 @@
|
||||
// let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await;
|
||||
// let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await;
|
||||
|
||||
// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
|
||||
// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||
// let workspace_a = client_a
|
||||
// .build_workspace(&project_a, cx_a)
|
||||
// .root(cx_a)
|
||||
// .unwrap();
|
||||
// let workspace_b = client_b
|
||||
// .build_workspace(&project_b, cx_b)
|
||||
// .root(cx_b)
|
||||
// .unwrap();
|
||||
|
||||
// cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx));
|
||||
// cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx));
|
||||
@@ -1455,6 +1521,12 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// todo!("could be wrong")
|
||||
// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
|
||||
// let cx_a = &mut cx_a;
|
||||
// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
|
||||
// let cx_b = &mut cx_b;
|
||||
|
||||
// workspace_a
|
||||
// .update(cx_a, |workspace, cx| {
|
||||
// workspace.open_path((worktree_id_a, "w.rs"), None, true, cx)
|
||||
@@ -1476,11 +1548,12 @@
|
||||
// let workspace_b_project_a = cx_b
|
||||
// .windows()
|
||||
// .iter()
|
||||
// .max_by_key(|window| window.id())
|
||||
// .max_by_key(|window| window.item_id())
|
||||
// .unwrap()
|
||||
// .downcast::<Workspace>()
|
||||
// .unwrap()
|
||||
// .root(cx_b);
|
||||
// .root(cx_b)
|
||||
// .unwrap();
|
||||
|
||||
// // assert that b is following a in project a in w.rs
|
||||
// workspace_b_project_a.update(cx_b, |workspace, cx| {
|
||||
@@ -1534,7 +1607,7 @@
|
||||
// workspace.leader_for_pane(workspace.active_pane())
|
||||
// );
|
||||
// let item = workspace.active_pane().read(cx).active_item().unwrap();
|
||||
// assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs"));
|
||||
// assert_eq!(item.tab_description(0, cx).unwrap(), "x.rs".into());
|
||||
// });
|
||||
|
||||
// // b moves to y.rs in b's project, a is still following but can't yet see
|
||||
@@ -1578,11 +1651,12 @@
|
||||
// let workspace_a_project_b = cx_a
|
||||
// .windows()
|
||||
// .iter()
|
||||
// .max_by_key(|window| window.id())
|
||||
// .max_by_key(|window| window.item_id())
|
||||
// .unwrap()
|
||||
// .downcast::<Workspace>()
|
||||
// .unwrap()
|
||||
// .root(cx_a);
|
||||
// .root(cx_a)
|
||||
// .unwrap();
|
||||
|
||||
// workspace_a_project_b.update(cx_a, |workspace, cx| {
|
||||
// assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
|
||||
@@ -1596,12 +1670,151 @@
|
||||
// });
|
||||
// }
|
||||
|
||||
// #[gpui::test]
|
||||
// async fn test_following_into_excluded_file(
|
||||
// executor: BackgroundExecutor,
|
||||
// mut cx_a: &mut TestAppContext,
|
||||
// mut cx_b: &mut TestAppContext,
|
||||
// ) {
|
||||
// let mut server = TestServer::start(executor.clone()).await;
|
||||
// let client_a = server.create_client(cx_a, "user_a").await;
|
||||
// let client_b = server.create_client(cx_b, "user_b").await;
|
||||
// for cx in [&mut cx_a, &mut cx_b] {
|
||||
// cx.update(|cx| {
|
||||
// cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
// store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||
// project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// server
|
||||
// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
// .await;
|
||||
// let active_call_a = cx_a.read(ActiveCall::global);
|
||||
// let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
// cx_a.update(editor::init);
|
||||
// cx_b.update(editor::init);
|
||||
|
||||
// client_a
|
||||
// .fs()
|
||||
// .insert_tree(
|
||||
// "/a",
|
||||
// json!({
|
||||
// ".git": {
|
||||
// "COMMIT_EDITMSG": "write your commit message here",
|
||||
// },
|
||||
// "1.txt": "one\none\none",
|
||||
// "2.txt": "two\ntwo\ntwo",
|
||||
// "3.txt": "three\nthree\nthree",
|
||||
// }),
|
||||
// )
|
||||
// .await;
|
||||
// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
// active_call_a
|
||||
// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// let project_id = active_call_a
|
||||
// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
// .await
|
||||
// .unwrap();
|
||||
// let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
// active_call_b
|
||||
// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// let window_a = client_a.build_workspace(&project_a, cx_a);
|
||||
// let workspace_a = window_a.root(cx_a).unwrap();
|
||||
// let peer_id_a = client_a.peer_id().unwrap();
|
||||
// let window_b = client_b.build_workspace(&project_b, cx_b);
|
||||
// let workspace_b = window_b.root(cx_b).unwrap();
|
||||
|
||||
// todo!("could be wrong")
|
||||
// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
|
||||
// let cx_a = &mut cx_a;
|
||||
// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
|
||||
// let cx_b = &mut cx_b;
|
||||
|
||||
// // Client A opens editors for a regular file and an excluded file.
|
||||
// let editor_for_regular = workspace_a
|
||||
// .update(cx_a, |workspace, cx| {
|
||||
// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
// })
|
||||
// .await
|
||||
// .unwrap()
|
||||
// .downcast::<Editor>()
|
||||
// .unwrap();
|
||||
// let editor_for_excluded_a = workspace_a
|
||||
// .update(cx_a, |workspace, cx| {
|
||||
// workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx)
|
||||
// })
|
||||
// .await
|
||||
// .unwrap()
|
||||
// .downcast::<Editor>()
|
||||
// .unwrap();
|
||||
|
||||
// // Client A updates their selections in those editors
|
||||
// editor_for_regular.update(cx_a, |editor, cx| {
|
||||
// editor.handle_input("a", cx);
|
||||
// editor.handle_input("b", cx);
|
||||
// editor.handle_input("c", cx);
|
||||
// editor.select_left(&Default::default(), cx);
|
||||
// assert_eq!(editor.selections.ranges(cx), vec![3..2]);
|
||||
// });
|
||||
// editor_for_excluded_a.update(cx_a, |editor, cx| {
|
||||
// editor.select_all(&Default::default(), cx);
|
||||
// editor.handle_input("new commit message", cx);
|
||||
// editor.select_left(&Default::default(), cx);
|
||||
// assert_eq!(editor.selections.ranges(cx), vec![18..17]);
|
||||
// });
|
||||
|
||||
// // When client B starts following client A, currently visible file is replicated
|
||||
// workspace_b
|
||||
// .update(cx_b, |workspace, cx| {
|
||||
// workspace.follow(peer_id_a, cx).unwrap()
|
||||
// })
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| {
|
||||
// workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
// .downcast::<Editor>()
|
||||
// .unwrap()
|
||||
// });
|
||||
// assert_eq!(
|
||||
// cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
|
||||
// Some((worktree_id, ".git/COMMIT_EDITMSG").into())
|
||||
// );
|
||||
// assert_eq!(
|
||||
// editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
// vec![18..17]
|
||||
// );
|
||||
|
||||
// // Changes from B to the excluded file are replicated in A's editor
|
||||
// editor_for_excluded_b.update(cx_b, |editor, cx| {
|
||||
// editor.handle_input("\nCo-Authored-By: B <b@b.b>", cx);
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// editor_for_excluded_a.update(cx_a, |editor, cx| {
|
||||
// assert_eq!(
|
||||
// editor.text(cx),
|
||||
// "new commit messag\nCo-Authored-By: B <b@b.b>"
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
|
||||
// fn visible_push_notifications(
|
||||
// cx: &mut TestAppContext,
|
||||
// ) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
|
||||
// ) -> Vec<gpui::View<ProjectSharedNotification>> {
|
||||
// let mut ret = Vec::new();
|
||||
// for window in cx.windows() {
|
||||
// window.read_with(cx, |window| {
|
||||
// window.update(cx, |window| {
|
||||
// if let Some(handle) = window
|
||||
// .root_view()
|
||||
// .clone()
|
||||
@@ -1645,8 +1858,8 @@
|
||||
// })
|
||||
// }
|
||||
|
||||
// fn pane_summaries(workspace: &ViewHandle<Workspace>, cx: &mut TestAppContext) -> Vec<PaneSummary> {
|
||||
// workspace.read_with(cx, |workspace, cx| {
|
||||
// fn pane_summaries(workspace: &View<Workspace>, cx: &mut WindowContext<'_>) -> Vec<PaneSummary> {
|
||||
// workspace.update(cx, |workspace, cx| {
|
||||
// let active_pane = workspace.active_pane();
|
||||
// workspace
|
||||
// .panes()
|
||||
|
||||
@@ -510,10 +510,9 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
|
||||
|
||||
// Simultaneously join channel 1 and then channel 2
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx))
|
||||
.update(cx_a, |call, cx| call.join_channel(channel_1, cx))
|
||||
.detach();
|
||||
let join_channel_2 =
|
||||
active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, None, cx));
|
||||
let join_channel_2 = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, cx));
|
||||
|
||||
join_channel_2.await.unwrap();
|
||||
|
||||
@@ -539,8 +538,7 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
|
||||
call.invite(client_c.user_id().unwrap(), None, cx)
|
||||
});
|
||||
|
||||
let join_channel =
|
||||
active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx));
|
||||
let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
|
||||
|
||||
b_invite.await.unwrap();
|
||||
c_invite.await.unwrap();
|
||||
@@ -569,8 +567,7 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
|
||||
.unwrap();
|
||||
|
||||
// Simultaneously join channel 1 and call user B and user C from client A.
|
||||
let join_channel =
|
||||
active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx));
|
||||
let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
|
||||
|
||||
let b_invite = active_call_a.update(cx_a, |call, cx| {
|
||||
call.invite(client_b.user_id().unwrap(), None, cx)
|
||||
@@ -2784,11 +2781,10 @@ async fn test_fs_operations(
|
||||
|
||||
let entry = project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "c.txt"), false, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "c.txt"), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
@@ -2815,8 +2811,8 @@ async fn test_fs_operations(
|
||||
.update(cx_b, |project, cx| {
|
||||
project.rename_entry(entry.id, Path::new("d.txt"), cx)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
@@ -2841,11 +2837,10 @@ async fn test_fs_operations(
|
||||
|
||||
let dir_entry = project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR"), true, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
@@ -2870,27 +2865,24 @@ async fn test_fs_operations(
|
||||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR/e.txt"), false, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR/e.txt"), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
@@ -2931,11 +2923,10 @@ async fn test_fs_operations(
|
||||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.copy_entry(entry.id, Path::new("f.txt"), cx)
|
||||
.unwrap()
|
||||
project.copy_entry(entry.id, Path::new("f.txt"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::db::ChannelRole;
|
||||
use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use gpui::{BackgroundExecutor, TestAppContext};
|
||||
use gpui::{BackgroundExecutor, SharedString, TestAppContext};
|
||||
use rand::prelude::*;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::{
|
||||
@@ -30,13 +30,13 @@ struct RandomChannelBufferTest;
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
enum ChannelBufferOperation {
|
||||
JoinChannelNotes {
|
||||
channel_name: String,
|
||||
channel_name: SharedString,
|
||||
},
|
||||
LeaveChannelNotes {
|
||||
channel_name: String,
|
||||
channel_name: SharedString,
|
||||
},
|
||||
EditChannelNotes {
|
||||
channel_name: String,
|
||||
channel_name: SharedString,
|
||||
edits: Vec<(Range<usize>, Arc<str>)>,
|
||||
},
|
||||
Noop,
|
||||
|
||||
@@ -665,7 +665,6 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
ensure_project_shared(&project, client, cx).await;
|
||||
project
|
||||
.update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
|
||||
.unwrap()
|
||||
.await?;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ use client::{
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::FakeFs;
|
||||
use futures::{channel::oneshot, StreamExt as _};
|
||||
use gpui::{BackgroundExecutor, Context, Model, TestAppContext, WindowHandle};
|
||||
use gpui::{BackgroundExecutor, Context, Model, TestAppContext, View, VisualTestContext};
|
||||
use language::LanguageRegistry;
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
|
||||
@@ -221,7 +221,6 @@ impl TestServer {
|
||||
fs: fs.clone(),
|
||||
build_window_options: |_, _, _| Default::default(),
|
||||
node_runtime: FakeNodeRuntime::new(),
|
||||
call_factory: |_, _| Box::new(workspace::TestCallHandler),
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
@@ -603,14 +602,12 @@ impl TestClient {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
//todo(workspace)
|
||||
#[allow(dead_code)]
|
||||
pub fn build_workspace(
|
||||
&self,
|
||||
pub fn build_workspace<'a>(
|
||||
&'a self,
|
||||
project: &Model<Project>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> WindowHandle<Workspace> {
|
||||
cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx))
|
||||
cx: &'a mut TestAppContext,
|
||||
) -> (View<Workspace>, &'a mut VisualTestContext) {
|
||||
cx.add_window_view(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ notifications = { package = "notifications2", path = "../notifications2" }
|
||||
rich_text = { package = "rich_text2", path = "../rich_text2" }
|
||||
picker = { package = "picker2", path = "../picker2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
# recent_projects = { path = "../recent_projects" }
|
||||
recent_projects = { package = "recent_projects2", path = "../recent_projects2" }
|
||||
rpc = { package ="rpc2", path = "../rpc2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
feature_flags = { package = "feature_flags2", path = "../feature_flags2"}
|
||||
|
||||
@@ -1,454 +1,444 @@
|
||||
// use anyhow::{anyhow, Result};
|
||||
// use call::report_call_event_for_channel;
|
||||
// use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
|
||||
// use client::{
|
||||
// proto::{self, PeerId},
|
||||
// Collaborator, ParticipantIndex,
|
||||
// };
|
||||
// use collections::HashMap;
|
||||
// use editor::{CollaborationHub, Editor};
|
||||
// use gpui::{
|
||||
// actions,
|
||||
// elements::{ChildView, Label},
|
||||
// geometry::vector::Vector2F,
|
||||
// AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View,
|
||||
// ViewContext, ViewHandle,
|
||||
// };
|
||||
// use project::Project;
|
||||
// use smallvec::SmallVec;
|
||||
// use std::{
|
||||
// any::{Any, TypeId},
|
||||
// sync::Arc,
|
||||
// };
|
||||
// use util::ResultExt;
|
||||
// use workspace::{
|
||||
// item::{FollowableItem, Item, ItemEvent, ItemHandle},
|
||||
// register_followable_item,
|
||||
// searchable::SearchableItemHandle,
|
||||
// ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
|
||||
// };
|
||||
use anyhow::Result;
|
||||
use call::report_call_event_for_channel;
|
||||
use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
|
||||
use client::{
|
||||
proto::{self, PeerId},
|
||||
Collaborator, ParticipantIndex,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use editor::{CollaborationHub, Editor, EditorEvent};
|
||||
use gpui::{
|
||||
actions, AnyElement, AnyView, AppContext, Entity as _, EventEmitter, FocusableView,
|
||||
IntoElement as _, Model, Pixels, Point, Render, Subscription, Task, View, ViewContext,
|
||||
VisualContext as _, WindowContext,
|
||||
};
|
||||
use project::Project;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
sync::Arc,
|
||||
};
|
||||
use ui::Label;
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
item::{FollowableItem, Item, ItemEvent, ItemHandle},
|
||||
register_followable_item,
|
||||
searchable::SearchableItemHandle,
|
||||
ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
// actions!(channel_view, [Deploy]);
|
||||
actions!(Deploy);
|
||||
|
||||
// pub fn init(cx: &mut AppContext) {
|
||||
// register_followable_item::<ChannelView>(cx)
|
||||
// }
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
register_followable_item::<ChannelView>(cx)
|
||||
}
|
||||
|
||||
// pub struct ChannelView {
|
||||
// pub editor: ViewHandle<Editor>,
|
||||
// project: ModelHandle<Project>,
|
||||
// channel_store: ModelHandle<ChannelStore>,
|
||||
// channel_buffer: ModelHandle<ChannelBuffer>,
|
||||
// remote_id: Option<ViewId>,
|
||||
// _editor_event_subscription: Subscription,
|
||||
// }
|
||||
pub struct ChannelView {
|
||||
pub editor: View<Editor>,
|
||||
project: Model<Project>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_buffer: Model<ChannelBuffer>,
|
||||
remote_id: Option<ViewId>,
|
||||
_editor_event_subscription: Subscription,
|
||||
}
|
||||
|
||||
// impl ChannelView {
|
||||
// pub fn open(
|
||||
// channel_id: ChannelId,
|
||||
// workspace: ViewHandle<Workspace>,
|
||||
// cx: &mut AppContext,
|
||||
// ) -> Task<Result<ViewHandle<Self>>> {
|
||||
// let pane = workspace.read(cx).active_pane().clone();
|
||||
// let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
|
||||
// cx.spawn(|mut cx| async move {
|
||||
// let channel_view = channel_view.await?;
|
||||
// pane.update(&mut cx, |pane, cx| {
|
||||
// report_call_event_for_channel(
|
||||
// "open channel notes",
|
||||
// channel_id,
|
||||
// &workspace.read(cx).app_state().client,
|
||||
// cx,
|
||||
// );
|
||||
// pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
|
||||
// });
|
||||
// anyhow::Ok(channel_view)
|
||||
// })
|
||||
// }
|
||||
impl ChannelView {
|
||||
pub fn open(
|
||||
channel_id: ChannelId,
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
let pane = workspace.read(cx).active_pane().clone();
|
||||
let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
|
||||
cx.spawn(|mut cx| async move {
|
||||
let channel_view = channel_view.await?;
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
report_call_event_for_channel(
|
||||
"open channel notes",
|
||||
channel_id,
|
||||
&workspace.read(cx).app_state().client,
|
||||
cx,
|
||||
);
|
||||
pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
|
||||
})?;
|
||||
anyhow::Ok(channel_view)
|
||||
})
|
||||
}
|
||||
|
||||
// pub fn open_in_pane(
|
||||
// channel_id: ChannelId,
|
||||
// pane: ViewHandle<Pane>,
|
||||
// workspace: ViewHandle<Workspace>,
|
||||
// cx: &mut AppContext,
|
||||
// ) -> Task<Result<ViewHandle<Self>>> {
|
||||
// let workspace = workspace.read(cx);
|
||||
// let project = workspace.project().to_owned();
|
||||
// let channel_store = ChannelStore::global(cx);
|
||||
// let language_registry = workspace.app_state().languages.clone();
|
||||
// let markdown = language_registry.language_for_name("Markdown");
|
||||
// let channel_buffer =
|
||||
// channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
|
||||
pub fn open_in_pane(
|
||||
channel_id: ChannelId,
|
||||
pane: View<Pane>,
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().to_owned();
|
||||
let channel_store = ChannelStore::global(cx);
|
||||
let language_registry = workspace.app_state().languages.clone();
|
||||
let markdown = language_registry.language_for_name("Markdown");
|
||||
let channel_buffer =
|
||||
channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
|
||||
|
||||
// cx.spawn(|mut cx| async move {
|
||||
// let channel_buffer = channel_buffer.await?;
|
||||
// let markdown = markdown.await.log_err();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let channel_buffer = channel_buffer.await?;
|
||||
let markdown = markdown.await.log_err();
|
||||
|
||||
// channel_buffer.update(&mut cx, |buffer, cx| {
|
||||
// buffer.buffer().update(cx, |buffer, cx| {
|
||||
// buffer.set_language_registry(language_registry);
|
||||
// if let Some(markdown) = markdown {
|
||||
// buffer.set_language(Some(markdown), cx);
|
||||
// }
|
||||
// })
|
||||
// });
|
||||
channel_buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, cx| {
|
||||
buffer.set_language_registry(language_registry);
|
||||
if let Some(markdown) = markdown {
|
||||
buffer.set_language(Some(markdown), cx);
|
||||
}
|
||||
})
|
||||
})?;
|
||||
|
||||
// pane.update(&mut cx, |pane, cx| {
|
||||
// let buffer_id = channel_buffer.read(cx).remote_id(cx);
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
let buffer_id = channel_buffer.read(cx).remote_id(cx);
|
||||
|
||||
// let existing_view = pane
|
||||
// .items_of_type::<Self>()
|
||||
// .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
|
||||
let existing_view = pane
|
||||
.items_of_type::<Self>()
|
||||
.find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
|
||||
|
||||
// // If this channel buffer is already open in this pane, just return it.
|
||||
// if let Some(existing_view) = existing_view.clone() {
|
||||
// if existing_view.read(cx).channel_buffer == channel_buffer {
|
||||
// return existing_view;
|
||||
// }
|
||||
// }
|
||||
// If this channel buffer is already open in this pane, just return it.
|
||||
if let Some(existing_view) = existing_view.clone() {
|
||||
if existing_view.read(cx).channel_buffer == channel_buffer {
|
||||
return existing_view;
|
||||
}
|
||||
}
|
||||
|
||||
// let view = cx.add_view(|cx| {
|
||||
// let mut this = Self::new(project, channel_store, channel_buffer, cx);
|
||||
// this.acknowledge_buffer_version(cx);
|
||||
// this
|
||||
// });
|
||||
let view = cx.build_view(|cx| {
|
||||
let mut this = Self::new(project, channel_store, channel_buffer, cx);
|
||||
this.acknowledge_buffer_version(cx);
|
||||
this
|
||||
});
|
||||
|
||||
// // If the pane contained a disconnected view for this channel buffer,
|
||||
// // replace that.
|
||||
// if let Some(existing_item) = existing_view {
|
||||
// if let Some(ix) = pane.index_for_item(&existing_item) {
|
||||
// pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx)
|
||||
// .detach();
|
||||
// pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
|
||||
// }
|
||||
// }
|
||||
// If the pane contained a disconnected view for this channel buffer,
|
||||
// replace that.
|
||||
if let Some(existing_item) = existing_view {
|
||||
if let Some(ix) = pane.index_for_item(&existing_item) {
|
||||
pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx)
|
||||
.detach();
|
||||
pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
|
||||
}
|
||||
}
|
||||
|
||||
// view
|
||||
// })
|
||||
// .ok_or_else(|| anyhow!("pane was dropped"))
|
||||
// })
|
||||
// }
|
||||
view
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// pub fn new(
|
||||
// project: ModelHandle<Project>,
|
||||
// channel_store: ModelHandle<ChannelStore>,
|
||||
// channel_buffer: ModelHandle<ChannelBuffer>,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> Self {
|
||||
// let buffer = channel_buffer.read(cx).buffer();
|
||||
// let editor = cx.add_view(|cx| {
|
||||
// let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||
// editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
|
||||
// channel_buffer.clone(),
|
||||
// )));
|
||||
// editor.set_read_only(
|
||||
// !channel_buffer
|
||||
// .read(cx)
|
||||
// .channel(cx)
|
||||
// .is_some_and(|c| c.can_edit_notes()),
|
||||
// );
|
||||
// editor
|
||||
// });
|
||||
// let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
|
||||
pub fn new(
|
||||
project: Model<Project>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_buffer: Model<ChannelBuffer>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let buffer = channel_buffer.read(cx).buffer();
|
||||
let editor = cx.build_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||
editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
|
||||
channel_buffer.clone(),
|
||||
)));
|
||||
editor.set_read_only(
|
||||
!channel_buffer
|
||||
.read(cx)
|
||||
.channel(cx)
|
||||
.is_some_and(|c| c.can_edit_notes()),
|
||||
);
|
||||
editor
|
||||
});
|
||||
let _editor_event_subscription =
|
||||
cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone()));
|
||||
|
||||
// cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
|
||||
// .detach();
|
||||
cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
|
||||
.detach();
|
||||
|
||||
// Self {
|
||||
// editor,
|
||||
// project,
|
||||
// channel_store,
|
||||
// channel_buffer,
|
||||
// remote_id: None,
|
||||
// _editor_event_subscription,
|
||||
// }
|
||||
// }
|
||||
Self {
|
||||
editor,
|
||||
project,
|
||||
channel_store,
|
||||
channel_buffer,
|
||||
remote_id: None,
|
||||
_editor_event_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
|
||||
// self.channel_buffer.read(cx).channel(cx)
|
||||
// }
|
||||
pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
|
||||
self.channel_buffer.read(cx).channel(cx)
|
||||
}
|
||||
|
||||
// fn handle_channel_buffer_event(
|
||||
// &mut self,
|
||||
// _: ModelHandle<ChannelBuffer>,
|
||||
// event: &ChannelBufferEvent,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) {
|
||||
// match event {
|
||||
// ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
|
||||
// editor.set_read_only(true);
|
||||
// cx.notify();
|
||||
// }),
|
||||
// ChannelBufferEvent::ChannelChanged => {
|
||||
// self.editor.update(cx, |editor, cx| {
|
||||
// editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
|
||||
// cx.emit(editor::Event::TitleChanged);
|
||||
// cx.notify()
|
||||
// });
|
||||
// }
|
||||
// ChannelBufferEvent::BufferEdited => {
|
||||
// if cx.is_self_focused() || self.editor.is_focused(cx) {
|
||||
// self.acknowledge_buffer_version(cx);
|
||||
// } else {
|
||||
// self.channel_store.update(cx, |store, cx| {
|
||||
// let channel_buffer = self.channel_buffer.read(cx);
|
||||
// store.notes_changed(
|
||||
// channel_buffer.channel_id,
|
||||
// channel_buffer.epoch(),
|
||||
// &channel_buffer.buffer().read(cx).version(),
|
||||
// cx,
|
||||
// )
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// ChannelBufferEvent::CollaboratorsChanged => {}
|
||||
// }
|
||||
// }
|
||||
fn handle_channel_buffer_event(
|
||||
&mut self,
|
||||
_: Model<ChannelBuffer>,
|
||||
event: &ChannelBufferEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
|
||||
editor.set_read_only(true);
|
||||
cx.notify();
|
||||
}),
|
||||
ChannelBufferEvent::ChannelChanged => {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
|
||||
cx.emit(editor::EditorEvent::TitleChanged);
|
||||
cx.notify()
|
||||
});
|
||||
}
|
||||
ChannelBufferEvent::BufferEdited => {
|
||||
if self.editor.read(cx).is_focused(cx) {
|
||||
self.acknowledge_buffer_version(cx);
|
||||
} else {
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
let channel_buffer = self.channel_buffer.read(cx);
|
||||
store.notes_changed(
|
||||
channel_buffer.channel_id,
|
||||
channel_buffer.epoch(),
|
||||
&channel_buffer.buffer().read(cx).version(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
ChannelBufferEvent::CollaboratorsChanged => {}
|
||||
}
|
||||
}
|
||||
|
||||
// fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) {
|
||||
// self.channel_store.update(cx, |store, cx| {
|
||||
// let channel_buffer = self.channel_buffer.read(cx);
|
||||
// store.acknowledge_notes_version(
|
||||
// channel_buffer.channel_id,
|
||||
// channel_buffer.epoch(),
|
||||
// &channel_buffer.buffer().read(cx).version(),
|
||||
// cx,
|
||||
// )
|
||||
// });
|
||||
// self.channel_buffer.update(cx, |buffer, cx| {
|
||||
// buffer.acknowledge_buffer_version(cx);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<ChannelView>) {
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
let channel_buffer = self.channel_buffer.read(cx);
|
||||
store.acknowledge_notes_version(
|
||||
channel_buffer.channel_id,
|
||||
channel_buffer.epoch(),
|
||||
&channel_buffer.buffer().read(cx).version(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.channel_buffer.update(cx, |buffer, cx| {
|
||||
buffer.acknowledge_buffer_version(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// impl Entity for ChannelView {
|
||||
// type Event = editor::Event;
|
||||
// }
|
||||
impl EventEmitter<EditorEvent> for ChannelView {}
|
||||
|
||||
// impl View for ChannelView {
|
||||
// fn ui_name() -> &'static str {
|
||||
// "ChannelView"
|
||||
// }
|
||||
impl Render for ChannelView {
|
||||
type Element = AnyView;
|
||||
|
||||
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
// ChildView::new(self.editor.as_any(), cx).into_any()
|
||||
// }
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
self.editor.clone().into()
|
||||
}
|
||||
}
|
||||
|
||||
// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
// if cx.is_self_focused() {
|
||||
// self.acknowledge_buffer_version(cx);
|
||||
// cx.focus(self.editor.as_any())
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
impl FocusableView for ChannelView {
|
||||
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||
self.editor.read(cx).focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
// impl Item for ChannelView {
|
||||
// fn act_as_type<'a>(
|
||||
// &'a self,
|
||||
// type_id: TypeId,
|
||||
// self_handle: &'a ViewHandle<Self>,
|
||||
// _: &'a AppContext,
|
||||
// ) -> Option<&'a AnyViewHandle> {
|
||||
// if type_id == TypeId::of::<Self>() {
|
||||
// Some(self_handle)
|
||||
// } else if type_id == TypeId::of::<Editor>() {
|
||||
// Some(&self.editor)
|
||||
// } else {
|
||||
// None
|
||||
// }
|
||||
// }
|
||||
impl Item for ChannelView {
|
||||
type Event = EditorEvent;
|
||||
|
||||
// fn tab_content<V: 'static>(
|
||||
// &self,
|
||||
// _: Option<usize>,
|
||||
// style: &theme::Tab,
|
||||
// cx: &gpui::AppContext,
|
||||
// ) -> AnyElement<V> {
|
||||
// let label = if let Some(channel) = self.channel(cx) {
|
||||
// match (
|
||||
// channel.can_edit_notes(),
|
||||
// self.channel_buffer.read(cx).is_connected(),
|
||||
// ) {
|
||||
// (true, true) => format!("#{}", channel.name),
|
||||
// (false, true) => format!("#{} (read-only)", channel.name),
|
||||
// (_, false) => format!("#{} (disconnected)", channel.name),
|
||||
// }
|
||||
// } else {
|
||||
// format!("channel notes (disconnected)")
|
||||
// };
|
||||
// Label::new(label, style.label.to_owned()).into_any()
|
||||
// }
|
||||
fn act_as_type<'a>(
|
||||
&'a self,
|
||||
type_id: TypeId,
|
||||
self_handle: &'a View<Self>,
|
||||
_: &'a AppContext,
|
||||
) -> Option<AnyView> {
|
||||
if type_id == TypeId::of::<Self>() {
|
||||
Some(self_handle.to_any())
|
||||
} else if type_id == TypeId::of::<Editor>() {
|
||||
Some(self.editor.to_any())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self> {
|
||||
// Some(Self::new(
|
||||
// self.project.clone(),
|
||||
// self.channel_store.clone(),
|
||||
// self.channel_buffer.clone(),
|
||||
// cx,
|
||||
// ))
|
||||
// }
|
||||
fn tab_content(&self, _: Option<usize>, cx: &WindowContext) -> AnyElement {
|
||||
let label = if let Some(channel) = self.channel(cx) {
|
||||
match (
|
||||
channel.can_edit_notes(),
|
||||
self.channel_buffer.read(cx).is_connected(),
|
||||
) {
|
||||
(true, true) => format!("#{}", channel.name),
|
||||
(false, true) => format!("#{} (read-only)", channel.name),
|
||||
(_, false) => format!("#{} (disconnected)", channel.name),
|
||||
}
|
||||
} else {
|
||||
format!("channel notes (disconnected)")
|
||||
};
|
||||
Label::new(label).into_any_element()
|
||||
}
|
||||
|
||||
// fn is_singleton(&self, _cx: &AppContext) -> bool {
|
||||
// false
|
||||
// }
|
||||
fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<View<Self>> {
|
||||
Some(cx.build_view(|cx| {
|
||||
Self::new(
|
||||
self.project.clone(),
|
||||
self.channel_store.clone(),
|
||||
self.channel_buffer.clone(),
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
// fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
// self.editor
|
||||
// .update(cx, |editor, cx| editor.navigate(data, cx))
|
||||
// }
|
||||
fn is_singleton(&self, _cx: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
// fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
// self.editor
|
||||
// .update(cx, |editor, cx| Item::deactivated(editor, cx))
|
||||
// }
|
||||
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.navigate(data, cx))
|
||||
}
|
||||
|
||||
// fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
|
||||
// self.editor
|
||||
// .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
|
||||
// }
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| Item::deactivated(editor, cx))
|
||||
}
|
||||
|
||||
// fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
// Some(Box::new(self.editor.clone()))
|
||||
// }
|
||||
fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
|
||||
}
|
||||
|
||||
// fn show_toolbar(&self) -> bool {
|
||||
// true
|
||||
// }
|
||||
fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(self.editor.clone()))
|
||||
}
|
||||
|
||||
// fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
|
||||
// self.editor.read(cx).pixel_position_of_cursor(cx)
|
||||
// }
|
||||
fn show_toolbar(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
// fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
|
||||
// editor::Editor::to_item_events(event)
|
||||
// }
|
||||
// }
|
||||
fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
|
||||
self.editor.read(cx).pixel_position_of_cursor(cx)
|
||||
}
|
||||
|
||||
// impl FollowableItem for ChannelView {
|
||||
// fn remote_id(&self) -> Option<workspace::ViewId> {
|
||||
// self.remote_id
|
||||
// }
|
||||
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
|
||||
Editor::to_item_events(event, f)
|
||||
}
|
||||
}
|
||||
|
||||
// fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
|
||||
// let channel_buffer = self.channel_buffer.read(cx);
|
||||
// if !channel_buffer.is_connected() {
|
||||
// return None;
|
||||
// }
|
||||
impl FollowableItem for ChannelView {
|
||||
fn remote_id(&self) -> Option<workspace::ViewId> {
|
||||
self.remote_id
|
||||
}
|
||||
|
||||
// Some(proto::view::Variant::ChannelView(
|
||||
// proto::view::ChannelView {
|
||||
// channel_id: channel_buffer.channel_id,
|
||||
// editor: if let Some(proto::view::Variant::Editor(proto)) =
|
||||
// self.editor.read(cx).to_state_proto(cx)
|
||||
// {
|
||||
// Some(proto)
|
||||
// } else {
|
||||
// None
|
||||
// },
|
||||
// },
|
||||
// ))
|
||||
// }
|
||||
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
|
||||
let channel_buffer = self.channel_buffer.read(cx);
|
||||
if !channel_buffer.is_connected() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// fn from_state_proto(
|
||||
// pane: ViewHandle<workspace::Pane>,
|
||||
// workspace: ViewHandle<workspace::Workspace>,
|
||||
// remote_id: workspace::ViewId,
|
||||
// state: &mut Option<proto::view::Variant>,
|
||||
// cx: &mut AppContext,
|
||||
// ) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
|
||||
// let Some(proto::view::Variant::ChannelView(_)) = state else {
|
||||
// return None;
|
||||
// };
|
||||
// let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
|
||||
// unreachable!()
|
||||
// };
|
||||
Some(proto::view::Variant::ChannelView(
|
||||
proto::view::ChannelView {
|
||||
channel_id: channel_buffer.channel_id,
|
||||
editor: if let Some(proto::view::Variant::Editor(proto)) =
|
||||
self.editor.read(cx).to_state_proto(cx)
|
||||
{
|
||||
Some(proto)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
// let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
|
||||
fn from_state_proto(
|
||||
pane: View<workspace::Pane>,
|
||||
workspace: View<workspace::Workspace>,
|
||||
remote_id: workspace::ViewId,
|
||||
state: &mut Option<proto::view::Variant>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<gpui::Task<anyhow::Result<View<Self>>>> {
|
||||
let Some(proto::view::Variant::ChannelView(_)) = state else {
|
||||
return None;
|
||||
};
|
||||
let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
// Some(cx.spawn(|mut cx| async move {
|
||||
// let this = open.await?;
|
||||
let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
|
||||
|
||||
// let task = this
|
||||
// .update(&mut cx, |this, cx| {
|
||||
// this.remote_id = Some(remote_id);
|
||||
Some(cx.spawn(|mut cx| async move {
|
||||
let this = open.await?;
|
||||
|
||||
// if let Some(state) = state.editor {
|
||||
// Some(this.editor.update(cx, |editor, cx| {
|
||||
// editor.apply_update_proto(
|
||||
// &this.project,
|
||||
// proto::update_view::Variant::Editor(proto::update_view::Editor {
|
||||
// selections: state.selections,
|
||||
// pending_selection: state.pending_selection,
|
||||
// scroll_top_anchor: state.scroll_top_anchor,
|
||||
// scroll_x: state.scroll_x,
|
||||
// scroll_y: state.scroll_y,
|
||||
// ..Default::default()
|
||||
// }),
|
||||
// cx,
|
||||
// )
|
||||
// }))
|
||||
// } else {
|
||||
// None
|
||||
// }
|
||||
// })
|
||||
// .ok_or_else(|| anyhow!("window was closed"))?;
|
||||
let task = this.update(&mut cx, |this, cx| {
|
||||
this.remote_id = Some(remote_id);
|
||||
|
||||
// if let Some(task) = task {
|
||||
// task.await?;
|
||||
// }
|
||||
if let Some(state) = state.editor {
|
||||
Some(this.editor.update(cx, |editor, cx| {
|
||||
editor.apply_update_proto(
|
||||
&this.project,
|
||||
proto::update_view::Variant::Editor(proto::update_view::Editor {
|
||||
selections: state.selections,
|
||||
pending_selection: state.pending_selection,
|
||||
scroll_top_anchor: state.scroll_top_anchor,
|
||||
scroll_x: state.scroll_x,
|
||||
scroll_y: state.scroll_y,
|
||||
..Default::default()
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?;
|
||||
|
||||
// Ok(this)
|
||||
// }))
|
||||
// }
|
||||
if let Some(task) = task {
|
||||
task.await?;
|
||||
}
|
||||
|
||||
// fn add_event_to_update_proto(
|
||||
// &self,
|
||||
// event: &Self::Event,
|
||||
// update: &mut Option<proto::update_view::Variant>,
|
||||
// cx: &AppContext,
|
||||
// ) -> bool {
|
||||
// self.editor
|
||||
// .read(cx)
|
||||
// .add_event_to_update_proto(event, update, cx)
|
||||
// }
|
||||
Ok(this)
|
||||
}))
|
||||
}
|
||||
|
||||
// fn apply_update_proto(
|
||||
// &mut self,
|
||||
// project: &ModelHandle<Project>,
|
||||
// message: proto::update_view::Variant,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> gpui::Task<anyhow::Result<()>> {
|
||||
// self.editor.update(cx, |editor, cx| {
|
||||
// editor.apply_update_proto(project, message, cx)
|
||||
// })
|
||||
// }
|
||||
fn add_event_to_update_proto(
|
||||
&self,
|
||||
event: &EditorEvent,
|
||||
update: &mut Option<proto::update_view::Variant>,
|
||||
cx: &WindowContext,
|
||||
) -> bool {
|
||||
self.editor
|
||||
.read(cx)
|
||||
.add_event_to_update_proto(event, update, cx)
|
||||
}
|
||||
|
||||
// fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
|
||||
// self.editor.update(cx, |editor, cx| {
|
||||
// editor.set_leader_peer_id(leader_peer_id, cx)
|
||||
// })
|
||||
// }
|
||||
fn apply_update_proto(
|
||||
&mut self,
|
||||
project: &Model<Project>,
|
||||
message: proto::update_view::Variant,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> gpui::Task<anyhow::Result<()>> {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.apply_update_proto(project, message, cx)
|
||||
})
|
||||
}
|
||||
|
||||
// fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
|
||||
// Editor::should_unfollow_on_event(event, cx)
|
||||
// }
|
||||
fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.set_leader_peer_id(leader_peer_id, cx)
|
||||
})
|
||||
}
|
||||
|
||||
// fn is_project_item(&self, _cx: &AppContext) -> bool {
|
||||
// false
|
||||
// }
|
||||
// }
|
||||
fn is_project_item(&self, _cx: &WindowContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
// struct ChannelBufferCollaborationHub(ModelHandle<ChannelBuffer>);
|
||||
fn to_follow_event(event: &Self::Event) -> Option<workspace::item::FollowEvent> {
|
||||
Editor::to_follow_event(event)
|
||||
}
|
||||
}
|
||||
|
||||
// impl CollaborationHub for ChannelBufferCollaborationHub {
|
||||
// fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
|
||||
// self.0.read(cx).collaborators()
|
||||
// }
|
||||
struct ChannelBufferCollaborationHub(Model<ChannelBuffer>);
|
||||
|
||||
// fn user_participant_indices<'a>(
|
||||
// &self,
|
||||
// cx: &'a AppContext,
|
||||
// ) -> &'a HashMap<u64, ParticipantIndex> {
|
||||
// self.0.read(cx).user_store().read(cx).participant_indices()
|
||||
// }
|
||||
// }
|
||||
impl CollaborationHub for ChannelBufferCollaborationHub {
|
||||
fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
|
||||
self.0.read(cx).collaborators()
|
||||
}
|
||||
|
||||
fn user_participant_indices<'a>(
|
||||
&self,
|
||||
cx: &'a AppContext,
|
||||
) -> &'a HashMap<u64, ParticipantIndex> {
|
||||
self.0.read(cx).user_store().read(cx).participant_indices()
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,13 +3,14 @@ use client::UserId;
|
||||
use collections::HashMap;
|
||||
use editor::{AnchorRangeExt, Editor};
|
||||
use gpui::{
|
||||
elements::ChildView, AnyElement, AsyncAppContext, Element, Entity, ModelHandle, Task, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
AnyView, AsyncWindowContext, FocusableView, Model, Render, SharedString, Task, View,
|
||||
ViewContext, WeakView,
|
||||
};
|
||||
use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
|
||||
use lazy_static::lazy_static;
|
||||
use project::search::SearchQuery;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use workspace::item::ItemHandle;
|
||||
|
||||
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
|
||||
|
||||
@@ -19,8 +20,8 @@ lazy_static! {
|
||||
}
|
||||
|
||||
pub struct MessageEditor {
|
||||
pub editor: ViewHandle<Editor>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
pub editor: View<Editor>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
users: HashMap<String, UserId>,
|
||||
mentions: Vec<UserId>,
|
||||
mentions_task: Option<Task<()>>,
|
||||
@@ -30,8 +31,8 @@ pub struct MessageEditor {
|
||||
impl MessageEditor {
|
||||
pub fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
editor: ViewHandle<Editor>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
editor: View<Editor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
editor.update(cx, |editor, cx| {
|
||||
@@ -48,15 +49,13 @@ impl MessageEditor {
|
||||
cx.subscribe(&buffer, Self::on_buffer_event).detach();
|
||||
|
||||
let markdown = language_registry.language_for_name("Markdown");
|
||||
cx.app_context()
|
||||
.spawn(|mut cx| async move {
|
||||
let markdown = markdown.await?;
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_language(Some(markdown), cx)
|
||||
});
|
||||
anyhow::Ok(())
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let markdown = markdown.await?;
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_language(Some(markdown), cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
Self {
|
||||
editor,
|
||||
@@ -71,7 +70,7 @@ impl MessageEditor {
|
||||
pub fn set_channel(
|
||||
&mut self,
|
||||
channel_id: u64,
|
||||
channel_name: Option<String>,
|
||||
channel_name: Option<SharedString>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
@@ -132,26 +131,28 @@ impl MessageEditor {
|
||||
|
||||
fn on_buffer_event(
|
||||
&mut self,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
buffer: Model<Buffer>,
|
||||
event: &language::Event,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let language::Event::Reparsed | language::Event::Edited = event {
|
||||
let buffer = buffer.read(cx).snapshot();
|
||||
self.mentions_task = Some(cx.spawn(|this, cx| async move {
|
||||
cx.background().timer(MENTIONS_DEBOUNCE_INTERVAL).await;
|
||||
cx.background_executor()
|
||||
.timer(MENTIONS_DEBOUNCE_INTERVAL)
|
||||
.await;
|
||||
Self::find_mentions(this, buffer, cx).await;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async fn find_mentions(
|
||||
this: WeakViewHandle<MessageEditor>,
|
||||
this: WeakView<MessageEditor>,
|
||||
buffer: BufferSnapshot,
|
||||
mut cx: AsyncAppContext,
|
||||
mut cx: AsyncWindowContext,
|
||||
) {
|
||||
let (buffer, ranges) = cx
|
||||
.background()
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
|
||||
(buffer, ranges)
|
||||
@@ -180,11 +181,7 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
editor.clear_highlights::<Self>(cx);
|
||||
editor.highlight_text::<Self>(
|
||||
anchor_ranges,
|
||||
theme::current(cx).chat_panel.rich_text.mention_highlight,
|
||||
cx,
|
||||
)
|
||||
editor.highlight_text::<Self>(anchor_ranges, gpui::red().into(), cx)
|
||||
});
|
||||
|
||||
this.mentions = mentioned_user_ids;
|
||||
@@ -192,21 +189,17 @@ impl MessageEditor {
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for MessageEditor {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for MessageEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
|
||||
ChildView::new(&self.editor, cx).into_any()
|
||||
pub(crate) fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
|
||||
self.editor.read(cx).focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.editor);
|
||||
}
|
||||
impl Render for MessageEditor {
|
||||
type Element = AnyView;
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
self.editor.to_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +207,7 @@ impl View for MessageEditor {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use client::{Client, User, UserStore};
|
||||
use gpui::{TestAppContext, WindowHandle};
|
||||
use gpui::{Context as _, TestAppContext, VisualContext as _};
|
||||
use language::{Language, LanguageConfig};
|
||||
use rpc::proto;
|
||||
use settings::SettingsStore;
|
||||
@@ -222,8 +215,17 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_message_editor(cx: &mut TestAppContext) {
|
||||
let editor = init_test(cx);
|
||||
let editor = editor.root(cx);
|
||||
let language_registry = init_test(cx);
|
||||
|
||||
let (editor, cx) = cx.add_window_view(|cx| {
|
||||
MessageEditor::new(
|
||||
language_registry,
|
||||
ChannelStore::global(cx),
|
||||
cx.build_view(|cx| Editor::auto_height(4, cx)),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_members(
|
||||
@@ -255,7 +257,7 @@ mod tests {
|
||||
});
|
||||
});
|
||||
|
||||
cx.foreground().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
|
||||
cx.executor().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let (text, ranges) = marked_text_ranges("Hello, «@a-b»! Have you met «@C_D»?", false);
|
||||
@@ -269,15 +271,14 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) -> WindowHandle<MessageEditor> {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) -> Arc<LanguageRegistry> {
|
||||
cx.update(|cx| {
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http.clone(), cx);
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
theme::init((), cx);
|
||||
let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
language::init(cx);
|
||||
editor::init(cx);
|
||||
client::init(&client, cx);
|
||||
@@ -292,16 +293,6 @@ mod tests {
|
||||
},
|
||||
Some(tree_sitter_markdown::language()),
|
||||
)));
|
||||
|
||||
let editor = cx.add_window(|cx| {
|
||||
MessageEditor::new(
|
||||
language_registry,
|
||||
ChannelStore::global(cx),
|
||||
cx.add_view(|cx| Editor::auto_height(4, None, cx)),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
editor
|
||||
language_registry
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,58 +3,55 @@ use client::{
|
||||
proto::{self, ChannelRole, ChannelVisibility},
|
||||
User, UserId, UserStore,
|
||||
};
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::*,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AppContext, ClipboardItem, Entity, ModelHandle, MouseState, Task, View, ViewContext,
|
||||
ViewHandle,
|
||||
actions, div, AppContext, ClipboardItem, DismissEvent, Div, Entity, EventEmitter,
|
||||
FocusableView, Model, ParentElement, Render, Styled, Task, View, ViewContext, VisualContext,
|
||||
WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::sync::Arc;
|
||||
use ui::prelude::*;
|
||||
use util::TryFutureExt;
|
||||
use workspace::Modal;
|
||||
use workspace::ModalView;
|
||||
|
||||
actions!(
|
||||
channel_modal,
|
||||
[
|
||||
SelectNextControl,
|
||||
ToggleMode,
|
||||
ToggleMemberAdmin,
|
||||
RemoveMember
|
||||
]
|
||||
SelectNextControl,
|
||||
ToggleMode,
|
||||
ToggleMemberAdmin,
|
||||
RemoveMember
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
Picker::<ChannelModalDelegate>::init(cx);
|
||||
cx.add_action(ChannelModal::toggle_mode);
|
||||
cx.add_action(ChannelModal::toggle_member_admin);
|
||||
cx.add_action(ChannelModal::remove_member);
|
||||
cx.add_action(ChannelModal::dismiss);
|
||||
}
|
||||
// pub fn init(cx: &mut AppContext) {
|
||||
// Picker::<ChannelModalDelegate>::init(cx);
|
||||
// cx.add_action(ChannelModal::toggle_mode);
|
||||
// cx.add_action(ChannelModal::toggle_member_admin);
|
||||
// cx.add_action(ChannelModal::remove_member);
|
||||
// cx.add_action(ChannelModal::dismiss);
|
||||
// }
|
||||
|
||||
pub struct ChannelModal {
|
||||
picker: ViewHandle<Picker<ChannelModalDelegate>>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
picker: View<Picker<ChannelModalDelegate>>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
has_focus: bool,
|
||||
}
|
||||
|
||||
impl ChannelModal {
|
||||
pub fn new(
|
||||
user_store: ModelHandle<UserStore>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
user_store: Model<UserStore>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
mode: Mode,
|
||||
members: Vec<ChannelMembership>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
|
||||
let picker = cx.add_view(|cx| {
|
||||
let channel_modal = cx.view().downgrade();
|
||||
let picker = cx.build_view(|cx| {
|
||||
Picker::new(
|
||||
ChannelModalDelegate {
|
||||
channel_modal,
|
||||
matching_users: Vec::new(),
|
||||
matching_member_indices: Vec::new(),
|
||||
selected_index: 0,
|
||||
@@ -64,20 +61,17 @@ impl ChannelModal {
|
||||
match_candidates: Vec::new(),
|
||||
members,
|
||||
mode,
|
||||
context_menu: cx.add_view(|cx| {
|
||||
let mut menu = ContextMenu::new(cx.view_id(), cx);
|
||||
menu.set_position_mode(OverlayPositionMode::Local);
|
||||
menu
|
||||
}),
|
||||
// context_menu: cx.add_view(|cx| {
|
||||
// let mut menu = ContextMenu::new(cx.view_id(), cx);
|
||||
// menu.set_position_mode(OverlayPositionMode::Local);
|
||||
// menu
|
||||
// }),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
|
||||
});
|
||||
|
||||
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
|
||||
|
||||
let has_focus = picker.read(cx).has_focus();
|
||||
let has_focus = picker.focus_handle(cx).contains_focused(cx);
|
||||
|
||||
Self {
|
||||
picker,
|
||||
@@ -88,7 +82,7 @@ impl ChannelModal {
|
||||
}
|
||||
|
||||
fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
|
||||
let mode = match self.picker.read(cx).delegate().mode {
|
||||
let mode = match self.picker.read(cx).delegate.mode {
|
||||
Mode::ManageMembers => Mode::InviteMembers,
|
||||
Mode::InviteMembers => Mode::ManageMembers,
|
||||
};
|
||||
@@ -103,20 +97,20 @@ impl ChannelModal {
|
||||
let mut members = channel_store
|
||||
.update(&mut cx, |channel_store, cx| {
|
||||
channel_store.get_channel_member_details(channel_id, cx)
|
||||
})
|
||||
})?
|
||||
.await?;
|
||||
|
||||
members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.picker
|
||||
.update(cx, |picker, _| picker.delegate_mut().members = members);
|
||||
.update(cx, |picker, _| picker.delegate.members = members);
|
||||
})?;
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.picker.update(cx, |picker, cx| {
|
||||
let delegate = picker.delegate_mut();
|
||||
let delegate = &mut picker.delegate;
|
||||
delegate.mode = mode;
|
||||
delegate.selected_index = 0;
|
||||
picker.set_query("", cx);
|
||||
@@ -131,203 +125,195 @@ impl ChannelModal {
|
||||
|
||||
fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.delegate_mut().toggle_selected_member_admin(cx);
|
||||
picker.delegate.toggle_selected_member_admin(cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.delegate_mut().remove_selected_member(cx);
|
||||
picker.delegate.remove_selected_member(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ChannelModal {
|
||||
type Event = PickerEvent;
|
||||
}
|
||||
impl EventEmitter<DismissEvent> for ChannelModal {}
|
||||
impl ModalView for ChannelModal {}
|
||||
|
||||
impl View for ChannelModal {
|
||||
fn ui_name() -> &'static str {
|
||||
"ChannelModal"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = &theme::current(cx).collab_panel.tabbed_modal;
|
||||
|
||||
let mode = self.picker.read(cx).delegate().mode;
|
||||
let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
|
||||
return Empty::new().into_any();
|
||||
};
|
||||
|
||||
enum InviteMembers {}
|
||||
enum ManageMembers {}
|
||||
|
||||
fn render_mode_button<T: 'static>(
|
||||
mode: Mode,
|
||||
text: &'static str,
|
||||
current_mode: Mode,
|
||||
theme: &theme::TabbedModal,
|
||||
cx: &mut ViewContext<ChannelModal>,
|
||||
) -> AnyElement<ChannelModal> {
|
||||
let active = mode == current_mode;
|
||||
MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
|
||||
let contained_text = theme.tab_button.style_for(active, state);
|
||||
Label::new(text, contained_text.text.clone())
|
||||
.contained()
|
||||
.with_style(contained_text.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if !active {
|
||||
this.set_mode(mode, cx);
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_visibility(
|
||||
channel_id: ChannelId,
|
||||
visibility: ChannelVisibility,
|
||||
theme: &theme::TabbedModal,
|
||||
cx: &mut ViewContext<ChannelModal>,
|
||||
) -> AnyElement<ChannelModal> {
|
||||
enum TogglePublic {}
|
||||
|
||||
if visibility == ChannelVisibility::Members {
|
||||
return Flex::row()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||
let style = theme.visibility_toggle.style_for(state);
|
||||
Label::new(format!("{}", "Public access: OFF"), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(
|
||||
channel_id,
|
||||
ChannelVisibility::Public,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand),
|
||||
)
|
||||
.into_any();
|
||||
}
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||
let style = theme.visibility_toggle.style_for(state);
|
||||
Label::new(format!("{}", "Public access: ON"), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(
|
||||
channel_id,
|
||||
ChannelVisibility::Members,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand),
|
||||
)
|
||||
.with_spacing(14.0)
|
||||
.with_child(
|
||||
MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
|
||||
let style = theme.channel_link.style_for(state);
|
||||
Label::new(format!("{}", "copy link"), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if let Some(channel) =
|
||||
this.channel_store.read(cx).channel_for_id(channel_id)
|
||||
{
|
||||
let item = ClipboardItem::new(channel.link());
|
||||
cx.write_to_clipboard(item);
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Label::new(format!("#{}", channel.name), theme.title.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.title.container.clone()),
|
||||
)
|
||||
.with_child(render_visibility(channel.id, channel.visibility, theme, cx))
|
||||
.with_child(Flex::row().with_children([
|
||||
render_mode_button::<InviteMembers>(
|
||||
Mode::InviteMembers,
|
||||
"Invite members",
|
||||
mode,
|
||||
theme,
|
||||
cx,
|
||||
),
|
||||
render_mode_button::<ManageMembers>(
|
||||
Mode::ManageMembers,
|
||||
"Manage members",
|
||||
mode,
|
||||
theme,
|
||||
cx,
|
||||
),
|
||||
]))
|
||||
.expanded()
|
||||
.contained()
|
||||
.with_style(theme.header),
|
||||
)
|
||||
.with_child(
|
||||
ChildView::new(&self.picker, cx)
|
||||
.contained()
|
||||
.with_style(theme.body),
|
||||
)
|
||||
.constrained()
|
||||
.with_max_height(theme.max_height)
|
||||
.with_max_width(theme.max_width)
|
||||
.contained()
|
||||
.with_style(theme.modal)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
self.has_focus = true;
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.picker)
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
self.has_focus = false;
|
||||
impl FocusableView for ChannelModal {
|
||||
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Modal for ChannelModal {
|
||||
fn has_focus(&self) -> bool {
|
||||
self.has_focus
|
||||
impl Render for ChannelModal {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
v_stack().w(rems(34.)).child(self.picker.clone())
|
||||
// let theme = &theme::current(cx).collab_panel.tabbed_modal;
|
||||
|
||||
// let mode = self.picker.read(cx).delegate().mode;
|
||||
// let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
|
||||
// return Empty::new().into_any();
|
||||
// };
|
||||
|
||||
// enum InviteMembers {}
|
||||
// enum ManageMembers {}
|
||||
|
||||
// fn render_mode_button<T: 'static>(
|
||||
// mode: Mode,
|
||||
// text: &'static str,
|
||||
// current_mode: Mode,
|
||||
// theme: &theme::TabbedModal,
|
||||
// cx: &mut ViewContext<ChannelModal>,
|
||||
// ) -> AnyElement<ChannelModal> {
|
||||
// let active = mode == current_mode;
|
||||
// MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
|
||||
// let contained_text = theme.tab_button.style_for(active, state);
|
||||
// Label::new(text, contained_text.text.clone())
|
||||
// .contained()
|
||||
// .with_style(contained_text.container.clone())
|
||||
// })
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// if !active {
|
||||
// this.set_mode(mode, cx);
|
||||
// }
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn render_visibility(
|
||||
// channel_id: ChannelId,
|
||||
// visibility: ChannelVisibility,
|
||||
// theme: &theme::TabbedModal,
|
||||
// cx: &mut ViewContext<ChannelModal>,
|
||||
// ) -> AnyElement<ChannelModal> {
|
||||
// enum TogglePublic {}
|
||||
|
||||
// if visibility == ChannelVisibility::Members {
|
||||
// return Flex::row()
|
||||
// .with_child(
|
||||
// MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||
// let style = theme.visibility_toggle.style_for(state);
|
||||
// Label::new(format!("{}", "Public access: OFF"), style.text.clone())
|
||||
// .contained()
|
||||
// .with_style(style.container.clone())
|
||||
// })
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// this.channel_store
|
||||
// .update(cx, |channel_store, cx| {
|
||||
// channel_store.set_channel_visibility(
|
||||
// channel_id,
|
||||
// ChannelVisibility::Public,
|
||||
// cx,
|
||||
// )
|
||||
// })
|
||||
// .detach_and_log_err(cx);
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand),
|
||||
// )
|
||||
// .into_any();
|
||||
// }
|
||||
|
||||
// Flex::row()
|
||||
// .with_child(
|
||||
// MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||
// let style = theme.visibility_toggle.style_for(state);
|
||||
// Label::new(format!("{}", "Public access: ON"), style.text.clone())
|
||||
// .contained()
|
||||
// .with_style(style.container.clone())
|
||||
// })
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// this.channel_store
|
||||
// .update(cx, |channel_store, cx| {
|
||||
// channel_store.set_channel_visibility(
|
||||
// channel_id,
|
||||
// ChannelVisibility::Members,
|
||||
// cx,
|
||||
// )
|
||||
// })
|
||||
// .detach_and_log_err(cx);
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand),
|
||||
// )
|
||||
// .with_spacing(14.0)
|
||||
// .with_child(
|
||||
// MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
|
||||
// let style = theme.channel_link.style_for(state);
|
||||
// Label::new(format!("{}", "copy link"), style.text.clone())
|
||||
// .contained()
|
||||
// .with_style(style.container.clone())
|
||||
// })
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// if let Some(channel) =
|
||||
// this.channel_store.read(cx).channel_for_id(channel_id)
|
||||
// {
|
||||
// let item = ClipboardItem::new(channel.link());
|
||||
// cx.write_to_clipboard(item);
|
||||
// }
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand),
|
||||
// )
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Label::new(format!("#{}", channel.name), theme.title.text.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.title.container.clone()),
|
||||
// )
|
||||
// .with_child(render_visibility(channel.id, channel.visibility, theme, cx))
|
||||
// .with_child(Flex::row().with_children([
|
||||
// render_mode_button::<InviteMembers>(
|
||||
// Mode::InviteMembers,
|
||||
// "Invite members",
|
||||
// mode,
|
||||
// theme,
|
||||
// cx,
|
||||
// ),
|
||||
// render_mode_button::<ManageMembers>(
|
||||
// Mode::ManageMembers,
|
||||
// "Manage members",
|
||||
// mode,
|
||||
// theme,
|
||||
// cx,
|
||||
// ),
|
||||
// ]))
|
||||
// .expanded()
|
||||
// .contained()
|
||||
// .with_style(theme.header),
|
||||
// )
|
||||
// .with_child(
|
||||
// ChildView::new(&self.picker, cx)
|
||||
// .contained()
|
||||
// .with_style(theme.body),
|
||||
// )
|
||||
// .constrained()
|
||||
// .with_max_height(theme.max_height)
|
||||
// .with_max_width(theme.max_width)
|
||||
// .contained()
|
||||
// .with_style(theme.modal)
|
||||
// .into_any()
|
||||
}
|
||||
|
||||
fn dismiss_on_event(event: &Self::Event) -> bool {
|
||||
match event {
|
||||
PickerEvent::Dismiss => true,
|
||||
}
|
||||
}
|
||||
// fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
// self.has_focus = true;
|
||||
// if cx.is_self_focused() {
|
||||
// cx.focus(&self.picker)
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
// self.has_focus = false;
|
||||
// }
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
@@ -337,19 +323,22 @@ pub enum Mode {
|
||||
}
|
||||
|
||||
pub struct ChannelModalDelegate {
|
||||
channel_modal: WeakView<ChannelModal>,
|
||||
matching_users: Vec<Arc<User>>,
|
||||
matching_member_indices: Vec<usize>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
user_store: Model<UserStore>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
selected_index: usize,
|
||||
mode: Mode,
|
||||
match_candidates: Vec<StringMatchCandidate>,
|
||||
members: Vec<ChannelMembership>,
|
||||
context_menu: ViewHandle<ContextMenu>,
|
||||
// context_menu: ViewHandle<ContextMenu>,
|
||||
}
|
||||
|
||||
impl PickerDelegate for ChannelModalDelegate {
|
||||
type ListItem = Div;
|
||||
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Search collaborator by username...".into()
|
||||
}
|
||||
@@ -382,19 +371,19 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
}
|
||||
}));
|
||||
|
||||
let matches = cx.background().block(match_strings(
|
||||
let matches = cx.background_executor().block(match_strings(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
true,
|
||||
usize::MAX,
|
||||
&Default::default(),
|
||||
cx.background().clone(),
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
picker
|
||||
.update(&mut cx, |picker, cx| {
|
||||
let delegate = picker.delegate_mut();
|
||||
let delegate = &mut picker.delegate;
|
||||
delegate.matching_member_indices.clear();
|
||||
delegate
|
||||
.matching_member_indices
|
||||
@@ -412,8 +401,7 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
async {
|
||||
let users = search_users.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
let delegate = picker.delegate_mut();
|
||||
delegate.matching_users = users;
|
||||
picker.delegate.matching_users = users;
|
||||
cx.notify();
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
@@ -445,138 +433,142 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
self.channel_modal
|
||||
.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
mouse_state: &mut MouseState,
|
||||
selected: bool,
|
||||
cx: &gpui::AppContext,
|
||||
) -> AnyElement<Picker<Self>> {
|
||||
let full_theme = &theme::current(cx);
|
||||
let theme = &full_theme.collab_panel.channel_modal;
|
||||
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
|
||||
let (user, role) = self.user_at_index(ix).unwrap();
|
||||
let request_status = self.member_status(user.id, cx);
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
None
|
||||
// let full_theme = &theme::current(cx);
|
||||
// let theme = &full_theme.collab_panel.channel_modal;
|
||||
// let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
|
||||
// let (user, role) = self.user_at_index(ix).unwrap();
|
||||
// let request_status = self.member_status(user.id, cx);
|
||||
|
||||
let style = tabbed_modal
|
||||
.picker
|
||||
.item
|
||||
.in_state(selected)
|
||||
.style_for(mouse_state);
|
||||
// let style = tabbed_modal
|
||||
// .picker
|
||||
// .item
|
||||
// .in_state(selected)
|
||||
// .style_for(mouse_state);
|
||||
|
||||
let in_manage = matches!(self.mode, Mode::ManageMembers);
|
||||
// let in_manage = matches!(self.mode, Mode::ManageMembers);
|
||||
|
||||
let mut result = Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(user.github_login.clone(), style.label.clone())
|
||||
.contained()
|
||||
.with_style(theme.contact_username)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
.with_children({
|
||||
(in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
|
||||
|| {
|
||||
Label::new("Invited", theme.member_tag.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.member_tag.container)
|
||||
.aligned()
|
||||
.left()
|
||||
},
|
||||
)
|
||||
})
|
||||
.with_children(if in_manage && role == Some(ChannelRole::Admin) {
|
||||
Some(
|
||||
Label::new("Admin", theme.member_tag.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.member_tag.container)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
} else if in_manage && role == Some(ChannelRole::Guest) {
|
||||
Some(
|
||||
Label::new("Guest", theme.member_tag.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.member_tag.container)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.with_children({
|
||||
let svg = match self.mode {
|
||||
Mode::ManageMembers => Some(
|
||||
Svg::new("icons/ellipsis.svg")
|
||||
.with_color(theme.member_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.button_width)
|
||||
.with_height(theme.member_icon.button_width)
|
||||
.contained()
|
||||
.with_style(theme.member_icon.container),
|
||||
),
|
||||
Mode::InviteMembers => match request_status {
|
||||
Some(proto::channel_member::Kind::Member) => Some(
|
||||
Svg::new("icons/check.svg")
|
||||
.with_color(theme.member_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.button_width)
|
||||
.with_height(theme.member_icon.button_width)
|
||||
.contained()
|
||||
.with_style(theme.member_icon.container),
|
||||
),
|
||||
Some(proto::channel_member::Kind::Invitee) => Some(
|
||||
Svg::new("icons/check.svg")
|
||||
.with_color(theme.invitee_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.invitee_icon.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(theme.invitee_icon.button_width)
|
||||
.with_height(theme.invitee_icon.button_width)
|
||||
.contained()
|
||||
.with_style(theme.invitee_icon.container),
|
||||
),
|
||||
Some(proto::channel_member::Kind::AncestorMember) | None => None,
|
||||
},
|
||||
};
|
||||
// let mut result = Flex::row()
|
||||
// .with_children(user.avatar.clone().map(|avatar| {
|
||||
// Image::from_data(avatar)
|
||||
// .with_style(theme.contact_avatar)
|
||||
// .aligned()
|
||||
// .left()
|
||||
// }))
|
||||
// .with_child(
|
||||
// Label::new(user.github_login.clone(), style.label.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.contact_username)
|
||||
// .aligned()
|
||||
// .left(),
|
||||
// )
|
||||
// .with_children({
|
||||
// (in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
|
||||
// || {
|
||||
// Label::new("Invited", theme.member_tag.text.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.member_tag.container)
|
||||
// .aligned()
|
||||
// .left()
|
||||
// },
|
||||
// )
|
||||
// })
|
||||
// .with_children(if in_manage && role == Some(ChannelRole::Admin) {
|
||||
// Some(
|
||||
// Label::new("Admin", theme.member_tag.text.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.member_tag.container)
|
||||
// .aligned()
|
||||
// .left(),
|
||||
// )
|
||||
// } else if in_manage && role == Some(ChannelRole::Guest) {
|
||||
// Some(
|
||||
// Label::new("Guest", theme.member_tag.text.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.member_tag.container)
|
||||
// .aligned()
|
||||
// .left(),
|
||||
// )
|
||||
// } else {
|
||||
// None
|
||||
// })
|
||||
// .with_children({
|
||||
// let svg = match self.mode {
|
||||
// Mode::ManageMembers => Some(
|
||||
// Svg::new("icons/ellipsis.svg")
|
||||
// .with_color(theme.member_icon.color)
|
||||
// .constrained()
|
||||
// .with_width(theme.member_icon.icon_width)
|
||||
// .aligned()
|
||||
// .constrained()
|
||||
// .with_width(theme.member_icon.button_width)
|
||||
// .with_height(theme.member_icon.button_width)
|
||||
// .contained()
|
||||
// .with_style(theme.member_icon.container),
|
||||
// ),
|
||||
// Mode::InviteMembers => match request_status {
|
||||
// Some(proto::channel_member::Kind::Member) => Some(
|
||||
// Svg::new("icons/check.svg")
|
||||
// .with_color(theme.member_icon.color)
|
||||
// .constrained()
|
||||
// .with_width(theme.member_icon.icon_width)
|
||||
// .aligned()
|
||||
// .constrained()
|
||||
// .with_width(theme.member_icon.button_width)
|
||||
// .with_height(theme.member_icon.button_width)
|
||||
// .contained()
|
||||
// .with_style(theme.member_icon.container),
|
||||
// ),
|
||||
// Some(proto::channel_member::Kind::Invitee) => Some(
|
||||
// Svg::new("icons/check.svg")
|
||||
// .with_color(theme.invitee_icon.color)
|
||||
// .constrained()
|
||||
// .with_width(theme.invitee_icon.icon_width)
|
||||
// .aligned()
|
||||
// .constrained()
|
||||
// .with_width(theme.invitee_icon.button_width)
|
||||
// .with_height(theme.invitee_icon.button_width)
|
||||
// .contained()
|
||||
// .with_style(theme.invitee_icon.container),
|
||||
// ),
|
||||
// Some(proto::channel_member::Kind::AncestorMember) | None => None,
|
||||
// },
|
||||
// };
|
||||
|
||||
svg.map(|svg| svg.aligned().flex_float().into_any())
|
||||
})
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_height(tabbed_modal.row_height)
|
||||
.into_any();
|
||||
// svg.map(|svg| svg.aligned().flex_float().into_any())
|
||||
// })
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// .constrained()
|
||||
// .with_height(tabbed_modal.row_height)
|
||||
// .into_any();
|
||||
|
||||
if selected {
|
||||
result = Stack::new()
|
||||
.with_child(result)
|
||||
.with_child(
|
||||
ChildView::new(&self.context_menu, cx)
|
||||
.aligned()
|
||||
.top()
|
||||
.right(),
|
||||
)
|
||||
.into_any();
|
||||
}
|
||||
// if selected {
|
||||
// result = Stack::new()
|
||||
// .with_child(result)
|
||||
// .with_child(
|
||||
// ChildView::new(&self.context_menu, cx)
|
||||
// .aligned()
|
||||
// .top()
|
||||
// .right(),
|
||||
// )
|
||||
// .into_any();
|
||||
// }
|
||||
|
||||
result
|
||||
// result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -623,7 +615,7 @@ impl ChannelModalDelegate {
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
update.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
let this = picker.delegate_mut();
|
||||
let this = &mut picker.delegate;
|
||||
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
|
||||
member.role = new_role;
|
||||
}
|
||||
@@ -644,7 +636,7 @@ impl ChannelModalDelegate {
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
update.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
let this = picker.delegate_mut();
|
||||
let this = &mut picker.delegate;
|
||||
if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
|
||||
this.members.remove(ix);
|
||||
this.matching_member_indices.retain_mut(|member_ix| {
|
||||
@@ -683,7 +675,7 @@ impl ChannelModalDelegate {
|
||||
kind: proto::channel_member::Kind::Invitee,
|
||||
role: ChannelRole::Member,
|
||||
};
|
||||
let members = &mut this.delegate_mut().members;
|
||||
let members = &mut this.delegate.members;
|
||||
match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
|
||||
Ok(ix) | Err(ix) => members.insert(ix, new_member),
|
||||
}
|
||||
@@ -695,23 +687,23 @@ impl ChannelModalDelegate {
|
||||
}
|
||||
|
||||
fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.context_menu.update(cx, |context_menu, cx| {
|
||||
context_menu.show(
|
||||
Default::default(),
|
||||
AnchorCorner::TopRight,
|
||||
vec![
|
||||
ContextMenuItem::action("Remove", RemoveMember),
|
||||
ContextMenuItem::action(
|
||||
if role == ChannelRole::Admin {
|
||||
"Make non-admin"
|
||||
} else {
|
||||
"Make admin"
|
||||
},
|
||||
ToggleMemberAdmin,
|
||||
),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
})
|
||||
// self.context_menu.update(cx, |context_menu, cx| {
|
||||
// context_menu.show(
|
||||
// Default::default(),
|
||||
// AnchorCorner::TopRight,
|
||||
// vec![
|
||||
// ContextMenuItem::action("Remove", RemoveMember),
|
||||
// ContextMenuItem::action(
|
||||
// if role == ChannelRole::Admin {
|
||||
// "Make non-admin"
|
||||
// } else {
|
||||
// "Make admin"
|
||||
// },
|
||||
// ToggleMemberAdmin,
|
||||
// ),
|
||||
// ],
|
||||
// cx,
|
||||
// )
|
||||
// })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ use gpui::{
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::sync::Arc;
|
||||
use theme::ActiveTheme as _;
|
||||
use ui::{h_stack, v_stack, Label};
|
||||
use ui::prelude::*;
|
||||
use util::{ResultExt as _, TryFutureExt};
|
||||
use workspace::ModalView;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
//Picker::<ContactFinderDelegate>::init(cx);
|
||||
@@ -58,7 +59,7 @@ impl Render for ContactFinder {
|
||||
.bg(cx.theme().colors().element_background),
|
||||
)
|
||||
.child(self.picker.clone())
|
||||
.w_96()
|
||||
.w(rems(34.))
|
||||
}
|
||||
|
||||
// fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
@@ -95,6 +96,7 @@ pub struct ContactFinderDelegate {
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ContactFinder {}
|
||||
impl ModalView for ContactFinder {}
|
||||
|
||||
impl FocusableView for ContactFinder {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
@@ -161,7 +163,7 @@ impl PickerDelegate for ContactFinderDelegate {
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
//cx.emit(PickerEvent::Dismiss);
|
||||
self.parent
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
@@ -181,12 +183,11 @@ impl PickerDelegate for ContactFinderDelegate {
|
||||
ContactRequestStatus::RequestSent => Some("icons/x.svg"),
|
||||
ContactRequestStatus::RequestAccepted => None,
|
||||
};
|
||||
dbg!(icon_path);
|
||||
Some(
|
||||
div()
|
||||
.flex_1()
|
||||
.justify_between()
|
||||
.children(user.avatar.clone().map(|avatar| img().data(avatar)))
|
||||
.children(user.avatar.clone().map(|avatar| img(avatar)))
|
||||
.child(Label::new(user.github_login.clone()))
|
||||
.children(icon_path.map(|icon_path| svg().path(icon_path))),
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,22 +9,22 @@ mod panel_settings;
|
||||
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
|
||||
use call::{report_call_event_for_room, ActiveCall, Room};
|
||||
pub use collab_panel::CollabPanel;
|
||||
pub use collab_titlebar_item::CollabTitlebarItem;
|
||||
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
|
||||
use gpui::{
|
||||
point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, WindowBounds, WindowKind,
|
||||
WindowOptions,
|
||||
actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds,
|
||||
WindowKind, WindowOptions,
|
||||
};
|
||||
pub use panel_settings::{
|
||||
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
|
||||
};
|
||||
use settings::Settings;
|
||||
use util::ResultExt;
|
||||
use workspace::AppState;
|
||||
|
||||
// actions!(
|
||||
// collab,
|
||||
// [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
|
||||
// );
|
||||
actions!(ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall);
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
CollaborationPanelSettings::register(cx);
|
||||
@@ -34,7 +34,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
// vcs_menu::init(cx);
|
||||
collab_titlebar_item::init(cx);
|
||||
collab_panel::init(cx);
|
||||
// chat_panel::init(cx);
|
||||
channel_view::init(cx);
|
||||
chat_panel::init(cx);
|
||||
notifications::init(&app_state, cx);
|
||||
|
||||
// cx.add_global_action(toggle_screen_sharing);
|
||||
@@ -42,61 +43,61 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
// cx.add_global_action(toggle_deafen);
|
||||
}
|
||||
|
||||
// pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
|
||||
// let call = ActiveCall::global(cx).read(cx);
|
||||
// if let Some(room) = call.room().cloned() {
|
||||
// let client = call.client();
|
||||
// let toggle_screen_sharing = room.update(cx, |room, cx| {
|
||||
// if room.is_screen_sharing() {
|
||||
// report_call_event_for_room(
|
||||
// "disable screen share",
|
||||
// room.id(),
|
||||
// room.channel_id(),
|
||||
// &client,
|
||||
// cx,
|
||||
// );
|
||||
// Task::ready(room.unshare_screen(cx))
|
||||
// } else {
|
||||
// report_call_event_for_room(
|
||||
// "enable screen share",
|
||||
// room.id(),
|
||||
// room.channel_id(),
|
||||
// &client,
|
||||
// cx,
|
||||
// );
|
||||
// room.share_screen(cx)
|
||||
// }
|
||||
// });
|
||||
// toggle_screen_sharing.detach_and_log_err(cx);
|
||||
// }
|
||||
// }
|
||||
pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
|
||||
let call = ActiveCall::global(cx).read(cx);
|
||||
if let Some(room) = call.room().cloned() {
|
||||
let client = call.client();
|
||||
let toggle_screen_sharing = room.update(cx, |room, cx| {
|
||||
if room.is_screen_sharing() {
|
||||
report_call_event_for_room(
|
||||
"disable screen share",
|
||||
room.id(),
|
||||
room.channel_id(),
|
||||
&client,
|
||||
cx,
|
||||
);
|
||||
Task::ready(room.unshare_screen(cx))
|
||||
} else {
|
||||
report_call_event_for_room(
|
||||
"enable screen share",
|
||||
room.id(),
|
||||
room.channel_id(),
|
||||
&client,
|
||||
cx,
|
||||
);
|
||||
room.share_screen(cx)
|
||||
}
|
||||
});
|
||||
toggle_screen_sharing.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
|
||||
// let call = ActiveCall::global(cx).read(cx);
|
||||
// if let Some(room) = call.room().cloned() {
|
||||
// let client = call.client();
|
||||
// room.update(cx, |room, cx| {
|
||||
// let operation = if room.is_muted(cx) {
|
||||
// "enable microphone"
|
||||
// } else {
|
||||
// "disable microphone"
|
||||
// };
|
||||
// report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
|
||||
pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
|
||||
let call = ActiveCall::global(cx).read(cx);
|
||||
if let Some(room) = call.room().cloned() {
|
||||
let client = call.client();
|
||||
room.update(cx, |room, cx| {
|
||||
let operation = if room.is_muted(cx) {
|
||||
"enable microphone"
|
||||
} else {
|
||||
"disable microphone"
|
||||
};
|
||||
report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
|
||||
|
||||
// room.toggle_mute(cx)
|
||||
// })
|
||||
// .map(|task| task.detach_and_log_err(cx))
|
||||
// .log_err();
|
||||
// }
|
||||
// }
|
||||
room.toggle_mute(cx)
|
||||
})
|
||||
.map(|task| task.detach_and_log_err(cx))
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
|
||||
// if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||
// room.update(cx, Room::toggle_deafen)
|
||||
// .map(|task| task.detach_and_log_err(cx))
|
||||
// .log_err();
|
||||
// }
|
||||
// }
|
||||
pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
|
||||
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||
room.update(cx, Room::toggle_deafen)
|
||||
.map(|task| task.detach_and_log_err(cx))
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
fn notification_window_options(
|
||||
screen: Rc<dyn PlatformDisplay>,
|
||||
@@ -157,6 +158,6 @@ fn notification_window_options(
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
|
||||
// cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
|
||||
// }
|
||||
fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
|
||||
cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use gpui::{
|
||||
div, AnyElement, Div, IntoElement as _, ParentElement as _, RenderOnce, Styled, WindowContext,
|
||||
div, AnyElement, Div, ElementId, IntoElement, ParentElement as _, RenderOnce, Styled,
|
||||
WindowContext,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, IntoElement)]
|
||||
pub struct FacePile {
|
||||
pub faces: Vec<AnyElement>,
|
||||
}
|
||||
@@ -15,64 +16,15 @@ impl RenderOnce for FacePile {
|
||||
let player_list = self.faces.into_iter().enumerate().map(|(ix, player)| {
|
||||
let isnt_last = ix < player_count - 1;
|
||||
|
||||
div().when(isnt_last, |div| div.neg_mr_1()).child(player)
|
||||
div()
|
||||
.z_index((player_count - ix) as u32)
|
||||
.when(isnt_last, |div| div.neg_mr_1())
|
||||
.child(player)
|
||||
});
|
||||
div().p_1().flex().items_center().children(player_list)
|
||||
}
|
||||
}
|
||||
|
||||
// impl Element for FacePile {
|
||||
// type State = ();
|
||||
// fn layout(
|
||||
// &mut self,
|
||||
// state: Option<Self::State>,
|
||||
// cx: &mut WindowContext,
|
||||
// ) -> (LayoutId, Self::State) {
|
||||
// let mut width = 0.;
|
||||
// let mut max_height = 0.;
|
||||
// let mut faces = Vec::with_capacity(self.faces.len());
|
||||
// for face in &mut self.faces {
|
||||
// let layout = face.layout(cx);
|
||||
// width += layout.x();
|
||||
// max_height = f32::max(max_height, layout.y());
|
||||
// faces.push(layout);
|
||||
// }
|
||||
// width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
|
||||
// (cx.request_layout(&Style::default(), faces), ())
|
||||
// // (
|
||||
// // Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
|
||||
// // (),
|
||||
// // ))
|
||||
// }
|
||||
|
||||
// fn paint(
|
||||
// &mut self,
|
||||
// bounds: RectF,
|
||||
// visible_bounds: RectF,
|
||||
// _layout: &mut Self::LayoutState,
|
||||
// view: &mut V,
|
||||
// cx: &mut ViewContext<V>,
|
||||
// ) -> Self::PaintState {
|
||||
// let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
|
||||
|
||||
// let origin_y = bounds.upper_right().y();
|
||||
// let mut origin_x = bounds.upper_right().x();
|
||||
|
||||
// for face in self.faces.iter_mut().rev() {
|
||||
// let size = face.size();
|
||||
// origin_x -= size.x();
|
||||
// let origin_y = origin_y + (bounds.height() - size.y()) / 2.0;
|
||||
|
||||
// cx.scene().push_layer(None);
|
||||
// face.paint(vec2f(origin_x, origin_y), visible_bounds, view, cx);
|
||||
// cx.scene().pop_layer();
|
||||
// origin_x += self.overlap;
|
||||
// }
|
||||
|
||||
// ()
|
||||
// }
|
||||
// }
|
||||
|
||||
impl Extend<AnyElement> for FacePile {
|
||||
fn extend<T: IntoIterator<Item = AnyElement>>(&mut self, children: T) {
|
||||
self.faces.extend(children);
|
||||
|
||||
@@ -3,9 +3,9 @@ use std::sync::Arc;
|
||||
use workspace::AppState;
|
||||
|
||||
pub mod incoming_call_notification;
|
||||
// pub mod project_shared_notification;
|
||||
pub mod project_shared_notification;
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
incoming_call_notification::init(app_state, cx);
|
||||
//project_shared_notification::init(app_state, cx);
|
||||
project_shared_notification::init(app_state, cx);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ use crate::notification_window_options;
|
||||
use call::{ActiveCall, IncomingCall};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
div, green, px, red, AppContext, Div, Element, ParentElement, Render, RenderOnce,
|
||||
StatefulInteractiveElement, Styled, ViewContext, VisualContext as _, WindowHandle,
|
||||
div, px, red, AppContext, Div, Element, ParentElement, Render, RenderOnce, Styled, ViewContext,
|
||||
VisualContext as _, WindowHandle,
|
||||
};
|
||||
use std::sync::{Arc, Weak};
|
||||
use ui::prelude::*;
|
||||
use ui::{h_stack, v_stack, Avatar, Button, Label};
|
||||
use util::ResultExt;
|
||||
use workspace::AppState;
|
||||
@@ -32,8 +33,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
height: px(64.),
|
||||
};
|
||||
|
||||
for window in unique_screens {
|
||||
let options = notification_window_options(window, window_size);
|
||||
for screen in unique_screens {
|
||||
let options = notification_window_options(screen, window_size);
|
||||
let window = cx
|
||||
.open_window(options, |cx| {
|
||||
cx.build_view(|_| {
|
||||
@@ -46,15 +47,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
.unwrap();
|
||||
notification_windows.push(window);
|
||||
}
|
||||
|
||||
// for screen in cx.platform().screens() {
|
||||
// let window = cx
|
||||
// .add_window(notification_window_options(screen, window_size), |_| {
|
||||
// IncomingCallNotification::new(incoming_call.clone(), app_state.clone())
|
||||
// });
|
||||
|
||||
// notification_windows.push(window);
|
||||
// }
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -83,21 +75,22 @@ impl IncomingCallNotificationState {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
if accept {
|
||||
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
|
||||
let caller_user_id = self.call.calling_user.id;
|
||||
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
|
||||
let app_state = self.app_state.clone();
|
||||
let cx: &mut AppContext = cx;
|
||||
cx.spawn(|cx| async move {
|
||||
join.await?;
|
||||
if let Some(_project_id) = initial_project_id {
|
||||
cx.update(|_cx| {
|
||||
if let Some(_app_state) = app_state.upgrade() {
|
||||
// workspace::join_remote_project(
|
||||
// project_id,
|
||||
// caller_user_id,
|
||||
// app_state,
|
||||
// cx,
|
||||
// )
|
||||
// .detach_and_log_err(cx);
|
||||
if let Some(project_id) = initial_project_id {
|
||||
cx.update(|cx| {
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
workspace::join_remote_project(
|
||||
project_id,
|
||||
caller_user_id,
|
||||
app_state,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
@@ -137,125 +130,25 @@ impl IncomingCallNotification {
|
||||
)))
|
||||
.child(self.render_buttons(cx)),
|
||||
)
|
||||
// let theme = &theme::current(cx).incoming_call_notification;
|
||||
// let default_project = proto::ParticipantProject::default();
|
||||
// let initial_project = self
|
||||
// .call
|
||||
// .initial_project
|
||||
// .as_ref()
|
||||
// .unwrap_or(&default_project);
|
||||
// Flex::row()
|
||||
// .with_children(self.call.calling_user.avatar.clone().map(|avatar| {
|
||||
// Image::from_data(avatar)
|
||||
// .with_style(theme.caller_avatar)
|
||||
// .aligned()
|
||||
// }))
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Label::new(
|
||||
// self.call.calling_user.github_login.clone(),
|
||||
// theme.caller_username.text.clone(),
|
||||
// )
|
||||
// .contained()
|
||||
// .with_style(theme.caller_username.container),
|
||||
// )
|
||||
// .with_child(
|
||||
// Label::new(
|
||||
// format!(
|
||||
// "is sharing a project in Zed{}",
|
||||
// if initial_project.worktree_root_names.is_empty() {
|
||||
// ""
|
||||
// } else {
|
||||
// ":"
|
||||
// }
|
||||
// ),
|
||||
// theme.caller_message.text.clone(),
|
||||
// )
|
||||
// .contained()
|
||||
// .with_style(theme.caller_message.container),
|
||||
// )
|
||||
// .with_children(if initial_project.worktree_root_names.is_empty() {
|
||||
// None
|
||||
// } else {
|
||||
// Some(
|
||||
// Label::new(
|
||||
// initial_project.worktree_root_names.join(", "),
|
||||
// theme.worktree_roots.text.clone(),
|
||||
// )
|
||||
// .contained()
|
||||
// .with_style(theme.worktree_roots.container),
|
||||
// )
|
||||
// })
|
||||
// .contained()
|
||||
// .with_style(theme.caller_metadata)
|
||||
// .aligned(),
|
||||
// )
|
||||
// .contained()
|
||||
// .with_style(theme.caller_container)
|
||||
// .flex(1., true)
|
||||
// .into_any()
|
||||
}
|
||||
|
||||
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> impl Element {
|
||||
h_stack()
|
||||
.child(Button::new("Accept").render(cx).bg(green()).on_click({
|
||||
.child(Button::new("accept", "Accept").render(cx).on_click({
|
||||
let state = self.state.clone();
|
||||
move |_, cx| state.respond(true, cx)
|
||||
}))
|
||||
.child(Button::new("Decline").render(cx).bg(red()).on_click({
|
||||
.child(Button::new("decline", "Decline").render(cx).on_click({
|
||||
let state = self.state.clone();
|
||||
move |_, cx| state.respond(false, cx)
|
||||
}))
|
||||
|
||||
// enum Accept {}
|
||||
// enum Decline {}
|
||||
|
||||
// let theme = theme::current(cx);
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// MouseEventHandler::new::<Accept, _>(0, cx, |_, _| {
|
||||
// let theme = &theme.incoming_call_notification;
|
||||
// Label::new("Accept", theme.accept_button.text.clone())
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(theme.accept_button.container)
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_click(MouseButton::Left, |_, this, cx| {
|
||||
// this.respond(true, cx);
|
||||
// })
|
||||
// .flex(1., true),
|
||||
// )
|
||||
// .with_child(
|
||||
// MouseEventHandler::new::<Decline, _>(0, cx, |_, _| {
|
||||
// let theme = &theme.incoming_call_notification;
|
||||
// Label::new("Decline", theme.decline_button.text.clone())
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(theme.decline_button.container)
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_click(MouseButton::Left, |_, this, cx| {
|
||||
// this.respond(false, cx);
|
||||
// })
|
||||
// .flex(1., true),
|
||||
// )
|
||||
// .constrained()
|
||||
// .with_width(theme.incoming_call_notification.button_width)
|
||||
// .into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for IncomingCallNotification {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
div().bg(red()).flex_none().child(self.render_caller(cx))
|
||||
// Flex::row()
|
||||
// .with_child()
|
||||
// .with_child(self.render_buttons(cx))
|
||||
// .contained()
|
||||
// .with_background_color(background)
|
||||
// .expanded()
|
||||
// .into_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,11 @@ use call::{room, ActiveCall};
|
||||
use client::User;
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
elements::*,
|
||||
geometry::vector::vec2f,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AppContext, Entity, View, ViewContext,
|
||||
px, AppContext, Div, Element, ParentElement, Render, RenderOnce, Size, Styled, ViewContext,
|
||||
VisualContext,
|
||||
};
|
||||
use std::sync::{Arc, Weak};
|
||||
use ui::{h_stack, v_stack, Avatar, Button, Clickable, Label};
|
||||
use workspace::AppState;
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
@@ -21,38 +20,54 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
project_id,
|
||||
worktree_root_names,
|
||||
} => {
|
||||
let theme = &theme::current(cx).project_shared_notification;
|
||||
let window_size = vec2f(theme.window_width, theme.window_height);
|
||||
let window_size = Size {
|
||||
width: px(380.),
|
||||
height: px(64.),
|
||||
};
|
||||
|
||||
for screen in cx.platform().screens() {
|
||||
let window =
|
||||
cx.add_window(notification_window_options(screen, window_size), |_| {
|
||||
for screen in cx.displays() {
|
||||
let options = notification_window_options(screen, window_size);
|
||||
let window = cx.open_window(options, |cx| {
|
||||
cx.build_view(|_| {
|
||||
ProjectSharedNotification::new(
|
||||
owner.clone(),
|
||||
*project_id,
|
||||
worktree_root_names.clone(),
|
||||
app_state.clone(),
|
||||
)
|
||||
});
|
||||
})
|
||||
});
|
||||
notification_windows
|
||||
.entry(*project_id)
|
||||
.or_insert(Vec::new())
|
||||
.push(window);
|
||||
}
|
||||
}
|
||||
|
||||
room::Event::RemoteProjectUnshared { project_id }
|
||||
| room::Event::RemoteProjectJoined { project_id }
|
||||
| room::Event::RemoteProjectInvitationDiscarded { project_id } => {
|
||||
if let Some(windows) = notification_windows.remove(&project_id) {
|
||||
for window in windows {
|
||||
window.remove(cx);
|
||||
window
|
||||
.update(cx, |_, cx| {
|
||||
// todo!()
|
||||
cx.remove_window();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
room::Event::Left => {
|
||||
for (_, windows) in notification_windows.drain() {
|
||||
for window in windows {
|
||||
window.remove(cx);
|
||||
window
|
||||
.update(cx, |_, cx| {
|
||||
// todo!()
|
||||
cx.remove_window();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,116 +117,60 @@ impl ProjectSharedNotification {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = &theme::current(cx).project_shared_notification;
|
||||
Flex::row()
|
||||
.with_children(self.owner.avatar.clone().map(|avatar| {
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.owner_avatar)
|
||||
.aligned()
|
||||
}))
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Label::new(
|
||||
self.owner.github_login.clone(),
|
||||
theme.owner_username.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.owner_username.container),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(
|
||||
format!(
|
||||
"is sharing a project in Zed{}",
|
||||
if self.worktree_root_names.is_empty() {
|
||||
""
|
||||
} else {
|
||||
":"
|
||||
}
|
||||
),
|
||||
theme.message.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.message.container),
|
||||
)
|
||||
.with_children(if self.worktree_root_names.is_empty() {
|
||||
fn render_owner(&self) -> impl Element {
|
||||
h_stack()
|
||||
.children(
|
||||
self.owner
|
||||
.avatar
|
||||
.clone()
|
||||
.map(|avatar| Avatar::data(avatar.clone())),
|
||||
)
|
||||
.child(
|
||||
v_stack()
|
||||
.child(Label::new(self.owner.github_login.clone()))
|
||||
.child(Label::new(format!(
|
||||
"is sharing a project in Zed{}",
|
||||
if self.worktree_root_names.is_empty() {
|
||||
""
|
||||
} else {
|
||||
":"
|
||||
}
|
||||
)))
|
||||
.children(if self.worktree_root_names.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Label::new(
|
||||
self.worktree_root_names.join(", "),
|
||||
theme.worktree_roots.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.worktree_roots.container),
|
||||
)
|
||||
})
|
||||
.contained()
|
||||
.with_style(theme.owner_metadata)
|
||||
.aligned(),
|
||||
Some(Label::new(self.worktree_root_names.join(", ")))
|
||||
}),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.owner_container)
|
||||
.flex(1., true)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
enum Open {}
|
||||
enum Dismiss {}
|
||||
|
||||
let theme = theme::current(cx);
|
||||
Flex::column()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<Open, _>(0, cx, |_, _| {
|
||||
let theme = &theme.project_shared_notification;
|
||||
Label::new("Open", theme.open_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.open_button.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| this.join(cx))
|
||||
.flex(1., true),
|
||||
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> impl Element {
|
||||
let this = cx.view().clone();
|
||||
v_stack()
|
||||
.child(Button::new("open", "Open").render(cx).on_click({
|
||||
let this = this.clone();
|
||||
move |_, cx| {
|
||||
this.update(cx, |this, cx| this.join(cx));
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
Button::new("dismiss", "Dismiss")
|
||||
.render(cx)
|
||||
.on_click(move |_, cx| {
|
||||
this.update(cx, |this, cx| this.dismiss(cx));
|
||||
}),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::new::<Dismiss, _>(0, cx, |_, _| {
|
||||
let theme = &theme.project_shared_notification;
|
||||
Label::new("Dismiss", theme.dismiss_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.dismiss_button.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
this.dismiss(cx);
|
||||
})
|
||||
.flex(1., true),
|
||||
)
|
||||
.constrained()
|
||||
.with_width(theme.project_shared_notification.button_width)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ProjectSharedNotification {
|
||||
type Event = ();
|
||||
}
|
||||
impl Render for ProjectSharedNotification {
|
||||
type Element = Div;
|
||||
|
||||
impl View for ProjectSharedNotification {
|
||||
fn ui_name() -> &'static str {
|
||||
"ProjectSharedNotification"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
|
||||
let background = theme::current(cx).project_shared_notification.background;
|
||||
Flex::row()
|
||||
.with_child(self.render_owner(cx))
|
||||
.with_child(self.render_buttons(cx))
|
||||
.contained()
|
||||
.with_background_color(background)
|
||||
.expanded()
|
||||
.into_any()
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
h_stack()
|
||||
.size_full()
|
||||
.bg(gpui::red())
|
||||
.child(self.render_owner())
|
||||
.child(self.render_buttons(cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,13 @@ pub type HashMap<K, V> = std::collections::HashMap<K, V>;
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
pub type HashSet<T> = std::collections::HashSet<T>;
|
||||
|
||||
use std::any::TypeId;
|
||||
pub use std::collections::*;
|
||||
|
||||
// NEW TYPES
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CommandPaletteFilter {
|
||||
pub filtered_namespaces: HashSet<&'static str>,
|
||||
pub hidden_namespaces: HashSet<&'static str>,
|
||||
pub hidden_action_types: HashSet<TypeId>,
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
let filtered = cx.read(|cx| {
|
||||
if cx.has_global::<CommandPaletteFilter>() {
|
||||
let filter = cx.global::<CommandPaletteFilter>();
|
||||
filter.filtered_namespaces.contains(action.namespace())
|
||||
filter.hidden_namespaces.contains(action.namespace())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -430,7 +430,7 @@ mod tests {
|
||||
// Add namespace filter, and redeploy the palette
|
||||
cx.update(|cx| {
|
||||
cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
|
||||
filter.filtered_namespaces.insert("editor");
|
||||
filter.hidden_namespaces.insert("editor");
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -7,16 +7,16 @@ use collections::{CommandPaletteFilter, HashMap};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
|
||||
Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
|
||||
ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
|
||||
use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem};
|
||||
use ui::{h_stack, prelude::*, v_stack, HighlightedLabel, KeyBinding, ListItem};
|
||||
use util::{
|
||||
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
|
||||
ResultExt,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
use workspace::{ModalView, Workspace};
|
||||
use zed_actions::OpenZedURL;
|
||||
|
||||
actions!(Toggle);
|
||||
@@ -26,6 +26,8 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(CommandPalette::register).detach();
|
||||
}
|
||||
|
||||
impl ModalView for CommandPalette {}
|
||||
|
||||
pub struct CommandPalette {
|
||||
picker: View<Picker<CommandPaletteDelegate>>,
|
||||
}
|
||||
@@ -49,14 +51,16 @@ impl CommandPalette {
|
||||
.filter_map(|action| {
|
||||
let name = gpui::remove_the_2(action.name());
|
||||
let namespace = name.split("::").next().unwrap_or("malformed action name");
|
||||
if filter.is_some_and(|f| f.filtered_namespaces.contains(namespace)) {
|
||||
if filter.is_some_and(|f| {
|
||||
f.hidden_namespaces.contains(namespace)
|
||||
|| f.hidden_action_types.contains(&action.type_id())
|
||||
}) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Command {
|
||||
name: humanize_action_name(&name),
|
||||
action,
|
||||
keystrokes: vec![], // todo!()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@@ -81,7 +85,7 @@ impl Render for CommandPalette {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
v_stack().min_w_96().child(self.picker.clone())
|
||||
v_stack().w(rems(34.)).child(self.picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +109,6 @@ pub struct CommandPaletteDelegate {
|
||||
struct Command {
|
||||
name: String,
|
||||
action: Box<dyn Action>,
|
||||
keystrokes: Vec<Keystroke>,
|
||||
}
|
||||
|
||||
impl Clone for Command {
|
||||
@@ -113,7 +116,6 @@ impl Clone for Command {
|
||||
Self {
|
||||
name: self.name.clone(),
|
||||
action: self.action.boxed_clone(),
|
||||
keystrokes: self.keystrokes.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,6 +226,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(CommandInterceptResult {
|
||||
action,
|
||||
string,
|
||||
@@ -239,7 +242,6 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
commands.push(Command {
|
||||
name: string.clone(),
|
||||
action,
|
||||
keystrokes: vec![],
|
||||
});
|
||||
matches.insert(
|
||||
0,
|
||||
@@ -251,6 +253,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
picker
|
||||
.update(&mut cx, |picker, _| {
|
||||
let delegate = &mut picker.delegate;
|
||||
@@ -269,7 +272,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.command_palette
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
@@ -280,6 +283,8 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
}
|
||||
let action_ix = self.matches[self.selected_ix].candidate_id;
|
||||
let command = self.commands.swap_remove(action_ix);
|
||||
self.matches.clear();
|
||||
self.commands.clear();
|
||||
cx.update_global(|hit_counts: &mut HitCounts, _| {
|
||||
*hit_counts.0.entry(command.name).or_default() += 1;
|
||||
});
|
||||
@@ -295,13 +300,8 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let Some(r#match) = self.matches.get(ix) else {
|
||||
return None;
|
||||
};
|
||||
let Some(command) = self.commands.get(r#match.candidate_id) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let r#match = self.matches.get(ix)?;
|
||||
let command = self.commands.get(r#match.candidate_id)?;
|
||||
Some(
|
||||
ListItem::new(ix).inset(true).selected(selected).child(
|
||||
h_stack()
|
||||
@@ -311,7 +311,11 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
command.name.clone(),
|
||||
r#match.positions.clone(),
|
||||
))
|
||||
.children(KeyBinding::for_action(&*command.action, cx)),
|
||||
.children(KeyBinding::for_action_in(
|
||||
&*command.action,
|
||||
&self.previous_focus_handle,
|
||||
cx,
|
||||
)),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -345,8 +349,7 @@ impl std::fmt::Debug for Command {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Command")
|
||||
.field("name", &self.name)
|
||||
.field("keystrokes", &self.keystrokes)
|
||||
.finish()
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,7 +432,7 @@ mod tests {
|
||||
cx.update(|cx| {
|
||||
cx.set_global(CommandPaletteFilter::default());
|
||||
cx.update_global::<CommandPaletteFilter, _>(|filter, _| {
|
||||
filter.filtered_namespaces.insert("editor");
|
||||
filter.hidden_namespaces.insert("editor");
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ theme = { path = "../theme" }
|
||||
lsp = { path = "../lsp" }
|
||||
node_runtime = { path = "../node_runtime"}
|
||||
util = { path = "../util" }
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
async-compression.workspace = true
|
||||
async-tar = "0.4.2"
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
@@ -58,16 +58,16 @@ pub fn init(
|
||||
cx.update_default_global::<collections::CommandPaletteFilter, _, _>(move |filter, _cx| {
|
||||
match status {
|
||||
Status::Disabled => {
|
||||
filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
|
||||
filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE);
|
||||
filter.hidden_namespaces.insert(COPILOT_NAMESPACE);
|
||||
filter.hidden_namespaces.insert(COPILOT_AUTH_NAMESPACE);
|
||||
}
|
||||
Status::Authorized => {
|
||||
filter.filtered_namespaces.remove(COPILOT_NAMESPACE);
|
||||
filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
filter.hidden_namespaces.remove(COPILOT_NAMESPACE);
|
||||
filter.hidden_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
}
|
||||
_ => {
|
||||
filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
|
||||
filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
filter.hidden_namespaces.insert(COPILOT_NAMESPACE);
|
||||
filter.hidden_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -28,7 +28,8 @@ theme = { package = "theme2", path = "../theme2" }
|
||||
lsp = { package = "lsp2", path = "../lsp2" }
|
||||
node_runtime = { path = "../node_runtime"}
|
||||
util = { path = "../util" }
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
async-compression.workspace = true
|
||||
async-tar = "0.4.2"
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
@@ -45,6 +46,6 @@ fs = { path = "../fs", features = ["test-support"] }
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
language = { package = "language2", path = "../language2", features = ["test-support"] }
|
||||
lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
|
||||
settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
|
||||
@@ -22,6 +22,7 @@ use request::StatusNotification;
|
||||
use settings::SettingsStore;
|
||||
use smol::{fs, io::BufReader, stream::StreamExt};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
ffi::OsString,
|
||||
mem,
|
||||
ops::Range,
|
||||
@@ -32,13 +33,14 @@ use util::{
|
||||
fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt,
|
||||
};
|
||||
|
||||
// todo!()
|
||||
// const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
|
||||
actions!(SignIn, SignOut);
|
||||
|
||||
// todo!()
|
||||
// const COPILOT_NAMESPACE: &'static str = "copilot";
|
||||
actions!(Suggest, NextSuggestion, PreviousSuggestion, Reinstall);
|
||||
actions!(
|
||||
Suggest,
|
||||
NextSuggestion,
|
||||
PreviousSuggestion,
|
||||
Reinstall,
|
||||
SignIn,
|
||||
SignOut
|
||||
);
|
||||
|
||||
pub fn init(
|
||||
new_server_id: LanguageServerId,
|
||||
@@ -51,52 +53,70 @@ pub fn init(
|
||||
move |cx| Copilot::start(new_server_id, http, node_runtime, cx)
|
||||
});
|
||||
cx.set_global(copilot.clone());
|
||||
cx.observe(&copilot, |handle, cx| {
|
||||
let copilot_action_types = [
|
||||
TypeId::of::<Suggest>(),
|
||||
TypeId::of::<NextSuggestion>(),
|
||||
TypeId::of::<PreviousSuggestion>(),
|
||||
TypeId::of::<Reinstall>(),
|
||||
];
|
||||
let copilot_auth_action_types = [TypeId::of::<SignOut>()];
|
||||
let copilot_no_auth_action_types = [TypeId::of::<SignIn>()];
|
||||
let status = handle.read(cx).status();
|
||||
let filter = cx.default_global::<collections::CommandPaletteFilter>();
|
||||
|
||||
// TODO
|
||||
// cx.observe(&copilot, |handle, cx| {
|
||||
// let status = handle.read(cx).status();
|
||||
// cx.update_default_global::<collections::CommandPaletteFilter, _, _>(move |filter, _cx| {
|
||||
// match status {
|
||||
// Status::Disabled => {
|
||||
// filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
|
||||
// filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE);
|
||||
// }
|
||||
// Status::Authorized => {
|
||||
// filter.filtered_namespaces.remove(COPILOT_NAMESPACE);
|
||||
// filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
// }
|
||||
// _ => {
|
||||
// filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
|
||||
// filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// })
|
||||
// .detach();
|
||||
match status {
|
||||
Status::Disabled => {
|
||||
filter.hidden_action_types.extend(copilot_action_types);
|
||||
filter.hidden_action_types.extend(copilot_auth_action_types);
|
||||
filter
|
||||
.hidden_action_types
|
||||
.extend(copilot_no_auth_action_types);
|
||||
}
|
||||
Status::Authorized => {
|
||||
filter
|
||||
.hidden_action_types
|
||||
.extend(copilot_no_auth_action_types);
|
||||
for type_id in copilot_action_types
|
||||
.iter()
|
||||
.chain(&copilot_auth_action_types)
|
||||
{
|
||||
filter.hidden_action_types.remove(type_id);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
filter.hidden_action_types.extend(copilot_action_types);
|
||||
filter.hidden_action_types.extend(copilot_auth_action_types);
|
||||
for type_id in &copilot_no_auth_action_types {
|
||||
filter.hidden_action_types.remove(type_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// sign_in::init(cx);
|
||||
// cx.add_global_action(|_: &SignIn, cx| {
|
||||
// if let Some(copilot) = Copilot::global(cx) {
|
||||
// copilot
|
||||
// .update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
// .detach_and_log_err(cx);
|
||||
// }
|
||||
// });
|
||||
// cx.add_global_action(|_: &SignOut, cx| {
|
||||
// if let Some(copilot) = Copilot::global(cx) {
|
||||
// copilot
|
||||
// .update(cx, |copilot, cx| copilot.sign_out(cx))
|
||||
// .detach_and_log_err(cx);
|
||||
// }
|
||||
// });
|
||||
|
||||
// cx.add_global_action(|_: &Reinstall, cx| {
|
||||
// if let Some(copilot) = Copilot::global(cx) {
|
||||
// copilot
|
||||
// .update(cx, |copilot, cx| copilot.reinstall(cx))
|
||||
// .detach();
|
||||
// }
|
||||
// });
|
||||
sign_in::init(cx);
|
||||
cx.on_action(|_: &SignIn, cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
cx.on_action(|_: &SignOut, cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_out(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
cx.on_action(|_: &Reinstall, cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.reinstall(cx))
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enum CopilotServer {
|
||||
@@ -1002,229 +1022,231 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use super::*;
|
||||
// use gpui::{executor::Deterministic, TestAppContext};
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
// #[gpui::test(iterations = 10)]
|
||||
// async fn test_buffer_management(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
|
||||
// deterministic.forbid_parking();
|
||||
// let (copilot, mut lsp) = Copilot::fake(cx);
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_buffer_management(cx: &mut TestAppContext) {
|
||||
let (copilot, mut lsp) = Copilot::fake(cx);
|
||||
|
||||
// let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Hello"));
|
||||
// let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.id()).parse().unwrap();
|
||||
// copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidOpenTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentItem::new(
|
||||
// buffer_1_uri.clone(),
|
||||
// "plaintext".into(),
|
||||
// 0,
|
||||
// "Hello".into()
|
||||
// ),
|
||||
// }
|
||||
// );
|
||||
let buffer_1 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "Hello"));
|
||||
let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.entity_id().as_u64())
|
||||
.parse()
|
||||
.unwrap();
|
||||
copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
.await,
|
||||
lsp::DidOpenTextDocumentParams {
|
||||
text_document: lsp::TextDocumentItem::new(
|
||||
buffer_1_uri.clone(),
|
||||
"plaintext".into(),
|
||||
0,
|
||||
"Hello".into()
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
// let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Goodbye"));
|
||||
// let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.id()).parse().unwrap();
|
||||
// copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidOpenTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentItem::new(
|
||||
// buffer_2_uri.clone(),
|
||||
// "plaintext".into(),
|
||||
// 0,
|
||||
// "Goodbye".into()
|
||||
// ),
|
||||
// }
|
||||
// );
|
||||
let buffer_2 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "Goodbye"));
|
||||
let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.entity_id().as_u64())
|
||||
.parse()
|
||||
.unwrap();
|
||||
copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
.await,
|
||||
lsp::DidOpenTextDocumentParams {
|
||||
text_document: lsp::TextDocumentItem::new(
|
||||
buffer_2_uri.clone(),
|
||||
"plaintext".into(),
|
||||
0,
|
||||
"Goodbye".into()
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
// buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx));
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidChangeTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidChangeTextDocumentParams {
|
||||
// text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1),
|
||||
// content_changes: vec![lsp::TextDocumentContentChangeEvent {
|
||||
// range: Some(lsp::Range::new(
|
||||
// lsp::Position::new(0, 5),
|
||||
// lsp::Position::new(0, 5)
|
||||
// )),
|
||||
// range_length: None,
|
||||
// text: " world".into(),
|
||||
// }],
|
||||
// }
|
||||
// );
|
||||
buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx));
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidChangeTextDocument>()
|
||||
.await,
|
||||
lsp::DidChangeTextDocumentParams {
|
||||
text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1),
|
||||
content_changes: vec![lsp::TextDocumentContentChangeEvent {
|
||||
range: Some(lsp::Range::new(
|
||||
lsp::Position::new(0, 5),
|
||||
lsp::Position::new(0, 5)
|
||||
)),
|
||||
range_length: None,
|
||||
text: " world".into(),
|
||||
}],
|
||||
}
|
||||
);
|
||||
|
||||
// // Ensure updates to the file are reflected in the LSP.
|
||||
// buffer_1
|
||||
// .update(cx, |buffer, cx| {
|
||||
// buffer.file_updated(
|
||||
// Arc::new(File {
|
||||
// abs_path: "/root/child/buffer-1".into(),
|
||||
// path: Path::new("child/buffer-1").into(),
|
||||
// }),
|
||||
// cx,
|
||||
// )
|
||||
// })
|
||||
// .await;
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidCloseTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
|
||||
// }
|
||||
// );
|
||||
// let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap();
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidOpenTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentItem::new(
|
||||
// buffer_1_uri.clone(),
|
||||
// "plaintext".into(),
|
||||
// 1,
|
||||
// "Hello world".into()
|
||||
// ),
|
||||
// }
|
||||
// );
|
||||
// Ensure updates to the file are reflected in the LSP.
|
||||
buffer_1.update(cx, |buffer, cx| {
|
||||
buffer.file_updated(
|
||||
Arc::new(File {
|
||||
abs_path: "/root/child/buffer-1".into(),
|
||||
path: Path::new("child/buffer-1").into(),
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
.await,
|
||||
lsp::DidCloseTextDocumentParams {
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
|
||||
}
|
||||
);
|
||||
let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap();
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
.await,
|
||||
lsp::DidOpenTextDocumentParams {
|
||||
text_document: lsp::TextDocumentItem::new(
|
||||
buffer_1_uri.clone(),
|
||||
"plaintext".into(),
|
||||
1,
|
||||
"Hello world".into()
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
// // Ensure all previously-registered buffers are closed when signing out.
|
||||
// lsp.handle_request::<request::SignOut, _, _>(|_, _| async {
|
||||
// Ok(request::SignOutResult {})
|
||||
// });
|
||||
// copilot
|
||||
// .update(cx, |copilot, cx| copilot.sign_out(cx))
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidCloseTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
|
||||
// }
|
||||
// );
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidCloseTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
|
||||
// }
|
||||
// );
|
||||
// Ensure all previously-registered buffers are closed when signing out.
|
||||
lsp.handle_request::<request::SignOut, _, _>(|_, _| async {
|
||||
Ok(request::SignOutResult {})
|
||||
});
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_out(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
// todo!() po: these notifications now happen in reverse order?
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
.await,
|
||||
lsp::DidCloseTextDocumentParams {
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
.await,
|
||||
lsp::DidCloseTextDocumentParams {
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
|
||||
}
|
||||
);
|
||||
|
||||
// // Ensure all previously-registered buffers are re-opened when signing in.
|
||||
// lsp.handle_request::<request::SignInInitiate, _, _>(|_, _| async {
|
||||
// Ok(request::SignInInitiateResult::AlreadySignedIn {
|
||||
// user: "user-1".into(),
|
||||
// })
|
||||
// });
|
||||
// copilot
|
||||
// .update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidOpenTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentItem::new(
|
||||
// buffer_2_uri.clone(),
|
||||
// "plaintext".into(),
|
||||
// 0,
|
||||
// "Goodbye".into()
|
||||
// ),
|
||||
// }
|
||||
// );
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidOpenTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentItem::new(
|
||||
// buffer_1_uri.clone(),
|
||||
// "plaintext".into(),
|
||||
// 0,
|
||||
// "Hello world".into()
|
||||
// ),
|
||||
// }
|
||||
// );
|
||||
// Ensure all previously-registered buffers are re-opened when signing in.
|
||||
lsp.handle_request::<request::SignInInitiate, _, _>(|_, _| async {
|
||||
Ok(request::SignInInitiateResult::AlreadySignedIn {
|
||||
user: "user-1".into(),
|
||||
})
|
||||
});
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// // Dropping a buffer causes it to be closed on the LSP side as well.
|
||||
// cx.update(|_| drop(buffer_2));
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidCloseTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri),
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
.await,
|
||||
lsp::DidOpenTextDocumentParams {
|
||||
text_document: lsp::TextDocumentItem::new(
|
||||
buffer_1_uri.clone(),
|
||||
"plaintext".into(),
|
||||
0,
|
||||
"Hello world".into()
|
||||
),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
.await,
|
||||
lsp::DidOpenTextDocumentParams {
|
||||
text_document: lsp::TextDocumentItem::new(
|
||||
buffer_2_uri.clone(),
|
||||
"plaintext".into(),
|
||||
0,
|
||||
"Goodbye".into()
|
||||
),
|
||||
}
|
||||
);
|
||||
// Dropping a buffer causes it to be closed on the LSP side as well.
|
||||
cx.update(|_| drop(buffer_2));
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
.await,
|
||||
lsp::DidCloseTextDocumentParams {
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// struct File {
|
||||
// abs_path: PathBuf,
|
||||
// path: Arc<Path>,
|
||||
// }
|
||||
struct File {
|
||||
abs_path: PathBuf,
|
||||
path: Arc<Path>,
|
||||
}
|
||||
|
||||
// impl language2::File for File {
|
||||
// fn as_local(&self) -> Option<&dyn language2::LocalFile> {
|
||||
// Some(self)
|
||||
// }
|
||||
impl language::File for File {
|
||||
fn as_local(&self) -> Option<&dyn language::LocalFile> {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
// fn mtime(&self) -> std::time::SystemTime {
|
||||
// unimplemented!()
|
||||
// }
|
||||
fn mtime(&self) -> std::time::SystemTime {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// fn path(&self) -> &Arc<Path> {
|
||||
// &self.path
|
||||
// }
|
||||
fn path(&self) -> &Arc<Path> {
|
||||
&self.path
|
||||
}
|
||||
|
||||
// fn full_path(&self, _: &AppContext) -> PathBuf {
|
||||
// unimplemented!()
|
||||
// }
|
||||
fn full_path(&self, _: &AppContext) -> PathBuf {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr {
|
||||
// unimplemented!()
|
||||
// }
|
||||
fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// fn is_deleted(&self) -> bool {
|
||||
// unimplemented!()
|
||||
// }
|
||||
fn is_deleted(&self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// fn as_any(&self) -> &dyn std::any::Any {
|
||||
// unimplemented!()
|
||||
// }
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// fn to_proto(&self) -> rpc::proto::File {
|
||||
// unimplemented!()
|
||||
// }
|
||||
fn to_proto(&self) -> rpc::proto::File {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// fn worktree_id(&self) -> usize {
|
||||
// 0
|
||||
// }
|
||||
// }
|
||||
fn worktree_id(&self) -> usize {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
// impl language::LocalFile for File {
|
||||
// fn abs_path(&self, _: &AppContext) -> PathBuf {
|
||||
// self.abs_path.clone()
|
||||
// }
|
||||
impl language::LocalFile for File {
|
||||
fn abs_path(&self, _: &AppContext) -> PathBuf {
|
||||
self.abs_path.clone()
|
||||
}
|
||||
|
||||
// fn load(&self, _: &AppContext) -> Task<Result<String>> {
|
||||
// unimplemented!()
|
||||
// }
|
||||
fn load(&self, _: &AppContext) -> Task<Result<String>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// fn buffer_reloaded(
|
||||
// &self,
|
||||
// _: u64,
|
||||
// _: &clock::Global,
|
||||
// _: language::RopeFingerprint,
|
||||
// _: language::LineEnding,
|
||||
// _: std::time::SystemTime,
|
||||
// _: &mut AppContext,
|
||||
// ) {
|
||||
// unimplemented!()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
fn buffer_reloaded(
|
||||
&self,
|
||||
_: u64,
|
||||
_: &clock::Global,
|
||||
_: language::RopeFingerprint,
|
||||
_: language::LineEnding,
|
||||
_: std::time::SystemTime,
|
||||
_: &mut AppContext,
|
||||
) {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,376 +1,213 @@
|
||||
// TODO add logging in
|
||||
// use crate::{request::PromptUserDeviceFlow, Copilot, Status};
|
||||
// use gpui::{
|
||||
// elements::*,
|
||||
// geometry::rect::RectF,
|
||||
// platform::{WindowBounds, WindowKind, WindowOptions},
|
||||
// AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext,
|
||||
// WindowHandle,
|
||||
// };
|
||||
// use theme::ui::modal;
|
||||
use crate::{request::PromptUserDeviceFlow, Copilot, Status};
|
||||
use gpui::{
|
||||
div, size, AppContext, Bounds, ClipboardItem, Div, Element, GlobalPixels, InteractiveElement,
|
||||
IntoElement, ParentElement, Point, Render, Stateful, Styled, ViewContext, VisualContext,
|
||||
WindowBounds, WindowHandle, WindowKind, WindowOptions,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{prelude::*, Button, Icon, IconElement, Label};
|
||||
|
||||
// #[derive(PartialEq, Eq, Debug, Clone)]
|
||||
// struct CopyUserCode;
|
||||
const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
|
||||
|
||||
// #[derive(PartialEq, Eq, Debug, Clone)]
|
||||
// struct OpenGithub;
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
let mut verification_window: Option<WindowHandle<CopilotCodeVerification>> = None;
|
||||
cx.observe(&copilot, move |copilot, cx| {
|
||||
let status = copilot.read(cx).status();
|
||||
|
||||
// const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
|
||||
match &status {
|
||||
crate::Status::SigningIn { prompt } => {
|
||||
if let Some(window) = verification_window.as_mut() {
|
||||
let updated = window
|
||||
.update(cx, |verification, cx| {
|
||||
verification.set_status(status.clone(), cx);
|
||||
cx.activate_window();
|
||||
})
|
||||
.is_ok();
|
||||
if !updated {
|
||||
verification_window = Some(create_copilot_auth_window(cx, &status));
|
||||
}
|
||||
} else if let Some(_prompt) = prompt {
|
||||
verification_window = Some(create_copilot_auth_window(cx, &status));
|
||||
}
|
||||
}
|
||||
Status::Authorized | Status::Unauthorized => {
|
||||
if let Some(window) = verification_window.as_ref() {
|
||||
window
|
||||
.update(cx, |verification, cx| {
|
||||
verification.set_status(status, cx);
|
||||
cx.activate(true);
|
||||
cx.activate_window();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if let Some(code_verification) = verification_window.take() {
|
||||
code_verification
|
||||
.update(cx, |_, cx| cx.remove_window())
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn init(cx: &mut AppContext) {
|
||||
// if let Some(copilot) = Copilot::global(cx) {
|
||||
// let mut verification_window: Option<WindowHandle<CopilotCodeVerification>> = None;
|
||||
// cx.observe(&copilot, move |copilot, cx| {
|
||||
// let status = copilot.read(cx).status();
|
||||
fn create_copilot_auth_window(
|
||||
cx: &mut AppContext,
|
||||
status: &Status,
|
||||
) -> WindowHandle<CopilotCodeVerification> {
|
||||
let window_size = size(GlobalPixels::from(280.), GlobalPixels::from(280.));
|
||||
let window_options = WindowOptions {
|
||||
bounds: WindowBounds::Fixed(Bounds::new(Point::default(), window_size)),
|
||||
titlebar: None,
|
||||
center: true,
|
||||
focus: true,
|
||||
show: true,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: true,
|
||||
display_id: None,
|
||||
};
|
||||
let window = cx.open_window(window_options, |cx| {
|
||||
cx.build_view(|_| CopilotCodeVerification::new(status.clone()))
|
||||
});
|
||||
window
|
||||
}
|
||||
|
||||
// match &status {
|
||||
// crate::Status::SigningIn { prompt } => {
|
||||
// if let Some(window) = verification_window.as_mut() {
|
||||
// let updated = window
|
||||
// .root(cx)
|
||||
// .map(|root| {
|
||||
// root.update(cx, |verification, cx| {
|
||||
// verification.set_status(status.clone(), cx);
|
||||
// cx.activate_window();
|
||||
// })
|
||||
// })
|
||||
// .is_some();
|
||||
// if !updated {
|
||||
// verification_window = Some(create_copilot_auth_window(cx, &status));
|
||||
// }
|
||||
// } else if let Some(_prompt) = prompt {
|
||||
// verification_window = Some(create_copilot_auth_window(cx, &status));
|
||||
// }
|
||||
// }
|
||||
// Status::Authorized | Status::Unauthorized => {
|
||||
// if let Some(window) = verification_window.as_ref() {
|
||||
// if let Some(verification) = window.root(cx) {
|
||||
// verification.update(cx, |verification, cx| {
|
||||
// verification.set_status(status, cx);
|
||||
// cx.platform().activate(true);
|
||||
// cx.activate_window();
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// _ => {
|
||||
// if let Some(code_verification) = verification_window.take() {
|
||||
// code_verification.update(cx, |cx| cx.remove_window());
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// .detach();
|
||||
// }
|
||||
// }
|
||||
pub struct CopilotCodeVerification {
|
||||
status: Status,
|
||||
connect_clicked: bool,
|
||||
}
|
||||
|
||||
// fn create_copilot_auth_window(
|
||||
// cx: &mut AppContext,
|
||||
// status: &Status,
|
||||
// ) -> WindowHandle<CopilotCodeVerification> {
|
||||
// let window_size = theme::current(cx).copilot.modal.dimensions();
|
||||
// let window_options = WindowOptions {
|
||||
// bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
|
||||
// titlebar: None,
|
||||
// center: true,
|
||||
// focus: true,
|
||||
// show: true,
|
||||
// kind: WindowKind::Normal,
|
||||
// is_movable: true,
|
||||
// screen: None,
|
||||
// };
|
||||
// cx.add_window(window_options, |_cx| {
|
||||
// CopilotCodeVerification::new(status.clone())
|
||||
// })
|
||||
// }
|
||||
impl CopilotCodeVerification {
|
||||
pub fn new(status: Status) -> Self {
|
||||
Self {
|
||||
status,
|
||||
connect_clicked: false,
|
||||
}
|
||||
}
|
||||
|
||||
// pub struct CopilotCodeVerification {
|
||||
// status: Status,
|
||||
// connect_clicked: bool,
|
||||
// }
|
||||
pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
|
||||
self.status = status;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
// impl CopilotCodeVerification {
|
||||
// pub fn new(status: Status) -> Self {
|
||||
// Self {
|
||||
// status,
|
||||
// connect_clicked: false,
|
||||
// }
|
||||
// }
|
||||
fn render_device_code(
|
||||
data: &PromptUserDeviceFlow,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let copied = cx
|
||||
.read_from_clipboard()
|
||||
.map(|item| item.text() == &data.user_code)
|
||||
.unwrap_or(false);
|
||||
h_stack()
|
||||
.cursor_pointer()
|
||||
.justify_between()
|
||||
.on_mouse_down(gpui::MouseButton::Left, {
|
||||
let user_code = data.user_code.clone();
|
||||
move |_, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new(user_code.clone()));
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.child(Label::new(data.user_code.clone()))
|
||||
.child(div())
|
||||
.child(Label::new(if copied { "Copied!" } else { "Copy" }))
|
||||
}
|
||||
|
||||
// pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
|
||||
// self.status = status;
|
||||
// cx.notify();
|
||||
// }
|
||||
fn render_prompting_modal(
|
||||
connect_clicked: bool,
|
||||
data: &PromptUserDeviceFlow,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl Element {
|
||||
let connect_button_label = if connect_clicked {
|
||||
"Waiting for connection..."
|
||||
} else {
|
||||
"Connect to Github"
|
||||
};
|
||||
v_stack()
|
||||
.flex_1()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.child(Label::new(
|
||||
"Enable Copilot by connecting your existing license",
|
||||
))
|
||||
.child(Self::render_device_code(data, cx))
|
||||
.child(
|
||||
Label::new("Paste this code into GitHub after clicking the button below.")
|
||||
.size(ui::LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
Button::new("connect-button", connect_button_label).on_click({
|
||||
let verification_uri = data.verification_uri.clone();
|
||||
cx.listener(move |this, _, cx| {
|
||||
cx.open_url(&verification_uri);
|
||||
this.connect_clicked = true;
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
fn render_enabled_modal() -> impl Element {
|
||||
v_stack()
|
||||
.child(Label::new("Copilot Enabled!"))
|
||||
.child(Label::new(
|
||||
"You can update your settings or sign out from the Copilot menu in the status bar.",
|
||||
))
|
||||
.child(
|
||||
Button::new("copilot-enabled-done-button", "Done")
|
||||
.on_click(|_, cx| cx.remove_window()),
|
||||
)
|
||||
}
|
||||
|
||||
// fn render_device_code(
|
||||
// data: &PromptUserDeviceFlow,
|
||||
// style: &theme::Copilot,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> impl IntoAnyElement<Self> {
|
||||
// let copied = cx
|
||||
// .read_from_clipboard()
|
||||
// .map(|item| item.text() == &data.user_code)
|
||||
// .unwrap_or(false);
|
||||
fn render_unauthorized_modal() -> impl Element {
|
||||
v_stack()
|
||||
.child(Label::new(
|
||||
"Enable Copilot by connecting your existing license.",
|
||||
))
|
||||
.child(
|
||||
Label::new("You must have an active Copilot license to use it in Zed.")
|
||||
.color(Color::Warning),
|
||||
)
|
||||
.child(
|
||||
Button::new("copilot-subscribe-button", "Subscibe on Github").on_click(|_, cx| {
|
||||
cx.remove_window();
|
||||
cx.open_url(COPILOT_SIGN_UP_URL)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// let device_code_style = &style.auth.prompting.device_code;
|
||||
impl Render for CopilotCodeVerification {
|
||||
type Element = Stateful<Div>;
|
||||
|
||||
// MouseEventHandler::new::<Self, _>(0, cx, |state, _cx| {
|
||||
// Flex::row()
|
||||
// .with_child(
|
||||
// Label::new(data.user_code.clone(), device_code_style.text.clone())
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(device_code_style.left_container)
|
||||
// .constrained()
|
||||
// .with_width(device_code_style.left),
|
||||
// )
|
||||
// .with_child(
|
||||
// Label::new(
|
||||
// if copied { "Copied!" } else { "Copy" },
|
||||
// device_code_style.cta.style_for(state).text.clone(),
|
||||
// )
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(*device_code_style.right_container.style_for(state))
|
||||
// .constrained()
|
||||
// .with_width(device_code_style.right),
|
||||
// )
|
||||
// .contained()
|
||||
// .with_style(device_code_style.cta.style_for(state).container)
|
||||
// })
|
||||
// .on_click(gpui::platform::MouseButton::Left, {
|
||||
// let user_code = data.user_code.clone();
|
||||
// move |_, _, cx| {
|
||||
// cx.platform()
|
||||
// .write_to_clipboard(ClipboardItem::new(user_code.clone()));
|
||||
// cx.notify();
|
||||
// }
|
||||
// })
|
||||
// .with_cursor_style(gpui::platform::CursorStyle::PointingHand)
|
||||
// }
|
||||
|
||||
// fn render_prompting_modal(
|
||||
// connect_clicked: bool,
|
||||
// data: &PromptUserDeviceFlow,
|
||||
// style: &theme::Copilot,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> AnyElement<Self> {
|
||||
// enum ConnectButton {}
|
||||
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// Label::new(
|
||||
// "Enable Copilot by connecting",
|
||||
// style.auth.prompting.subheading.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new(
|
||||
// "your existing license.",
|
||||
// style.auth.prompting.subheading.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// .contained()
|
||||
// .with_style(style.auth.prompting.subheading.container),
|
||||
// )
|
||||
// .with_child(Self::render_device_code(data, &style, cx))
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// Label::new(
|
||||
// "Paste this code into GitHub after",
|
||||
// style.auth.prompting.hint.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new(
|
||||
// "clicking the button below.",
|
||||
// style.auth.prompting.hint.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// .contained()
|
||||
// .with_style(style.auth.prompting.hint.container.clone()),
|
||||
// )
|
||||
// .with_child(theme::ui::cta_button::<ConnectButton, _, _, _>(
|
||||
// if connect_clicked {
|
||||
// "Waiting for connection..."
|
||||
// } else {
|
||||
// "Connect to GitHub"
|
||||
// },
|
||||
// style.auth.content_width,
|
||||
// &style.auth.cta_button,
|
||||
// cx,
|
||||
// {
|
||||
// let verification_uri = data.verification_uri.clone();
|
||||
// move |_, verification, cx| {
|
||||
// cx.platform().open_url(&verification_uri);
|
||||
// verification.connect_clicked = true;
|
||||
// }
|
||||
// },
|
||||
// ))
|
||||
// .align_children_center()
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn render_enabled_modal(
|
||||
// style: &theme::Copilot,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> AnyElement<Self> {
|
||||
// enum DoneButton {}
|
||||
|
||||
// let enabled_style = &style.auth.authorized;
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Label::new("Copilot Enabled!", enabled_style.subheading.text.clone())
|
||||
// .contained()
|
||||
// .with_style(enabled_style.subheading.container)
|
||||
// .aligned(),
|
||||
// )
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// Label::new(
|
||||
// "You can update your settings or",
|
||||
// enabled_style.hint.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new(
|
||||
// "sign out from the Copilot menu in",
|
||||
// enabled_style.hint.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new("the status bar.", enabled_style.hint.text.clone()).aligned(),
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// .contained()
|
||||
// .with_style(enabled_style.hint.container),
|
||||
// )
|
||||
// .with_child(theme::ui::cta_button::<DoneButton, _, _, _>(
|
||||
// "Done",
|
||||
// style.auth.content_width,
|
||||
// &style.auth.cta_button,
|
||||
// cx,
|
||||
// |_, _, cx| cx.remove_window(),
|
||||
// ))
|
||||
// .align_children_center()
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn render_unauthorized_modal(
|
||||
// style: &theme::Copilot,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> AnyElement<Self> {
|
||||
// let unauthorized_style = &style.auth.not_authorized;
|
||||
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// Label::new(
|
||||
// "Enable Copilot by connecting",
|
||||
// unauthorized_style.subheading.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new(
|
||||
// "your existing license.",
|
||||
// unauthorized_style.subheading.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// .contained()
|
||||
// .with_style(unauthorized_style.subheading.container),
|
||||
// )
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// Label::new(
|
||||
// "You must have an active copilot",
|
||||
// unauthorized_style.warning.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new(
|
||||
// "license to use it in Zed.",
|
||||
// unauthorized_style.warning.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// .contained()
|
||||
// .with_style(unauthorized_style.warning.container),
|
||||
// )
|
||||
// .with_child(theme::ui::cta_button::<Self, _, _, _>(
|
||||
// "Subscribe on GitHub",
|
||||
// style.auth.content_width,
|
||||
// &style.auth.cta_button,
|
||||
// cx,
|
||||
// |_, _, cx| {
|
||||
// cx.remove_window();
|
||||
// cx.platform().open_url(COPILOT_SIGN_UP_URL)
|
||||
// },
|
||||
// ))
|
||||
// .align_children_center()
|
||||
// .into_any()
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl Entity for CopilotCodeVerification {
|
||||
// type Event = ();
|
||||
// }
|
||||
|
||||
// impl View for CopilotCodeVerification {
|
||||
// fn ui_name() -> &'static str {
|
||||
// "CopilotCodeVerification"
|
||||
// }
|
||||
|
||||
// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
// cx.notify()
|
||||
// }
|
||||
|
||||
// fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
// cx.notify()
|
||||
// }
|
||||
|
||||
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
// enum ConnectModal {}
|
||||
|
||||
// let style = theme::current(cx).clone();
|
||||
|
||||
// modal::<ConnectModal, _, _, _, _>(
|
||||
// "Connect Copilot to Zed",
|
||||
// &style.copilot.modal,
|
||||
// cx,
|
||||
// |cx| {
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// theme::ui::icon(&style.copilot.auth.header).into_any(),
|
||||
// match &self.status {
|
||||
// Status::SigningIn {
|
||||
// prompt: Some(prompt),
|
||||
// } => Self::render_prompting_modal(
|
||||
// self.connect_clicked,
|
||||
// &prompt,
|
||||
// &style.copilot,
|
||||
// cx,
|
||||
// ),
|
||||
// Status::Unauthorized => {
|
||||
// self.connect_clicked = false;
|
||||
// Self::render_unauthorized_modal(&style.copilot, cx)
|
||||
// }
|
||||
// Status::Authorized => {
|
||||
// self.connect_clicked = false;
|
||||
// Self::render_enabled_modal(&style.copilot, cx)
|
||||
// }
|
||||
// _ => Empty::new().into_any(),
|
||||
// },
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// },
|
||||
// )
|
||||
// .into_any()
|
||||
// }
|
||||
// }
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let prompt = match &self.status {
|
||||
Status::SigningIn {
|
||||
prompt: Some(prompt),
|
||||
} => Self::render_prompting_modal(self.connect_clicked, &prompt, cx).into_any_element(),
|
||||
Status::Unauthorized => {
|
||||
self.connect_clicked = false;
|
||||
Self::render_unauthorized_modal().into_any_element()
|
||||
}
|
||||
Status::Authorized => {
|
||||
self.connect_clicked = false;
|
||||
Self::render_enabled_modal().into_any_element()
|
||||
}
|
||||
_ => div().into_any_element(),
|
||||
};
|
||||
div()
|
||||
.id("copilot code verification")
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.items_center()
|
||||
.p_10()
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.child(ui::Label::new("Connect Copilot to Zed"))
|
||||
.child(IconElement::new(Icon::ZedXCopilot))
|
||||
.child(prompt)
|
||||
}
|
||||
}
|
||||
|
||||
27
crates/copilot_button2/Cargo.toml
Normal file
27
crates/copilot_button2/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "copilot_button2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/copilot_button.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
copilot = { package = "copilot2", path = "../copilot2" }
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
fs = { package = "fs2", path = "../fs2" }
|
||||
zed-actions = { package="zed_actions2", path = "../zed_actions2"}
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
util = { path = "../util" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
anyhow.workspace = true
|
||||
smol.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
370
crates/copilot_button2/src/copilot_button.rs
Normal file
370
crates/copilot_button2/src/copilot_button.rs
Normal file
@@ -0,0 +1,370 @@
|
||||
#![allow(unused)]
|
||||
use anyhow::Result;
|
||||
use copilot::{Copilot, SignOut, Status};
|
||||
use editor::{scroll::autoscroll::Autoscroll, Editor};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
div, Action, AnchorCorner, AppContext, AsyncAppContext, AsyncWindowContext, Div, Entity,
|
||||
ParentElement, Render, Subscription, View, ViewContext, WeakView, WindowContext,
|
||||
};
|
||||
use language::{
|
||||
language_settings::{self, all_language_settings, AllLanguageSettings},
|
||||
File, Language,
|
||||
};
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use util::{paths, ResultExt};
|
||||
use workspace::{
|
||||
create_and_open_local_file,
|
||||
item::ItemHandle,
|
||||
ui::{
|
||||
popover_menu, ButtonCommon, Clickable, ContextMenu, Icon, IconButton, PopoverMenu, Tooltip,
|
||||
},
|
||||
StatusItemView, Toast, Workspace,
|
||||
};
|
||||
use zed_actions::OpenBrowser;
|
||||
|
||||
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
|
||||
const COPILOT_STARTING_TOAST_ID: usize = 1337;
|
||||
const COPILOT_ERROR_TOAST_ID: usize = 1338;
|
||||
|
||||
pub struct CopilotButton {
|
||||
editor_subscription: Option<(Subscription, usize)>,
|
||||
editor_enabled: Option<bool>,
|
||||
language: Option<Arc<Language>>,
|
||||
file: Option<Arc<dyn File>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
impl Render for CopilotButton {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let all_language_settings = all_language_settings(None, cx);
|
||||
if !all_language_settings.copilot.feature_enabled {
|
||||
return div();
|
||||
}
|
||||
|
||||
let Some(copilot) = Copilot::global(cx) else {
|
||||
return div();
|
||||
};
|
||||
let status = copilot.read(cx).status();
|
||||
|
||||
let enabled = self
|
||||
.editor_enabled
|
||||
.unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
|
||||
|
||||
let icon = match status {
|
||||
Status::Error(_) => Icon::CopilotError,
|
||||
Status::Authorized => {
|
||||
if enabled {
|
||||
Icon::Copilot
|
||||
} else {
|
||||
Icon::CopilotDisabled
|
||||
}
|
||||
}
|
||||
_ => Icon::CopilotInit,
|
||||
};
|
||||
|
||||
if let Status::Error(e) = status {
|
||||
return div().child(
|
||||
IconButton::new("copilot-error", icon)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(
|
||||
Toast::new(
|
||||
COPILOT_ERROR_TOAST_ID,
|
||||
format!("Copilot can't be started: {}", e),
|
||||
)
|
||||
.on_click(
|
||||
"Reinstall Copilot",
|
||||
|cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.reinstall(cx))
|
||||
.detach();
|
||||
}
|
||||
},
|
||||
),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
|
||||
);
|
||||
}
|
||||
let this = cx.view().clone();
|
||||
|
||||
div().child(
|
||||
popover_menu("copilot")
|
||||
.menu(move |cx| match status {
|
||||
Status::Authorized => this.update(cx, |this, cx| this.build_copilot_menu(cx)),
|
||||
_ => this.update(cx, |this, cx| this.build_copilot_start_menu(cx)),
|
||||
})
|
||||
.anchor(AnchorCorner::BottomRight)
|
||||
.trigger(
|
||||
IconButton::new("copilot-icon", icon)
|
||||
.tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl CopilotButton {
|
||||
pub fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<Self>) -> Self {
|
||||
Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach());
|
||||
|
||||
cx.observe_global::<SettingsStore>(move |_, cx| cx.notify())
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
editor_subscription: None,
|
||||
editor_enabled: None,
|
||||
language: None,
|
||||
file: None,
|
||||
fs,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
|
||||
let fs = self.fs.clone();
|
||||
ContextMenu::build(cx, |menu, cx| {
|
||||
menu.entry("Sign In", initiate_sign_in)
|
||||
.entry("Disable Copilot", move |cx| hide_copilot(fs.clone(), cx))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_copilot_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
|
||||
let fs = self.fs.clone();
|
||||
|
||||
return ContextMenu::build(cx, move |mut menu, cx| {
|
||||
if let Some(language) = self.language.clone() {
|
||||
let fs = fs.clone();
|
||||
let language_enabled =
|
||||
language_settings::language_settings(Some(&language), None, cx)
|
||||
.show_copilot_suggestions;
|
||||
|
||||
menu = menu.entry(
|
||||
format!(
|
||||
"{} Suggestions for {}",
|
||||
if language_enabled { "Hide" } else { "Show" },
|
||||
language.name()
|
||||
),
|
||||
move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
|
||||
);
|
||||
}
|
||||
|
||||
let settings = AllLanguageSettings::get_global(cx);
|
||||
|
||||
if let Some(file) = &self.file {
|
||||
let path = file.path().clone();
|
||||
let path_enabled = settings.copilot_enabled_for_path(&path);
|
||||
|
||||
menu = menu.entry(
|
||||
format!(
|
||||
"{} Suggestions for This Path",
|
||||
if path_enabled { "Hide" } else { "Show" }
|
||||
),
|
||||
move |cx| {
|
||||
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
|
||||
if let Ok(workspace) = workspace.root_view(cx) {
|
||||
let workspace = workspace.downgrade();
|
||||
cx.spawn(|cx| {
|
||||
configure_disabled_globs(
|
||||
workspace,
|
||||
path_enabled.then_some(path.clone()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let globally_enabled = settings.copilot_enabled(None, None);
|
||||
menu.entry(
|
||||
if globally_enabled {
|
||||
"Hide Suggestions for All Files"
|
||||
} else {
|
||||
"Show Suggestions for All Files"
|
||||
},
|
||||
move |cx| toggle_copilot_globally(fs.clone(), cx),
|
||||
)
|
||||
.separator()
|
||||
.link(
|
||||
"Copilot Settings",
|
||||
OpenBrowser {
|
||||
url: COPILOT_SETTINGS_URL.to_string(),
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
.action("Sign Out", SignOut.boxed_clone())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn update_enabled(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
|
||||
let editor = editor.read(cx);
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let suggestion_anchor = editor.selections.newest_anchor().start;
|
||||
let language = snapshot.language_at(suggestion_anchor);
|
||||
let file = snapshot.file_at(suggestion_anchor).cloned();
|
||||
|
||||
self.editor_enabled = Some(
|
||||
all_language_settings(self.file.as_ref(), cx)
|
||||
.copilot_enabled(language, file.as_ref().map(|file| file.path().as_ref())),
|
||||
);
|
||||
self.language = language.cloned();
|
||||
self.file = file;
|
||||
|
||||
cx.notify()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for CopilotButton {
|
||||
fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
|
||||
if let Some(editor) = item.map(|item| item.act_as::<Editor>(cx)).flatten() {
|
||||
self.editor_subscription = Some((
|
||||
cx.observe(&editor, Self::update_enabled),
|
||||
editor.entity_id().as_u64() as usize,
|
||||
));
|
||||
self.update_enabled(editor, cx);
|
||||
} else {
|
||||
self.language = None;
|
||||
self.editor_subscription = None;
|
||||
self.editor_enabled = None;
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
async fn configure_disabled_globs(
|
||||
workspace: WeakView<Workspace>,
|
||||
path_to_disable: Option<Arc<Path>>,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let settings_editor = workspace
|
||||
.update(&mut cx, |_, cx| {
|
||||
create_and_open_local_file(&paths::SETTINGS, cx, || {
|
||||
settings::initial_user_settings_content().as_ref().into()
|
||||
})
|
||||
})?
|
||||
.await?
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
settings_editor.downgrade().update(&mut cx, |item, cx| {
|
||||
let text = item.buffer().read(cx).snapshot(cx).text();
|
||||
|
||||
let settings = cx.global::<SettingsStore>();
|
||||
let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
|
||||
let copilot = file.copilot.get_or_insert_with(Default::default);
|
||||
let globs = copilot.disabled_globs.get_or_insert_with(|| {
|
||||
settings
|
||||
.get::<AllLanguageSettings>(None)
|
||||
.copilot
|
||||
.disabled_globs
|
||||
.iter()
|
||||
.map(|glob| glob.glob().to_string())
|
||||
.collect()
|
||||
});
|
||||
|
||||
if let Some(path_to_disable) = &path_to_disable {
|
||||
globs.push(path_to_disable.to_string_lossy().into_owned());
|
||||
} else {
|
||||
globs.clear();
|
||||
}
|
||||
});
|
||||
|
||||
if !edits.is_empty() {
|
||||
item.change_selections(Some(Autoscroll::newest()), cx, |selections| {
|
||||
selections.select_ranges(edits.iter().map(|e| e.0.clone()));
|
||||
});
|
||||
|
||||
// When *enabling* a path, don't actually perform an edit, just select the range.
|
||||
if path_to_disable.is_some() {
|
||||
item.edit(edits.iter().cloned(), cx);
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None);
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||
file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
|
||||
});
|
||||
}
|
||||
|
||||
fn toggle_copilot_for_language(language: Arc<Language>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
let show_copilot_suggestions =
|
||||
all_language_settings(None, cx).copilot_enabled(Some(&language), None);
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||
file.languages
|
||||
.entry(language.name())
|
||||
.or_default()
|
||||
.show_copilot_suggestions = Some(!show_copilot_suggestions);
|
||||
});
|
||||
}
|
||||
|
||||
fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||
file.features.get_or_insert(Default::default()).copilot = Some(false);
|
||||
});
|
||||
}
|
||||
|
||||
fn initiate_sign_in(cx: &mut WindowContext) {
|
||||
let Some(copilot) = Copilot::global(cx) else {
|
||||
return;
|
||||
};
|
||||
let status = copilot.read(cx).status();
|
||||
|
||||
match status {
|
||||
Status::Starting { task } => {
|
||||
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(workspace) = workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(
|
||||
Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot is starting..."),
|
||||
cx,
|
||||
);
|
||||
workspace.weak_handle()
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
task.await;
|
||||
if let Some(copilot) = cx.update(|_, cx| Copilot::global(cx)).ok().flatten() {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
|
||||
Status::Authorized => workspace.show_toast(
|
||||
Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot has started!"),
|
||||
cx,
|
||||
),
|
||||
_ => {
|
||||
workspace.dismiss_toast(COPILOT_STARTING_TOAST_ID, cx);
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
_ => {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ use std::{
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
pub use toolbar_controls::ToolbarControls;
|
||||
use ui::{h_stack, Color, HighlightedLabel, Icon, IconElement, Label};
|
||||
use ui::{h_stack, prelude::*, HighlightedLabel, Icon, IconElement, Label};
|
||||
use util::TryFutureExt;
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
|
||||
@@ -88,7 +88,7 @@ struct DiagnosticGroupState {
|
||||
block_count: usize,
|
||||
}
|
||||
|
||||
impl EventEmitter<ItemEvent> for ProjectDiagnosticsEditor {}
|
||||
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
|
||||
|
||||
impl Render for ProjectDiagnosticsEditor {
|
||||
type Element = Focusable<Div>;
|
||||
@@ -158,7 +158,7 @@ impl ProjectDiagnosticsEditor {
|
||||
});
|
||||
let editor_event_subscription =
|
||||
cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| {
|
||||
Self::emit_item_event_for_editor_event(event, cx);
|
||||
cx.emit(event.clone());
|
||||
if event == &EditorEvent::Focused && this.path_states.is_empty() {
|
||||
cx.focus(&this.focus_handle);
|
||||
}
|
||||
@@ -183,40 +183,6 @@ impl ProjectDiagnosticsEditor {
|
||||
this
|
||||
}
|
||||
|
||||
fn emit_item_event_for_editor_event(event: &EditorEvent, cx: &mut ViewContext<Self>) {
|
||||
match event {
|
||||
EditorEvent::Closed => cx.emit(ItemEvent::CloseItem),
|
||||
|
||||
EditorEvent::Saved | EditorEvent::TitleChanged => {
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::Reparsed => {
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::SelectionsChanged { local } if *local => {
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::DirtyChanged => {
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
}
|
||||
|
||||
EditorEvent::BufferEdited => {
|
||||
cx.emit(ItemEvent::Edit);
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => {
|
||||
cx.emit(ItemEvent::Edit);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
|
||||
if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
|
||||
workspace.activate_item(&existing, cx);
|
||||
@@ -333,8 +299,7 @@ impl ProjectDiagnosticsEditor {
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.summary = this.project.read(cx).diagnostic_summary(false, cx);
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
@@ -649,6 +614,12 @@ impl FocusableView for ProjectDiagnosticsEditor {
|
||||
}
|
||||
|
||||
impl Item for ProjectDiagnosticsEditor {
|
||||
type Event = EditorEvent;
|
||||
|
||||
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
|
||||
Editor::to_item_events(event, f)
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, |editor, cx| editor.deactivated(cx));
|
||||
}
|
||||
@@ -774,24 +745,39 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
||||
Arc::new(move |_| {
|
||||
h_stack()
|
||||
.id("diagnostic header")
|
||||
.gap_3()
|
||||
.bg(gpui::red())
|
||||
.map(|stack| {
|
||||
let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
|
||||
IconElement::new(Icon::XCircle).color(Color::Error)
|
||||
} else {
|
||||
IconElement::new(Icon::ExclamationTriangle).color(Color::Warning)
|
||||
};
|
||||
|
||||
stack.child(div().pl_8().child(icon))
|
||||
})
|
||||
.when_some(diagnostic.source.as_ref(), |stack, source| {
|
||||
stack.child(Label::new(format!("{source}:")).color(Color::Accent))
|
||||
})
|
||||
.child(HighlightedLabel::new(message.clone(), highlights.clone()))
|
||||
.when_some(diagnostic.code.as_ref(), |stack, code| {
|
||||
stack.child(Label::new(code.clone()))
|
||||
})
|
||||
.py_2()
|
||||
.pl_10()
|
||||
.pr_5()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_3()
|
||||
.map(|stack| {
|
||||
let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
|
||||
IconElement::new(Icon::XCircle).color(Color::Error)
|
||||
} else {
|
||||
IconElement::new(Icon::ExclamationTriangle).color(Color::Warning)
|
||||
};
|
||||
stack.child(icon)
|
||||
})
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(HighlightedLabel::new(message.clone(), highlights.clone()))
|
||||
.when_some(diagnostic.code.as_ref(), |stack, code| {
|
||||
stack.child(Label::new(format!("({code})")).color(Color::Muted))
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.when_some(diagnostic.source.as_ref(), |stack, source| {
|
||||
stack.child(Label::new(format!("{source}")).color(Color::Muted))
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
@@ -802,11 +788,22 @@ pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement {
|
||||
label.into_any_element()
|
||||
} else {
|
||||
h_stack()
|
||||
.bg(gpui::red())
|
||||
.child(IconElement::new(Icon::XCircle))
|
||||
.child(Label::new(summary.error_count.to_string()))
|
||||
.child(IconElement::new(Icon::ExclamationTriangle))
|
||||
.child(Label::new(summary.warning_count.to_string()))
|
||||
.gap_1()
|
||||
.when(summary.error_count > 0, |then| {
|
||||
then.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(IconElement::new(Icon::XCircle).color(Color::Error))
|
||||
.child(Label::new(summary.error_count.to_string())),
|
||||
)
|
||||
})
|
||||
.when(summary.warning_count > 0, |then| {
|
||||
then.child(
|
||||
h_stack()
|
||||
.child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning))
|
||||
.child(Label::new(summary.warning_count.to_string())),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::ProjectDiagnosticsEditor;
|
||||
use gpui::{div, Div, EventEmitter, ParentElement, Render, ViewContext, WeakView};
|
||||
use ui::prelude::*;
|
||||
use ui::{Icon, IconButton, Tooltip};
|
||||
use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -162,7 +162,7 @@ impl WrapMap {
|
||||
{
|
||||
let tab_snapshot = new_snapshot.tab_snapshot.clone();
|
||||
let range = TabPoint::zero()..tab_snapshot.max_point();
|
||||
let edits = new_snapshot
|
||||
edits = new_snapshot
|
||||
.update(
|
||||
tab_snapshot,
|
||||
&[TabEdit {
|
||||
@@ -741,49 +741,48 @@ impl WrapSnapshot {
|
||||
}
|
||||
|
||||
fn check_invariants(&self) {
|
||||
// todo!()
|
||||
// #[cfg(test)]
|
||||
// {
|
||||
// assert_eq!(
|
||||
// TabPoint::from(self.transforms.summary().input.lines),
|
||||
// self.tab_snapshot.max_point()
|
||||
// );
|
||||
#[cfg(test)]
|
||||
{
|
||||
assert_eq!(
|
||||
TabPoint::from(self.transforms.summary().input.lines),
|
||||
self.tab_snapshot.max_point()
|
||||
);
|
||||
|
||||
// {
|
||||
// let mut transforms = self.transforms.cursor::<()>().peekable();
|
||||
// while let Some(transform) = transforms.next() {
|
||||
// if let Some(next_transform) = transforms.peek() {
|
||||
// assert!(transform.is_isomorphic() != next_transform.is_isomorphic());
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
{
|
||||
let mut transforms = self.transforms.cursor::<()>().peekable();
|
||||
while let Some(transform) = transforms.next() {
|
||||
if let Some(next_transform) = transforms.peek() {
|
||||
assert!(transform.is_isomorphic() != next_transform.is_isomorphic());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// let text = language::Rope::from(self.text().as_str());
|
||||
// let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0);
|
||||
// let mut expected_buffer_rows = Vec::new();
|
||||
// let mut prev_tab_row = 0;
|
||||
// for display_row in 0..=self.max_point().row() {
|
||||
// let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0));
|
||||
// if tab_point.row() == prev_tab_row && display_row != 0 {
|
||||
// expected_buffer_rows.push(None);
|
||||
// } else {
|
||||
// expected_buffer_rows.push(input_buffer_rows.next().unwrap());
|
||||
// }
|
||||
let text = language::Rope::from(self.text().as_str());
|
||||
let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0);
|
||||
let mut expected_buffer_rows = Vec::new();
|
||||
let mut prev_tab_row = 0;
|
||||
for display_row in 0..=self.max_point().row() {
|
||||
let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0));
|
||||
if tab_point.row() == prev_tab_row && display_row != 0 {
|
||||
expected_buffer_rows.push(None);
|
||||
} else {
|
||||
expected_buffer_rows.push(input_buffer_rows.next().unwrap());
|
||||
}
|
||||
|
||||
// prev_tab_row = tab_point.row();
|
||||
// assert_eq!(self.line_len(display_row), text.line_len(display_row));
|
||||
// }
|
||||
prev_tab_row = tab_point.row();
|
||||
assert_eq!(self.line_len(display_row), text.line_len(display_row));
|
||||
}
|
||||
|
||||
// for start_display_row in 0..expected_buffer_rows.len() {
|
||||
// assert_eq!(
|
||||
// self.buffer_rows(start_display_row as u32)
|
||||
// .collect::<Vec<_>>(),
|
||||
// &expected_buffer_rows[start_display_row..],
|
||||
// "invalid buffer_rows({}..)",
|
||||
// start_display_row
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
for start_display_row in 0..expected_buffer_rows.len() {
|
||||
assert_eq!(
|
||||
self.buffer_rows(start_display_row as u32)
|
||||
.collect::<Vec<_>>(),
|
||||
&expected_buffer_rows[start_display_row..],
|
||||
"invalid buffer_rows({}..)",
|
||||
start_display_row
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1026,337 +1025,334 @@ fn consolidate_wrap_edits(edits: &mut Vec<WrapEdit>) {
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use super::*;
|
||||
// use crate::{
|
||||
// display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
|
||||
// MultiBuffer,
|
||||
// };
|
||||
// use gpui::test::observe;
|
||||
// use rand::prelude::*;
|
||||
// use settings::SettingsStore;
|
||||
// use smol::stream::StreamExt;
|
||||
// use std::{cmp, env, num::NonZeroU32};
|
||||
// use text::Rope;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
|
||||
MultiBuffer,
|
||||
};
|
||||
use gpui::{font, px, test::observe};
|
||||
use rand::prelude::*;
|
||||
use settings::SettingsStore;
|
||||
use smol::stream::StreamExt;
|
||||
use std::{cmp, env, num::NonZeroU32};
|
||||
use text::Rope;
|
||||
use theme::LoadThemes;
|
||||
|
||||
// #[gpui::test(iterations = 100)]
|
||||
// async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
|
||||
// init_test(cx);
|
||||
#[gpui::test(iterations = 100)]
|
||||
async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
|
||||
// todo!() this test is flaky
|
||||
init_test(cx);
|
||||
|
||||
// cx.foreground().set_block_on_ticks(0..=50);
|
||||
// let operations = env::var("OPERATIONS")
|
||||
// .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||||
// .unwrap_or(10);
|
||||
cx.background_executor.set_block_on_ticks(0..=50);
|
||||
let operations = env::var("OPERATIONS")
|
||||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||||
.unwrap_or(10);
|
||||
|
||||
// let font_cache = cx.font_cache().clone();
|
||||
// let font_system = cx.platform().fonts();
|
||||
// let mut wrap_width = if rng.gen_bool(0.1) {
|
||||
// None
|
||||
// } else {
|
||||
// Some(rng.gen_range(0.0..=1000.0))
|
||||
// };
|
||||
// let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
|
||||
// let family_id = font_cache
|
||||
// .load_family(&["Helvetica"], &Default::default())
|
||||
// .unwrap();
|
||||
// let font_id = font_cache
|
||||
// .select_font(family_id, &Default::default())
|
||||
// .unwrap();
|
||||
// let font_size = 14.0;
|
||||
let text_system = cx.read(|cx| cx.text_system().clone());
|
||||
let mut wrap_width = if rng.gen_bool(0.1) {
|
||||
None
|
||||
} else {
|
||||
Some(px(rng.gen_range(0.0..=1000.0)))
|
||||
};
|
||||
let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
|
||||
let font = font("Helvetica");
|
||||
let font_id = text_system.font_id(&font).unwrap();
|
||||
let font_size = px(14.0);
|
||||
|
||||
// log::info!("Tab size: {}", tab_size);
|
||||
// log::info!("Wrap width: {:?}", wrap_width);
|
||||
log::info!("Tab size: {}", tab_size);
|
||||
log::info!("Wrap width: {:?}", wrap_width);
|
||||
|
||||
// let buffer = cx.update(|cx| {
|
||||
// if rng.gen() {
|
||||
// MultiBuffer::build_random(&mut rng, cx)
|
||||
// } else {
|
||||
// let len = rng.gen_range(0..10);
|
||||
// let text = util::RandomCharIter::new(&mut rng)
|
||||
// .take(len)
|
||||
// .collect::<String>();
|
||||
// MultiBuffer::build_simple(&text, cx)
|
||||
// }
|
||||
// });
|
||||
// let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
|
||||
// log::info!("Buffer text: {:?}", buffer_snapshot.text());
|
||||
// let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
|
||||
// log::info!("InlayMap text: {:?}", inlay_snapshot.text());
|
||||
// let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone());
|
||||
// log::info!("FoldMap text: {:?}", fold_snapshot.text());
|
||||
// let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
|
||||
// let tabs_snapshot = tab_map.set_max_expansion_column(32);
|
||||
// log::info!("TabMap text: {:?}", tabs_snapshot.text());
|
||||
let buffer = cx.update(|cx| {
|
||||
if rng.gen() {
|
||||
MultiBuffer::build_random(&mut rng, cx)
|
||||
} else {
|
||||
let len = rng.gen_range(0..10);
|
||||
let text = util::RandomCharIter::new(&mut rng)
|
||||
.take(len)
|
||||
.collect::<String>();
|
||||
MultiBuffer::build_simple(&text, cx)
|
||||
}
|
||||
});
|
||||
let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
|
||||
log::info!("Buffer text: {:?}", buffer_snapshot.text());
|
||||
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
|
||||
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
|
||||
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone());
|
||||
log::info!("FoldMap text: {:?}", fold_snapshot.text());
|
||||
let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
|
||||
let tabs_snapshot = tab_map.set_max_expansion_column(32);
|
||||
log::info!("TabMap text: {:?}", tabs_snapshot.text());
|
||||
|
||||
// let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system);
|
||||
// let unwrapped_text = tabs_snapshot.text();
|
||||
// let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
|
||||
let mut line_wrapper = text_system.line_wrapper(font.clone(), font_size).unwrap();
|
||||
let unwrapped_text = tabs_snapshot.text();
|
||||
let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
|
||||
|
||||
// let (wrap_map, _) =
|
||||
// cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx));
|
||||
// let mut notifications = observe(&wrap_map, cx);
|
||||
let (wrap_map, _) =
|
||||
cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font, font_size, wrap_width, cx));
|
||||
let mut notifications = observe(&wrap_map, cx);
|
||||
|
||||
// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
|
||||
// notifications.next().await.unwrap();
|
||||
// }
|
||||
if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
|
||||
notifications.next().await.unwrap();
|
||||
}
|
||||
|
||||
// let (initial_snapshot, _) = wrap_map.update(cx, |map, cx| {
|
||||
// assert!(!map.is_rewrapping());
|
||||
// map.sync(tabs_snapshot.clone(), Vec::new(), cx)
|
||||
// });
|
||||
let (initial_snapshot, _) = wrap_map.update(cx, |map, cx| {
|
||||
assert!(!map.is_rewrapping());
|
||||
map.sync(tabs_snapshot.clone(), Vec::new(), cx)
|
||||
});
|
||||
|
||||
// let actual_text = initial_snapshot.text();
|
||||
// assert_eq!(
|
||||
// actual_text, expected_text,
|
||||
// "unwrapped text is: {:?}",
|
||||
// unwrapped_text
|
||||
// );
|
||||
// log::info!("Wrapped text: {:?}", actual_text);
|
||||
let actual_text = initial_snapshot.text();
|
||||
assert_eq!(
|
||||
actual_text, expected_text,
|
||||
"unwrapped text is: {:?}",
|
||||
unwrapped_text
|
||||
);
|
||||
log::info!("Wrapped text: {:?}", actual_text);
|
||||
|
||||
// let mut next_inlay_id = 0;
|
||||
// let mut edits = Vec::new();
|
||||
// for _i in 0..operations {
|
||||
// log::info!("{} ==============================================", _i);
|
||||
let mut next_inlay_id = 0;
|
||||
let mut edits = Vec::new();
|
||||
for _i in 0..operations {
|
||||
log::info!("{} ==============================================", _i);
|
||||
|
||||
// let mut buffer_edits = Vec::new();
|
||||
// match rng.gen_range(0..=100) {
|
||||
// 0..=19 => {
|
||||
// wrap_width = if rng.gen_bool(0.2) {
|
||||
// None
|
||||
// } else {
|
||||
// Some(rng.gen_range(0.0..=1000.0))
|
||||
// };
|
||||
// log::info!("Setting wrap width to {:?}", wrap_width);
|
||||
// wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
|
||||
// }
|
||||
// 20..=39 => {
|
||||
// for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
|
||||
// let (tabs_snapshot, tab_edits) =
|
||||
// tab_map.sync(fold_snapshot, fold_edits, tab_size);
|
||||
// let (mut snapshot, wrap_edits) =
|
||||
// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
|
||||
// snapshot.check_invariants();
|
||||
// snapshot.verify_chunks(&mut rng);
|
||||
// edits.push((snapshot, wrap_edits));
|
||||
// }
|
||||
// }
|
||||
// 40..=59 => {
|
||||
// let (inlay_snapshot, inlay_edits) =
|
||||
// inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
|
||||
// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
|
||||
// let (tabs_snapshot, tab_edits) =
|
||||
// tab_map.sync(fold_snapshot, fold_edits, tab_size);
|
||||
// let (mut snapshot, wrap_edits) =
|
||||
// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
|
||||
// snapshot.check_invariants();
|
||||
// snapshot.verify_chunks(&mut rng);
|
||||
// edits.push((snapshot, wrap_edits));
|
||||
// }
|
||||
// _ => {
|
||||
// buffer.update(cx, |buffer, cx| {
|
||||
// let subscription = buffer.subscribe();
|
||||
// let edit_count = rng.gen_range(1..=5);
|
||||
// buffer.randomly_mutate(&mut rng, edit_count, cx);
|
||||
// buffer_snapshot = buffer.snapshot(cx);
|
||||
// buffer_edits.extend(subscription.consume());
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
let mut buffer_edits = Vec::new();
|
||||
match rng.gen_range(0..=100) {
|
||||
0..=19 => {
|
||||
wrap_width = if rng.gen_bool(0.2) {
|
||||
None
|
||||
} else {
|
||||
Some(px(rng.gen_range(0.0..=1000.0)))
|
||||
};
|
||||
log::info!("Setting wrap width to {:?}", wrap_width);
|
||||
wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
|
||||
}
|
||||
20..=39 => {
|
||||
for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
|
||||
let (tabs_snapshot, tab_edits) =
|
||||
tab_map.sync(fold_snapshot, fold_edits, tab_size);
|
||||
let (mut snapshot, wrap_edits) =
|
||||
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
|
||||
snapshot.check_invariants();
|
||||
snapshot.verify_chunks(&mut rng);
|
||||
edits.push((snapshot, wrap_edits));
|
||||
}
|
||||
}
|
||||
40..=59 => {
|
||||
let (inlay_snapshot, inlay_edits) =
|
||||
inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
|
||||
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
|
||||
let (tabs_snapshot, tab_edits) =
|
||||
tab_map.sync(fold_snapshot, fold_edits, tab_size);
|
||||
let (mut snapshot, wrap_edits) =
|
||||
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
|
||||
snapshot.check_invariants();
|
||||
snapshot.verify_chunks(&mut rng);
|
||||
edits.push((snapshot, wrap_edits));
|
||||
}
|
||||
_ => {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let subscription = buffer.subscribe();
|
||||
let edit_count = rng.gen_range(1..=5);
|
||||
buffer.randomly_mutate(&mut rng, edit_count, cx);
|
||||
buffer_snapshot = buffer.snapshot(cx);
|
||||
buffer_edits.extend(subscription.consume());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// log::info!("Buffer text: {:?}", buffer_snapshot.text());
|
||||
// let (inlay_snapshot, inlay_edits) =
|
||||
// inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
|
||||
// log::info!("InlayMap text: {:?}", inlay_snapshot.text());
|
||||
// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
|
||||
// log::info!("FoldMap text: {:?}", fold_snapshot.text());
|
||||
// let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
|
||||
// log::info!("TabMap text: {:?}", tabs_snapshot.text());
|
||||
log::info!("Buffer text: {:?}", buffer_snapshot.text());
|
||||
let (inlay_snapshot, inlay_edits) =
|
||||
inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
|
||||
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
|
||||
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
|
||||
log::info!("FoldMap text: {:?}", fold_snapshot.text());
|
||||
let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
|
||||
log::info!("TabMap text: {:?}", tabs_snapshot.text());
|
||||
|
||||
// let unwrapped_text = tabs_snapshot.text();
|
||||
// let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
|
||||
// let (mut snapshot, wrap_edits) =
|
||||
// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx));
|
||||
// snapshot.check_invariants();
|
||||
// snapshot.verify_chunks(&mut rng);
|
||||
// edits.push((snapshot, wrap_edits));
|
||||
let unwrapped_text = tabs_snapshot.text();
|
||||
let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
|
||||
let (mut snapshot, wrap_edits) =
|
||||
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx));
|
||||
snapshot.check_invariants();
|
||||
snapshot.verify_chunks(&mut rng);
|
||||
edits.push((snapshot, wrap_edits));
|
||||
|
||||
// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) {
|
||||
// log::info!("Waiting for wrapping to finish");
|
||||
// while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
|
||||
// notifications.next().await.unwrap();
|
||||
// }
|
||||
// wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty()));
|
||||
// }
|
||||
if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) {
|
||||
log::info!("Waiting for wrapping to finish");
|
||||
while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
|
||||
notifications.next().await.unwrap();
|
||||
}
|
||||
wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty()));
|
||||
}
|
||||
|
||||
// if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
|
||||
// let (mut wrapped_snapshot, wrap_edits) =
|
||||
// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx));
|
||||
// let actual_text = wrapped_snapshot.text();
|
||||
// let actual_longest_row = wrapped_snapshot.longest_row();
|
||||
// log::info!("Wrapping finished: {:?}", actual_text);
|
||||
// wrapped_snapshot.check_invariants();
|
||||
// wrapped_snapshot.verify_chunks(&mut rng);
|
||||
// edits.push((wrapped_snapshot.clone(), wrap_edits));
|
||||
// assert_eq!(
|
||||
// actual_text, expected_text,
|
||||
// "unwrapped text is: {:?}",
|
||||
// unwrapped_text
|
||||
// );
|
||||
if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
|
||||
let (mut wrapped_snapshot, wrap_edits) =
|
||||
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx));
|
||||
let actual_text = wrapped_snapshot.text();
|
||||
let actual_longest_row = wrapped_snapshot.longest_row();
|
||||
log::info!("Wrapping finished: {:?}", actual_text);
|
||||
wrapped_snapshot.check_invariants();
|
||||
wrapped_snapshot.verify_chunks(&mut rng);
|
||||
edits.push((wrapped_snapshot.clone(), wrap_edits));
|
||||
assert_eq!(
|
||||
actual_text, expected_text,
|
||||
"unwrapped text is: {:?}",
|
||||
unwrapped_text
|
||||
);
|
||||
|
||||
// let mut summary = TextSummary::default();
|
||||
// for (ix, item) in wrapped_snapshot
|
||||
// .transforms
|
||||
// .items(&())
|
||||
// .into_iter()
|
||||
// .enumerate()
|
||||
// {
|
||||
// summary += &item.summary.output;
|
||||
// log::info!("{} summary: {:?}", ix, item.summary.output,);
|
||||
// }
|
||||
let mut summary = TextSummary::default();
|
||||
for (ix, item) in wrapped_snapshot
|
||||
.transforms
|
||||
.items(&())
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
summary += &item.summary.output;
|
||||
log::info!("{} summary: {:?}", ix, item.summary.output,);
|
||||
}
|
||||
|
||||
// if tab_size.get() == 1
|
||||
// || !wrapped_snapshot
|
||||
// .tab_snapshot
|
||||
// .fold_snapshot
|
||||
// .text()
|
||||
// .contains('\t')
|
||||
// {
|
||||
// let mut expected_longest_rows = Vec::new();
|
||||
// let mut longest_line_len = -1;
|
||||
// for (row, line) in expected_text.split('\n').enumerate() {
|
||||
// let line_char_count = line.chars().count() as isize;
|
||||
// if line_char_count > longest_line_len {
|
||||
// expected_longest_rows.clear();
|
||||
// longest_line_len = line_char_count;
|
||||
// }
|
||||
// if line_char_count >= longest_line_len {
|
||||
// expected_longest_rows.push(row as u32);
|
||||
// }
|
||||
// }
|
||||
if tab_size.get() == 1
|
||||
|| !wrapped_snapshot
|
||||
.tab_snapshot
|
||||
.fold_snapshot
|
||||
.text()
|
||||
.contains('\t')
|
||||
{
|
||||
let mut expected_longest_rows = Vec::new();
|
||||
let mut longest_line_len = -1;
|
||||
for (row, line) in expected_text.split('\n').enumerate() {
|
||||
let line_char_count = line.chars().count() as isize;
|
||||
if line_char_count > longest_line_len {
|
||||
expected_longest_rows.clear();
|
||||
longest_line_len = line_char_count;
|
||||
}
|
||||
if line_char_count >= longest_line_len {
|
||||
expected_longest_rows.push(row as u32);
|
||||
}
|
||||
}
|
||||
|
||||
// assert!(
|
||||
// expected_longest_rows.contains(&actual_longest_row),
|
||||
// "incorrect longest row {}. expected {:?} with length {}",
|
||||
// actual_longest_row,
|
||||
// expected_longest_rows,
|
||||
// longest_line_len,
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
assert!(
|
||||
expected_longest_rows.contains(&actual_longest_row),
|
||||
"incorrect longest row {}. expected {:?} with length {}",
|
||||
actual_longest_row,
|
||||
expected_longest_rows,
|
||||
longest_line_len,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// let mut initial_text = Rope::from(initial_snapshot.text().as_str());
|
||||
// for (snapshot, patch) in edits {
|
||||
// let snapshot_text = Rope::from(snapshot.text().as_str());
|
||||
// for edit in &patch {
|
||||
// let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0));
|
||||
// let old_end = initial_text.point_to_offset(cmp::min(
|
||||
// Point::new(edit.new.start + edit.old.len() as u32, 0),
|
||||
// initial_text.max_point(),
|
||||
// ));
|
||||
// let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0));
|
||||
// let new_end = snapshot_text.point_to_offset(cmp::min(
|
||||
// Point::new(edit.new.end, 0),
|
||||
// snapshot_text.max_point(),
|
||||
// ));
|
||||
// let new_text = snapshot_text
|
||||
// .chunks_in_range(new_start..new_end)
|
||||
// .collect::<String>();
|
||||
let mut initial_text = Rope::from(initial_snapshot.text().as_str());
|
||||
for (snapshot, patch) in edits {
|
||||
let snapshot_text = Rope::from(snapshot.text().as_str());
|
||||
for edit in &patch {
|
||||
let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0));
|
||||
let old_end = initial_text.point_to_offset(cmp::min(
|
||||
Point::new(edit.new.start + edit.old.len() as u32, 0),
|
||||
initial_text.max_point(),
|
||||
));
|
||||
let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0));
|
||||
let new_end = snapshot_text.point_to_offset(cmp::min(
|
||||
Point::new(edit.new.end, 0),
|
||||
snapshot_text.max_point(),
|
||||
));
|
||||
let new_text = snapshot_text
|
||||
.chunks_in_range(new_start..new_end)
|
||||
.collect::<String>();
|
||||
|
||||
// initial_text.replace(old_start..old_end, &new_text);
|
||||
// }
|
||||
// assert_eq!(initial_text.to_string(), snapshot_text.to_string());
|
||||
// }
|
||||
initial_text.replace(old_start..old_end, &new_text);
|
||||
}
|
||||
assert_eq!(initial_text.to_string(), snapshot_text.to_string());
|
||||
}
|
||||
|
||||
// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
|
||||
// log::info!("Waiting for wrapping to finish");
|
||||
// while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
|
||||
// notifications.next().await.unwrap();
|
||||
// }
|
||||
// }
|
||||
// wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty()));
|
||||
// }
|
||||
if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
|
||||
log::info!("Waiting for wrapping to finish");
|
||||
while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
|
||||
notifications.next().await.unwrap();
|
||||
}
|
||||
}
|
||||
wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty()));
|
||||
}
|
||||
|
||||
// fn init_test(cx: &mut gpui::TestAppContext) {
|
||||
// cx.foreground().forbid_parking();
|
||||
// cx.update(|cx| {
|
||||
// cx.set_global(SettingsStore::test(cx));
|
||||
// theme::init((), cx);
|
||||
// });
|
||||
// }
|
||||
fn init_test(cx: &mut gpui::TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
theme::init(LoadThemes::JustBase, cx);
|
||||
});
|
||||
}
|
||||
|
||||
// fn wrap_text(
|
||||
// unwrapped_text: &str,
|
||||
// wrap_width: Option<f32>,
|
||||
// line_wrapper: &mut LineWrapper,
|
||||
// ) -> String {
|
||||
// if let Some(wrap_width) = wrap_width {
|
||||
// let mut wrapped_text = String::new();
|
||||
// for (row, line) in unwrapped_text.split('\n').enumerate() {
|
||||
// if row > 0 {
|
||||
// wrapped_text.push('\n')
|
||||
// }
|
||||
fn wrap_text(
|
||||
unwrapped_text: &str,
|
||||
wrap_width: Option<Pixels>,
|
||||
line_wrapper: &mut LineWrapper,
|
||||
) -> String {
|
||||
if let Some(wrap_width) = wrap_width {
|
||||
let mut wrapped_text = String::new();
|
||||
for (row, line) in unwrapped_text.split('\n').enumerate() {
|
||||
if row > 0 {
|
||||
wrapped_text.push('\n')
|
||||
}
|
||||
|
||||
// let mut prev_ix = 0;
|
||||
// for boundary in line_wrapper.wrap_line(line, wrap_width) {
|
||||
// wrapped_text.push_str(&line[prev_ix..boundary.ix]);
|
||||
// wrapped_text.push('\n');
|
||||
// wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize));
|
||||
// prev_ix = boundary.ix;
|
||||
// }
|
||||
// wrapped_text.push_str(&line[prev_ix..]);
|
||||
// }
|
||||
// wrapped_text
|
||||
// } else {
|
||||
// unwrapped_text.to_string()
|
||||
// }
|
||||
// }
|
||||
let mut prev_ix = 0;
|
||||
for boundary in line_wrapper.wrap_line(line, wrap_width) {
|
||||
wrapped_text.push_str(&line[prev_ix..boundary.ix]);
|
||||
wrapped_text.push('\n');
|
||||
wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize));
|
||||
prev_ix = boundary.ix;
|
||||
}
|
||||
wrapped_text.push_str(&line[prev_ix..]);
|
||||
}
|
||||
wrapped_text
|
||||
} else {
|
||||
unwrapped_text.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// impl WrapSnapshot {
|
||||
// pub fn text(&self) -> String {
|
||||
// self.text_chunks(0).collect()
|
||||
// }
|
||||
impl WrapSnapshot {
|
||||
pub fn text(&self) -> String {
|
||||
self.text_chunks(0).collect()
|
||||
}
|
||||
|
||||
// pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
|
||||
// self.chunks(
|
||||
// wrap_row..self.max_point().row() + 1,
|
||||
// false,
|
||||
// Highlights::default(),
|
||||
// )
|
||||
// .map(|h| h.text)
|
||||
// }
|
||||
pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
|
||||
self.chunks(
|
||||
wrap_row..self.max_point().row() + 1,
|
||||
false,
|
||||
Highlights::default(),
|
||||
)
|
||||
.map(|h| h.text)
|
||||
}
|
||||
|
||||
// fn verify_chunks(&mut self, rng: &mut impl Rng) {
|
||||
// for _ in 0..5 {
|
||||
// let mut end_row = rng.gen_range(0..=self.max_point().row());
|
||||
// let start_row = rng.gen_range(0..=end_row);
|
||||
// end_row += 1;
|
||||
fn verify_chunks(&mut self, rng: &mut impl Rng) {
|
||||
for _ in 0..5 {
|
||||
let mut end_row = rng.gen_range(0..=self.max_point().row());
|
||||
let start_row = rng.gen_range(0..=end_row);
|
||||
end_row += 1;
|
||||
|
||||
// let mut expected_text = self.text_chunks(start_row).collect::<String>();
|
||||
// if expected_text.ends_with('\n') {
|
||||
// expected_text.push('\n');
|
||||
// }
|
||||
// let mut expected_text = expected_text
|
||||
// .lines()
|
||||
// .take((end_row - start_row) as usize)
|
||||
// .collect::<Vec<_>>()
|
||||
// .join("\n");
|
||||
// if end_row <= self.max_point().row() {
|
||||
// expected_text.push('\n');
|
||||
// }
|
||||
let mut expected_text = self.text_chunks(start_row).collect::<String>();
|
||||
if expected_text.ends_with('\n') {
|
||||
expected_text.push('\n');
|
||||
}
|
||||
let mut expected_text = expected_text
|
||||
.lines()
|
||||
.take((end_row - start_row) as usize)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
if end_row <= self.max_point().row() {
|
||||
expected_text.push('\n');
|
||||
}
|
||||
|
||||
// let actual_text = self
|
||||
// .chunks(start_row..end_row, true, Highlights::default())
|
||||
// .map(|c| c.text)
|
||||
// .collect::<String>();
|
||||
// assert_eq!(
|
||||
// expected_text,
|
||||
// actual_text,
|
||||
// "chunks != highlighted_chunks for rows {:?}",
|
||||
// start_row..end_row
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
let actual_text = self
|
||||
.chunks(start_row..end_row, true, Highlights::default())
|
||||
.map(|c| c.text)
|
||||
.collect::<String>();
|
||||
assert_eq!(
|
||||
expected_text,
|
||||
actual_text,
|
||||
"chunks != highlighted_chunks for rows {:?}",
|
||||
start_row..end_row
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -88,195 +88,195 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) ->
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(any(test, feature = "test_support"))]
|
||||
// mod tests {
|
||||
// // use crate::editor_tests::init_test;
|
||||
// use crate::Point;
|
||||
// use gpui::TestAppContext;
|
||||
// use multi_buffer::{ExcerptRange, MultiBuffer};
|
||||
// use project::{FakeFs, Project};
|
||||
// use unindent::Unindent;
|
||||
// #[gpui::test]
|
||||
// async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
|
||||
// use git::diff::DiffHunkStatus;
|
||||
// init_test(cx, |_| {});
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::editor_tests::init_test;
|
||||
use crate::Point;
|
||||
use gpui::{Context, TestAppContext};
|
||||
use multi_buffer::{ExcerptRange, MultiBuffer};
|
||||
use project::{FakeFs, Project};
|
||||
use unindent::Unindent;
|
||||
#[gpui::test]
|
||||
async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
|
||||
use git::diff::DiffHunkStatus;
|
||||
init_test(cx, |_| {});
|
||||
|
||||
// let fs = FakeFs::new(cx.background());
|
||||
// let project = Project::test(fs, [], cx).await;
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
|
||||
// // buffer has two modified hunks with two rows each
|
||||
// let buffer_1 = project
|
||||
// .update(cx, |project, cx| {
|
||||
// project.create_buffer(
|
||||
// "
|
||||
// 1.zero
|
||||
// 1.ONE
|
||||
// 1.TWO
|
||||
// 1.three
|
||||
// 1.FOUR
|
||||
// 1.FIVE
|
||||
// 1.six
|
||||
// "
|
||||
// .unindent()
|
||||
// .as_str(),
|
||||
// None,
|
||||
// cx,
|
||||
// )
|
||||
// })
|
||||
// .unwrap();
|
||||
// buffer_1.update(cx, |buffer, cx| {
|
||||
// buffer.set_diff_base(
|
||||
// Some(
|
||||
// "
|
||||
// 1.zero
|
||||
// 1.one
|
||||
// 1.two
|
||||
// 1.three
|
||||
// 1.four
|
||||
// 1.five
|
||||
// 1.six
|
||||
// "
|
||||
// .unindent(),
|
||||
// ),
|
||||
// cx,
|
||||
// );
|
||||
// });
|
||||
// buffer has two modified hunks with two rows each
|
||||
let buffer_1 = project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_buffer(
|
||||
"
|
||||
1.zero
|
||||
1.ONE
|
||||
1.TWO
|
||||
1.three
|
||||
1.FOUR
|
||||
1.FIVE
|
||||
1.six
|
||||
"
|
||||
.unindent()
|
||||
.as_str(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
buffer_1.update(cx, |buffer, cx| {
|
||||
buffer.set_diff_base(
|
||||
Some(
|
||||
"
|
||||
1.zero
|
||||
1.one
|
||||
1.two
|
||||
1.three
|
||||
1.four
|
||||
1.five
|
||||
1.six
|
||||
"
|
||||
.unindent(),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// // buffer has a deletion hunk and an insertion hunk
|
||||
// let buffer_2 = project
|
||||
// .update(cx, |project, cx| {
|
||||
// project.create_buffer(
|
||||
// "
|
||||
// 2.zero
|
||||
// 2.one
|
||||
// 2.two
|
||||
// 2.three
|
||||
// 2.four
|
||||
// 2.five
|
||||
// 2.six
|
||||
// "
|
||||
// .unindent()
|
||||
// .as_str(),
|
||||
// None,
|
||||
// cx,
|
||||
// )
|
||||
// })
|
||||
// .unwrap();
|
||||
// buffer_2.update(cx, |buffer, cx| {
|
||||
// buffer.set_diff_base(
|
||||
// Some(
|
||||
// "
|
||||
// 2.zero
|
||||
// 2.one
|
||||
// 2.one-and-a-half
|
||||
// 2.two
|
||||
// 2.three
|
||||
// 2.four
|
||||
// 2.six
|
||||
// "
|
||||
// .unindent(),
|
||||
// ),
|
||||
// cx,
|
||||
// );
|
||||
// });
|
||||
// buffer has a deletion hunk and an insertion hunk
|
||||
let buffer_2 = project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_buffer(
|
||||
"
|
||||
2.zero
|
||||
2.one
|
||||
2.two
|
||||
2.three
|
||||
2.four
|
||||
2.five
|
||||
2.six
|
||||
"
|
||||
.unindent()
|
||||
.as_str(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
buffer_2.update(cx, |buffer, cx| {
|
||||
buffer.set_diff_base(
|
||||
Some(
|
||||
"
|
||||
2.zero
|
||||
2.one
|
||||
2.one-and-a-half
|
||||
2.two
|
||||
2.three
|
||||
2.four
|
||||
2.six
|
||||
"
|
||||
.unindent(),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// cx.foreground().run_until_parked();
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
// let multibuffer = cx.add_model(|cx| {
|
||||
// let mut multibuffer = MultiBuffer::new(0);
|
||||
// multibuffer.push_excerpts(
|
||||
// buffer_1.clone(),
|
||||
// [
|
||||
// // excerpt ends in the middle of a modified hunk
|
||||
// ExcerptRange {
|
||||
// context: Point::new(0, 0)..Point::new(1, 5),
|
||||
// primary: Default::default(),
|
||||
// },
|
||||
// // excerpt begins in the middle of a modified hunk
|
||||
// ExcerptRange {
|
||||
// context: Point::new(5, 0)..Point::new(6, 5),
|
||||
// primary: Default::default(),
|
||||
// },
|
||||
// ],
|
||||
// cx,
|
||||
// );
|
||||
// multibuffer.push_excerpts(
|
||||
// buffer_2.clone(),
|
||||
// [
|
||||
// // excerpt ends at a deletion
|
||||
// ExcerptRange {
|
||||
// context: Point::new(0, 0)..Point::new(1, 5),
|
||||
// primary: Default::default(),
|
||||
// },
|
||||
// // excerpt starts at a deletion
|
||||
// ExcerptRange {
|
||||
// context: Point::new(2, 0)..Point::new(2, 5),
|
||||
// primary: Default::default(),
|
||||
// },
|
||||
// // excerpt fully contains a deletion hunk
|
||||
// ExcerptRange {
|
||||
// context: Point::new(1, 0)..Point::new(2, 5),
|
||||
// primary: Default::default(),
|
||||
// },
|
||||
// // excerpt fully contains an insertion hunk
|
||||
// ExcerptRange {
|
||||
// context: Point::new(4, 0)..Point::new(6, 5),
|
||||
// primary: Default::default(),
|
||||
// },
|
||||
// ],
|
||||
// cx,
|
||||
// );
|
||||
// multibuffer
|
||||
// });
|
||||
let multibuffer = cx.build_model(|cx| {
|
||||
let mut multibuffer = MultiBuffer::new(0);
|
||||
multibuffer.push_excerpts(
|
||||
buffer_1.clone(),
|
||||
[
|
||||
// excerpt ends in the middle of a modified hunk
|
||||
ExcerptRange {
|
||||
context: Point::new(0, 0)..Point::new(1, 5),
|
||||
primary: Default::default(),
|
||||
},
|
||||
// excerpt begins in the middle of a modified hunk
|
||||
ExcerptRange {
|
||||
context: Point::new(5, 0)..Point::new(6, 5),
|
||||
primary: Default::default(),
|
||||
},
|
||||
],
|
||||
cx,
|
||||
);
|
||||
multibuffer.push_excerpts(
|
||||
buffer_2.clone(),
|
||||
[
|
||||
// excerpt ends at a deletion
|
||||
ExcerptRange {
|
||||
context: Point::new(0, 0)..Point::new(1, 5),
|
||||
primary: Default::default(),
|
||||
},
|
||||
// excerpt starts at a deletion
|
||||
ExcerptRange {
|
||||
context: Point::new(2, 0)..Point::new(2, 5),
|
||||
primary: Default::default(),
|
||||
},
|
||||
// excerpt fully contains a deletion hunk
|
||||
ExcerptRange {
|
||||
context: Point::new(1, 0)..Point::new(2, 5),
|
||||
primary: Default::default(),
|
||||
},
|
||||
// excerpt fully contains an insertion hunk
|
||||
ExcerptRange {
|
||||
context: Point::new(4, 0)..Point::new(6, 5),
|
||||
primary: Default::default(),
|
||||
},
|
||||
],
|
||||
cx,
|
||||
);
|
||||
multibuffer
|
||||
});
|
||||
|
||||
// let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx));
|
||||
let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx));
|
||||
|
||||
// assert_eq!(
|
||||
// snapshot.text(),
|
||||
// "
|
||||
// 1.zero
|
||||
// 1.ONE
|
||||
// 1.FIVE
|
||||
// 1.six
|
||||
// 2.zero
|
||||
// 2.one
|
||||
// 2.two
|
||||
// 2.one
|
||||
// 2.two
|
||||
// 2.four
|
||||
// 2.five
|
||||
// 2.six"
|
||||
// .unindent()
|
||||
// );
|
||||
assert_eq!(
|
||||
snapshot.text(),
|
||||
"
|
||||
1.zero
|
||||
1.ONE
|
||||
1.FIVE
|
||||
1.six
|
||||
2.zero
|
||||
2.one
|
||||
2.two
|
||||
2.one
|
||||
2.two
|
||||
2.four
|
||||
2.five
|
||||
2.six"
|
||||
.unindent()
|
||||
);
|
||||
|
||||
// let expected = [
|
||||
// (DiffHunkStatus::Modified, 1..2),
|
||||
// (DiffHunkStatus::Modified, 2..3),
|
||||
// //TODO: Define better when and where removed hunks show up at range extremities
|
||||
// (DiffHunkStatus::Removed, 6..6),
|
||||
// (DiffHunkStatus::Removed, 8..8),
|
||||
// (DiffHunkStatus::Added, 10..11),
|
||||
// ];
|
||||
let expected = [
|
||||
(DiffHunkStatus::Modified, 1..2),
|
||||
(DiffHunkStatus::Modified, 2..3),
|
||||
//TODO: Define better when and where removed hunks show up at range extremities
|
||||
(DiffHunkStatus::Removed, 6..6),
|
||||
(DiffHunkStatus::Removed, 8..8),
|
||||
(DiffHunkStatus::Added, 10..11),
|
||||
];
|
||||
|
||||
// assert_eq!(
|
||||
// snapshot
|
||||
// .git_diff_hunks_in_range(0..12)
|
||||
// .map(|hunk| (hunk.status(), hunk.buffer_range))
|
||||
// .collect::<Vec<_>>(),
|
||||
// &expected,
|
||||
// );
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.git_diff_hunks_in_range(0..12)
|
||||
.map(|hunk| (hunk.status(), hunk.buffer_range))
|
||||
.collect::<Vec<_>>(),
|
||||
&expected,
|
||||
);
|
||||
|
||||
// assert_eq!(
|
||||
// snapshot
|
||||
// .git_diff_hunks_in_range_rev(0..12)
|
||||
// .map(|hunk| (hunk.status(), hunk.buffer_range))
|
||||
// .collect::<Vec<_>>(),
|
||||
// expected
|
||||
// .iter()
|
||||
// .rev()
|
||||
// .cloned()
|
||||
// .collect::<Vec<_>>()
|
||||
// .as_slice(),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.git_diff_hunks_in_range_rev(0..12)
|
||||
.map(|hunk| (hunk.status(), hunk.buffer_range))
|
||||
.collect::<Vec<_>>(),
|
||||
expected
|
||||
.iter()
|
||||
.rev()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.as_slice(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{Editor, RangeToAnchorExt};
|
||||
enum MatchingBracketHighlight {}
|
||||
|
||||
pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
|
||||
// editor.clear_background_highlights::<MatchingBracketHighlight>(cx);
|
||||
editor.clear_background_highlights::<MatchingBracketHighlight>(cx);
|
||||
|
||||
let newest_selection = editor.selections.newest::<usize>(cx);
|
||||
// Don't highlight brackets if the selection isn't empty
|
||||
@@ -30,109 +30,109 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use super::*;
|
||||
// use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
|
||||
// use indoc::indoc;
|
||||
// use language::{BracketPair, BracketPairConfig, Language, LanguageConfig};
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
|
||||
use indoc::indoc;
|
||||
use language::{BracketPair, BracketPairConfig, Language, LanguageConfig};
|
||||
|
||||
// #[gpui::test]
|
||||
// async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) {
|
||||
// init_test(cx, |_| {});
|
||||
#[gpui::test]
|
||||
async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
// let mut cx = EditorLspTestContext::new(
|
||||
// Language::new(
|
||||
// LanguageConfig {
|
||||
// name: "Rust".into(),
|
||||
// path_suffixes: vec!["rs".to_string()],
|
||||
// brackets: BracketPairConfig {
|
||||
// pairs: vec![
|
||||
// BracketPair {
|
||||
// start: "{".to_string(),
|
||||
// end: "}".to_string(),
|
||||
// close: false,
|
||||
// newline: true,
|
||||
// },
|
||||
// BracketPair {
|
||||
// start: "(".to_string(),
|
||||
// end: ")".to_string(),
|
||||
// close: false,
|
||||
// newline: true,
|
||||
// },
|
||||
// ],
|
||||
// ..Default::default()
|
||||
// },
|
||||
// ..Default::default()
|
||||
// },
|
||||
// Some(tree_sitter_rust::language()),
|
||||
// )
|
||||
// .with_brackets_query(indoc! {r#"
|
||||
// ("{" @open "}" @close)
|
||||
// ("(" @open ")" @close)
|
||||
// "#})
|
||||
// .unwrap(),
|
||||
// Default::default(),
|
||||
// cx,
|
||||
// )
|
||||
// .await;
|
||||
let mut cx = EditorLspTestContext::new(
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
brackets: BracketPairConfig {
|
||||
pairs: vec![
|
||||
BracketPair {
|
||||
start: "{".to_string(),
|
||||
end: "}".to_string(),
|
||||
close: false,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "(".to_string(),
|
||||
end: ")".to_string(),
|
||||
close: false,
|
||||
newline: true,
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)
|
||||
.with_brackets_query(indoc! {r#"
|
||||
("{" @open "}" @close)
|
||||
("(" @open ")" @close)
|
||||
"#})
|
||||
.unwrap(),
|
||||
Default::default(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
// // positioning cursor inside bracket highlights both
|
||||
// cx.set_state(indoc! {r#"
|
||||
// pub fn test("Test ˇargument") {
|
||||
// another_test(1, 2, 3);
|
||||
// }
|
||||
// "#});
|
||||
// cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
// pub fn test«(»"Test argument"«)» {
|
||||
// another_test(1, 2, 3);
|
||||
// }
|
||||
// "#});
|
||||
// positioning cursor inside bracket highlights both
|
||||
cx.set_state(indoc! {r#"
|
||||
pub fn test("Test ˇargument") {
|
||||
another_test(1, 2, 3);
|
||||
}
|
||||
"#});
|
||||
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
pub fn test«(»"Test argument"«)» {
|
||||
another_test(1, 2, 3);
|
||||
}
|
||||
"#});
|
||||
|
||||
// cx.set_state(indoc! {r#"
|
||||
// pub fn test("Test argument") {
|
||||
// another_test(1, ˇ2, 3);
|
||||
// }
|
||||
// "#});
|
||||
// cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
// pub fn test("Test argument") {
|
||||
// another_test«(»1, 2, 3«)»;
|
||||
// }
|
||||
// "#});
|
||||
cx.set_state(indoc! {r#"
|
||||
pub fn test("Test argument") {
|
||||
another_test(1, ˇ2, 3);
|
||||
}
|
||||
"#});
|
||||
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
pub fn test("Test argument") {
|
||||
another_test«(»1, 2, 3«)»;
|
||||
}
|
||||
"#});
|
||||
|
||||
// cx.set_state(indoc! {r#"
|
||||
// pub fn test("Test argument") {
|
||||
// anotherˇ_test(1, 2, 3);
|
||||
// }
|
||||
// "#});
|
||||
// cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
// pub fn test("Test argument") «{»
|
||||
// another_test(1, 2, 3);
|
||||
// «}»
|
||||
// "#});
|
||||
cx.set_state(indoc! {r#"
|
||||
pub fn test("Test argument") {
|
||||
anotherˇ_test(1, 2, 3);
|
||||
}
|
||||
"#});
|
||||
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
pub fn test("Test argument") «{»
|
||||
another_test(1, 2, 3);
|
||||
«}»
|
||||
"#});
|
||||
|
||||
// // positioning outside of brackets removes highlight
|
||||
// cx.set_state(indoc! {r#"
|
||||
// pub fˇn test("Test argument") {
|
||||
// another_test(1, 2, 3);
|
||||
// }
|
||||
// "#});
|
||||
// cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
// pub fn test("Test argument") {
|
||||
// another_test(1, 2, 3);
|
||||
// }
|
||||
// "#});
|
||||
// positioning outside of brackets removes highlight
|
||||
cx.set_state(indoc! {r#"
|
||||
pub fˇn test("Test argument") {
|
||||
another_test(1, 2, 3);
|
||||
}
|
||||
"#});
|
||||
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
pub fn test("Test argument") {
|
||||
another_test(1, 2, 3);
|
||||
}
|
||||
"#});
|
||||
|
||||
// // non empty selection dismisses highlight
|
||||
// cx.set_state(indoc! {r#"
|
||||
// pub fn test("Te«st argˇ»ument") {
|
||||
// another_test(1, 2, 3);
|
||||
// }
|
||||
// "#});
|
||||
// cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
// pub fn test("Test argument") {
|
||||
// another_test(1, 2, 3);
|
||||
// }
|
||||
// "#});
|
||||
// }
|
||||
// }
|
||||
// non empty selection dismisses highlight
|
||||
cx.set_state(indoc! {r#"
|
||||
pub fn test("Te«st argˇ»ument") {
|
||||
another_test(1, 2, 3);
|
||||
}
|
||||
"#});
|
||||
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
pub fn test("Test argument") {
|
||||
another_test(1, 2, 3);
|
||||
}
|
||||
"#});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ use lsp::DiagnosticSeverity;
|
||||
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
|
||||
use settings::Settings;
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use ui::Tooltip;
|
||||
use ui::{StyledExt, Tooltip};
|
||||
use util::TryFutureExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -476,8 +476,10 @@ impl InfoPopover {
|
||||
) -> AnyElement {
|
||||
div()
|
||||
.id("info_popover")
|
||||
.elevation_2(cx)
|
||||
.text_ui()
|
||||
.p_2()
|
||||
.overflow_y_scroll()
|
||||
.bg(gpui::red())
|
||||
.max_w(max_size.width)
|
||||
.max_h(max_size.height)
|
||||
// Prevent a mouse move on the popover from being propagated to the editor,
|
||||
|
||||
@@ -2432,13 +2432,13 @@ pub mod tests {
|
||||
let language = Arc::new(language);
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
"main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
|
||||
"other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
"/a",
|
||||
json!({
|
||||
"main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
|
||||
"other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs, ["/a".as_ref()], cx).await;
|
||||
project.update(cx, |project, _| {
|
||||
project.languages().add(Arc::clone(&language))
|
||||
@@ -2598,24 +2598,22 @@ pub mod tests {
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let expected_hints = vec![
|
||||
"main hint #0".to_string(),
|
||||
"main hint #1".to_string(),
|
||||
"main hint #2".to_string(),
|
||||
"main hint #3".to_string(),
|
||||
// todo!() there used to be no these hints, but new gpui2 presumably scrolls a bit farther
|
||||
// (or renders less?) note that tests below pass
|
||||
"main hint #4".to_string(),
|
||||
"main hint #5".to_string(),
|
||||
];
|
||||
assert_eq!(
|
||||
expected_hints,
|
||||
cached_hint_labels(editor),
|
||||
"When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
|
||||
);
|
||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||
assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison");
|
||||
});
|
||||
let expected_hints = vec![
|
||||
"main hint #0".to_string(),
|
||||
"main hint #1".to_string(),
|
||||
"main hint #2".to_string(),
|
||||
"main hint #3".to_string(),
|
||||
"main hint #4".to_string(),
|
||||
"main hint #5".to_string(),
|
||||
];
|
||||
assert_eq!(
|
||||
expected_hints,
|
||||
cached_hint_labels(editor),
|
||||
"When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
|
||||
);
|
||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||
assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison");
|
||||
});
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::Next), cx, |s| {
|
||||
@@ -2630,23 +2628,23 @@ pub mod tests {
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
editor.update(cx, |editor, cx| {
|
||||
let expected_hints = vec![
|
||||
"main hint #0".to_string(),
|
||||
"main hint #1".to_string(),
|
||||
"main hint #2".to_string(),
|
||||
"main hint #3".to_string(),
|
||||
"main hint #4".to_string(),
|
||||
"main hint #5".to_string(),
|
||||
"other hint #0".to_string(),
|
||||
"other hint #1".to_string(),
|
||||
"other hint #2".to_string(),
|
||||
];
|
||||
assert_eq!(expected_hints, cached_hint_labels(editor),
|
||||
"With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
|
||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||
assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(),
|
||||
"Due to every excerpt having one hint, we update cache per new excerpt scrolled");
|
||||
});
|
||||
let expected_hints = vec![
|
||||
"main hint #0".to_string(),
|
||||
"main hint #1".to_string(),
|
||||
"main hint #2".to_string(),
|
||||
"main hint #3".to_string(),
|
||||
"main hint #4".to_string(),
|
||||
"main hint #5".to_string(),
|
||||
"other hint #0".to_string(),
|
||||
"other hint #1".to_string(),
|
||||
"other hint #2".to_string(),
|
||||
];
|
||||
assert_eq!(expected_hints, cached_hint_labels(editor),
|
||||
"With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
|
||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||
assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(),
|
||||
"Due to every excerpt having one hint, we update cache per new excerpt scrolled");
|
||||
});
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::Next), cx, |s| {
|
||||
@@ -2658,26 +2656,26 @@ pub mod tests {
|
||||
));
|
||||
cx.executor().run_until_parked();
|
||||
let last_scroll_update_version = editor.update(cx, |editor, cx| {
|
||||
let expected_hints = vec![
|
||||
"main hint #0".to_string(),
|
||||
"main hint #1".to_string(),
|
||||
"main hint #2".to_string(),
|
||||
"main hint #3".to_string(),
|
||||
"main hint #4".to_string(),
|
||||
"main hint #5".to_string(),
|
||||
"other hint #0".to_string(),
|
||||
"other hint #1".to_string(),
|
||||
"other hint #2".to_string(),
|
||||
"other hint #3".to_string(),
|
||||
"other hint #4".to_string(),
|
||||
"other hint #5".to_string(),
|
||||
];
|
||||
assert_eq!(expected_hints, cached_hint_labels(editor),
|
||||
"After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
|
||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||
assert_eq!(editor.inlay_hint_cache().version, expected_hints.len());
|
||||
expected_hints.len()
|
||||
}).unwrap();
|
||||
let expected_hints = vec![
|
||||
"main hint #0".to_string(),
|
||||
"main hint #1".to_string(),
|
||||
"main hint #2".to_string(),
|
||||
"main hint #3".to_string(),
|
||||
"main hint #4".to_string(),
|
||||
"main hint #5".to_string(),
|
||||
"other hint #0".to_string(),
|
||||
"other hint #1".to_string(),
|
||||
"other hint #2".to_string(),
|
||||
"other hint #3".to_string(),
|
||||
"other hint #4".to_string(),
|
||||
"other hint #5".to_string(),
|
||||
];
|
||||
assert_eq!(expected_hints, cached_hint_labels(editor),
|
||||
"After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
|
||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||
assert_eq!(editor.inlay_hint_cache().version, expected_hints.len());
|
||||
expected_hints.len()
|
||||
}).unwrap();
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::Next), cx, |s| {
|
||||
@@ -2686,30 +2684,31 @@ pub mod tests {
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
editor.update(cx, |editor, cx| {
|
||||
let expected_hints = vec![
|
||||
"main hint #0".to_string(),
|
||||
"main hint #1".to_string(),
|
||||
"main hint #2".to_string(),
|
||||
"main hint #3".to_string(),
|
||||
"main hint #4".to_string(),
|
||||
"main hint #5".to_string(),
|
||||
"other hint #0".to_string(),
|
||||
"other hint #1".to_string(),
|
||||
"other hint #2".to_string(),
|
||||
"other hint #3".to_string(),
|
||||
"other hint #4".to_string(),
|
||||
"other hint #5".to_string(),
|
||||
];
|
||||
assert_eq!(expected_hints, cached_hint_labels(editor),
|
||||
"After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
|
||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||
assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer");
|
||||
});
|
||||
let expected_hints = vec![
|
||||
"main hint #0".to_string(),
|
||||
"main hint #1".to_string(),
|
||||
"main hint #2".to_string(),
|
||||
"main hint #3".to_string(),
|
||||
"main hint #4".to_string(),
|
||||
"main hint #5".to_string(),
|
||||
"other hint #0".to_string(),
|
||||
"other hint #1".to_string(),
|
||||
"other hint #2".to_string(),
|
||||
"other hint #3".to_string(),
|
||||
"other hint #4".to_string(),
|
||||
"other hint #5".to_string(),
|
||||
];
|
||||
assert_eq!(expected_hints, cached_hint_labels(editor),
|
||||
"After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
|
||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||
assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer");
|
||||
});
|
||||
|
||||
editor_edited.store(true, Ordering::Release);
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(56, 0)..Point::new(56, 0)])
|
||||
// TODO if this gets set to hint boundary (e.g. 56) we sometimes get an extra cache version bump, why?
|
||||
s.select_ranges([Point::new(57, 0)..Point::new(57, 0)])
|
||||
});
|
||||
editor.handle_input("++++more text++++", cx);
|
||||
});
|
||||
@@ -2729,15 +2728,15 @@ pub mod tests {
|
||||
expected_hints,
|
||||
cached_hint_labels(editor),
|
||||
"After multibuffer edit, editor gets scolled back to the last selection; \
|
||||
all hints should be invalidated and requeried for all of its visible excerpts"
|
||||
all hints should be invalidated and requeried for all of its visible excerpts"
|
||||
);
|
||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||
|
||||
let current_cache_version = editor.inlay_hint_cache().version;
|
||||
let minimum_expected_version = last_scroll_update_version + expected_hints.len();
|
||||
assert!(
|
||||
current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1,
|
||||
"Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update"
|
||||
assert_eq!(
|
||||
current_cache_version,
|
||||
last_scroll_update_version + expected_hints.len(),
|
||||
"We should have updated cache N times == N of new hints arrived (separately from each excerpt)"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,13 +4,14 @@ use crate::{
|
||||
EditorEvent, EditorSettings, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot,
|
||||
NavigationData, ToPoint as _,
|
||||
};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::HashSet;
|
||||
use futures::future::try_join_all;
|
||||
use gpui::{
|
||||
div, point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, EventEmitter,
|
||||
FocusHandle, Model, ParentElement, Pixels, SharedString, Styled, Subscription, Task, View,
|
||||
ViewContext, VisualContext, WeakView, WindowContext,
|
||||
div, point, AnyElement, AppContext, AsyncAppContext, AsyncWindowContext, Context, Div, Entity,
|
||||
EntityId, EventEmitter, FocusHandle, IntoElement, Model, ParentElement, Pixels, Render,
|
||||
SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
|
||||
WindowContext,
|
||||
};
|
||||
use language::{
|
||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt,
|
||||
@@ -20,6 +21,7 @@ use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPat
|
||||
use rpc::proto::{self, update_view, PeerId};
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
use std::fmt::Write;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::{self, Ordering},
|
||||
@@ -30,9 +32,12 @@ use std::{
|
||||
};
|
||||
use text::Selection;
|
||||
use theme::{ActiveTheme, Theme};
|
||||
use ui::{Color, Label};
|
||||
use util::{paths::PathExt, ResultExt, TryFutureExt};
|
||||
use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle};
|
||||
use ui::{h_stack, prelude::*, Label};
|
||||
use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, FollowEvent, FollowableItemHandle},
|
||||
StatusItemView,
|
||||
};
|
||||
use workspace::{
|
||||
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
|
||||
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
|
||||
@@ -41,27 +46,7 @@ use workspace::{
|
||||
|
||||
pub const MAX_TAB_TITLE_LEN: usize = 24;
|
||||
|
||||
impl FollowableEvents for EditorEvent {
|
||||
fn to_follow_event(&self) -> Option<workspace::item::FollowEvent> {
|
||||
match self {
|
||||
EditorEvent::Edited => Some(FollowEvent::Unfollow),
|
||||
EditorEvent::SelectionsChanged { local }
|
||||
| EditorEvent::ScrollPositionChanged { local, .. } => {
|
||||
if *local {
|
||||
Some(FollowEvent::Unfollow)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ItemEvent> for Editor {}
|
||||
|
||||
impl FollowableItem for Editor {
|
||||
type FollowableEvent = EditorEvent;
|
||||
fn remote_id(&self) -> Option<ViewId> {
|
||||
self.remote_id
|
||||
}
|
||||
@@ -71,110 +56,108 @@ impl FollowableItem for Editor {
|
||||
workspace: View<Workspace>,
|
||||
remote_id: ViewId,
|
||||
state: &mut Option<proto::view::Variant>,
|
||||
cx: &mut AppContext,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<Task<Result<View<Self>>>> {
|
||||
todo!()
|
||||
let project = workspace.read(cx).project().to_owned();
|
||||
let Some(proto::view::Variant::Editor(_)) = state else {
|
||||
return None;
|
||||
};
|
||||
let Some(proto::view::Variant::Editor(state)) = state.take() else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let client = project.read(cx).client();
|
||||
let replica_id = project.read(cx).replica_id();
|
||||
let buffer_ids = state
|
||||
.excerpts
|
||||
.iter()
|
||||
.map(|excerpt| excerpt.buffer_id)
|
||||
.collect::<HashSet<_>>();
|
||||
let buffers = project.update(cx, |project, cx| {
|
||||
buffer_ids
|
||||
.iter()
|
||||
.map(|id| project.open_buffer_by_id(*id, cx))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
let pane = pane.downgrade();
|
||||
Some(cx.spawn(|mut cx| async move {
|
||||
let mut buffers = futures::future::try_join_all(buffers).await?;
|
||||
let editor = pane.update(&mut cx, |pane, cx| {
|
||||
let mut editors = pane.items_of_type::<Self>();
|
||||
editors.find(|editor| {
|
||||
let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
|
||||
let singleton_buffer_matches = state.singleton
|
||||
&& buffers.first()
|
||||
== editor.read(cx).buffer.read(cx).as_singleton().as_ref();
|
||||
ids_match || singleton_buffer_matches
|
||||
})
|
||||
})?;
|
||||
|
||||
let editor = if let Some(editor) = editor {
|
||||
editor
|
||||
} else {
|
||||
pane.update(&mut cx, |_, cx| {
|
||||
let multibuffer = cx.build_model(|cx| {
|
||||
let mut multibuffer;
|
||||
if state.singleton && buffers.len() == 1 {
|
||||
multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
|
||||
} else {
|
||||
multibuffer = MultiBuffer::new(replica_id);
|
||||
let mut excerpts = state.excerpts.into_iter().peekable();
|
||||
while let Some(excerpt) = excerpts.peek() {
|
||||
let buffer_id = excerpt.buffer_id;
|
||||
let buffer_excerpts = iter::from_fn(|| {
|
||||
let excerpt = excerpts.peek()?;
|
||||
(excerpt.buffer_id == buffer_id)
|
||||
.then(|| excerpts.next().unwrap())
|
||||
});
|
||||
let buffer =
|
||||
buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
|
||||
if let Some(buffer) = buffer {
|
||||
multibuffer.push_excerpts(
|
||||
buffer.clone(),
|
||||
buffer_excerpts.filter_map(deserialize_excerpt_range),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(title) = &state.title {
|
||||
multibuffer = multibuffer.with_title(title.clone())
|
||||
}
|
||||
|
||||
multibuffer
|
||||
});
|
||||
|
||||
cx.build_view(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_multibuffer(multibuffer, Some(project.clone()), cx);
|
||||
editor.remote_id = Some(remote_id);
|
||||
editor
|
||||
})
|
||||
})?
|
||||
};
|
||||
|
||||
update_editor_from_message(
|
||||
editor.downgrade(),
|
||||
project,
|
||||
proto::update_view::Editor {
|
||||
selections: state.selections,
|
||||
pending_selection: state.pending_selection,
|
||||
scroll_top_anchor: state.scroll_top_anchor,
|
||||
scroll_x: state.scroll_x,
|
||||
scroll_y: state.scroll_y,
|
||||
..Default::default()
|
||||
},
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(editor)
|
||||
}))
|
||||
}
|
||||
// let project = workspace.read(cx).project().to_owned();
|
||||
// let Some(proto::view::Variant::Editor(_)) = state else {
|
||||
// return None;
|
||||
// };
|
||||
// let Some(proto::view::Variant::Editor(state)) = state.take() else {
|
||||
// unreachable!()
|
||||
// };
|
||||
|
||||
// let client = project.read(cx).client();
|
||||
// let replica_id = project.read(cx).replica_id();
|
||||
// let buffer_ids = state
|
||||
// .excerpts
|
||||
// .iter()
|
||||
// .map(|excerpt| excerpt.buffer_id)
|
||||
// .collect::<HashSet<_>>();
|
||||
// let buffers = project.update(cx, |project, cx| {
|
||||
// buffer_ids
|
||||
// .iter()
|
||||
// .map(|id| project.open_buffer_by_id(*id, cx))
|
||||
// .collect::<Vec<_>>()
|
||||
// });
|
||||
|
||||
// let pane = pane.downgrade();
|
||||
// Some(cx.spawn(|mut cx| async move {
|
||||
// let mut buffers = futures::future::try_join_all(buffers).await?;
|
||||
// let editor = pane.read_with(&cx, |pane, cx| {
|
||||
// let mut editors = pane.items_of_type::<Self>();
|
||||
// editors.find(|editor| {
|
||||
// let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
|
||||
// let singleton_buffer_matches = state.singleton
|
||||
// && buffers.first()
|
||||
// == editor.read(cx).buffer.read(cx).as_singleton().as_ref();
|
||||
// ids_match || singleton_buffer_matches
|
||||
// })
|
||||
// })?;
|
||||
|
||||
// let editor = if let Some(editor) = editor {
|
||||
// editor
|
||||
// } else {
|
||||
// pane.update(&mut cx, |_, cx| {
|
||||
// let multibuffer = cx.add_model(|cx| {
|
||||
// let mut multibuffer;
|
||||
// if state.singleton && buffers.len() == 1 {
|
||||
// multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
|
||||
// } else {
|
||||
// multibuffer = MultiBuffer::new(replica_id);
|
||||
// let mut excerpts = state.excerpts.into_iter().peekable();
|
||||
// while let Some(excerpt) = excerpts.peek() {
|
||||
// let buffer_id = excerpt.buffer_id;
|
||||
// let buffer_excerpts = iter::from_fn(|| {
|
||||
// let excerpt = excerpts.peek()?;
|
||||
// (excerpt.buffer_id == buffer_id)
|
||||
// .then(|| excerpts.next().unwrap())
|
||||
// });
|
||||
// let buffer =
|
||||
// buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
|
||||
// if let Some(buffer) = buffer {
|
||||
// multibuffer.push_excerpts(
|
||||
// buffer.clone(),
|
||||
// buffer_excerpts.filter_map(deserialize_excerpt_range),
|
||||
// cx,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
|
||||
// if let Some(title) = &state.title {
|
||||
// multibuffer = multibuffer.with_title(title.clone())
|
||||
// }
|
||||
|
||||
// multibuffer
|
||||
// });
|
||||
|
||||
// cx.add_view(|cx| {
|
||||
// let mut editor =
|
||||
// Editor::for_multibuffer(multibuffer, Some(project.clone()), cx);
|
||||
// editor.remote_id = Some(remote_id);
|
||||
// editor
|
||||
// })
|
||||
// })?
|
||||
// };
|
||||
|
||||
// update_editor_from_message(
|
||||
// editor.downgrade(),
|
||||
// project,
|
||||
// proto::update_view::Editor {
|
||||
// selections: state.selections,
|
||||
// pending_selection: state.pending_selection,
|
||||
// scroll_top_anchor: state.scroll_top_anchor,
|
||||
// scroll_x: state.scroll_x,
|
||||
// scroll_y: state.scroll_y,
|
||||
// ..Default::default()
|
||||
// },
|
||||
// &mut cx,
|
||||
// )
|
||||
// .await?;
|
||||
|
||||
// Ok(editor)
|
||||
// }))
|
||||
// }
|
||||
|
||||
fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
|
||||
self.leader_peer_id = leader_peer_id;
|
||||
@@ -195,7 +178,7 @@ impl FollowableItem for Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
|
||||
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let scroll_anchor = self.scroll_manager.anchor();
|
||||
let excerpts = buffer
|
||||
@@ -238,11 +221,26 @@ impl FollowableItem for Editor {
|
||||
}))
|
||||
}
|
||||
|
||||
fn to_follow_event(event: &EditorEvent) -> Option<workspace::item::FollowEvent> {
|
||||
match event {
|
||||
EditorEvent::Edited => Some(FollowEvent::Unfollow),
|
||||
EditorEvent::SelectionsChanged { local }
|
||||
| EditorEvent::ScrollPositionChanged { local, .. } => {
|
||||
if *local {
|
||||
Some(FollowEvent::Unfollow)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_event_to_update_proto(
|
||||
&self,
|
||||
event: &Self::FollowableEvent,
|
||||
event: &EditorEvent,
|
||||
update: &mut Option<proto::update_view::Variant>,
|
||||
cx: &AppContext,
|
||||
cx: &WindowContext,
|
||||
) -> bool {
|
||||
let update =
|
||||
update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default()));
|
||||
@@ -315,7 +313,7 @@ impl FollowableItem for Editor {
|
||||
})
|
||||
}
|
||||
|
||||
fn is_project_item(&self, _cx: &AppContext) -> bool {
|
||||
fn is_project_item(&self, _cx: &WindowContext) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -324,132 +322,129 @@ async fn update_editor_from_message(
|
||||
this: WeakView<Editor>,
|
||||
project: Model<Project>,
|
||||
message: proto::update_view::Editor,
|
||||
cx: &mut AsyncAppContext,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
todo!()
|
||||
// Open all of the buffers of which excerpts were added to the editor.
|
||||
let inserted_excerpt_buffer_ids = message
|
||||
.inserted_excerpts
|
||||
.iter()
|
||||
.filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
|
||||
.collect::<HashSet<_>>();
|
||||
let inserted_excerpt_buffers = project.update(cx, |project, cx| {
|
||||
inserted_excerpt_buffer_ids
|
||||
.into_iter()
|
||||
.map(|id| project.open_buffer_by_id(id, cx))
|
||||
.collect::<Vec<_>>()
|
||||
})?;
|
||||
let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?;
|
||||
|
||||
// Update the editor's excerpts.
|
||||
this.update(cx, |editor, cx| {
|
||||
editor.buffer.update(cx, |multibuffer, cx| {
|
||||
let mut removed_excerpt_ids = message
|
||||
.deleted_excerpts
|
||||
.into_iter()
|
||||
.map(ExcerptId::from_proto)
|
||||
.collect::<Vec<_>>();
|
||||
removed_excerpt_ids.sort_by({
|
||||
let multibuffer = multibuffer.read(cx);
|
||||
move |a, b| a.cmp(&b, &multibuffer)
|
||||
});
|
||||
|
||||
let mut insertions = message.inserted_excerpts.into_iter().peekable();
|
||||
while let Some(insertion) = insertions.next() {
|
||||
let Some(excerpt) = insertion.excerpt else {
|
||||
continue;
|
||||
};
|
||||
let Some(previous_excerpt_id) = insertion.previous_excerpt_id else {
|
||||
continue;
|
||||
};
|
||||
let buffer_id = excerpt.buffer_id;
|
||||
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let adjacent_excerpts = iter::from_fn(|| {
|
||||
let insertion = insertions.peek()?;
|
||||
if insertion.previous_excerpt_id.is_none()
|
||||
&& insertion.excerpt.as_ref()?.buffer_id == buffer_id
|
||||
{
|
||||
insertions.next()?.excerpt
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
multibuffer.insert_excerpts_with_ids_after(
|
||||
ExcerptId::from_proto(previous_excerpt_id),
|
||||
buffer,
|
||||
[excerpt]
|
||||
.into_iter()
|
||||
.chain(adjacent_excerpts)
|
||||
.filter_map(|excerpt| {
|
||||
Some((
|
||||
ExcerptId::from_proto(excerpt.id),
|
||||
deserialize_excerpt_range(excerpt)?,
|
||||
))
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
multibuffer.remove_excerpts(removed_excerpt_ids, cx);
|
||||
});
|
||||
})?;
|
||||
|
||||
// Deserialize the editor state.
|
||||
let (selections, pending_selection, scroll_top_anchor) = this.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer.read(cx).read(cx);
|
||||
let selections = message
|
||||
.selections
|
||||
.into_iter()
|
||||
.filter_map(|selection| deserialize_selection(&buffer, selection))
|
||||
.collect::<Vec<_>>();
|
||||
let pending_selection = message
|
||||
.pending_selection
|
||||
.and_then(|selection| deserialize_selection(&buffer, selection));
|
||||
let scroll_top_anchor = message
|
||||
.scroll_top_anchor
|
||||
.and_then(|anchor| deserialize_anchor(&buffer, anchor));
|
||||
anyhow::Ok((selections, pending_selection, scroll_top_anchor))
|
||||
})??;
|
||||
|
||||
// Wait until the buffer has received all of the operations referenced by
|
||||
// the editor's new state.
|
||||
this.update(cx, |editor, cx| {
|
||||
editor.buffer.update(cx, |buffer, cx| {
|
||||
buffer.wait_for_anchors(
|
||||
selections
|
||||
.iter()
|
||||
.chain(pending_selection.as_ref())
|
||||
.flat_map(|selection| [selection.start, selection.end])
|
||||
.chain(scroll_top_anchor),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
.await?;
|
||||
|
||||
// Update the editor's state.
|
||||
this.update(cx, |editor, cx| {
|
||||
if !selections.is_empty() || pending_selection.is_some() {
|
||||
editor.set_selections_from_remote(selections, pending_selection, cx);
|
||||
editor.request_autoscroll_remotely(Autoscroll::newest(), cx);
|
||||
} else if let Some(scroll_top_anchor) = scroll_top_anchor {
|
||||
editor.set_scroll_anchor_remote(
|
||||
ScrollAnchor {
|
||||
anchor: scroll_top_anchor,
|
||||
offset: point(message.scroll_x, message.scroll_y),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
// Previous implementation of the above
|
||||
// // Open all of the buffers of which excerpts were added to the editor.
|
||||
// let inserted_excerpt_buffer_ids = message
|
||||
// .inserted_excerpts
|
||||
// .iter()
|
||||
// .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
|
||||
// .collect::<HashSet<_>>();
|
||||
// let inserted_excerpt_buffers = project.update(cx, |project, cx| {
|
||||
// inserted_excerpt_buffer_ids
|
||||
// .into_iter()
|
||||
// .map(|id| project.open_buffer_by_id(id, cx))
|
||||
// .collect::<Vec<_>>()
|
||||
// })?;
|
||||
// let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?;
|
||||
|
||||
// // Update the editor's excerpts.
|
||||
// this.update(cx, |editor, cx| {
|
||||
// editor.buffer.update(cx, |multibuffer, cx| {
|
||||
// let mut removed_excerpt_ids = message
|
||||
// .deleted_excerpts
|
||||
// .into_iter()
|
||||
// .map(ExcerptId::from_proto)
|
||||
// .collect::<Vec<_>>();
|
||||
// removed_excerpt_ids.sort_by({
|
||||
// let multibuffer = multibuffer.read(cx);
|
||||
// move |a, b| a.cmp(&b, &multibuffer)
|
||||
// });
|
||||
|
||||
// let mut insertions = message.inserted_excerpts.into_iter().peekable();
|
||||
// while let Some(insertion) = insertions.next() {
|
||||
// let Some(excerpt) = insertion.excerpt else {
|
||||
// continue;
|
||||
// };
|
||||
// let Some(previous_excerpt_id) = insertion.previous_excerpt_id else {
|
||||
// continue;
|
||||
// };
|
||||
// let buffer_id = excerpt.buffer_id;
|
||||
// let Some(buffer) = project.read(cx).buffer_for_id(buffer_id) else {
|
||||
// continue;
|
||||
// };
|
||||
|
||||
// let adjacent_excerpts = iter::from_fn(|| {
|
||||
// let insertion = insertions.peek()?;
|
||||
// if insertion.previous_excerpt_id.is_none()
|
||||
// && insertion.excerpt.as_ref()?.buffer_id == buffer_id
|
||||
// {
|
||||
// insertions.next()?.excerpt
|
||||
// } else {
|
||||
// None
|
||||
// }
|
||||
// });
|
||||
|
||||
// multibuffer.insert_excerpts_with_ids_after(
|
||||
// ExcerptId::from_proto(previous_excerpt_id),
|
||||
// buffer,
|
||||
// [excerpt]
|
||||
// .into_iter()
|
||||
// .chain(adjacent_excerpts)
|
||||
// .filter_map(|excerpt| {
|
||||
// Some((
|
||||
// ExcerptId::from_proto(excerpt.id),
|
||||
// deserialize_excerpt_range(excerpt)?,
|
||||
// ))
|
||||
// }),
|
||||
// cx,
|
||||
// );
|
||||
// }
|
||||
|
||||
// multibuffer.remove_excerpts(removed_excerpt_ids, cx);
|
||||
// });
|
||||
// })?;
|
||||
|
||||
// // Deserialize the editor state.
|
||||
// let (selections, pending_selection, scroll_top_anchor) = this.update(cx, |editor, cx| {
|
||||
// let buffer = editor.buffer.read(cx).read(cx);
|
||||
// let selections = message
|
||||
// .selections
|
||||
// .into_iter()
|
||||
// .filter_map(|selection| deserialize_selection(&buffer, selection))
|
||||
// .collect::<Vec<_>>();
|
||||
// let pending_selection = message
|
||||
// .pending_selection
|
||||
// .and_then(|selection| deserialize_selection(&buffer, selection));
|
||||
// let scroll_top_anchor = message
|
||||
// .scroll_top_anchor
|
||||
// .and_then(|anchor| deserialize_anchor(&buffer, anchor));
|
||||
// anyhow::Ok((selections, pending_selection, scroll_top_anchor))
|
||||
// })??;
|
||||
|
||||
// // Wait until the buffer has received all of the operations referenced by
|
||||
// // the editor's new state.
|
||||
// this.update(cx, |editor, cx| {
|
||||
// editor.buffer.update(cx, |buffer, cx| {
|
||||
// buffer.wait_for_anchors(
|
||||
// selections
|
||||
// .iter()
|
||||
// .chain(pending_selection.as_ref())
|
||||
// .flat_map(|selection| [selection.start, selection.end])
|
||||
// .chain(scroll_top_anchor),
|
||||
// cx,
|
||||
// )
|
||||
// })
|
||||
// })?
|
||||
// .await?;
|
||||
|
||||
// // Update the editor's state.
|
||||
// this.update(cx, |editor, cx| {
|
||||
// if !selections.is_empty() || pending_selection.is_some() {
|
||||
// editor.set_selections_from_remote(selections, pending_selection, cx);
|
||||
// editor.request_autoscroll_remotely(Autoscroll::newest(), cx);
|
||||
// } else if let Some(scroll_top_anchor) = scroll_top_anchor {
|
||||
// editor.set_scroll_anchor_remote(
|
||||
// ScrollAnchor {
|
||||
// anchor: scroll_top_anchor,
|
||||
// offset: point(message.scroll_x, message.scroll_y),
|
||||
// },
|
||||
// cx,
|
||||
// );
|
||||
// }
|
||||
// })?;
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
fn serialize_excerpt(
|
||||
buffer_id: u64,
|
||||
@@ -528,40 +523,41 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor)
|
||||
}
|
||||
|
||||
impl Item for Editor {
|
||||
type Event = EditorEvent;
|
||||
|
||||
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
todo!();
|
||||
// if let Ok(data) = data.downcast::<NavigationData>() {
|
||||
// let newest_selection = self.selections.newest::<Point>(cx);
|
||||
// let buffer = self.buffer.read(cx).read(cx);
|
||||
// let offset = if buffer.can_resolve(&data.cursor_anchor) {
|
||||
// data.cursor_anchor.to_point(&buffer)
|
||||
// } else {
|
||||
// buffer.clip_point(data.cursor_position, Bias::Left)
|
||||
// };
|
||||
if let Ok(data) = data.downcast::<NavigationData>() {
|
||||
let newest_selection = self.selections.newest::<Point>(cx);
|
||||
let buffer = self.buffer.read(cx).read(cx);
|
||||
let offset = if buffer.can_resolve(&data.cursor_anchor) {
|
||||
data.cursor_anchor.to_point(&buffer)
|
||||
} else {
|
||||
buffer.clip_point(data.cursor_position, Bias::Left)
|
||||
};
|
||||
|
||||
// let mut scroll_anchor = data.scroll_anchor;
|
||||
// if !buffer.can_resolve(&scroll_anchor.anchor) {
|
||||
// scroll_anchor.anchor = buffer.anchor_before(
|
||||
// buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left),
|
||||
// );
|
||||
// }
|
||||
let mut scroll_anchor = data.scroll_anchor;
|
||||
if !buffer.can_resolve(&scroll_anchor.anchor) {
|
||||
scroll_anchor.anchor = buffer.anchor_before(
|
||||
buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left),
|
||||
);
|
||||
}
|
||||
|
||||
// drop(buffer);
|
||||
drop(buffer);
|
||||
|
||||
// if newest_selection.head() == offset {
|
||||
// false
|
||||
// } else {
|
||||
// let nav_history = self.nav_history.take();
|
||||
// self.set_scroll_anchor(scroll_anchor, cx);
|
||||
// self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
// s.select_ranges([offset..offset])
|
||||
// });
|
||||
// self.nav_history = nav_history;
|
||||
// true
|
||||
// }
|
||||
// } else {
|
||||
// false
|
||||
// }
|
||||
if newest_selection.head() == offset {
|
||||
false
|
||||
} else {
|
||||
let nav_history = self.nav_history.take();
|
||||
self.set_scroll_anchor(scroll_anchor, cx);
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges([offset..offset])
|
||||
});
|
||||
self.nav_history = nav_history;
|
||||
true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
|
||||
@@ -587,28 +583,25 @@ impl Item for Editor {
|
||||
fn tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
|
||||
AnyElement::new(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(Label::new(self.title(cx).to_string()))
|
||||
.children(detail.and_then(|detail| {
|
||||
let path = path_for_buffer(&self.buffer, detail, false, cx)?;
|
||||
let description = path.to_string_lossy();
|
||||
let description = detail.and_then(|detail| {
|
||||
let path = path_for_buffer(&self.buffer, detail, false, cx)?;
|
||||
let description = path.to_string_lossy();
|
||||
let description = description.trim();
|
||||
|
||||
Some(
|
||||
div().child(
|
||||
Label::new(util::truncate_and_trailoff(
|
||||
&description,
|
||||
MAX_TAB_TITLE_LEN,
|
||||
))
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
})),
|
||||
)
|
||||
if description.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN))
|
||||
});
|
||||
|
||||
h_stack()
|
||||
.gap_2()
|
||||
.child(Label::new(self.title(cx).to_string()))
|
||||
.when_some(description, |this, description| {
|
||||
this.child(Label::new(description).color(Color::Muted))
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn for_each_project_item(
|
||||
@@ -765,35 +758,34 @@ impl Item for Editor {
|
||||
}
|
||||
|
||||
fn breadcrumbs(&self, variant: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
|
||||
todo!();
|
||||
// let cursor = self.selections.newest_anchor().head();
|
||||
// let multibuffer = &self.buffer().read(cx);
|
||||
// let (buffer_id, symbols) =
|
||||
// multibuffer.symbols_containing(cursor, Some(&theme.editor.syntax), cx)?;
|
||||
// let buffer = multibuffer.buffer(buffer_id)?;
|
||||
let cursor = self.selections.newest_anchor().head();
|
||||
let multibuffer = &self.buffer().read(cx);
|
||||
let (buffer_id, symbols) =
|
||||
multibuffer.symbols_containing(cursor, Some(&variant.syntax()), cx)?;
|
||||
let buffer = multibuffer.buffer(buffer_id)?;
|
||||
|
||||
// let buffer = buffer.read(cx);
|
||||
// let filename = buffer
|
||||
// .snapshot()
|
||||
// .resolve_file_path(
|
||||
// cx,
|
||||
// self.project
|
||||
// .as_ref()
|
||||
// .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
|
||||
// .unwrap_or_default(),
|
||||
// )
|
||||
// .map(|path| path.to_string_lossy().to_string())
|
||||
// .unwrap_or_else(|| "untitled".to_string());
|
||||
let buffer = buffer.read(cx);
|
||||
let filename = buffer
|
||||
.snapshot()
|
||||
.resolve_file_path(
|
||||
cx,
|
||||
self.project
|
||||
.as_ref()
|
||||
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
|
||||
// let mut breadcrumbs = vec![BreadcrumbText {
|
||||
// text: filename,
|
||||
// highlights: None,
|
||||
// }];
|
||||
// breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText {
|
||||
// text: symbol.text,
|
||||
// highlights: Some(symbol.highlight_ranges),
|
||||
// }));
|
||||
// Some(breadcrumbs)
|
||||
let mut breadcrumbs = vec![BreadcrumbText {
|
||||
text: filename,
|
||||
highlights: None,
|
||||
}];
|
||||
breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText {
|
||||
text: symbol.text,
|
||||
highlights: Some(symbol.highlight_ranges),
|
||||
}));
|
||||
Some(breadcrumbs)
|
||||
}
|
||||
|
||||
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
|
||||
@@ -843,6 +835,40 @@ impl Item for Editor {
|
||||
Some("Editor")
|
||||
}
|
||||
|
||||
fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) {
|
||||
match event {
|
||||
EditorEvent::Closed => f(ItemEvent::CloseItem),
|
||||
|
||||
EditorEvent::Saved | EditorEvent::TitleChanged => {
|
||||
f(ItemEvent::UpdateTab);
|
||||
f(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::Reparsed => {
|
||||
f(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::SelectionsChanged { local } if *local => {
|
||||
f(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::DirtyChanged => {
|
||||
f(ItemEvent::UpdateTab);
|
||||
}
|
||||
|
||||
EditorEvent::BufferEdited => {
|
||||
f(ItemEvent::Edit);
|
||||
f(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => {
|
||||
f(ItemEvent::Edit);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
project: Model<Project>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
@@ -913,7 +939,7 @@ impl SearchableItem for Editor {
|
||||
fn update_matches(&mut self, matches: Vec<Range<Anchor>>, cx: &mut ViewContext<Self>) {
|
||||
self.highlight_background::<BufferSearchHighlights>(
|
||||
matches,
|
||||
|theme| theme.title_bar_background, // todo: update theme
|
||||
|theme| theme.search_match_background,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -1120,86 +1146,78 @@ pub struct CursorPosition {
|
||||
_observe_active_editor: Option<Subscription>,
|
||||
}
|
||||
|
||||
// impl Default for CursorPosition {
|
||||
// fn default() -> Self {
|
||||
// Self::new()
|
||||
// }
|
||||
// }
|
||||
impl Default for CursorPosition {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
// impl CursorPosition {
|
||||
// pub fn new() -> Self {
|
||||
// Self {
|
||||
// position: None,
|
||||
// selected_count: 0,
|
||||
// _observe_active_editor: None,
|
||||
// }
|
||||
// }
|
||||
impl CursorPosition {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
position: None,
|
||||
selected_count: 0,
|
||||
_observe_active_editor: None,
|
||||
}
|
||||
}
|
||||
|
||||
// fn update_position(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
|
||||
// let editor = editor.read(cx);
|
||||
// let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
fn update_position(&mut self, editor: View<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.selections.all::<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));
|
||||
self.selected_count = 0;
|
||||
let mut last_selection: Option<Selection<usize>> = None;
|
||||
for selection in editor.selections.all::<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();
|
||||
// }
|
||||
// }
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
// impl Entity for CursorPosition {
|
||||
// type Event = ();
|
||||
// }
|
||||
impl Render for CursorPosition {
|
||||
type Element = Div;
|
||||
|
||||
// impl View for CursorPosition {
|
||||
// fn ui_name() -> &'static str {
|
||||
// "CursorPosition"
|
||||
// }
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
div().when_some(self.position, |el, position| {
|
||||
let mut text = format!(
|
||||
"{}{FILE_ROW_COLUMN_DELIMITER}{}",
|
||||
position.row + 1,
|
||||
position.column + 1
|
||||
);
|
||||
if self.selected_count > 0 {
|
||||
write!(text, " ({} selected)", self.selected_count).unwrap();
|
||||
}
|
||||
|
||||
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
// if let Some(position) = self.position {
|
||||
// let theme = &theme::current(cx).workspace.status_bar;
|
||||
// let mut text = format!(
|
||||
// "{}{FILE_ROW_COLUMN_DELIMITER}{}",
|
||||
// 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()).into_any()
|
||||
// } else {
|
||||
// Empty::new().into_any()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
el.child(Label::new(text))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// impl StatusItemView for CursorPosition {
|
||||
// fn set_active_pane_item(
|
||||
// &mut self,
|
||||
// active_pane_item: Option<&dyn ItemHandle>,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) {
|
||||
// if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
|
||||
// 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;
|
||||
// }
|
||||
impl StatusItemView for CursorPosition {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
|
||||
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();
|
||||
// }
|
||||
// }
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn path_for_buffer<'a>(
|
||||
buffer: &Model<MultiBuffer>,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,14 @@
|
||||
use crate::{DisplayPoint, Editor, EditorMode, SelectMode};
|
||||
use gpui::{Pixels, Point, ViewContext};
|
||||
use crate::{
|
||||
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition,
|
||||
Rename, RevealInFinder, SelectMode, ToggleCodeActions,
|
||||
};
|
||||
use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
|
||||
|
||||
pub struct MouseContextMenu {
|
||||
pub(crate) position: Point<Pixels>,
|
||||
pub(crate) context_menu: View<ui::ContextMenu>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
pub fn deploy_context_menu(
|
||||
editor: &mut Editor,
|
||||
@@ -7,88 +16,95 @@ pub fn deploy_context_menu(
|
||||
point: DisplayPoint,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
todo!();
|
||||
if !editor.is_focused(cx) {
|
||||
editor.focus(cx);
|
||||
}
|
||||
|
||||
// if !editor.focused {
|
||||
// cx.focus_self();
|
||||
// }
|
||||
// Don't show context menu for inline editors
|
||||
if editor.mode() != EditorMode::Full {
|
||||
return;
|
||||
}
|
||||
|
||||
// // Don't show context menu for inline editors
|
||||
// if editor.mode() != EditorMode::Full {
|
||||
// return;
|
||||
// }
|
||||
// Don't show the context menu if there isn't a project associated with this editor
|
||||
if editor.project.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
// // Don't show the context menu if there isn't a project associated with this editor
|
||||
// if editor.project.is_none() {
|
||||
// return;
|
||||
// }
|
||||
// Move the cursor to the clicked location so that dispatched actions make sense
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.clear_disjoint();
|
||||
s.set_pending_display_range(point..point, SelectMode::Character);
|
||||
});
|
||||
|
||||
// // Move the cursor to the clicked location so that dispatched actions make sense
|
||||
// editor.change_selections(None, cx, |s| {
|
||||
// s.clear_disjoint();
|
||||
// s.set_pending_display_range(point..point, SelectMode::Character);
|
||||
// });
|
||||
let context_menu = ui::ContextMenu::build(cx, |menu, cx| {
|
||||
menu.action("Rename Symbol", Box::new(Rename))
|
||||
.action("Go to Definition", Box::new(GoToDefinition))
|
||||
.action("Go to Type Definition", Box::new(GoToTypeDefinition))
|
||||
.action("Find All References", Box::new(FindAllReferences))
|
||||
.action(
|
||||
"Code Actions",
|
||||
Box::new(ToggleCodeActions {
|
||||
deployed_from_indicator: false,
|
||||
}),
|
||||
)
|
||||
.separator()
|
||||
.action("Reveal in Finder", Box::new(RevealInFinder))
|
||||
});
|
||||
let context_menu_focus = context_menu.focus_handle(cx);
|
||||
cx.focus(&context_menu_focus);
|
||||
|
||||
// editor.mouse_context_menu.update(cx, |menu, cx| {
|
||||
// menu.show(
|
||||
// position,
|
||||
// AnchorCorner::TopLeft,
|
||||
// vec![
|
||||
// ContextMenuItem::action("Rename Symbol", Rename),
|
||||
// ContextMenuItem::action("Go to Definition", GoToDefinition),
|
||||
// ContextMenuItem::action("Go to Type Definition", GoToTypeDefinition),
|
||||
// ContextMenuItem::action("Find All References", FindAllReferences),
|
||||
// ContextMenuItem::action(
|
||||
// "Code Actions",
|
||||
// ToggleCodeActions {
|
||||
// deployed_from_indicator: false,
|
||||
// },
|
||||
// ),
|
||||
// ContextMenuItem::Separator,
|
||||
// ContextMenuItem::action("Reveal in Finder", RevealInFinder),
|
||||
// ],
|
||||
// cx,
|
||||
// );
|
||||
// });
|
||||
// cx.notify();
|
||||
let _subscription = cx.subscribe(&context_menu, move |this, _, event: &DismissEvent, cx| {
|
||||
this.mouse_context_menu.take();
|
||||
if context_menu_focus.contains_focused(cx) {
|
||||
this.focus(cx);
|
||||
}
|
||||
});
|
||||
|
||||
editor.mouse_context_menu = Some(MouseContextMenu {
|
||||
position,
|
||||
context_menu,
|
||||
_subscription,
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use super::*;
|
||||
// use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
|
||||
// use indoc::indoc;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
|
||||
use indoc::indoc;
|
||||
|
||||
// #[gpui::test]
|
||||
// async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) {
|
||||
// init_test(cx, |_| {});
|
||||
#[gpui::test]
|
||||
async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
// let mut cx = EditorLspTestContext::new_rust(
|
||||
// lsp::ServerCapabilities {
|
||||
// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
|
||||
// ..Default::default()
|
||||
// },
|
||||
// cx,
|
||||
// )
|
||||
// .await;
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
// cx.set_state(indoc! {"
|
||||
// fn teˇst() {
|
||||
// do_work();
|
||||
// }
|
||||
// "});
|
||||
// let point = cx.display_point(indoc! {"
|
||||
// fn test() {
|
||||
// do_wˇork();
|
||||
// }
|
||||
// "});
|
||||
// cx.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx));
|
||||
cx.set_state(indoc! {"
|
||||
fn teˇst() {
|
||||
do_work();
|
||||
}
|
||||
"});
|
||||
let point = cx.display_point(indoc! {"
|
||||
fn test() {
|
||||
do_wˇork();
|
||||
}
|
||||
"});
|
||||
cx.editor(|editor, app| assert!(editor.mouse_context_menu.is_none()));
|
||||
cx.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx));
|
||||
|
||||
// cx.assert_editor_state(indoc! {"
|
||||
// fn test() {
|
||||
// do_wˇork();
|
||||
// }
|
||||
// "});
|
||||
// cx.editor(|editor, app| assert!(editor.mouse_context_menu.read(app).visible()));
|
||||
// }
|
||||
// }
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn test() {
|
||||
do_wˇork();
|
||||
}
|
||||
"});
|
||||
cx.editor(|editor, app| assert!(editor.mouse_context_menu.is_some()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,483 +452,475 @@ pub fn split_display_range_by_lines(
|
||||
result
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use super::*;
|
||||
// use crate::{
|
||||
// display_map::Inlay,
|
||||
// test::{},
|
||||
// Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer,
|
||||
// };
|
||||
// use project::Project;
|
||||
// use settings::SettingsStore;
|
||||
// use util::post_inc;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
display_map::Inlay,
|
||||
test::{editor_test_context::EditorTestContext, marked_display_snapshot},
|
||||
Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer,
|
||||
};
|
||||
use gpui::{font, Context as _};
|
||||
use project::Project;
|
||||
use settings::SettingsStore;
|
||||
use util::post_inc;
|
||||
|
||||
// #[gpui::test]
|
||||
// fn test_previous_word_start(cx: &mut gpui::AppContext) {
|
||||
// init_test(cx);
|
||||
#[gpui::test]
|
||||
fn test_previous_word_start(cx: &mut gpui::AppContext) {
|
||||
init_test(cx);
|
||||
|
||||
// fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
|
||||
// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
// assert_eq!(
|
||||
// previous_word_start(&snapshot, display_points[1]),
|
||||
// display_points[0]
|
||||
// );
|
||||
// }
|
||||
fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
|
||||
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
assert_eq!(
|
||||
previous_word_start(&snapshot, display_points[1]),
|
||||
display_points[0]
|
||||
);
|
||||
}
|
||||
|
||||
// assert("\nˇ ˇlorem", cx);
|
||||
// assert("ˇ\nˇ lorem", cx);
|
||||
// assert(" ˇloremˇ", cx);
|
||||
// assert("ˇ ˇlorem", cx);
|
||||
// assert(" ˇlorˇem", cx);
|
||||
// assert("\nlorem\nˇ ˇipsum", cx);
|
||||
// assert("\n\nˇ\nˇ", cx);
|
||||
// assert(" ˇlorem ˇipsum", cx);
|
||||
// assert("loremˇ-ˇipsum", cx);
|
||||
// assert("loremˇ-#$@ˇipsum", cx);
|
||||
// assert("ˇlorem_ˇipsum", cx);
|
||||
// assert(" ˇdefγˇ", cx);
|
||||
// assert(" ˇbcΔˇ", cx);
|
||||
// assert(" abˇ——ˇcd", cx);
|
||||
// }
|
||||
assert("\nˇ ˇlorem", cx);
|
||||
assert("ˇ\nˇ lorem", cx);
|
||||
assert(" ˇloremˇ", cx);
|
||||
assert("ˇ ˇlorem", cx);
|
||||
assert(" ˇlorˇem", cx);
|
||||
assert("\nlorem\nˇ ˇipsum", cx);
|
||||
assert("\n\nˇ\nˇ", cx);
|
||||
assert(" ˇlorem ˇipsum", cx);
|
||||
assert("loremˇ-ˇipsum", cx);
|
||||
assert("loremˇ-#$@ˇipsum", cx);
|
||||
assert("ˇlorem_ˇipsum", cx);
|
||||
assert(" ˇdefγˇ", cx);
|
||||
assert(" ˇbcΔˇ", cx);
|
||||
assert(" abˇ——ˇcd", cx);
|
||||
}
|
||||
|
||||
// #[gpui::test]
|
||||
// fn test_previous_subword_start(cx: &mut gpui::AppContext) {
|
||||
// init_test(cx);
|
||||
#[gpui::test]
|
||||
fn test_previous_subword_start(cx: &mut gpui::AppContext) {
|
||||
init_test(cx);
|
||||
|
||||
// fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
|
||||
// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
// assert_eq!(
|
||||
// previous_subword_start(&snapshot, display_points[1]),
|
||||
// display_points[0]
|
||||
// );
|
||||
// }
|
||||
fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
|
||||
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
assert_eq!(
|
||||
previous_subword_start(&snapshot, display_points[1]),
|
||||
display_points[0]
|
||||
);
|
||||
}
|
||||
|
||||
// // Subword boundaries are respected
|
||||
// assert("lorem_ˇipˇsum", cx);
|
||||
// assert("lorem_ˇipsumˇ", cx);
|
||||
// assert("ˇlorem_ˇipsum", cx);
|
||||
// assert("lorem_ˇipsum_ˇdolor", cx);
|
||||
// assert("loremˇIpˇsum", cx);
|
||||
// assert("loremˇIpsumˇ", cx);
|
||||
// Subword boundaries are respected
|
||||
assert("lorem_ˇipˇsum", cx);
|
||||
assert("lorem_ˇipsumˇ", cx);
|
||||
assert("ˇlorem_ˇipsum", cx);
|
||||
assert("lorem_ˇipsum_ˇdolor", cx);
|
||||
assert("loremˇIpˇsum", cx);
|
||||
assert("loremˇIpsumˇ", cx);
|
||||
|
||||
// // Word boundaries are still respected
|
||||
// assert("\nˇ ˇlorem", cx);
|
||||
// assert(" ˇloremˇ", cx);
|
||||
// assert(" ˇlorˇem", cx);
|
||||
// assert("\nlorem\nˇ ˇipsum", cx);
|
||||
// assert("\n\nˇ\nˇ", cx);
|
||||
// assert(" ˇlorem ˇipsum", cx);
|
||||
// assert("loremˇ-ˇipsum", cx);
|
||||
// assert("loremˇ-#$@ˇipsum", cx);
|
||||
// assert(" ˇdefγˇ", cx);
|
||||
// assert(" bcˇΔˇ", cx);
|
||||
// assert(" ˇbcδˇ", cx);
|
||||
// assert(" abˇ——ˇcd", cx);
|
||||
// }
|
||||
// Word boundaries are still respected
|
||||
assert("\nˇ ˇlorem", cx);
|
||||
assert(" ˇloremˇ", cx);
|
||||
assert(" ˇlorˇem", cx);
|
||||
assert("\nlorem\nˇ ˇipsum", cx);
|
||||
assert("\n\nˇ\nˇ", cx);
|
||||
assert(" ˇlorem ˇipsum", cx);
|
||||
assert("loremˇ-ˇipsum", cx);
|
||||
assert("loremˇ-#$@ˇipsum", cx);
|
||||
assert(" ˇdefγˇ", cx);
|
||||
assert(" bcˇΔˇ", cx);
|
||||
assert(" ˇbcδˇ", cx);
|
||||
assert(" abˇ——ˇcd", cx);
|
||||
}
|
||||
|
||||
// #[gpui::test]
|
||||
// fn test_find_preceding_boundary(cx: &mut gpui::AppContext) {
|
||||
// init_test(cx);
|
||||
#[gpui::test]
|
||||
fn test_find_preceding_boundary(cx: &mut gpui::AppContext) {
|
||||
init_test(cx);
|
||||
|
||||
// fn assert(
|
||||
// marked_text: &str,
|
||||
// cx: &mut gpui::AppContext,
|
||||
// is_boundary: impl FnMut(char, char) -> bool,
|
||||
// ) {
|
||||
// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
// assert_eq!(
|
||||
// find_preceding_boundary(
|
||||
// &snapshot,
|
||||
// display_points[1],
|
||||
// FindRange::MultiLine,
|
||||
// is_boundary
|
||||
// ),
|
||||
// display_points[0]
|
||||
// );
|
||||
// }
|
||||
fn assert(
|
||||
marked_text: &str,
|
||||
cx: &mut gpui::AppContext,
|
||||
is_boundary: impl FnMut(char, char) -> bool,
|
||||
) {
|
||||
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
assert_eq!(
|
||||
find_preceding_boundary(
|
||||
&snapshot,
|
||||
display_points[1],
|
||||
FindRange::MultiLine,
|
||||
is_boundary
|
||||
),
|
||||
display_points[0]
|
||||
);
|
||||
}
|
||||
|
||||
// assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
|
||||
// left == 'c' && right == 'd'
|
||||
// });
|
||||
// assert("abcdef\nˇgh\nijˇk", cx, |left, right| {
|
||||
// left == '\n' && right == 'g'
|
||||
// });
|
||||
// let mut line_count = 0;
|
||||
// assert("abcdef\nˇgh\nijˇk", cx, |left, _| {
|
||||
// if left == '\n' {
|
||||
// line_count += 1;
|
||||
// line_count == 2
|
||||
// } else {
|
||||
// false
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
|
||||
left == 'c' && right == 'd'
|
||||
});
|
||||
assert("abcdef\nˇgh\nijˇk", cx, |left, right| {
|
||||
left == '\n' && right == 'g'
|
||||
});
|
||||
let mut line_count = 0;
|
||||
assert("abcdef\nˇgh\nijˇk", cx, |left, _| {
|
||||
if left == '\n' {
|
||||
line_count += 1;
|
||||
line_count == 2
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// #[gpui::test]
|
||||
// fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::AppContext) {
|
||||
// init_test(cx);
|
||||
#[gpui::test]
|
||||
fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::AppContext) {
|
||||
init_test(cx);
|
||||
|
||||
// let input_text = "abcdefghijklmnopqrstuvwxys";
|
||||
// let family_id = cx
|
||||
// .font_cache()
|
||||
// .load_family(&["Helvetica"], &Default::default())
|
||||
// .unwrap();
|
||||
// let font_id = cx
|
||||
// .font_cache()
|
||||
// .select_font(family_id, &Default::default())
|
||||
// .unwrap();
|
||||
// let font_size = 14.0;
|
||||
// let buffer = MultiBuffer::build_simple(input_text, cx);
|
||||
// let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
// let display_map =
|
||||
// cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
|
||||
let input_text = "abcdefghijklmnopqrstuvwxys";
|
||||
let font = font("Helvetica");
|
||||
let font_size = px(14.0);
|
||||
let buffer = MultiBuffer::build_simple(input_text, cx);
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
let display_map =
|
||||
cx.build_model(|cx| DisplayMap::new(buffer, font, font_size, None, 1, 1, cx));
|
||||
|
||||
// // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary
|
||||
// let mut id = 0;
|
||||
// let inlays = (0..buffer_snapshot.len())
|
||||
// .map(|offset| {
|
||||
// [
|
||||
// Inlay {
|
||||
// id: InlayId::Suggestion(post_inc(&mut id)),
|
||||
// position: buffer_snapshot.anchor_at(offset, Bias::Left),
|
||||
// text: format!("test").into(),
|
||||
// },
|
||||
// Inlay {
|
||||
// id: InlayId::Suggestion(post_inc(&mut id)),
|
||||
// position: buffer_snapshot.anchor_at(offset, Bias::Right),
|
||||
// text: format!("test").into(),
|
||||
// },
|
||||
// Inlay {
|
||||
// id: InlayId::Hint(post_inc(&mut id)),
|
||||
// position: buffer_snapshot.anchor_at(offset, Bias::Left),
|
||||
// text: format!("test").into(),
|
||||
// },
|
||||
// Inlay {
|
||||
// id: InlayId::Hint(post_inc(&mut id)),
|
||||
// position: buffer_snapshot.anchor_at(offset, Bias::Right),
|
||||
// text: format!("test").into(),
|
||||
// },
|
||||
// ]
|
||||
// })
|
||||
// .flatten()
|
||||
// .collect();
|
||||
// let snapshot = display_map.update(cx, |map, cx| {
|
||||
// map.splice_inlays(Vec::new(), inlays, cx);
|
||||
// map.snapshot(cx)
|
||||
// });
|
||||
// add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary
|
||||
let mut id = 0;
|
||||
let inlays = (0..buffer_snapshot.len())
|
||||
.map(|offset| {
|
||||
[
|
||||
Inlay {
|
||||
id: InlayId::Suggestion(post_inc(&mut id)),
|
||||
position: buffer_snapshot.anchor_at(offset, Bias::Left),
|
||||
text: format!("test").into(),
|
||||
},
|
||||
Inlay {
|
||||
id: InlayId::Suggestion(post_inc(&mut id)),
|
||||
position: buffer_snapshot.anchor_at(offset, Bias::Right),
|
||||
text: format!("test").into(),
|
||||
},
|
||||
Inlay {
|
||||
id: InlayId::Hint(post_inc(&mut id)),
|
||||
position: buffer_snapshot.anchor_at(offset, Bias::Left),
|
||||
text: format!("test").into(),
|
||||
},
|
||||
Inlay {
|
||||
id: InlayId::Hint(post_inc(&mut id)),
|
||||
position: buffer_snapshot.anchor_at(offset, Bias::Right),
|
||||
text: format!("test").into(),
|
||||
},
|
||||
]
|
||||
})
|
||||
.flatten()
|
||||
.collect();
|
||||
let snapshot = display_map.update(cx, |map, cx| {
|
||||
map.splice_inlays(Vec::new(), inlays, cx);
|
||||
map.snapshot(cx)
|
||||
});
|
||||
|
||||
// assert_eq!(
|
||||
// find_preceding_boundary(
|
||||
// &snapshot,
|
||||
// buffer_snapshot.len().to_display_point(&snapshot),
|
||||
// FindRange::MultiLine,
|
||||
// |left, _| left == 'e',
|
||||
// ),
|
||||
// snapshot
|
||||
// .buffer_snapshot
|
||||
// .offset_to_point(5)
|
||||
// .to_display_point(&snapshot),
|
||||
// "Should not stop at inlays when looking for boundaries"
|
||||
// );
|
||||
// }
|
||||
assert_eq!(
|
||||
find_preceding_boundary(
|
||||
&snapshot,
|
||||
buffer_snapshot.len().to_display_point(&snapshot),
|
||||
FindRange::MultiLine,
|
||||
|left, _| left == 'e',
|
||||
),
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.offset_to_point(5)
|
||||
.to_display_point(&snapshot),
|
||||
"Should not stop at inlays when looking for boundaries"
|
||||
);
|
||||
}
|
||||
|
||||
// #[gpui::test]
|
||||
// fn test_next_word_end(cx: &mut gpui::AppContext) {
|
||||
// init_test(cx);
|
||||
#[gpui::test]
|
||||
fn test_next_word_end(cx: &mut gpui::AppContext) {
|
||||
init_test(cx);
|
||||
|
||||
// fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
|
||||
// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
// assert_eq!(
|
||||
// next_word_end(&snapshot, display_points[0]),
|
||||
// display_points[1]
|
||||
// );
|
||||
// }
|
||||
fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
|
||||
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
assert_eq!(
|
||||
next_word_end(&snapshot, display_points[0]),
|
||||
display_points[1]
|
||||
);
|
||||
}
|
||||
|
||||
// assert("\nˇ loremˇ", cx);
|
||||
// assert(" ˇloremˇ", cx);
|
||||
// assert(" lorˇemˇ", cx);
|
||||
// assert(" loremˇ ˇ\nipsum\n", cx);
|
||||
// assert("\nˇ\nˇ\n\n", cx);
|
||||
// assert("loremˇ ipsumˇ ", cx);
|
||||
// assert("loremˇ-ˇipsum", cx);
|
||||
// assert("loremˇ#$@-ˇipsum", cx);
|
||||
// assert("loremˇ_ipsumˇ", cx);
|
||||
// assert(" ˇbcΔˇ", cx);
|
||||
// assert(" abˇ——ˇcd", cx);
|
||||
// }
|
||||
assert("\nˇ loremˇ", cx);
|
||||
assert(" ˇloremˇ", cx);
|
||||
assert(" lorˇemˇ", cx);
|
||||
assert(" loremˇ ˇ\nipsum\n", cx);
|
||||
assert("\nˇ\nˇ\n\n", cx);
|
||||
assert("loremˇ ipsumˇ ", cx);
|
||||
assert("loremˇ-ˇipsum", cx);
|
||||
assert("loremˇ#$@-ˇipsum", cx);
|
||||
assert("loremˇ_ipsumˇ", cx);
|
||||
assert(" ˇbcΔˇ", cx);
|
||||
assert(" abˇ——ˇcd", cx);
|
||||
}
|
||||
|
||||
// #[gpui::test]
|
||||
// fn test_next_subword_end(cx: &mut gpui::AppContext) {
|
||||
// init_test(cx);
|
||||
#[gpui::test]
|
||||
fn test_next_subword_end(cx: &mut gpui::AppContext) {
|
||||
init_test(cx);
|
||||
|
||||
// fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
|
||||
// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
// assert_eq!(
|
||||
// next_subword_end(&snapshot, display_points[0]),
|
||||
// display_points[1]
|
||||
// );
|
||||
// }
|
||||
fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
|
||||
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
assert_eq!(
|
||||
next_subword_end(&snapshot, display_points[0]),
|
||||
display_points[1]
|
||||
);
|
||||
}
|
||||
|
||||
// // Subword boundaries are respected
|
||||
// assert("loˇremˇ_ipsum", cx);
|
||||
// assert("ˇloremˇ_ipsum", cx);
|
||||
// assert("loremˇ_ipsumˇ", cx);
|
||||
// assert("loremˇ_ipsumˇ_dolor", cx);
|
||||
// assert("loˇremˇIpsum", cx);
|
||||
// assert("loremˇIpsumˇDolor", cx);
|
||||
// Subword boundaries are respected
|
||||
assert("loˇremˇ_ipsum", cx);
|
||||
assert("ˇloremˇ_ipsum", cx);
|
||||
assert("loremˇ_ipsumˇ", cx);
|
||||
assert("loremˇ_ipsumˇ_dolor", cx);
|
||||
assert("loˇremˇIpsum", cx);
|
||||
assert("loremˇIpsumˇDolor", cx);
|
||||
|
||||
// // Word boundaries are still respected
|
||||
// assert("\nˇ loremˇ", cx);
|
||||
// assert(" ˇloremˇ", cx);
|
||||
// assert(" lorˇemˇ", cx);
|
||||
// assert(" loremˇ ˇ\nipsum\n", cx);
|
||||
// assert("\nˇ\nˇ\n\n", cx);
|
||||
// assert("loremˇ ipsumˇ ", cx);
|
||||
// assert("loremˇ-ˇipsum", cx);
|
||||
// assert("loremˇ#$@-ˇipsum", cx);
|
||||
// assert("loremˇ_ipsumˇ", cx);
|
||||
// assert(" ˇbcˇΔ", cx);
|
||||
// assert(" abˇ——ˇcd", cx);
|
||||
// }
|
||||
// Word boundaries are still respected
|
||||
assert("\nˇ loremˇ", cx);
|
||||
assert(" ˇloremˇ", cx);
|
||||
assert(" lorˇemˇ", cx);
|
||||
assert(" loremˇ ˇ\nipsum\n", cx);
|
||||
assert("\nˇ\nˇ\n\n", cx);
|
||||
assert("loremˇ ipsumˇ ", cx);
|
||||
assert("loremˇ-ˇipsum", cx);
|
||||
assert("loremˇ#$@-ˇipsum", cx);
|
||||
assert("loremˇ_ipsumˇ", cx);
|
||||
assert(" ˇbcˇΔ", cx);
|
||||
assert(" abˇ——ˇcd", cx);
|
||||
}
|
||||
|
||||
// #[gpui::test]
|
||||
// fn test_find_boundary(cx: &mut gpui::AppContext) {
|
||||
// init_test(cx);
|
||||
#[gpui::test]
|
||||
fn test_find_boundary(cx: &mut gpui::AppContext) {
|
||||
init_test(cx);
|
||||
|
||||
// fn assert(
|
||||
// marked_text: &str,
|
||||
// cx: &mut gpui::AppContext,
|
||||
// is_boundary: impl FnMut(char, char) -> bool,
|
||||
// ) {
|
||||
// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
// assert_eq!(
|
||||
// find_boundary(
|
||||
// &snapshot,
|
||||
// display_points[0],
|
||||
// FindRange::MultiLine,
|
||||
// is_boundary
|
||||
// ),
|
||||
// display_points[1]
|
||||
// );
|
||||
// }
|
||||
fn assert(
|
||||
marked_text: &str,
|
||||
cx: &mut gpui::AppContext,
|
||||
is_boundary: impl FnMut(char, char) -> bool,
|
||||
) {
|
||||
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
assert_eq!(
|
||||
find_boundary(
|
||||
&snapshot,
|
||||
display_points[0],
|
||||
FindRange::MultiLine,
|
||||
is_boundary
|
||||
),
|
||||
display_points[1]
|
||||
);
|
||||
}
|
||||
|
||||
// assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
|
||||
// left == 'j' && right == 'k'
|
||||
// });
|
||||
// assert("abˇcdef\ngh\nˇijk", cx, |left, right| {
|
||||
// left == '\n' && right == 'i'
|
||||
// });
|
||||
// let mut line_count = 0;
|
||||
// assert("abcˇdef\ngh\nˇijk", cx, |left, _| {
|
||||
// if left == '\n' {
|
||||
// line_count += 1;
|
||||
// line_count == 2
|
||||
// } else {
|
||||
// false
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
|
||||
left == 'j' && right == 'k'
|
||||
});
|
||||
assert("abˇcdef\ngh\nˇijk", cx, |left, right| {
|
||||
left == '\n' && right == 'i'
|
||||
});
|
||||
let mut line_count = 0;
|
||||
assert("abcˇdef\ngh\nˇijk", cx, |left, _| {
|
||||
if left == '\n' {
|
||||
line_count += 1;
|
||||
line_count == 2
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// #[gpui::test]
|
||||
// fn test_surrounding_word(cx: &mut gpui::AppContext) {
|
||||
// init_test(cx);
|
||||
#[gpui::test]
|
||||
fn test_surrounding_word(cx: &mut gpui::AppContext) {
|
||||
init_test(cx);
|
||||
|
||||
// fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
|
||||
// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
// assert_eq!(
|
||||
// surrounding_word(&snapshot, display_points[1]),
|
||||
// display_points[0]..display_points[2],
|
||||
// "{}",
|
||||
// marked_text.to_string()
|
||||
// );
|
||||
// }
|
||||
fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
|
||||
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
assert_eq!(
|
||||
surrounding_word(&snapshot, display_points[1]),
|
||||
display_points[0]..display_points[2],
|
||||
"{}",
|
||||
marked_text.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
// assert("ˇˇloremˇ ipsum", cx);
|
||||
// assert("ˇloˇremˇ ipsum", cx);
|
||||
// assert("ˇloremˇˇ ipsum", cx);
|
||||
// assert("loremˇ ˇ ˇipsum", cx);
|
||||
// assert("lorem\nˇˇˇ\nipsum", cx);
|
||||
// assert("lorem\nˇˇipsumˇ", cx);
|
||||
// assert("loremˇ,ˇˇ ipsum", cx);
|
||||
// assert("ˇloremˇˇ, ipsum", cx);
|
||||
// }
|
||||
assert("ˇˇloremˇ ipsum", cx);
|
||||
assert("ˇloˇremˇ ipsum", cx);
|
||||
assert("ˇloremˇˇ ipsum", cx);
|
||||
assert("loremˇ ˇ ˇipsum", cx);
|
||||
assert("lorem\nˇˇˇ\nipsum", cx);
|
||||
assert("lorem\nˇˇipsumˇ", cx);
|
||||
assert("loremˇ,ˇˇ ipsum", cx);
|
||||
assert("ˇloremˇˇ, ipsum", cx);
|
||||
}
|
||||
|
||||
// #[gpui::test]
|
||||
// async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) {
|
||||
// cx.update(|cx| {
|
||||
// init_test(cx);
|
||||
// });
|
||||
#[gpui::test]
|
||||
async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
init_test(cx);
|
||||
});
|
||||
|
||||
// let mut cx = EditorTestContext::new(cx).await;
|
||||
// let editor = cx.editor.clone();
|
||||
// let window = cx.window.clone();
|
||||
// cx.update_window(window, |cx| {
|
||||
// let text_layout_details =
|
||||
// editor.read_with(cx, |editor, cx| editor.text_layout_details(cx));
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let editor = cx.editor.clone();
|
||||
let window = cx.window.clone();
|
||||
cx.update_window(window, |_, cx| {
|
||||
let text_layout_details =
|
||||
editor.update(cx, |editor, cx| editor.text_layout_details(cx));
|
||||
|
||||
// let family_id = cx
|
||||
// .font_cache()
|
||||
// .load_family(&["Helvetica"], &Default::default())
|
||||
// .unwrap();
|
||||
// let font_id = cx
|
||||
// .font_cache()
|
||||
// .select_font(family_id, &Default::default())
|
||||
// .unwrap();
|
||||
let font = font("Helvetica");
|
||||
|
||||
// let buffer =
|
||||
// cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn"));
|
||||
// let multibuffer = cx.add_model(|cx| {
|
||||
// let mut multibuffer = MultiBuffer::new(0);
|
||||
// multibuffer.push_excerpts(
|
||||
// buffer.clone(),
|
||||
// [
|
||||
// ExcerptRange {
|
||||
// context: Point::new(0, 0)..Point::new(1, 4),
|
||||
// primary: None,
|
||||
// },
|
||||
// ExcerptRange {
|
||||
// context: Point::new(2, 0)..Point::new(3, 2),
|
||||
// primary: None,
|
||||
// },
|
||||
// ],
|
||||
// cx,
|
||||
// );
|
||||
// multibuffer
|
||||
// });
|
||||
// let display_map =
|
||||
// cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx));
|
||||
// let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let buffer = cx
|
||||
.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abc\ndefg\nhijkl\nmn"));
|
||||
let multibuffer = cx.build_model(|cx| {
|
||||
let mut multibuffer = MultiBuffer::new(0);
|
||||
multibuffer.push_excerpts(
|
||||
buffer.clone(),
|
||||
[
|
||||
ExcerptRange {
|
||||
context: Point::new(0, 0)..Point::new(1, 4),
|
||||
primary: None,
|
||||
},
|
||||
ExcerptRange {
|
||||
context: Point::new(2, 0)..Point::new(3, 2),
|
||||
primary: None,
|
||||
},
|
||||
],
|
||||
cx,
|
||||
);
|
||||
multibuffer
|
||||
});
|
||||
let display_map =
|
||||
cx.build_model(|cx| DisplayMap::new(multibuffer, font, px(14.0), None, 2, 2, cx));
|
||||
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
// assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
|
||||
assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
|
||||
|
||||
// let col_2_x = snapshot.x_for_point(DisplayPoint::new(2, 2), &text_layout_details);
|
||||
let col_2_x =
|
||||
snapshot.x_for_display_point(DisplayPoint::new(2, 2), &text_layout_details);
|
||||
|
||||
// // Can't move up into the first excerpt's header
|
||||
// assert_eq!(
|
||||
// up(
|
||||
// &snapshot,
|
||||
// DisplayPoint::new(2, 2),
|
||||
// SelectionGoal::HorizontalPosition(col_2_x),
|
||||
// false,
|
||||
// &text_layout_details
|
||||
// ),
|
||||
// (
|
||||
// DisplayPoint::new(2, 0),
|
||||
// SelectionGoal::HorizontalPosition(0.0)
|
||||
// ),
|
||||
// );
|
||||
// assert_eq!(
|
||||
// up(
|
||||
// &snapshot,
|
||||
// DisplayPoint::new(2, 0),
|
||||
// SelectionGoal::None,
|
||||
// false,
|
||||
// &text_layout_details
|
||||
// ),
|
||||
// (
|
||||
// DisplayPoint::new(2, 0),
|
||||
// SelectionGoal::HorizontalPosition(0.0)
|
||||
// ),
|
||||
// );
|
||||
// Can't move up into the first excerpt's header
|
||||
assert_eq!(
|
||||
up(
|
||||
&snapshot,
|
||||
DisplayPoint::new(2, 2),
|
||||
SelectionGoal::HorizontalPosition(col_2_x.0),
|
||||
false,
|
||||
&text_layout_details
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(2, 0),
|
||||
SelectionGoal::HorizontalPosition(0.0)
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
up(
|
||||
&snapshot,
|
||||
DisplayPoint::new(2, 0),
|
||||
SelectionGoal::None,
|
||||
false,
|
||||
&text_layout_details
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(2, 0),
|
||||
SelectionGoal::HorizontalPosition(0.0)
|
||||
),
|
||||
);
|
||||
|
||||
// let col_4_x = snapshot.x_for_point(DisplayPoint::new(3, 4), &text_layout_details);
|
||||
let col_4_x =
|
||||
snapshot.x_for_display_point(DisplayPoint::new(3, 4), &text_layout_details);
|
||||
|
||||
// // Move up and down within first excerpt
|
||||
// assert_eq!(
|
||||
// up(
|
||||
// &snapshot,
|
||||
// DisplayPoint::new(3, 4),
|
||||
// SelectionGoal::HorizontalPosition(col_4_x),
|
||||
// false,
|
||||
// &text_layout_details
|
||||
// ),
|
||||
// (
|
||||
// DisplayPoint::new(2, 3),
|
||||
// SelectionGoal::HorizontalPosition(col_4_x)
|
||||
// ),
|
||||
// );
|
||||
// assert_eq!(
|
||||
// down(
|
||||
// &snapshot,
|
||||
// DisplayPoint::new(2, 3),
|
||||
// SelectionGoal::HorizontalPosition(col_4_x),
|
||||
// false,
|
||||
// &text_layout_details
|
||||
// ),
|
||||
// (
|
||||
// DisplayPoint::new(3, 4),
|
||||
// SelectionGoal::HorizontalPosition(col_4_x)
|
||||
// ),
|
||||
// );
|
||||
// Move up and down within first excerpt
|
||||
assert_eq!(
|
||||
up(
|
||||
&snapshot,
|
||||
DisplayPoint::new(3, 4),
|
||||
SelectionGoal::HorizontalPosition(col_4_x.0),
|
||||
false,
|
||||
&text_layout_details
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(2, 3),
|
||||
SelectionGoal::HorizontalPosition(col_4_x.0)
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(2, 3),
|
||||
SelectionGoal::HorizontalPosition(col_4_x.0),
|
||||
false,
|
||||
&text_layout_details
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(3, 4),
|
||||
SelectionGoal::HorizontalPosition(col_4_x.0)
|
||||
),
|
||||
);
|
||||
|
||||
// let col_5_x = snapshot.x_for_point(DisplayPoint::new(6, 5), &text_layout_details);
|
||||
let col_5_x =
|
||||
snapshot.x_for_display_point(DisplayPoint::new(6, 5), &text_layout_details);
|
||||
|
||||
// // Move up and down across second excerpt's header
|
||||
// assert_eq!(
|
||||
// up(
|
||||
// &snapshot,
|
||||
// DisplayPoint::new(6, 5),
|
||||
// SelectionGoal::HorizontalPosition(col_5_x),
|
||||
// false,
|
||||
// &text_layout_details
|
||||
// ),
|
||||
// (
|
||||
// DisplayPoint::new(3, 4),
|
||||
// SelectionGoal::HorizontalPosition(col_5_x)
|
||||
// ),
|
||||
// );
|
||||
// assert_eq!(
|
||||
// down(
|
||||
// &snapshot,
|
||||
// DisplayPoint::new(3, 4),
|
||||
// SelectionGoal::HorizontalPosition(col_5_x),
|
||||
// false,
|
||||
// &text_layout_details
|
||||
// ),
|
||||
// (
|
||||
// DisplayPoint::new(6, 5),
|
||||
// SelectionGoal::HorizontalPosition(col_5_x)
|
||||
// ),
|
||||
// );
|
||||
// Move up and down across second excerpt's header
|
||||
assert_eq!(
|
||||
up(
|
||||
&snapshot,
|
||||
DisplayPoint::new(6, 5),
|
||||
SelectionGoal::HorizontalPosition(col_5_x.0),
|
||||
false,
|
||||
&text_layout_details
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(3, 4),
|
||||
SelectionGoal::HorizontalPosition(col_5_x.0)
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(3, 4),
|
||||
SelectionGoal::HorizontalPosition(col_5_x.0),
|
||||
false,
|
||||
&text_layout_details
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(6, 5),
|
||||
SelectionGoal::HorizontalPosition(col_5_x.0)
|
||||
),
|
||||
);
|
||||
|
||||
// let max_point_x = snapshot.x_for_point(DisplayPoint::new(7, 2), &text_layout_details);
|
||||
let max_point_x =
|
||||
snapshot.x_for_display_point(DisplayPoint::new(7, 2), &text_layout_details);
|
||||
|
||||
// // Can't move down off the end
|
||||
// assert_eq!(
|
||||
// down(
|
||||
// &snapshot,
|
||||
// DisplayPoint::new(7, 0),
|
||||
// SelectionGoal::HorizontalPosition(0.0),
|
||||
// false,
|
||||
// &text_layout_details
|
||||
// ),
|
||||
// (
|
||||
// DisplayPoint::new(7, 2),
|
||||
// SelectionGoal::HorizontalPosition(max_point_x)
|
||||
// ),
|
||||
// );
|
||||
// assert_eq!(
|
||||
// down(
|
||||
// &snapshot,
|
||||
// DisplayPoint::new(7, 2),
|
||||
// SelectionGoal::HorizontalPosition(max_point_x),
|
||||
// false,
|
||||
// &text_layout_details
|
||||
// ),
|
||||
// (
|
||||
// DisplayPoint::new(7, 2),
|
||||
// SelectionGoal::HorizontalPosition(max_point_x)
|
||||
// ),
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
// Can't move down off the end
|
||||
assert_eq!(
|
||||
down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(7, 0),
|
||||
SelectionGoal::HorizontalPosition(0.0),
|
||||
false,
|
||||
&text_layout_details
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(7, 2),
|
||||
SelectionGoal::HorizontalPosition(max_point_x.0)
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(7, 2),
|
||||
SelectionGoal::HorizontalPosition(max_point_x.0),
|
||||
false,
|
||||
&text_layout_details
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(7, 2),
|
||||
SelectionGoal::HorizontalPosition(max_point_x.0)
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// fn init_test(cx: &mut gpui::AppContext) {
|
||||
// cx.set_global(SettingsStore::test(cx));
|
||||
// theme::init(cx);
|
||||
// language::init(cx);
|
||||
// crate::init(cx);
|
||||
// Project::init_settings(cx);
|
||||
// }
|
||||
// }
|
||||
fn init_test(cx: &mut gpui::AppContext) {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
language::init(cx);
|
||||
crate::init(cx);
|
||||
Project::init_settings(cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,14 +315,11 @@ impl SelectionsCollection {
|
||||
|
||||
let line = display_map.layout_row(row, &text_layout_details);
|
||||
|
||||
dbg!("****START COL****");
|
||||
let start_col = line.closest_index_for_x(positions.start) as u32;
|
||||
if start_col < line_len || (is_empty && positions.start == line.width) {
|
||||
let start = DisplayPoint::new(row, start_col);
|
||||
dbg!("****END COL****");
|
||||
let end_col = line.closest_index_for_x(positions.end) as u32;
|
||||
let end = DisplayPoint::new(row, end_col);
|
||||
dbg!(start_col, end_col);
|
||||
|
||||
Some(Selection {
|
||||
id: post_inc(&mut self.next_selection_id),
|
||||
@@ -595,31 +592,32 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
self.select(selections)
|
||||
}
|
||||
|
||||
pub fn select_anchor_ranges<I: IntoIterator<Item = Range<Anchor>>>(&mut self, ranges: I) {
|
||||
todo!()
|
||||
// let buffer = self.buffer.read(self.cx).snapshot(self.cx);
|
||||
// let selections = ranges
|
||||
// .into_iter()
|
||||
// .map(|range| {
|
||||
// let mut start = range.start;
|
||||
// let mut end = range.end;
|
||||
// let reversed = if start.cmp(&end, &buffer).is_gt() {
|
||||
// mem::swap(&mut start, &mut end);
|
||||
// true
|
||||
// } else {
|
||||
// false
|
||||
// };
|
||||
// Selection {
|
||||
// id: post_inc(&mut self.collection.next_selection_id),
|
||||
// start,
|
||||
// end,
|
||||
// reversed,
|
||||
// goal: SelectionGoal::None,
|
||||
// }
|
||||
// })
|
||||
// .collect::<Vec<_>>();
|
||||
|
||||
// self.select_anchors(selections)
|
||||
pub fn select_anchor_ranges<I>(&mut self, ranges: I)
|
||||
where
|
||||
I: IntoIterator<Item = Range<Anchor>>,
|
||||
{
|
||||
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
|
||||
let selections = ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
let mut start = range.start;
|
||||
let mut end = range.end;
|
||||
let reversed = if start.cmp(&end, &buffer).is_gt() {
|
||||
mem::swap(&mut start, &mut end);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
Selection {
|
||||
id: post_inc(&mut self.collection.next_selection_id),
|
||||
start,
|
||||
end,
|
||||
reversed,
|
||||
goal: SelectionGoal::None,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
self.select_anchors(selections)
|
||||
}
|
||||
|
||||
pub fn new_selection_id(&mut self) -> usize {
|
||||
|
||||
@@ -27,7 +27,7 @@ pub fn marked_display_snapshot(
|
||||
let (unmarked_text, markers) = marked_text_offsets(text);
|
||||
|
||||
let font = cx.text_style().font();
|
||||
let font_size: Pixels = 14.into();
|
||||
let font_size: Pixels = 14usize.into();
|
||||
|
||||
let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
|
||||
let display_map = cx.build_model(|cx| DisplayMap::new(buffer, font, font_size, None, 1, 1, cx));
|
||||
|
||||
@@ -30,11 +30,11 @@ pub trait FeatureFlagViewExt<V: 'static> {
|
||||
|
||||
impl<V> FeatureFlagViewExt<V> for ViewContext<'_, V>
|
||||
where
|
||||
V: 'static + Send + Sync,
|
||||
V: 'static,
|
||||
{
|
||||
fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
|
||||
where
|
||||
F: Fn(bool, &mut V, &mut ViewContext<V>) + Send + Sync + 'static,
|
||||
F: Fn(bool, &mut V, &mut ViewContext<V>) + 'static,
|
||||
{
|
||||
self.observe_global::<FeatureFlags>(move |v, cx| {
|
||||
let feature_flags = cx.global::<FeatureFlags>();
|
||||
|
||||
44
crates/feedback2/Cargo.toml
Normal file
44
crates/feedback2/Cargo.toml
Normal file
@@ -0,0 +1,44 @@
|
||||
[package]
|
||||
name = "feedback2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/feedback2.rs"
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
client = { package = "client2", path = "../client2" }
|
||||
db = { package = "db2", path = "../db2" }
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
menu = { package = "menu2", path = "../menu2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
regex.workspace = true
|
||||
search = { package = "search2", path = "../search2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
util = { path = "../util" }
|
||||
workspace = { package = "workspace2", path = "../workspace2"}
|
||||
|
||||
log.workspace = true
|
||||
futures.workspace = true
|
||||
anyhow.workspace = true
|
||||
smallvec.workspace = true
|
||||
human_bytes = "0.4.1"
|
||||
isahc.workspace = true
|
||||
lazy_static.workspace = true
|
||||
postage.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
sysinfo.workspace = true
|
||||
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
|
||||
urlencoding = "2.1.2"
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
50
crates/feedback2/src/deploy_feedback_button.rs
Normal file
50
crates/feedback2/src/deploy_feedback_button.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use gpui::{AnyElement, Render, ViewContext, WeakView};
|
||||
use ui::{prelude::*, ButtonCommon, Icon, IconButton, Tooltip};
|
||||
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
||||
|
||||
use crate::{feedback_modal::FeedbackModal, GiveFeedback};
|
||||
|
||||
pub struct DeployFeedbackButton {
|
||||
workspace: WeakView<Workspace>,
|
||||
}
|
||||
|
||||
impl DeployFeedbackButton {
|
||||
pub fn new(workspace: &Workspace) -> Self {
|
||||
DeployFeedbackButton {
|
||||
workspace: workspace.weak_handle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DeployFeedbackButton {
|
||||
type Element = AnyElement;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let is_open = self
|
||||
.workspace
|
||||
.upgrade()
|
||||
.and_then(|workspace| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.active_modal::<FeedbackModal>(cx)
|
||||
})
|
||||
})
|
||||
.is_some();
|
||||
IconButton::new("give-feedback", Icon::Envelope)
|
||||
.style(ui::ButtonStyle::Subtle)
|
||||
.selected(is_open)
|
||||
.tooltip(|cx| Tooltip::text("Share Feedback", cx))
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(GiveFeedback));
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for DeployFeedbackButton {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
_item: Option<&dyn ItemHandle>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user