Compare commits
444 Commits
v0.72.2-pr
...
collab-v0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14497027d4 | ||
|
|
ae510c80db | ||
|
|
4179ed66a6 | ||
|
|
d173b1d412 | ||
|
|
4f4af55329 | ||
|
|
1e5aff9e51 | ||
|
|
ee154feda4 | ||
|
|
7163ba429b | ||
|
|
c832e4406e | ||
|
|
515724821e | ||
|
|
0867162c87 | ||
|
|
aba2914a31 | ||
|
|
246a6adab7 | ||
|
|
620890c411 | ||
|
|
0ec984f924 | ||
|
|
01bbf20962 | ||
|
|
996294ba67 | ||
|
|
ddf2f2cb0a | ||
|
|
bd4d7551a5 | ||
|
|
5097cf5cb7 | ||
|
|
13212d274e | ||
|
|
b9110c9268 | ||
|
|
b9573872e1 | ||
|
|
3ec71a742d | ||
|
|
50682dc685 | ||
|
|
2bca64f13b | ||
|
|
606d683f29 | ||
|
|
ff2e6bc3bd | ||
|
|
218f2fd0fe | ||
|
|
bb0257bbac | ||
|
|
929ebd7175 | ||
|
|
124aa74b03 | ||
|
|
a2f75eb031 | ||
|
|
6194c5df16 | ||
|
|
d14b684237 | ||
|
|
7a8cba0544 | ||
|
|
f1b5bf051a | ||
|
|
ad4201f768 | ||
|
|
75a9cfdabe | ||
|
|
3b6f66791f | ||
|
|
9311e01271 | ||
|
|
6d068e926b | ||
|
|
6854063d0b | ||
|
|
7ca0b38048 | ||
|
|
a598f0b13c | ||
|
|
eb6088701e | ||
|
|
24ba47e75d | ||
|
|
3dd5b3f426 | ||
|
|
9f86ca8574 | ||
|
|
bc2ea58c6a | ||
|
|
b343e8056a | ||
|
|
6a2a1303c4 | ||
|
|
a366ba19af | ||
|
|
70cb2fa8d7 | ||
|
|
f67c3f1f1d | ||
|
|
bde0456111 | ||
|
|
8734bd8435 | ||
|
|
34fbffb4cc | ||
|
|
368d2a73ea | ||
|
|
e7b56f6342 | ||
|
|
1deff43639 | ||
|
|
a890b8f3b7 | ||
|
|
7faa0da5c7 | ||
|
|
ff85bc6d42 | ||
|
|
b00e467ede | ||
|
|
2e1adb0724 | ||
|
|
269df10a16 | ||
|
|
8358efbd6c | ||
|
|
dc11d2726e | ||
|
|
41d3c5287b | ||
|
|
2198c295b3 | ||
|
|
6cf62a5b02 | ||
|
|
f8401394f5 | ||
|
|
b53d1eef71 | ||
|
|
c397fd9a71 | ||
|
|
9b8adecf05 | ||
|
|
e0f553c0f5 | ||
|
|
37a2ef9d41 | ||
|
|
89b93d4f6f | ||
|
|
2036fc48b5 | ||
|
|
cb3e873a67 | ||
|
|
da78abd99f | ||
|
|
637e8ada42 | ||
|
|
e3061066c9 | ||
|
|
514da604d7 | ||
|
|
b9811e48e4 | ||
|
|
fb69611568 | ||
|
|
a8a045e8bf | ||
|
|
59bd503696 | ||
|
|
fb7818f93c | ||
|
|
3fb426e8b2 | ||
|
|
f0a31f86c7 | ||
|
|
dc7fe72f18 | ||
|
|
b3dffeaf2a | ||
|
|
81cbefec22 | ||
|
|
4f9a07cffc | ||
|
|
184f37015a | ||
|
|
c9997a81a3 | ||
|
|
df798c1a7f | ||
|
|
465fcec36d | ||
|
|
40c2409b80 | ||
|
|
46dc347a1a | ||
|
|
f84046b74f | ||
|
|
8c51a62a8d | ||
|
|
794e6e22a6 | ||
|
|
504d88d56c | ||
|
|
94c76c45e6 | ||
|
|
f2d6a03dff | ||
|
|
3b19a409f8 | ||
|
|
7854f4a1ef | ||
|
|
6cb35536b3 | ||
|
|
161373710c | ||
|
|
11e2caff15 | ||
|
|
36ada13966 | ||
|
|
2c61eeb56d | ||
|
|
ccae9448d4 | ||
|
|
bb46b26494 | ||
|
|
098e6969f7 | ||
|
|
d910eed1f1 | ||
|
|
64b07dbfeb | ||
|
|
4f307c7601 | ||
|
|
23c967418a | ||
|
|
77ed437cda | ||
|
|
0b1334b8c5 | ||
|
|
cdc6566d87 | ||
|
|
36f3d3d738 | ||
|
|
27712c25ef | ||
|
|
68af726ee4 | ||
|
|
0ea7959ba4 | ||
|
|
bcb7b80517 | ||
|
|
10a30cf330 | ||
|
|
06a86162bb | ||
|
|
b986c38a31 | ||
|
|
69fd273367 | ||
|
|
8e828947fb | ||
|
|
2d8adf4c56 | ||
|
|
0b48e238f2 | ||
|
|
04495aa8cd | ||
|
|
5fea49e639 | ||
|
|
0704d9dcdb | ||
|
|
a57fcf5afc | ||
|
|
e910fd8493 | ||
|
|
d5123bc832 | ||
|
|
8656708de4 | ||
|
|
72197802a2 | ||
|
|
f8f1a3f86e | ||
|
|
2ec25bef84 | ||
|
|
89ddf14b0e | ||
|
|
be86cb35ba | ||
|
|
465d8cc2ff | ||
|
|
93b9e762ec | ||
|
|
fbc934b884 | ||
|
|
350b7b82f7 | ||
|
|
b179fc2b99 | ||
|
|
8860346324 | ||
|
|
9004640586 | ||
|
|
03498314fa | ||
|
|
ce4b672a14 | ||
|
|
3f9405f8f1 | ||
|
|
2276d25bdf | ||
|
|
ffe53bed87 | ||
|
|
37f910949d | ||
|
|
1e3b4f0387 | ||
|
|
e1df85e86d | ||
|
|
f6601f64e5 | ||
|
|
6ccc90327c | ||
|
|
bbeb33bc7e | ||
|
|
e74db2d180 | ||
|
|
74e0bed38f | ||
|
|
832549f1a3 | ||
|
|
b965333325 | ||
|
|
2be0283bf2 | ||
|
|
59a66190e5 | ||
|
|
9334267bd0 | ||
|
|
a0daf47134 | ||
|
|
9a729a2e64 | ||
|
|
1c636500de | ||
|
|
65a9ac449f | ||
|
|
bf5c3d963a | ||
|
|
c33d0f940a | ||
|
|
24e0a027ee | ||
|
|
d49e35f947 | ||
|
|
40aee8d7bc | ||
|
|
d33d27faa4 | ||
|
|
46ead28971 | ||
|
|
111aff29cc | ||
|
|
e2a2e40599 | ||
|
|
b73423daaa | ||
|
|
0324ca3b08 | ||
|
|
36040cd0e1 | ||
|
|
a07867d628 | ||
|
|
812145f9ab | ||
|
|
dbe5b0205c | ||
|
|
3d6c81584f | ||
|
|
81ece4fd44 | ||
|
|
2ec5c88f98 | ||
|
|
7b559176f1 | ||
|
|
d7305077bf | ||
|
|
4798b72cb8 | ||
|
|
71d8ead318 | ||
|
|
9b92a8e3fe | ||
|
|
7f4da80386 | ||
|
|
6a731233c5 | ||
|
|
b7cf426908 | ||
|
|
0dc92bec5c | ||
|
|
c75aca25b6 | ||
|
|
ae87961a77 | ||
|
|
e9464815e0 | ||
|
|
1ed47663ef | ||
|
|
dd02bc7748 | ||
|
|
e403b868b7 | ||
|
|
3105ecd0bd | ||
|
|
05e9615507 | ||
|
|
1abb7794cb | ||
|
|
50e681bbb1 | ||
|
|
3fb8395085 | ||
|
|
4513c40993 | ||
|
|
4ffc8cd9fd | ||
|
|
33c265d3cf | ||
|
|
58c41778e7 | ||
|
|
2592ec7265 | ||
|
|
d6462c611c | ||
|
|
28786a3c18 | ||
|
|
a5fd0250ab | ||
|
|
f68eda97fb | ||
|
|
99236f1875 | ||
|
|
bf8658067f | ||
|
|
c697c1a96a | ||
|
|
2b6aa3f5d1 | ||
|
|
e96d52f35a | ||
|
|
ed2f1ddd2d | ||
|
|
8dd249a7cd | ||
|
|
24fcad3fa2 | ||
|
|
46af9a90ce | ||
|
|
1c69e289b7 | ||
|
|
9d782be4c8 | ||
|
|
cae9e733a1 | ||
|
|
77c396a0ab | ||
|
|
b500ed3171 | ||
|
|
6b6e4e3bfe | ||
|
|
1683a54698 | ||
|
|
14488619a3 | ||
|
|
cf4e719484 | ||
|
|
8c3232bb9b | ||
|
|
ebf1da1de8 | ||
|
|
3564e95f27 | ||
|
|
ecf77a510a | ||
|
|
927f7b3363 | ||
|
|
07bb42898f | ||
|
|
a11165ad0a | ||
|
|
ab82e13167 | ||
|
|
0e0170712e | ||
|
|
8be844a13f | ||
|
|
7c98395e77 | ||
|
|
8922156923 | ||
|
|
bda37ffb9c | ||
|
|
2982a98d1c | ||
|
|
010eba509c | ||
|
|
56b7eb6b6f | ||
|
|
6551742c58 | ||
|
|
4bb986b3be | ||
|
|
efafd1d8d3 | ||
|
|
0981244797 | ||
|
|
159d3ab00c | ||
|
|
3fb6e31b92 | ||
|
|
04df00b221 | ||
|
|
dc6f7fd577 | ||
|
|
ac3e8f61ef | ||
|
|
fc811d14b1 | ||
|
|
cdf64b6cad | ||
|
|
3a7cfc3901 | ||
|
|
5e4d113308 | ||
|
|
de6eb00e2b | ||
|
|
76975c29a9 | ||
|
|
57a7ff9a6f | ||
|
|
eebce28b32 | ||
|
|
31dac39e34 | ||
|
|
5cfe206433 | ||
|
|
ff2fb06b2c | ||
|
|
a5ad2f544e | ||
|
|
7b291df21f | ||
|
|
6e33f33da1 | ||
|
|
4ea7a24b93 | ||
|
|
48b76f96fc | ||
|
|
c72a50e203 | ||
|
|
43f61ab413 | ||
|
|
cf83ecccbb | ||
|
|
848c6b78d5 | ||
|
|
b90fc046ca | ||
|
|
98b51634c4 | ||
|
|
28eb69e74e | ||
|
|
b03eebeb6c | ||
|
|
eac33d732e | ||
|
|
2d39358323 | ||
|
|
a4a179763a | ||
|
|
19b686ad65 | ||
|
|
ac882c7db5 | ||
|
|
baee6d0342 | ||
|
|
50ccf16de1 | ||
|
|
bef2013c7f | ||
|
|
2c904cb0bf | ||
|
|
33306846a6 | ||
|
|
30caeeaeb5 | ||
|
|
0ba051a754 | ||
|
|
32191e318e | ||
|
|
7037842bef | ||
|
|
8bd20d8c3a | ||
|
|
df1775326c | ||
|
|
df0715e7c9 | ||
|
|
e56dfd9177 | ||
|
|
afb375f909 | ||
|
|
bcf7a32284 | ||
|
|
5fbc9736e5 | ||
|
|
fbd23986e3 | ||
|
|
114eef8592 | ||
|
|
5df50e2fc9 | ||
|
|
7a667f390b | ||
|
|
2482a1a9ce | ||
|
|
7641965326 | ||
|
|
8db131a3a1 | ||
|
|
4f1e8c953e | ||
|
|
c2de0f6b5e | ||
|
|
17e8172dc3 | ||
|
|
8e9d95fefc | ||
|
|
3a7ac9c0ff | ||
|
|
88c6b890bc | ||
|
|
1012cea4af | ||
|
|
4a2b7e4820 | ||
|
|
6c0b35acb0 | ||
|
|
888fcb5b1b | ||
|
|
015b8db1c3 | ||
|
|
ebe1fa7a96 | ||
|
|
7be868e372 | ||
|
|
087d51634d | ||
|
|
ea663f3017 | ||
|
|
5041300b52 | ||
|
|
2c9199fd32 | ||
|
|
327932ba3b | ||
|
|
459060764a | ||
|
|
3d53336916 | ||
|
|
c1812ddc27 | ||
|
|
d80dba1fe3 | ||
|
|
6703264600 | ||
|
|
5ce147a2ad | ||
|
|
a32c0d1c9b | ||
|
|
e65c0810ba | ||
|
|
1fcfa5d272 | ||
|
|
addfcdc1f4 | ||
|
|
4501a5a7ee | ||
|
|
a120996f0d | ||
|
|
0a50d271b7 | ||
|
|
01a590a1fb | ||
|
|
d42d495cb0 | ||
|
|
187fac1579 | ||
|
|
0acb820f04 | ||
|
|
dda0febf39 | ||
|
|
9143790602 | ||
|
|
b31813fad3 | ||
|
|
0e238210bb | ||
|
|
912c396b37 | ||
|
|
436ab6e454 | ||
|
|
889b15683d | ||
|
|
135dcf19a2 | ||
|
|
5d23aaacc8 | ||
|
|
a789476c95 | ||
|
|
da5a6a8b4f | ||
|
|
76685406ed | ||
|
|
70eedbb48e | ||
|
|
42b5fa1fa3 | ||
|
|
7de04abdcb | ||
|
|
373e88e9fb | ||
|
|
c3a88857f9 | ||
|
|
f787f6054b | ||
|
|
6f342bb2c6 | ||
|
|
0ba44c6dc4 | ||
|
|
2ff82732b9 | ||
|
|
cbfdfa8124 | ||
|
|
57e10ce7c6 | ||
|
|
a9c2f42f70 | ||
|
|
83f9d51dee | ||
|
|
767d2f9766 | ||
|
|
3fb14d7caf | ||
|
|
952cdb4e98 | ||
|
|
bbe8297297 | ||
|
|
76c066baee | ||
|
|
654ee48feb | ||
|
|
ef16963772 | ||
|
|
37c052f53d | ||
|
|
582f5d0114 | ||
|
|
fd016b9bcd | ||
|
|
317eb7535c | ||
|
|
55589533e2 | ||
|
|
9a8585ce0c | ||
|
|
aa0a18968a | ||
|
|
0777f459ba | ||
|
|
2732cc2cbe | ||
|
|
e8dad56af9 | ||
|
|
87cf8ac60e | ||
|
|
f44658ad2a | ||
|
|
20377ea4e9 | ||
|
|
db2aaa4367 | ||
|
|
099b79910f | ||
|
|
fe25994fb3 | ||
|
|
7cef4a5d40 | ||
|
|
0c49030ade | ||
|
|
e15ffc8560 | ||
|
|
58987275fc | ||
|
|
9bff82f161 | ||
|
|
be0241bab1 | ||
|
|
de0b136be2 | ||
|
|
4e80ae13ec | ||
|
|
b020955ac4 | ||
|
|
a606058537 | ||
|
|
f065399799 | ||
|
|
926b59b15d | ||
|
|
2d6219ebe2 | ||
|
|
8228618b9e | ||
|
|
d4d9a142fc | ||
|
|
035901127a | ||
|
|
37bfeed2e6 | ||
|
|
4642817e72 | ||
|
|
83e21387af | ||
|
|
3e92e4d110 | ||
|
|
303216291b | ||
|
|
8be9d21340 | ||
|
|
9742bd7fd4 | ||
|
|
3014cc5299 | ||
|
|
d6b728409f | ||
|
|
433f284571 | ||
|
|
7270f950b8 | ||
|
|
ae15673dfd | ||
|
|
8697f81a37 | ||
|
|
21ded7639a | ||
|
|
3f95788d45 | ||
|
|
1afd6f859d | ||
|
|
2b0592da21 | ||
|
|
8f61134e7e | ||
|
|
888145ebed | ||
|
|
a50f0181fb | ||
|
|
62d32db66c | ||
|
|
d6962d957b | ||
|
|
fd2a9b3df9 | ||
|
|
460dc62888 | ||
|
|
e35db69dbd |
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -1,6 +1,6 @@
|
||||
## Description of feature or change
|
||||
|
||||
## Link to related issues from zed or insiders
|
||||
## Link to related issues from zed or community
|
||||
|
||||
## Before Merging
|
||||
|
||||
|
||||
23
.github/workflows/ci.yml
vendored
23
.github/workflows/ci.yml
vendored
@@ -17,6 +17,26 @@ env:
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
name: Check formatting
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
clean: false
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: cargo fmt
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
tests:
|
||||
name: Run tests
|
||||
runs-on:
|
||||
@@ -42,6 +62,9 @@ jobs:
|
||||
clean: false
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Run check
|
||||
run: cargo check --workspace
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --workspace --no-fail-fast
|
||||
|
||||
|
||||
17
.github/workflows/release_actions.yml
vendored
17
.github/workflows/release_actions.yml
vendored
@@ -13,23 +13,14 @@ jobs:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: |
|
||||
📣 Zed ${{ github.event.release.tag_name }} was just released!
|
||||
|
||||
|
||||
Restart your Zed or head to https://zed.dev/releases/latest to grab it.
|
||||
|
||||
|
||||
```md
|
||||
# Changelog
|
||||
|
||||
|
||||
${{ github.event.release.body }}
|
||||
```
|
||||
discourse_release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v2
|
||||
if: ${{ ! github.event.release.prerelease }}
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: script/discourse_release ${{ secrets.DISCOURSE_RELEASES_API_KEY }} ${{ github.event.release.tag_name }} ${{ github.event.release.body }}
|
||||
mixpanel_release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -40,7 +31,7 @@ jobs:
|
||||
architecture: "x64"
|
||||
cache: "pip"
|
||||
- run: pip install -r script/mixpanel_release/requirements.txt
|
||||
- run: >
|
||||
- run: >
|
||||
python script/mixpanel_release/main.py
|
||||
${{ github.event.release.tag_name }}
|
||||
${{ secrets.MIXPANEL_PROJECT_ID }}
|
||||
|
||||
147
Cargo.lock
generated
147
Cargo.lock
generated
@@ -259,6 +259,21 @@ dependencies = [
|
||||
"futures-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-global-executor"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"blocking",
|
||||
"futures-lite",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "1.12.0"
|
||||
@@ -350,6 +365,32 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-std"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-global-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"crossbeam-utils 0.8.14",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"gloo-timers",
|
||||
"kv-log-macro",
|
||||
"log",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"pin-project-lite 0.2.9",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
"wasm-bindgen-futures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
version = "0.3.3"
|
||||
@@ -371,6 +412,20 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-tar"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c49359998a76e32ef6e870dbc079ebad8f1e53e8441c5dd39d27b44493fe331"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"filetime",
|
||||
"libc",
|
||||
"pin-project",
|
||||
"redox_syscall",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "4.0.3"
|
||||
@@ -739,7 +794,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "bromberg_sl2"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/zed-industries/bromberg_sl2?rev=dac565a90e8f9245f48ff46225c915dc50f76920#dac565a90e8f9245f48ff46225c915dc50f76920"
|
||||
source = "git+https://github.com/zed-industries/bromberg_sl2?rev=950bc5482c216c395049ae33ae4501e08975f17f#950bc5482c216c395049ae33ae4501e08975f17f"
|
||||
dependencies = [
|
||||
"digest 0.9.0",
|
||||
"lazy_static",
|
||||
@@ -828,6 +883,7 @@ dependencies = [
|
||||
"media",
|
||||
"postage",
|
||||
"project",
|
||||
"settings",
|
||||
"util",
|
||||
]
|
||||
|
||||
@@ -1132,7 +1188,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.5.4"
|
||||
version = "0.6.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-tungstenite",
|
||||
@@ -1196,10 +1252,12 @@ name = "collab_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"auto_update",
|
||||
"call",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"context_menu",
|
||||
"editor",
|
||||
"futures 0.3.25",
|
||||
"fuzzy",
|
||||
@@ -1900,6 +1958,7 @@ dependencies = [
|
||||
"tree-sitter-html",
|
||||
"tree-sitter-javascript",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript 0.20.2",
|
||||
"unindent",
|
||||
"util",
|
||||
"workspace",
|
||||
@@ -2079,6 +2138,18 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.4.2"
|
||||
@@ -2527,6 +2598,18 @@ dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-timers"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "go_to_line"
|
||||
version = "0.1.0"
|
||||
@@ -3143,6 +3226,15 @@ dependencies = [
|
||||
"arrayvec 0.7.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kv-log-macro"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
|
||||
dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "language"
|
||||
version = "0.1.0"
|
||||
@@ -3160,6 +3252,7 @@ dependencies = [
|
||||
"fuzzy",
|
||||
"git",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"lsp",
|
||||
@@ -3186,7 +3279,7 @@ dependencies = [
|
||||
"tree-sitter-python",
|
||||
"tree-sitter-ruby",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript",
|
||||
"tree-sitter-typescript 0.20.1",
|
||||
"unicase",
|
||||
"unindent",
|
||||
"util",
|
||||
@@ -4499,6 +4592,7 @@ dependencies = [
|
||||
"lsp",
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"pulldown-cmark",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
@@ -6015,7 +6109,6 @@ dependencies = [
|
||||
"libsqlite3-sys",
|
||||
"parking_lot 0.11.2",
|
||||
"smol",
|
||||
"sqlez_macros",
|
||||
"thread_local",
|
||||
"uuid 1.2.2",
|
||||
]
|
||||
@@ -6914,7 +7007,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
version = "0.20.9"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter?rev=36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da#36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter?rev=c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14#c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"regex",
|
||||
@@ -7016,6 +7109,16 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-lua"
|
||||
version = "0.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d489873fd1a2fa6d5f04930bfc5c081c96f0c038c1437104518b5b842c69b282"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-markdown"
|
||||
version = "0.0.1"
|
||||
@@ -7092,6 +7195,24 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-typescript"
|
||||
version = "0.20.2"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter-typescript?rev=5d20856f34315b068c41edaee2ac8a100081d259#5d20856f34315b068c41edaee2ac8a100081d259"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-yaml"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/zed-industries/tree-sitter-yaml?rev=9050a4a4a847ed29e25485b1292a36eab8ae3492#9050a4a4a847ed29e25485b1292a36eab8ae3492"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.3"
|
||||
@@ -8193,6 +8314,15 @@ dependencies = [
|
||||
"winapi-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.8.4"
|
||||
@@ -8228,13 +8358,14 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.72.0"
|
||||
version = "0.77.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
"assets",
|
||||
"async-compression",
|
||||
"async-recursion 0.3.2",
|
||||
"async-tar",
|
||||
"async-trait",
|
||||
"auto_update",
|
||||
"backtrace",
|
||||
@@ -8312,6 +8443,7 @@ dependencies = [
|
||||
"tree-sitter-go",
|
||||
"tree-sitter-html",
|
||||
"tree-sitter-json 0.20.0",
|
||||
"tree-sitter-lua",
|
||||
"tree-sitter-markdown",
|
||||
"tree-sitter-python",
|
||||
"tree-sitter-racket",
|
||||
@@ -8319,7 +8451,8 @@ dependencies = [
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-scheme",
|
||||
"tree-sitter-toml",
|
||||
"tree-sitter-typescript",
|
||||
"tree-sitter-typescript 0.20.2",
|
||||
"tree-sitter-yaml",
|
||||
"unindent",
|
||||
"url",
|
||||
"urlencoding",
|
||||
|
||||
@@ -69,7 +69,7 @@ serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
|
||||
rand = { version = "0.8" }
|
||||
|
||||
[patch.crates-io]
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da" }
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" }
|
||||
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
|
||||
|
||||
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457
|
||||
|
||||
14
README.md
14
README.md
@@ -23,10 +23,18 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
|
||||
git clone https://github.com/zed-industries/zed.dev
|
||||
```
|
||||
|
||||
* Set up a local `zed` database and seed it with some initial users:
|
||||
* Initialize submodules
|
||||
|
||||
```
|
||||
script/bootstrap
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
* Set up a local `zed` database and seed it with some initial users:
|
||||
|
||||
Create a personal GitHub token to run `script/bootstrap` once successfully. Then delete that token.
|
||||
|
||||
```
|
||||
GITHUB_TOKEN=<$token> script/bootstrap
|
||||
```
|
||||
|
||||
### Testing against locally-running servers
|
||||
@@ -51,7 +59,7 @@ If you trigger `cmd-alt-i`, Zed will copy a JSON representation of the current w
|
||||
|
||||
### Licensing
|
||||
|
||||
We use `[cargo-about](https://github.com/EmbarkStudios/cargo-about)` to automatically comply with open source licenses. If CI is failing, check the following:
|
||||
We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following:
|
||||
|
||||
- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml.
|
||||
- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`.
|
||||
|
||||
3
assets/icons/ellipsis_14.svg
Normal file
3
assets/icons/ellipsis_14.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="4" viewBox="0 0 14 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.125 2C3.125 2.62132 2.62132 3.125 2 3.125C1.37868 3.125 0.875 2.62132 0.875 2C0.875 1.37868 1.37868 0.875 2 0.875C2.62132 0.875 3.125 1.37868 3.125 2ZM8.125 2C8.125 2.62132 7.62132 3.125 7 3.125C6.37868 3.125 5.875 2.62132 5.875 2C5.875 1.37868 6.37868 0.875 7 0.875C7.62132 0.875 8.125 1.37868 8.125 2ZM12 3.125C12.6213 3.125 13.125 2.62132 13.125 2C13.125 1.37868 12.6213 0.875 12 0.875C11.3787 0.875 10.875 1.37868 10.875 2C10.875 2.62132 11.3787 3.125 12 3.125Z" fill="#ABB2BF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 637 B |
3
assets/icons/leave_12.svg
Normal file
3
assets/icons/leave_12.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 1C0 0.585786 0.335786 0.25 0.75 0.25H7.25C7.66421 0.25 8 0.585786 8 1C8 1.41421 7.66421 1.75 7.25 1.75H1.5V10.25H7.25C7.66421 10.25 8 10.5858 8 11C8 11.4142 7.66421 11.75 7.25 11.75H0.75C0.335786 11.75 0 11.4142 0 11V1ZM8.78148 2.91435C9.10493 2.65559 9.57689 2.70803 9.83565 3.03148L11.8357 5.53148C12.0548 5.80539 12.0548 6.19461 11.8357 6.46852L9.83565 8.96852C9.57689 9.29197 9.10493 9.34441 8.78148 9.08565C8.45803 8.82689 8.40559 8.35493 8.66435 8.03148L9.68953 6.75H3.75C3.33579 6.75 3 6.41421 3 6C3 5.58579 3.33579 5.25 3.75 5.25H9.68953L8.66435 3.96852C8.40559 3.64507 8.45803 3.17311 8.78148 2.91435Z" fill="#ABB2BF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 784 B |
@@ -164,6 +164,7 @@
|
||||
"bindings": {
|
||||
"enter": "editor::Newline",
|
||||
"cmd-enter": "editor::NewlineBelow",
|
||||
"alt-z": "editor::ToggleSoftWrap",
|
||||
"cmd-f": [
|
||||
"buffer_search::Deploy",
|
||||
{
|
||||
@@ -227,7 +228,13 @@
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"cmd-/": "editor::ToggleComments",
|
||||
"cmd-k cmd-i": "editor::Hover",
|
||||
"cmd-/": [
|
||||
"editor::ToggleComments",
|
||||
{
|
||||
"advance_downwards": false
|
||||
}
|
||||
],
|
||||
"alt-up": "editor::SelectLargerSyntaxNode",
|
||||
"alt-down": "editor::SelectSmallerSyntaxNode",
|
||||
"cmd-u": "editor::UndoSelection",
|
||||
@@ -412,7 +419,7 @@
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
|
||||
"cmd-shift-c": "collab::ToggleCollaborationMenu",
|
||||
"cmd-shift-c": "collab::ToggleContactsMenu",
|
||||
"cmd-alt-i": "zed::DebugElements"
|
||||
}
|
||||
},
|
||||
@@ -433,8 +440,7 @@
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"shift-escape": "dock::FocusDock",
|
||||
"cmd-shift-b": "workspace::ToggleRightSidebar"
|
||||
"shift-escape": "dock::FocusDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -445,15 +451,16 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"shift-escape": "dock::HideDock"
|
||||
"cmd-escape": "dock::AddTabToDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"context": "Pane && docked",
|
||||
"bindings": {
|
||||
"cmd-escape": "dock::MoveActiveItemToDock"
|
||||
"shift-escape": "dock::HideDock",
|
||||
"cmd-escape": "dock::RemoveTabFromDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"h": "vim::Left",
|
||||
"backspace": "vim::Backspace",
|
||||
"j": "vim::Down",
|
||||
"enter": "vim::NextLineStart",
|
||||
"k": "vim::Up",
|
||||
"l": "vim::Right",
|
||||
"$": "vim::EndOfLine",
|
||||
@@ -233,7 +234,8 @@
|
||||
"escape": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
]
|
||||
],
|
||||
"d": "editor::GoToDefinition"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -315,7 +317,9 @@
|
||||
{
|
||||
"context": "Editor && VimWaiting",
|
||||
"bindings": {
|
||||
"*": "gpui::KeyPressed"
|
||||
"tab": "vim::Tab",
|
||||
"enter": "vim::Enter",
|
||||
"escape": "editor::Cancel"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -20,6 +20,8 @@
|
||||
// Whether to pop the completions menu while typing in an editor without
|
||||
// explicitly requesting it.
|
||||
"show_completions_on_input": true,
|
||||
// Whether the screen sharing icon is showed in the os status bar.
|
||||
"show_call_status_icon": true,
|
||||
// Whether new projects should start out 'online'. Online projects
|
||||
// appear in the contacts panel under your name, so that your contacts
|
||||
// can see which projects you are working on. Regardless of this
|
||||
@@ -49,6 +51,12 @@
|
||||
// 3. Position the dock full screen over the entire workspace"
|
||||
// "default_dock_anchor": "expanded"
|
||||
"default_dock_anchor": "right",
|
||||
// Whether or not to remove any trailing whitespace from lines of a buffer
|
||||
// before saving it.
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
// Whether or not to ensure there's a single newline at the end of a buffer
|
||||
// when saving it.
|
||||
"ensure_final_newline_on_save": true,
|
||||
// Whether or not to perform a buffer format before saving
|
||||
"format_on_save": "on",
|
||||
// How to perform a buffer format. This setting can take two values:
|
||||
@@ -81,13 +89,15 @@
|
||||
"hard_tabs": false,
|
||||
// How many columns a tab should occupy.
|
||||
"tab_size": 4,
|
||||
// Control what info Zed sends to our servers
|
||||
// Control what info is collected by Zed.
|
||||
"telemetry": {
|
||||
// Send debug info like crash reports.
|
||||
"diagnostics": true,
|
||||
// Send anonymized usage data like what languages you're using Zed with.
|
||||
"metrics": true
|
||||
},
|
||||
// Automatically update Zed
|
||||
"auto_update": true,
|
||||
// Git gutter behavior configuration.
|
||||
"git": {
|
||||
// Control whether the git gutter is shown. May take 2 values:
|
||||
@@ -219,6 +229,9 @@
|
||||
},
|
||||
"TSX": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"YAML": {
|
||||
"tab_size": 2
|
||||
}
|
||||
},
|
||||
// LSP Specific settings.
|
||||
|
||||
@@ -33,6 +33,19 @@ struct LspStatus {
|
||||
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,
|
||||
action: Option<Box<dyn Action>>,
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(ActivityIndicator::show_error_message);
|
||||
cx.add_action(ActivityIndicator::dismiss_error_message);
|
||||
@@ -69,6 +82,8 @@ impl ActivityIndicator {
|
||||
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(),
|
||||
@@ -130,7 +145,7 @@ impl ActivityIndicator {
|
||||
fn pending_language_server_work<'a>(
|
||||
&self,
|
||||
cx: &'a AppContext,
|
||||
) -> impl Iterator<Item = (&'a str, &'a str, &'a LanguageServerProgress)> {
|
||||
) -> impl Iterator<Item = PendingWork<'a>> {
|
||||
self.project
|
||||
.read(cx)
|
||||
.language_server_statuses()
|
||||
@@ -142,23 +157,29 @@ impl ActivityIndicator {
|
||||
let mut pending_work = status
|
||||
.pending_work
|
||||
.iter()
|
||||
.map(|(token, progress)| (status.name.as_str(), token.as_str(), progress))
|
||||
.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(|(_, _, progress)| Reverse(progress.last_update_at));
|
||||
pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
|
||||
Some(pending_work)
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn content_to_render(
|
||||
&mut self,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> (Option<&'static str>, String, Option<Box<dyn Action>>) {
|
||||
fn content_to_render(&mut self, cx: &mut RenderContext<Self>) -> Content {
|
||||
// Show any language server has pending activity.
|
||||
let mut pending_work = self.pending_language_server_work(cx);
|
||||
if let Some((lang_server_name, progress_token, progress)) = pending_work.next() {
|
||||
let mut message = lang_server_name.to_string();
|
||||
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() {
|
||||
@@ -176,7 +197,11 @@ impl ActivityIndicator {
|
||||
write!(&mut message, " + {} more", additional_work_count).unwrap();
|
||||
}
|
||||
|
||||
return (None, message, None);
|
||||
return Content {
|
||||
icon: None,
|
||||
message,
|
||||
action: None,
|
||||
};
|
||||
}
|
||||
|
||||
// Show any language server installation info.
|
||||
@@ -199,19 +224,19 @@ impl ActivityIndicator {
|
||||
}
|
||||
|
||||
if !downloading.is_empty() {
|
||||
return (
|
||||
Some(DOWNLOAD_ICON),
|
||||
format!(
|
||||
return Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: format!(
|
||||
"Downloading {} language server{}...",
|
||||
downloading.join(", "),
|
||||
if downloading.len() > 1 { "s" } else { "" }
|
||||
),
|
||||
None,
|
||||
);
|
||||
action: None,
|
||||
};
|
||||
} else if !checking_for_update.is_empty() {
|
||||
return (
|
||||
Some(DOWNLOAD_ICON),
|
||||
format!(
|
||||
return Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: format!(
|
||||
"Checking for updates to {} language server{}...",
|
||||
checking_for_update.join(", "),
|
||||
if checking_for_update.len() > 1 {
|
||||
@@ -220,49 +245,61 @@ impl ActivityIndicator {
|
||||
""
|
||||
}
|
||||
),
|
||||
None,
|
||||
);
|
||||
action: None,
|
||||
};
|
||||
} else if !failed.is_empty() {
|
||||
return (
|
||||
Some(WARNING_ICON),
|
||||
format!(
|
||||
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 { "" }
|
||||
),
|
||||
Some(Box::new(ShowErrorMessage)),
|
||||
);
|
||||
action: Some(Box::new(ShowErrorMessage)),
|
||||
};
|
||||
}
|
||||
|
||||
// Show any application auto-update info.
|
||||
if let Some(updater) = &self.auto_updater {
|
||||
match &updater.read(cx).status() {
|
||||
AutoUpdateStatus::Checking => (
|
||||
Some(DOWNLOAD_ICON),
|
||||
"Checking for Zed updates…".to_string(),
|
||||
None,
|
||||
),
|
||||
AutoUpdateStatus::Downloading => (
|
||||
Some(DOWNLOAD_ICON),
|
||||
"Downloading Zed update…".to_string(),
|
||||
None,
|
||||
),
|
||||
AutoUpdateStatus::Installing => (
|
||||
Some(DOWNLOAD_ICON),
|
||||
"Installing Zed update…".to_string(),
|
||||
None,
|
||||
),
|
||||
AutoUpdateStatus::Updated => (None, "Restart to update Zed".to_string(), None),
|
||||
AutoUpdateStatus::Errored => (
|
||||
Some(WARNING_ICON),
|
||||
"Auto update failed".to_string(),
|
||||
Some(Box::new(DismissErrorMessage)),
|
||||
),
|
||||
return match &updater.read(cx).status() {
|
||||
AutoUpdateStatus::Checking => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Checking for Zed updates…".to_string(),
|
||||
action: None,
|
||||
},
|
||||
AutoUpdateStatus::Downloading => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Downloading Zed update…".to_string(),
|
||||
action: None,
|
||||
},
|
||||
AutoUpdateStatus::Installing => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Installing Zed update…".to_string(),
|
||||
action: None,
|
||||
},
|
||||
AutoUpdateStatus::Updated => Content {
|
||||
icon: None,
|
||||
message: "Click to restart and update Zed".to_string(),
|
||||
action: Some(Box::new(workspace::Restart)),
|
||||
},
|
||||
AutoUpdateStatus::Errored => Content {
|
||||
icon: Some(WARNING_ICON),
|
||||
message: "Auto update failed".to_string(),
|
||||
action: Some(Box::new(DismissErrorMessage)),
|
||||
},
|
||||
AutoUpdateStatus::Idle => Default::default(),
|
||||
}
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
|
||||
return Content {
|
||||
icon: None,
|
||||
message: most_recent_active_task.to_string(),
|
||||
action: None,
|
||||
};
|
||||
}
|
||||
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,7 +313,11 @@ impl View for ActivityIndicator {
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let (icon, message, action) = self.content_to_render(cx);
|
||||
let Content {
|
||||
icon,
|
||||
message,
|
||||
action,
|
||||
} = self.content_to_render(cx);
|
||||
|
||||
let mut element = MouseEventHandler::<Self>::new(0, cx, |state, cx| {
|
||||
let theme = &cx
|
||||
|
||||
@@ -9,6 +9,7 @@ use gpui::{
|
||||
MutableAppContext, Task, WeakViewHandle,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use smol::{fs::File, io::AsyncReadExt, process::Command};
|
||||
use std::{ffi::OsString, sync::Arc, time::Duration};
|
||||
use update_notification::UpdateNotification;
|
||||
@@ -53,7 +54,23 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut Mutab
|
||||
let server_url = server_url;
|
||||
let auto_updater = cx.add_model(|cx| {
|
||||
let updater = AutoUpdater::new(version, http_client, server_url.clone());
|
||||
updater.start_polling(cx).detach();
|
||||
|
||||
let mut update_subscription = cx
|
||||
.global::<Settings>()
|
||||
.auto_update
|
||||
.then(|| updater.start_polling(cx));
|
||||
|
||||
cx.observe_global::<Settings, _>(move |updater, cx| {
|
||||
if cx.global::<Settings>().auto_update {
|
||||
if update_subscription.is_none() {
|
||||
*(&mut update_subscription) = Some(updater.start_polling(cx))
|
||||
}
|
||||
} else {
|
||||
(&mut update_subscription).take();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
updater
|
||||
});
|
||||
cx.set_global(Some(auto_updater));
|
||||
|
||||
@@ -78,7 +78,7 @@ impl View for UpdateNotification {
|
||||
)
|
||||
.with_child({
|
||||
let style = theme.action_message.style_for(state, false);
|
||||
Text::new("View the release notes".to_string(), style.text.clone())
|
||||
Text::new("View the release notes", style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
|
||||
@@ -47,7 +47,7 @@ impl View for Breadcrumbs {
|
||||
{
|
||||
Flex::row()
|
||||
.with_children(Itertools::intersperse_with(breadcrumbs.into_iter(), || {
|
||||
Label::new(" 〉 ".to_string(), theme.breadcrumbs.text.clone()).boxed()
|
||||
Label::new(" 〉 ", theme.breadcrumbs.text.clone()).boxed()
|
||||
}))
|
||||
.contained()
|
||||
.with_style(theme.breadcrumbs.container)
|
||||
|
||||
@@ -28,6 +28,7 @@ fs = { path = "../fs" }
|
||||
language = { path = "../language" }
|
||||
media = { path = "../media" }
|
||||
project = { path = "../project" }
|
||||
settings = { path = "../settings" }
|
||||
util = { path = "../util" }
|
||||
|
||||
anyhow = "1.0.38"
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
pub mod participant;
|
||||
pub mod room;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::{proto, Client, TypedEnvelope, User, UserStore};
|
||||
use collections::HashSet;
|
||||
use futures::{future::Shared, FutureExt};
|
||||
use postage::watch;
|
||||
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
|
||||
Subscription, Task, WeakModelHandle,
|
||||
};
|
||||
pub use participant::ParticipantLocation;
|
||||
use postage::watch;
|
||||
use project::Project;
|
||||
|
||||
pub use participant::ParticipantLocation;
|
||||
pub use room::Room;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn init(client: Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut MutableAppContext) {
|
||||
let active_call = cx.add_model(|cx| ActiveCall::new(client, user_store, cx));
|
||||
@@ -27,8 +31,10 @@ pub struct IncomingCall {
|
||||
pub initial_project: Option<proto::ParticipantProject>,
|
||||
}
|
||||
|
||||
/// Singleton global maintaining the user's participation in a room across workspaces.
|
||||
pub struct ActiveCall {
|
||||
room: Option<(ModelHandle<Room>, Vec<Subscription>)>,
|
||||
pending_room_creation: Option<Shared<Task<Result<ModelHandle<Room>, Arc<anyhow::Error>>>>>,
|
||||
location: Option<WeakModelHandle<Project>>,
|
||||
pending_invites: HashSet<u64>,
|
||||
incoming_call: (
|
||||
@@ -52,6 +58,7 @@ impl ActiveCall {
|
||||
) -> Self {
|
||||
Self {
|
||||
room: None,
|
||||
pending_room_creation: None,
|
||||
location: None,
|
||||
pending_invites: Default::default(),
|
||||
incoming_call: watch::channel(),
|
||||
@@ -120,45 +127,74 @@ impl ActiveCall {
|
||||
initial_project: Option<ModelHandle<Project>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
if !self.pending_invites.insert(called_user_id) {
|
||||
return Task::ready(Err(anyhow!("user was already invited")));
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let invite = async {
|
||||
if let Some(room) = this.read_with(&cx, |this, _| this.room().cloned()) {
|
||||
let initial_project_id = if let Some(initial_project) = initial_project {
|
||||
Some(
|
||||
room.update(&mut cx, |room, cx| {
|
||||
room.share_project(initial_project, cx)
|
||||
})
|
||||
|
||||
let room = if let Some(room) = self.room().cloned() {
|
||||
Some(Task::ready(Ok(room)).shared())
|
||||
} else {
|
||||
self.pending_room_creation.clone()
|
||||
};
|
||||
|
||||
let invite = if let Some(room) = room {
|
||||
cx.spawn_weak(|_, mut cx| async move {
|
||||
let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
|
||||
|
||||
let initial_project_id = if let Some(initial_project) = initial_project {
|
||||
Some(
|
||||
room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
room.update(&mut cx, |room, cx| {
|
||||
room.call(called_user_id, initial_project_id, cx)
|
||||
})
|
||||
.await?;
|
||||
)
|
||||
} else {
|
||||
let room = cx
|
||||
.update(|cx| {
|
||||
Room::create(called_user_id, initial_project, client, user_store, cx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| this.set_room(Some(room), cx))
|
||||
.await?;
|
||||
None
|
||||
};
|
||||
|
||||
Ok(())
|
||||
};
|
||||
room.update(&mut cx, |room, cx| {
|
||||
room.call(called_user_id, initial_project_id, cx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
} else {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let room = cx
|
||||
.spawn(|this, mut cx| async move {
|
||||
let create_room = async {
|
||||
let room = cx
|
||||
.update(|cx| {
|
||||
Room::create(
|
||||
called_user_id,
|
||||
initial_project,
|
||||
client,
|
||||
user_store,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(room)
|
||||
};
|
||||
|
||||
let room = create_room.await;
|
||||
this.update(&mut cx, |this, _| this.pending_room_creation = None);
|
||||
room.map_err(Arc::new)
|
||||
})
|
||||
.shared();
|
||||
self.pending_room_creation = Some(room.clone());
|
||||
cx.foreground().spawn(async move {
|
||||
room.await.map_err(|err| anyhow!("{:?}", err))?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
};
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let result = invite.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.pending_invites.remove(&called_user_id);
|
||||
@@ -248,6 +284,18 @@ impl ActiveCall {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unshare_project(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
room.update(cx, |room, cx| room.unshare_project(project, cx))
|
||||
} else {
|
||||
Err(anyhow!("no active call"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_location(
|
||||
&mut self,
|
||||
project: Option<&ModelHandle<Project>>,
|
||||
|
||||
@@ -20,7 +20,7 @@ use project::Project;
|
||||
use std::{mem, sync::Arc, time::Duration};
|
||||
use util::{post_inc, ResultExt, TryFutureExt};
|
||||
|
||||
pub const RECONNECT_TIMEOUT: Duration = client::RECEIVE_TIMEOUT;
|
||||
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Event {
|
||||
@@ -55,6 +55,7 @@ pub struct Room {
|
||||
leave_when_empty: bool,
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec<PeerId>>,
|
||||
subscriptions: Vec<client::Subscription>,
|
||||
pending_room_update: Option<Task<()>>,
|
||||
maintain_connection: Option<Task<Option<()>>>,
|
||||
@@ -148,6 +149,7 @@ impl Room {
|
||||
pending_room_update: None,
|
||||
client,
|
||||
user_store,
|
||||
follows_by_leader_id_project_id: Default::default(),
|
||||
maintain_connection: Some(maintain_connection),
|
||||
}
|
||||
}
|
||||
@@ -275,14 +277,12 @@ impl Room {
|
||||
) -> Result<()> {
|
||||
let mut client_status = client.status();
|
||||
loop {
|
||||
let is_connected = client_status
|
||||
.next()
|
||||
.await
|
||||
.map_or(false, |s| s.is_connected());
|
||||
|
||||
let _ = client_status.try_recv();
|
||||
let is_connected = client_status.borrow().is_connected();
|
||||
// Even if we're initially connected, any future change of the status means we momentarily disconnected.
|
||||
if !is_connected || client_status.next().await.is_some() {
|
||||
log::info!("detected client disconnection");
|
||||
|
||||
this.upgrade(&cx)
|
||||
.ok_or_else(|| anyhow!("room was dropped"))?
|
||||
.update(&mut cx, |this, cx| {
|
||||
@@ -296,12 +296,7 @@ impl Room {
|
||||
let client_reconnection = async {
|
||||
let mut remaining_attempts = 3;
|
||||
while remaining_attempts > 0 {
|
||||
log::info!(
|
||||
"waiting for client status change, remaining attempts {}",
|
||||
remaining_attempts
|
||||
);
|
||||
let Some(status) = client_status.next().await else { break };
|
||||
if status.is_connected() {
|
||||
if client_status.borrow().is_connected() {
|
||||
log::info!("client reconnected, attempting to rejoin room");
|
||||
|
||||
let Some(this) = this.upgrade(&cx) else { break };
|
||||
@@ -315,7 +310,15 @@ impl Room {
|
||||
} else {
|
||||
remaining_attempts -= 1;
|
||||
}
|
||||
} else if client_status.borrow().is_signed_out() {
|
||||
return false;
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"waiting for client status change, remaining attempts {}",
|
||||
remaining_attempts
|
||||
);
|
||||
client_status.next().await;
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -337,18 +340,20 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
// The client failed to re-establish a connection to the server
|
||||
// or an error occurred while trying to re-join the room. Either way
|
||||
// we leave the room and return an error.
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
log::info!("reconnection failed, leaving room");
|
||||
let _ = this.update(&mut cx, |this, cx| this.leave(cx));
|
||||
}
|
||||
return Err(anyhow!(
|
||||
"can't reconnect to room: client failed to re-establish connection"
|
||||
));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// The client failed to re-establish a connection to the server
|
||||
// or an error occurred while trying to re-join the room. Either way
|
||||
// we leave the room and return an error.
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
log::info!("reconnection failed, leaving room");
|
||||
let _ = this.update(&mut cx, |this, cx| this.leave(cx));
|
||||
}
|
||||
Err(anyhow!(
|
||||
"can't reconnect to room: client failed to re-establish connection"
|
||||
))
|
||||
}
|
||||
|
||||
fn rejoin(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
@@ -457,6 +462,12 @@ impl Room {
|
||||
self.participant_user_ids.contains(&user_id)
|
||||
}
|
||||
|
||||
pub fn followers_for(&self, leader_id: PeerId, project_id: u64) -> &[PeerId] {
|
||||
self.follows_by_leader_id_project_id
|
||||
.get(&(leader_id, project_id))
|
||||
.map_or(&[], |v| v.as_slice())
|
||||
}
|
||||
|
||||
async fn handle_room_updated(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::RoomUpdated>,
|
||||
@@ -487,11 +498,13 @@ impl Room {
|
||||
.iter()
|
||||
.map(|p| p.user_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let remote_participant_user_ids = room
|
||||
.participants
|
||||
.iter()
|
||||
.map(|p| p.user_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (remote_participants, pending_participants) =
|
||||
self.user_store.update(cx, move |user_store, cx| {
|
||||
(
|
||||
@@ -499,6 +512,7 @@ impl Room {
|
||||
user_store.get_users(pending_participant_user_ids, cx),
|
||||
)
|
||||
});
|
||||
|
||||
self.pending_room_update = Some(cx.spawn(|this, mut cx| async move {
|
||||
let (remote_participants, pending_participants) =
|
||||
futures::join!(remote_participants, pending_participants);
|
||||
@@ -620,6 +634,27 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
this.follows_by_leader_id_project_id.clear();
|
||||
for follower in room.followers {
|
||||
let project_id = follower.project_id;
|
||||
let (leader, follower) = match (follower.leader_id, follower.follower_id) {
|
||||
(Some(leader), Some(follower)) => (leader, follower),
|
||||
|
||||
_ => {
|
||||
log::error!("Follower message {follower:?} missing some state");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let list = this
|
||||
.follows_by_leader_id_project_id
|
||||
.entry((leader, project_id))
|
||||
.or_insert(Vec::new());
|
||||
if !list.contains(&follower) {
|
||||
list.push(follower);
|
||||
}
|
||||
}
|
||||
|
||||
this.pending_room_update.take();
|
||||
if this.should_leave() {
|
||||
log::info!("room is empty, leaving");
|
||||
@@ -793,6 +828,20 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn unshare_project(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
let project_id = match project.read(cx).remote_id() {
|
||||
Some(project_id) => project_id,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
self.client.send(proto::UnshareProject { project_id })?;
|
||||
project.update(cx, |this, cx| this.unshare(cx))
|
||||
}
|
||||
|
||||
pub(crate) fn set_location(
|
||||
&mut self,
|
||||
project: Option<&ModelHandle<Project>>,
|
||||
|
||||
@@ -66,7 +66,7 @@ pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
|
||||
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
|
||||
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
actions!(client, [Authenticate]);
|
||||
actions!(client, [Authenticate, SignOut]);
|
||||
|
||||
pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
|
||||
cx.add_global_action({
|
||||
@@ -79,6 +79,16 @@ pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
cx.add_global_action({
|
||||
let client = client.clone();
|
||||
move |_: &SignOut, cx| {
|
||||
let client = client.clone();
|
||||
cx.spawn(|cx| async move {
|
||||
client.disconnect(&cx);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub struct Client {
|
||||
@@ -169,6 +179,10 @@ impl Status {
|
||||
pub fn is_connected(&self) -> bool {
|
||||
matches!(self, Self::Connected { .. })
|
||||
}
|
||||
|
||||
pub fn is_signed_out(&self) -> bool {
|
||||
matches!(self, Self::SignedOut | Self::UpgradeRequired)
|
||||
}
|
||||
}
|
||||
|
||||
struct ClientState {
|
||||
@@ -1152,11 +1166,9 @@ impl Client {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) -> Result<()> {
|
||||
let conn_id = self.connection_id()?;
|
||||
self.peer.disconnect(conn_id);
|
||||
pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) {
|
||||
self.peer.teardown();
|
||||
self.set_status(Status::SignedOut, cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn connection_id(&self) -> Result<ConnectionId> {
|
||||
@@ -1324,6 +1336,10 @@ impl Client {
|
||||
pub fn metrics_id(&self) -> Option<Arc<str>> {
|
||||
self.telemetry.metrics_id()
|
||||
}
|
||||
|
||||
pub fn is_staff(&self) -> Option<bool> {
|
||||
self.telemetry.is_staff()
|
||||
}
|
||||
}
|
||||
|
||||
impl WeakSubscriber {
|
||||
|
||||
@@ -9,7 +9,7 @@ pub use isahc::{
|
||||
Error,
|
||||
};
|
||||
use smol::future::FutureExt;
|
||||
use std::sync::Arc;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
pub use url::Url;
|
||||
|
||||
pub type Request = isahc::Request<AsyncBody>;
|
||||
@@ -41,7 +41,13 @@ pub trait HttpClient: Send + Sync {
|
||||
}
|
||||
|
||||
pub fn client() -> Arc<dyn HttpClient> {
|
||||
Arc::new(isahc::HttpClient::builder().build().unwrap())
|
||||
Arc::new(
|
||||
isahc::HttpClient::builder()
|
||||
.connect_timeout(Duration::from_secs(5))
|
||||
.low_speed_timeout(100, Duration::from_secs(5))
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
impl HttpClient for isahc::HttpClient {
|
||||
|
||||
@@ -40,6 +40,7 @@ struct TelemetryState {
|
||||
next_event_id: usize,
|
||||
flush_task: Option<Task<()>>,
|
||||
log_file: Option<NamedTempFile>,
|
||||
is_staff: Option<bool>,
|
||||
}
|
||||
|
||||
const MIXPANEL_EVENTS_URL: &'static str = "https://api.mixpanel.com/track";
|
||||
@@ -125,6 +126,7 @@ impl Telemetry {
|
||||
flush_task: Default::default(),
|
||||
next_event_id: 0,
|
||||
log_file: None,
|
||||
is_staff: None,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -202,6 +204,7 @@ impl Telemetry {
|
||||
let device_id = state.device_id.clone();
|
||||
let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
|
||||
state.metrics_id = metrics_id.clone();
|
||||
state.is_staff = Some(is_staff);
|
||||
drop(state);
|
||||
|
||||
if let Some((token, device_id)) = MIXPANEL_TOKEN.as_ref().zip(device_id) {
|
||||
@@ -282,6 +285,10 @@ impl Telemetry {
|
||||
self.state.lock().metrics_id.clone()
|
||||
}
|
||||
|
||||
pub fn is_staff(self: &Arc<Self>) -> Option<bool> {
|
||||
self.state.lock().is_staff
|
||||
}
|
||||
|
||||
fn flush(self: &Arc<Self>) {
|
||||
let mut state = self.state.lock();
|
||||
let mut events = mem::take(&mut state.queue);
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.5.4"
|
||||
version = "0.6.2"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
|
||||
@@ -143,3 +143,17 @@ CREATE TABLE "servers" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"environment" VARCHAR NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "followers" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"room_id" INTEGER NOT NULL REFERENCES rooms (id) ON DELETE CASCADE,
|
||||
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
|
||||
"leader_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
|
||||
"leader_connection_id" INTEGER NOT NULL,
|
||||
"follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
|
||||
"follower_connection_id" INTEGER NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX
|
||||
"index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id"
|
||||
ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id");
|
||||
CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");
|
||||
|
||||
15
crates/collab/migrations/20230202155735_followers.sql
Normal file
15
crates/collab/migrations/20230202155735_followers.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE IF NOT EXISTS "followers" (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"room_id" INTEGER NOT NULL REFERENCES rooms (id) ON DELETE CASCADE,
|
||||
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
|
||||
"leader_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
|
||||
"leader_connection_id" INTEGER NOT NULL,
|
||||
"follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
|
||||
"follower_connection_id" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX
|
||||
"index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id"
|
||||
ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id");
|
||||
|
||||
CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");
|
||||
@@ -1,5 +1,6 @@
|
||||
mod access_token;
|
||||
mod contact;
|
||||
mod follower;
|
||||
mod language_server;
|
||||
mod project;
|
||||
mod project_collaborator;
|
||||
@@ -157,7 +158,7 @@ impl Database {
|
||||
room_id: RoomId,
|
||||
new_server_id: ServerId,
|
||||
) -> Result<RoomGuard<RefreshedRoom>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let stale_participant_filter = Condition::all()
|
||||
.add(room_participant::Column::RoomId.eq(room_id))
|
||||
.add(room_participant::Column::AnsweringConnectionId.is_not_null())
|
||||
@@ -190,17 +191,18 @@ impl Database {
|
||||
.filter(room_participant::Column::RoomId.eq(room_id))
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
project::Entity::delete_many()
|
||||
.filter(project::Column::RoomId.eq(room_id))
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
room::Entity::delete_by_id(room_id).exec(&*tx).await?;
|
||||
}
|
||||
|
||||
Ok((
|
||||
room_id,
|
||||
RefreshedRoom {
|
||||
room,
|
||||
stale_participant_user_ids,
|
||||
canceled_calls_to_user_ids,
|
||||
},
|
||||
))
|
||||
Ok(RefreshedRoom {
|
||||
room,
|
||||
stale_participant_user_ids,
|
||||
canceled_calls_to_user_ids,
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -1129,18 +1131,16 @@ impl Database {
|
||||
user_id: UserId,
|
||||
connection: ConnectionId,
|
||||
live_kit_room: &str,
|
||||
) -> Result<RoomGuard<proto::Room>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
) -> Result<proto::Room> {
|
||||
self.transaction(|tx| async move {
|
||||
let room = room::ActiveModel {
|
||||
live_kit_room: ActiveValue::set(live_kit_room.into()),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&*tx)
|
||||
.await?;
|
||||
let room_id = room.id;
|
||||
|
||||
room_participant::ActiveModel {
|
||||
room_id: ActiveValue::set(room_id),
|
||||
room_id: ActiveValue::set(room.id),
|
||||
user_id: ActiveValue::set(user_id),
|
||||
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
||||
answering_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||
@@ -1157,8 +1157,8 @@ impl Database {
|
||||
.insert(&*tx)
|
||||
.await?;
|
||||
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
Ok((room_id, room))
|
||||
let room = self.get_room(room.id, &tx).await?;
|
||||
Ok(room)
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -1171,7 +1171,7 @@ impl Database {
|
||||
called_user_id: UserId,
|
||||
initial_project_id: Option<ProjectId>,
|
||||
) -> Result<RoomGuard<(proto::Room, proto::IncomingCall)>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
room_participant::ActiveModel {
|
||||
room_id: ActiveValue::set(room_id),
|
||||
user_id: ActiveValue::set(called_user_id),
|
||||
@@ -1190,7 +1190,7 @@ impl Database {
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
let incoming_call = Self::build_incoming_call(&room, called_user_id)
|
||||
.ok_or_else(|| anyhow!("failed to build incoming call"))?;
|
||||
Ok((room_id, (room, incoming_call)))
|
||||
Ok((room, incoming_call))
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -1200,7 +1200,7 @@ impl Database {
|
||||
room_id: RoomId,
|
||||
called_user_id: UserId,
|
||||
) -> Result<RoomGuard<proto::Room>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
room_participant::Entity::delete_many()
|
||||
.filter(
|
||||
room_participant::Column::RoomId
|
||||
@@ -1210,7 +1210,7 @@ impl Database {
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
Ok((room_id, room))
|
||||
Ok(room)
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -1257,7 +1257,7 @@ impl Database {
|
||||
calling_connection: ConnectionId,
|
||||
called_user_id: UserId,
|
||||
) -> Result<RoomGuard<proto::Room>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let participant = room_participant::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
@@ -1276,14 +1276,13 @@ impl Database {
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no call to cancel"))?;
|
||||
let room_id = participant.room_id;
|
||||
|
||||
room_participant::Entity::delete(participant.into_active_model())
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
Ok((room_id, room))
|
||||
Ok(room)
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -1294,7 +1293,7 @@ impl Database {
|
||||
user_id: UserId,
|
||||
connection: ConnectionId,
|
||||
) -> Result<RoomGuard<proto::Room>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let result = room_participant::Entity::update_many()
|
||||
.filter(
|
||||
Condition::all()
|
||||
@@ -1316,7 +1315,7 @@ impl Database {
|
||||
Err(anyhow!("room does not exist or was already joined"))?
|
||||
} else {
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
Ok((room_id, room))
|
||||
Ok(room)
|
||||
}
|
||||
})
|
||||
.await
|
||||
@@ -1328,9 +1327,9 @@ impl Database {
|
||||
user_id: UserId,
|
||||
connection: ConnectionId,
|
||||
) -> Result<RoomGuard<RejoinedRoom>> {
|
||||
self.room_transaction(|tx| async {
|
||||
let room_id = RoomId::from_proto(rejoin_room.id);
|
||||
self.room_transaction(room_id, |tx| async {
|
||||
let tx = tx;
|
||||
let room_id = RoomId::from_proto(rejoin_room.id);
|
||||
let participant_update = room_participant::Entity::update_many()
|
||||
.filter(
|
||||
Condition::all()
|
||||
@@ -1549,14 +1548,11 @@ impl Database {
|
||||
}
|
||||
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
Ok((
|
||||
room_id,
|
||||
RejoinedRoom {
|
||||
room,
|
||||
rejoined_projects,
|
||||
reshared_projects,
|
||||
},
|
||||
))
|
||||
Ok(RejoinedRoom {
|
||||
room,
|
||||
rejoined_projects,
|
||||
reshared_projects,
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -1717,13 +1713,75 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn follow(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
leader_connection: ConnectionId,
|
||||
follower_connection: ConnectionId,
|
||||
) -> Result<RoomGuard<proto::Room>> {
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
follower::ActiveModel {
|
||||
room_id: ActiveValue::set(room_id),
|
||||
project_id: ActiveValue::set(project_id),
|
||||
leader_connection_server_id: ActiveValue::set(ServerId(
|
||||
leader_connection.owner_id as i32,
|
||||
)),
|
||||
leader_connection_id: ActiveValue::set(leader_connection.id as i32),
|
||||
follower_connection_server_id: ActiveValue::set(ServerId(
|
||||
follower_connection.owner_id as i32,
|
||||
)),
|
||||
follower_connection_id: ActiveValue::set(follower_connection.id as i32),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&*tx)
|
||||
.await?;
|
||||
|
||||
let room = self.get_room(room_id, &*tx).await?;
|
||||
Ok(room)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn unfollow(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
leader_connection: ConnectionId,
|
||||
follower_connection: ConnectionId,
|
||||
) -> Result<RoomGuard<proto::Room>> {
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
follower::Entity::delete_many()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(follower::Column::ProjectId.eq(project_id))
|
||||
.add(
|
||||
follower::Column::LeaderConnectionServerId
|
||||
.eq(leader_connection.owner_id),
|
||||
)
|
||||
.add(follower::Column::LeaderConnectionId.eq(leader_connection.id))
|
||||
.add(
|
||||
follower::Column::FollowerConnectionServerId
|
||||
.eq(follower_connection.owner_id),
|
||||
)
|
||||
.add(follower::Column::FollowerConnectionId.eq(follower_connection.id)),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
let room = self.get_room(room_id, &*tx).await?;
|
||||
Ok(room)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update_room_participant_location(
|
||||
&self,
|
||||
room_id: RoomId,
|
||||
connection: ConnectionId,
|
||||
location: proto::ParticipantLocation,
|
||||
) -> Result<RoomGuard<proto::Room>> {
|
||||
self.room_transaction(|tx| async {
|
||||
self.room_transaction(room_id, |tx| async {
|
||||
let tx = tx;
|
||||
let location_kind;
|
||||
let location_project_id;
|
||||
@@ -1769,7 +1827,7 @@ impl Database {
|
||||
|
||||
if result.rows_affected == 1 {
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
Ok((room_id, room))
|
||||
Ok(room)
|
||||
} else {
|
||||
Err(anyhow!("could not update room participant location"))?
|
||||
}
|
||||
@@ -1926,12 +1984,25 @@ impl Database {
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(db_projects);
|
||||
|
||||
let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?;
|
||||
let mut followers = Vec::new();
|
||||
while let Some(db_follower) = db_followers.next().await {
|
||||
let db_follower = db_follower?;
|
||||
followers.push(proto::Follower {
|
||||
leader_id: Some(db_follower.leader_connection().into()),
|
||||
follower_id: Some(db_follower.follower_connection().into()),
|
||||
project_id: db_follower.project_id.to_proto(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(proto::Room {
|
||||
id: db_room.id.to_proto(),
|
||||
live_kit_room: db_room.live_kit_room,
|
||||
participants: participants.into_values().collect(),
|
||||
pending_participants,
|
||||
followers,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1963,7 +2034,7 @@ impl Database {
|
||||
connection: ConnectionId,
|
||||
worktrees: &[proto::WorktreeMetadata],
|
||||
) -> Result<RoomGuard<(ProjectId, proto::Room)>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let participant = room_participant::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
@@ -2024,7 +2095,7 @@ impl Database {
|
||||
.await?;
|
||||
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
Ok((room_id, (project.id, room)))
|
||||
Ok((project.id, room))
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -2034,7 +2105,8 @@ impl Database {
|
||||
project_id: ProjectId,
|
||||
connection: ConnectionId,
|
||||
) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
|
||||
|
||||
let project = project::Entity::find_by_id(project_id)
|
||||
@@ -2042,12 +2114,11 @@ impl Database {
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("project not found"))?;
|
||||
if project.host_connection()? == connection {
|
||||
let room_id = project.room_id;
|
||||
project::Entity::delete(project.into_active_model())
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
Ok((room_id, (room, guest_connection_ids)))
|
||||
Ok((room, guest_connection_ids))
|
||||
} else {
|
||||
Err(anyhow!("cannot unshare a project hosted by another user"))?
|
||||
}
|
||||
@@ -2061,7 +2132,8 @@ impl Database {
|
||||
connection: ConnectionId,
|
||||
worktrees: &[proto::WorktreeMetadata],
|
||||
) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let project = project::Entity::find_by_id(project_id)
|
||||
.filter(
|
||||
Condition::all()
|
||||
@@ -2079,7 +2151,7 @@ impl Database {
|
||||
|
||||
let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?;
|
||||
let room = self.get_room(project.room_id, &tx).await?;
|
||||
Ok((project.room_id, (room, guest_connection_ids)))
|
||||
Ok((room, guest_connection_ids))
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -2124,12 +2196,12 @@ impl Database {
|
||||
update: &proto::UpdateWorktree,
|
||||
connection: ConnectionId,
|
||||
) -> Result<RoomGuard<Vec<ConnectionId>>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
let project_id = ProjectId::from_proto(update.project_id);
|
||||
let worktree_id = update.worktree_id as i64;
|
||||
|
||||
let project_id = ProjectId::from_proto(update.project_id);
|
||||
let worktree_id = update.worktree_id as i64;
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
// Ensure the update comes from the host.
|
||||
let project = project::Entity::find_by_id(project_id)
|
||||
let _project = project::Entity::find_by_id(project_id)
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(project::Column::HostConnectionId.eq(connection.id as i32))
|
||||
@@ -2140,7 +2212,6 @@ impl Database {
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such project"))?;
|
||||
let room_id = project.room_id;
|
||||
|
||||
// Update metadata.
|
||||
worktree::Entity::update(worktree::ActiveModel {
|
||||
@@ -2220,7 +2291,7 @@ impl Database {
|
||||
}
|
||||
|
||||
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
|
||||
Ok((room_id, connection_ids))
|
||||
Ok(connection_ids)
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -2230,9 +2301,10 @@ impl Database {
|
||||
update: &proto::UpdateDiagnosticSummary,
|
||||
connection: ConnectionId,
|
||||
) -> Result<RoomGuard<Vec<ConnectionId>>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
let project_id = ProjectId::from_proto(update.project_id);
|
||||
let worktree_id = update.worktree_id as i64;
|
||||
let project_id = ProjectId::from_proto(update.project_id);
|
||||
let worktree_id = update.worktree_id as i64;
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let summary = update
|
||||
.summary
|
||||
.as_ref()
|
||||
@@ -2274,7 +2346,7 @@ impl Database {
|
||||
.await?;
|
||||
|
||||
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
|
||||
Ok((project.room_id, connection_ids))
|
||||
Ok(connection_ids)
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -2284,8 +2356,9 @@ impl Database {
|
||||
update: &proto::StartLanguageServer,
|
||||
connection: ConnectionId,
|
||||
) -> Result<RoomGuard<Vec<ConnectionId>>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
let project_id = ProjectId::from_proto(update.project_id);
|
||||
let project_id = ProjectId::from_proto(update.project_id);
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let server = update
|
||||
.server
|
||||
.as_ref()
|
||||
@@ -2319,7 +2392,7 @@ impl Database {
|
||||
.await?;
|
||||
|
||||
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
|
||||
Ok((project.room_id, connection_ids))
|
||||
Ok(connection_ids)
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -2329,7 +2402,8 @@ impl Database {
|
||||
project_id: ProjectId,
|
||||
connection: ConnectionId,
|
||||
) -> Result<RoomGuard<(Project, ReplicaId)>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let participant = room_participant::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
@@ -2455,7 +2529,6 @@ impl Database {
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let room_id = project.room_id;
|
||||
let project = Project {
|
||||
collaborators: collaborators
|
||||
.into_iter()
|
||||
@@ -2475,7 +2548,7 @@ impl Database {
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
Ok((room_id, (project, replica_id as ReplicaId)))
|
||||
Ok((project, replica_id as ReplicaId))
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -2484,8 +2557,9 @@ impl Database {
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
connection: ConnectionId,
|
||||
) -> Result<RoomGuard<LeftProject>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
) -> Result<RoomGuard<(proto::Room, LeftProject)>> {
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let result = project_collaborator::Entity::delete_many()
|
||||
.filter(
|
||||
Condition::all()
|
||||
@@ -2515,13 +2589,39 @@ impl Database {
|
||||
.map(|collaborator| collaborator.connection())
|
||||
.collect();
|
||||
|
||||
follower::Entity::delete_many()
|
||||
.filter(
|
||||
Condition::any()
|
||||
.add(
|
||||
Condition::all()
|
||||
.add(follower::Column::ProjectId.eq(project_id))
|
||||
.add(
|
||||
follower::Column::LeaderConnectionServerId
|
||||
.eq(connection.owner_id),
|
||||
)
|
||||
.add(follower::Column::LeaderConnectionId.eq(connection.id)),
|
||||
)
|
||||
.add(
|
||||
Condition::all()
|
||||
.add(follower::Column::ProjectId.eq(project_id))
|
||||
.add(
|
||||
follower::Column::FollowerConnectionServerId
|
||||
.eq(connection.owner_id),
|
||||
)
|
||||
.add(follower::Column::FollowerConnectionId.eq(connection.id)),
|
||||
),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
let room = self.get_room(project.room_id, &tx).await?;
|
||||
let left_project = LeftProject {
|
||||
id: project_id,
|
||||
host_user_id: project.host_user_id,
|
||||
host_connection_id: project.host_connection()?,
|
||||
connection_ids,
|
||||
};
|
||||
Ok((project.room_id, left_project))
|
||||
Ok((room, left_project))
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -2531,11 +2631,8 @@ impl Database {
|
||||
project_id: ProjectId,
|
||||
connection_id: ConnectionId,
|
||||
) -> Result<RoomGuard<Vec<ProjectCollaborator>>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
let project = project::Entity::find_by_id(project_id)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such project"))?;
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let collaborators = project_collaborator::Entity::find()
|
||||
.filter(project_collaborator::Column::ProjectId.eq(project_id))
|
||||
.all(&*tx)
|
||||
@@ -2553,7 +2650,7 @@ impl Database {
|
||||
.iter()
|
||||
.any(|collaborator| collaborator.connection_id == connection_id)
|
||||
{
|
||||
Ok((project.room_id, collaborators))
|
||||
Ok(collaborators)
|
||||
} else {
|
||||
Err(anyhow!("no such project"))?
|
||||
}
|
||||
@@ -2566,11 +2663,8 @@ impl Database {
|
||||
project_id: ProjectId,
|
||||
connection_id: ConnectionId,
|
||||
) -> Result<RoomGuard<HashSet<ConnectionId>>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
let project = project::Entity::find_by_id(project_id)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such project"))?;
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let mut collaborators = project_collaborator::Entity::find()
|
||||
.filter(project_collaborator::Column::ProjectId.eq(project_id))
|
||||
.stream(&*tx)
|
||||
@@ -2583,7 +2677,7 @@ impl Database {
|
||||
}
|
||||
|
||||
if connection_ids.contains(&connection_id) {
|
||||
Ok((project.room_id, connection_ids))
|
||||
Ok(connection_ids)
|
||||
} else {
|
||||
Err(anyhow!("no such project"))?
|
||||
}
|
||||
@@ -2613,6 +2707,17 @@ impl Database {
|
||||
Ok(guest_connection_ids)
|
||||
}
|
||||
|
||||
async fn room_id_for_project(&self, project_id: ProjectId) -> Result<RoomId> {
|
||||
self.transaction(|tx| async move {
|
||||
let project = project::Entity::find_by_id(project_id)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("project {} not found", project_id))?;
|
||||
Ok(project.room_id)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
// access tokens
|
||||
|
||||
pub async fn create_access_token_hash(
|
||||
@@ -2763,21 +2868,48 @@ impl Database {
|
||||
self.run(body).await
|
||||
}
|
||||
|
||||
async fn room_transaction<F, Fut, T>(&self, f: F) -> Result<RoomGuard<T>>
|
||||
async fn room_transaction<F, Fut, T>(&self, room_id: RoomId, f: F) -> Result<RoomGuard<T>>
|
||||
where
|
||||
F: Send + Fn(TransactionHandle) -> Fut,
|
||||
Fut: Send + Future<Output = Result<(RoomId, T)>>,
|
||||
Fut: Send + Future<Output = Result<T>>,
|
||||
{
|
||||
let data = self
|
||||
.optional_room_transaction(move |tx| {
|
||||
let future = f(tx);
|
||||
async {
|
||||
let data = future.await?;
|
||||
Ok(Some(data))
|
||||
let body = async {
|
||||
loop {
|
||||
let lock = self.rooms.entry(room_id).or_default().clone();
|
||||
let _guard = lock.lock_owned().await;
|
||||
let (tx, result) = self.with_transaction(&f).await?;
|
||||
match result {
|
||||
Ok(data) => {
|
||||
match tx.commit().await.map_err(Into::into) {
|
||||
Ok(()) => {
|
||||
return Ok(RoomGuard {
|
||||
data,
|
||||
_guard,
|
||||
_not_send: PhantomData,
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
if is_serialization_error(&error) {
|
||||
// Retry (don't break the loop)
|
||||
} else {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
tx.rollback().await?;
|
||||
if is_serialization_error(&error) {
|
||||
// Retry (don't break the loop)
|
||||
} else {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
Ok(data.unwrap())
|
||||
}
|
||||
};
|
||||
|
||||
self.run(body).await
|
||||
}
|
||||
|
||||
async fn with_transaction<F, Fut, T>(&self, f: &F) -> Result<(DatabaseTransaction, Result<T>)>
|
||||
@@ -3011,6 +3143,7 @@ macro_rules! id_type {
|
||||
|
||||
id_type!(AccessTokenId);
|
||||
id_type!(ContactId);
|
||||
id_type!(FollowerId);
|
||||
id_type!(RoomId);
|
||||
id_type!(RoomParticipantId);
|
||||
id_type!(ProjectId);
|
||||
|
||||
51
crates/collab/src/db/follower.rs
Normal file
51
crates/collab/src/db/follower.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use super::{FollowerId, ProjectId, RoomId, ServerId};
|
||||
use rpc::ConnectionId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)]
|
||||
#[sea_orm(table_name = "followers")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: FollowerId,
|
||||
pub room_id: RoomId,
|
||||
pub project_id: ProjectId,
|
||||
pub leader_connection_server_id: ServerId,
|
||||
pub leader_connection_id: i32,
|
||||
pub follower_connection_server_id: ServerId,
|
||||
pub follower_connection_id: i32,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn leader_connection(&self) -> ConnectionId {
|
||||
ConnectionId {
|
||||
owner_id: self.leader_connection_server_id.0 as u32,
|
||||
id: self.leader_connection_id as u32,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn follower_connection(&self) -> ConnectionId {
|
||||
ConnectionId {
|
||||
owner_id: self.follower_connection_server_id.0 as u32,
|
||||
id: self.follower_connection_id as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::room::Entity",
|
||||
from = "Column::RoomId",
|
||||
to = "super::room::Column::Id"
|
||||
)]
|
||||
Room,
|
||||
}
|
||||
|
||||
impl Related<super::room::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Room.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -15,6 +15,8 @@ pub enum Relation {
|
||||
RoomParticipant,
|
||||
#[sea_orm(has_many = "super::project::Entity")]
|
||||
Project,
|
||||
#[sea_orm(has_many = "super::follower::Entity")]
|
||||
Follower,
|
||||
}
|
||||
|
||||
impl Related<super::room_participant::Entity> for Entity {
|
||||
@@ -29,4 +31,10 @@ impl Related<super::project::Entity> for Entity {
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::follower::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Follower.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
@@ -57,7 +57,7 @@ use tokio::sync::watch;
|
||||
use tower::ServiceBuilder;
|
||||
use tracing::{info_span, instrument, Instrument};
|
||||
|
||||
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
lazy_static! {
|
||||
@@ -270,8 +270,11 @@ impl Server {
|
||||
let mut live_kit_room = String::new();
|
||||
let mut delete_live_kit_room = false;
|
||||
|
||||
if let Ok(mut refreshed_room) =
|
||||
app_state.db.refresh_room(room_id, server_id).await
|
||||
if let Some(mut refreshed_room) = app_state
|
||||
.db
|
||||
.refresh_room(room_id, server_id)
|
||||
.await
|
||||
.trace_err()
|
||||
{
|
||||
tracing::info!(
|
||||
room_id = room_id.0,
|
||||
@@ -1312,6 +1315,7 @@ async fn join_project(
|
||||
.filter(|collaborator| collaborator.connection_id != session.connection_id)
|
||||
.map(|collaborator| collaborator.to_proto())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let worktrees = project
|
||||
.worktrees
|
||||
.iter()
|
||||
@@ -1404,7 +1408,7 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
|
||||
let sender_id = session.connection_id;
|
||||
let project_id = ProjectId::from_proto(request.project_id);
|
||||
|
||||
let project = session
|
||||
let (room, project) = &*session
|
||||
.db()
|
||||
.await
|
||||
.leave_project(project_id, sender_id)
|
||||
@@ -1415,7 +1419,9 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
|
||||
host_connection_id = %project.host_connection_id,
|
||||
"leave project"
|
||||
);
|
||||
|
||||
project_left(&project, &session);
|
||||
room_updated(&room, &session.peer);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1724,6 +1730,7 @@ async fn follow(
|
||||
.ok_or_else(|| anyhow!("invalid leader id"))?
|
||||
.into();
|
||||
let follower_id = session.connection_id;
|
||||
|
||||
{
|
||||
let project_connection_ids = session
|
||||
.db()
|
||||
@@ -1744,6 +1751,14 @@ async fn follow(
|
||||
.views
|
||||
.retain(|view| view.leader_id != Some(follower_id.into()));
|
||||
response.send(response_payload)?;
|
||||
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
.follow(project_id, leader_id, follower_id)
|
||||
.await?;
|
||||
room_updated(&room, &session.peer);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1753,17 +1768,29 @@ async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> {
|
||||
.leader_id
|
||||
.ok_or_else(|| anyhow!("invalid leader id"))?
|
||||
.into();
|
||||
let project_connection_ids = session
|
||||
let follower_id = session.connection_id;
|
||||
|
||||
if !session
|
||||
.db()
|
||||
.await
|
||||
.project_connection_ids(project_id, session.connection_id)
|
||||
.await?;
|
||||
if !project_connection_ids.contains(&leader_id) {
|
||||
.await?
|
||||
.contains(&leader_id)
|
||||
{
|
||||
Err(anyhow!("no such peer"))?;
|
||||
}
|
||||
|
||||
session
|
||||
.peer
|
||||
.forward_send(session.connection_id, leader_id, request)?;
|
||||
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
.unfollow(project_id, leader_id, follower_id)
|
||||
.await?;
|
||||
room_updated(&room, &session.peer);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -166,9 +166,67 @@ async fn test_basic_calls(
|
||||
}
|
||||
);
|
||||
|
||||
// Call user C again from user A.
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.invite(client_c.user_id().unwrap(), None, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
assert_eq!(
|
||||
room_participants(&room_a, cx_a),
|
||||
RoomParticipants {
|
||||
remote: vec!["user_b".to_string()],
|
||||
pending: vec!["user_c".to_string()]
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
room_participants(&room_b, cx_b),
|
||||
RoomParticipants {
|
||||
remote: vec!["user_a".to_string()],
|
||||
pending: vec!["user_c".to_string()]
|
||||
}
|
||||
);
|
||||
|
||||
// User C accepts the call.
|
||||
let call_c = incoming_call_c.next().await.unwrap().unwrap();
|
||||
assert_eq!(call_c.calling_user.github_login, "user_a");
|
||||
active_call_c
|
||||
.update(cx_c, |call, cx| call.accept_incoming(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(incoming_call_c.next().await.unwrap().is_none());
|
||||
let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone());
|
||||
|
||||
deterministic.run_until_parked();
|
||||
assert_eq!(
|
||||
room_participants(&room_a, cx_a),
|
||||
RoomParticipants {
|
||||
remote: vec!["user_b".to_string(), "user_c".to_string()],
|
||||
pending: Default::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
room_participants(&room_b, cx_b),
|
||||
RoomParticipants {
|
||||
remote: vec!["user_a".to_string(), "user_c".to_string()],
|
||||
pending: Default::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
room_participants(&room_c, cx_c),
|
||||
RoomParticipants {
|
||||
remote: vec!["user_a".to_string(), "user_b".to_string()],
|
||||
pending: Default::default()
|
||||
}
|
||||
);
|
||||
|
||||
// User A shares their screen
|
||||
let display = MacOSDisplay::new();
|
||||
let events_b = active_call_events(cx_b);
|
||||
let events_c = active_call_events(cx_c);
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.room().unwrap().update(cx, |room, cx| {
|
||||
@@ -181,9 +239,10 @@ async fn test_basic_calls(
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// User B observes the remote screen sharing track.
|
||||
assert_eq!(events_b.borrow().len(), 1);
|
||||
let event = events_b.borrow().first().unwrap().clone();
|
||||
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event {
|
||||
let event_b = events_b.borrow().first().unwrap().clone();
|
||||
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_b {
|
||||
assert_eq!(participant_id, client_a.peer_id().unwrap());
|
||||
room_b.read_with(cx_b, |room, _| {
|
||||
assert_eq!(
|
||||
@@ -197,6 +256,23 @@ async fn test_basic_calls(
|
||||
panic!("unexpected event")
|
||||
}
|
||||
|
||||
// User C observes the remote screen sharing track.
|
||||
assert_eq!(events_c.borrow().len(), 1);
|
||||
let event_c = events_c.borrow().first().unwrap().clone();
|
||||
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_c {
|
||||
assert_eq!(participant_id, client_a.peer_id().unwrap());
|
||||
room_c.read_with(cx_c, |room, _| {
|
||||
assert_eq!(
|
||||
room.remote_participants()[&client_a.user_id().unwrap()]
|
||||
.tracks
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
});
|
||||
} else {
|
||||
panic!("unexpected event")
|
||||
}
|
||||
|
||||
// User A leaves the room.
|
||||
active_call_a.update(cx_a, |call, cx| {
|
||||
call.hang_up(cx).unwrap();
|
||||
@@ -213,18 +289,28 @@ async fn test_basic_calls(
|
||||
assert_eq!(
|
||||
room_participants(&room_b, cx_b),
|
||||
RoomParticipants {
|
||||
remote: Default::default(),
|
||||
remote: vec!["user_c".to_string()],
|
||||
pending: Default::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
room_participants(&room_c, cx_c),
|
||||
RoomParticipants {
|
||||
remote: vec!["user_b".to_string()],
|
||||
pending: Default::default()
|
||||
}
|
||||
);
|
||||
|
||||
// User B gets disconnected from the LiveKit server, which causes them
|
||||
// to automatically leave the room.
|
||||
// to automatically leave the room. User C leaves the room as well because
|
||||
// nobody else is in there.
|
||||
server
|
||||
.test_live_kit_server
|
||||
.disconnect_client(client_b.peer_id().unwrap().to_string())
|
||||
.disconnect_client(client_b.user_id().unwrap().to_string())
|
||||
.await;
|
||||
active_call_b.update(cx_b, |call, _| assert!(call.room().is_none()));
|
||||
deterministic.run_until_parked();
|
||||
active_call_b.read_with(cx_b, |call, _| assert!(call.room().is_none()));
|
||||
active_call_c.read_with(cx_c, |call, _| assert!(call.room().is_none()));
|
||||
assert_eq!(
|
||||
room_participants(&room_a, cx_a),
|
||||
RoomParticipants {
|
||||
@@ -239,6 +325,141 @@ async fn test_basic_calls(
|
||||
pending: Default::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
room_participants(&room_c, cx_c),
|
||||
RoomParticipants {
|
||||
remote: Default::default(),
|
||||
pending: Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_calling_multiple_users_simultaneously(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
cx_c: &mut TestAppContext,
|
||||
cx_d: &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;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
let client_d = server.create_client(cx_d, "user_d").await;
|
||||
server
|
||||
.make_contacts(&mut [
|
||||
(&client_a, cx_a),
|
||||
(&client_b, cx_b),
|
||||
(&client_c, cx_c),
|
||||
(&client_d, cx_d),
|
||||
])
|
||||
.await;
|
||||
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
let active_call_c = cx_c.read(ActiveCall::global);
|
||||
let active_call_d = cx_d.read(ActiveCall::global);
|
||||
|
||||
// Simultaneously call user B and user C from client A.
|
||||
let b_invite = active_call_a.update(cx_a, |call, cx| {
|
||||
call.invite(client_b.user_id().unwrap(), None, cx)
|
||||
});
|
||||
let c_invite = active_call_a.update(cx_a, |call, cx| {
|
||||
call.invite(client_c.user_id().unwrap(), None, cx)
|
||||
});
|
||||
b_invite.await.unwrap();
|
||||
c_invite.await.unwrap();
|
||||
|
||||
let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
|
||||
deterministic.run_until_parked();
|
||||
assert_eq!(
|
||||
room_participants(&room_a, cx_a),
|
||||
RoomParticipants {
|
||||
remote: Default::default(),
|
||||
pending: vec!["user_b".to_string(), "user_c".to_string()]
|
||||
}
|
||||
);
|
||||
|
||||
// Call client D from client A.
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.invite(client_d.user_id().unwrap(), None, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
assert_eq!(
|
||||
room_participants(&room_a, cx_a),
|
||||
RoomParticipants {
|
||||
remote: Default::default(),
|
||||
pending: vec![
|
||||
"user_b".to_string(),
|
||||
"user_c".to_string(),
|
||||
"user_d".to_string()
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
// Accept the call on all clients simultaneously.
|
||||
let accept_b = active_call_b.update(cx_b, |call, cx| call.accept_incoming(cx));
|
||||
let accept_c = active_call_c.update(cx_c, |call, cx| call.accept_incoming(cx));
|
||||
let accept_d = active_call_d.update(cx_d, |call, cx| call.accept_incoming(cx));
|
||||
accept_b.await.unwrap();
|
||||
accept_c.await.unwrap();
|
||||
accept_d.await.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
|
||||
let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone());
|
||||
let room_d = active_call_d.read_with(cx_d, |call, _| call.room().unwrap().clone());
|
||||
assert_eq!(
|
||||
room_participants(&room_a, cx_a),
|
||||
RoomParticipants {
|
||||
remote: vec![
|
||||
"user_b".to_string(),
|
||||
"user_c".to_string(),
|
||||
"user_d".to_string(),
|
||||
],
|
||||
pending: Default::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
room_participants(&room_b, cx_b),
|
||||
RoomParticipants {
|
||||
remote: vec![
|
||||
"user_a".to_string(),
|
||||
"user_c".to_string(),
|
||||
"user_d".to_string(),
|
||||
],
|
||||
pending: Default::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
room_participants(&room_c, cx_c),
|
||||
RoomParticipants {
|
||||
remote: vec![
|
||||
"user_a".to_string(),
|
||||
"user_b".to_string(),
|
||||
"user_d".to_string(),
|
||||
],
|
||||
pending: Default::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
room_participants(&room_d, cx_d),
|
||||
RoomParticipants {
|
||||
remote: vec![
|
||||
"user_a".to_string(),
|
||||
"user_b".to_string(),
|
||||
"user_c".to_string(),
|
||||
],
|
||||
pending: Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
@@ -512,6 +733,14 @@ async fn test_server_restarts(
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
client_a
|
||||
.fs
|
||||
.insert_tree("/a", json!({ "a.txt": "a-contents" }))
|
||||
.await;
|
||||
|
||||
// Invite client B to collaborate on a project
|
||||
let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
|
||||
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
let client_d = server.create_client(cx_d, "user_d").await;
|
||||
@@ -532,19 +761,19 @@ async fn test_server_restarts(
|
||||
// User A calls users B, C, and D.
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.invite(client_b.user_id().unwrap(), None, cx)
|
||||
call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.invite(client_c.user_id().unwrap(), None, cx)
|
||||
call.invite(client_c.user_id().unwrap(), Some(project_a.clone()), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.invite(client_d.user_id().unwrap(), None, cx)
|
||||
call.invite(client_d.user_id().unwrap(), Some(project_a.clone()), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -600,7 +829,7 @@ async fn test_server_restarts(
|
||||
|
||||
// Users A and B reconnect to the call. User C has troubles reconnecting, so it leaves the room.
|
||||
client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
|
||||
deterministic.advance_clock(RECEIVE_TIMEOUT);
|
||||
deterministic.advance_clock(RECONNECT_TIMEOUT);
|
||||
assert_eq!(
|
||||
room_participants(&room_a, cx_a),
|
||||
RoomParticipants {
|
||||
@@ -772,7 +1001,7 @@ async fn test_server_restarts(
|
||||
client_a.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
|
||||
client_b.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
|
||||
client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
|
||||
deterministic.advance_clock(RECEIVE_TIMEOUT);
|
||||
deterministic.advance_clock(RECONNECT_TIMEOUT);
|
||||
assert_eq!(
|
||||
room_participants(&room_a, cx_a),
|
||||
RoomParticipants {
|
||||
@@ -862,7 +1091,7 @@ async fn test_calls_on_multiple_connections(
|
||||
assert!(incoming_call_b2.next().await.unwrap().is_none());
|
||||
|
||||
// User B disconnects the client that is not on the call. Everything should be fine.
|
||||
client_b1.disconnect(&cx_b1.to_async()).unwrap();
|
||||
client_b1.disconnect(&cx_b1.to_async());
|
||||
deterministic.advance_clock(RECEIVE_TIMEOUT);
|
||||
client_b1
|
||||
.authenticate_and_connect(false, &cx_b1.to_async())
|
||||
@@ -2023,7 +2252,9 @@ async fn test_propagate_saves_and_fs_changes(
|
||||
});
|
||||
|
||||
// Edit the buffer as the host and concurrently save as guest B.
|
||||
let save_b = buffer_b.update(cx_b, |buf, cx| buf.save(cx));
|
||||
let save_b = project_b.update(cx_b, |project, cx| {
|
||||
project.save_buffer(buffer_b.clone(), cx)
|
||||
});
|
||||
buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx));
|
||||
save_b.await.unwrap();
|
||||
assert_eq!(
|
||||
@@ -2092,6 +2323,41 @@ async fn test_propagate_saves_and_fs_changes(
|
||||
assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
|
||||
assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
|
||||
});
|
||||
|
||||
let new_buffer_a = project_a
|
||||
.update(cx_a, |p, cx| p.create_buffer("", None, cx))
|
||||
.unwrap();
|
||||
let new_buffer_id = new_buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id());
|
||||
let new_buffer_b = project_b
|
||||
.update(cx_b, |p, cx| p.open_buffer_by_id(new_buffer_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
new_buffer_b.read_with(cx_b, |buffer, _| {
|
||||
assert!(buffer.file().is_none());
|
||||
});
|
||||
|
||||
new_buffer_a.update(cx_a, |buffer, cx| {
|
||||
buffer.edit([(0..0, "ok")], None, cx);
|
||||
});
|
||||
project_a
|
||||
.update(cx_a, |project, cx| {
|
||||
project.save_buffer_as(new_buffer_a.clone(), "/a/file3.rs".into(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
new_buffer_b.read_with(cx_b, |buffer_b, _| {
|
||||
assert_eq!(
|
||||
buffer_b.file().unwrap().path().as_ref(),
|
||||
Path::new("file3.rs")
|
||||
);
|
||||
|
||||
new_buffer_a.read_with(cx_a, |buffer_a, _| {
|
||||
assert_eq!(buffer_b.saved_mtime(), buffer_a.saved_mtime());
|
||||
assert_eq!(buffer_b.saved_version(), buffer_a.saved_version());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
@@ -2572,7 +2838,7 @@ async fn test_fs_operations(
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
assert_eq!(
|
||||
worktree
|
||||
@@ -2661,7 +2927,12 @@ async fn test_buffer_conflict_after_save(
|
||||
assert!(!buf.has_conflict());
|
||||
});
|
||||
|
||||
buffer_b.update(cx_b, |buf, cx| buf.save(cx)).await.unwrap();
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project.save_buffer(buffer_b.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.foreground().forbid_parking();
|
||||
buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
|
||||
buffer_b.read_with(cx_b, |buf, _| {
|
||||
@@ -2964,7 +3235,7 @@ async fn test_leaving_project(
|
||||
buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
|
||||
|
||||
// Drop client B's connection and ensure client A and client C observe client B leaving.
|
||||
client_b.disconnect(&cx_b.to_async()).unwrap();
|
||||
client_b.disconnect(&cx_b.to_async());
|
||||
deterministic.advance_clock(RECONNECT_TIMEOUT);
|
||||
project_a.read_with(cx_a, |project, _| {
|
||||
assert_eq!(project.collaborators().len(), 1);
|
||||
@@ -3621,9 +3892,11 @@ async fn test_formatting_buffer(
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The edits from the LSP are applied, and a final newline is added.
|
||||
assert_eq!(
|
||||
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
|
||||
"let honey = \"two\""
|
||||
"let honey = \"two\"\n"
|
||||
);
|
||||
|
||||
// Ensure buffer can be formatted using an external command. Notice how the
|
||||
@@ -5509,7 +5782,7 @@ async fn test_contact_requests(
|
||||
.is_empty());
|
||||
|
||||
async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
|
||||
client.disconnect(&cx.to_async()).unwrap();
|
||||
client.disconnect(&cx.to_async());
|
||||
client.clear_contacts(cx).await;
|
||||
client
|
||||
.authenticate_and_connect(false, &cx.to_async())
|
||||
@@ -5519,10 +5792,12 @@ async fn test_contact_requests(
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_following(
|
||||
async fn test_basic_following(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
cx_c: &mut TestAppContext,
|
||||
cx_d: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
cx_a.update(editor::init);
|
||||
@@ -5531,8 +5806,15 @@ async fn test_following(
|
||||
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;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
let client_d = server.create_client(cx_d, "user_d").await;
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.create_room(&mut [
|
||||
(&client_a, cx_a),
|
||||
(&client_b, cx_b),
|
||||
(&client_c, cx_c),
|
||||
(&client_d, cx_d),
|
||||
])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
@@ -5564,8 +5846,10 @@ async fn test_following(
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client A opens some editors.
|
||||
let workspace_a = client_a.build_workspace(&project_a, cx_a);
|
||||
let workspace_b = client_b.build_workspace(&project_b, cx_b);
|
||||
|
||||
// Client A opens some editors.
|
||||
let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
let editor_a1 = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
@@ -5585,7 +5869,6 @@ async fn test_following(
|
||||
.unwrap();
|
||||
|
||||
// Client B opens an editor.
|
||||
let workspace_b = client_b.build_workspace(&project_b, cx_b);
|
||||
let editor_b1 = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
@@ -5595,29 +5878,184 @@ async fn test_following(
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
let client_a_id = project_b.read_with(cx_b, |project, _| {
|
||||
project.collaborators().values().next().unwrap().peer_id
|
||||
});
|
||||
let client_b_id = project_a.read_with(cx_a, |project, _| {
|
||||
project.collaborators().values().next().unwrap().peer_id
|
||||
});
|
||||
let peer_id_a = client_a.peer_id().unwrap();
|
||||
let peer_id_b = client_b.peer_id().unwrap();
|
||||
let peer_id_c = client_c.peer_id().unwrap();
|
||||
let peer_id_d = client_d.peer_id().unwrap();
|
||||
|
||||
// When client B starts following client A, all visible view states are replicated to client B.
|
||||
// Client A updates their selections in those editors
|
||||
editor_a1.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
|
||||
});
|
||||
editor_a2.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
|
||||
});
|
||||
|
||||
// When client B starts following client A, all visible view states are replicated to client B.
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace
|
||||
.toggle_follow(&ToggleFollow(client_a_id), cx)
|
||||
.toggle_follow(&ToggleFollow(peer_id_a), cx)
|
||||
.unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx_c.foreground().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 workspace_c = client_c.build_workspace(&project_c, cx_c);
|
||||
active_call_c
|
||||
.update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
drop(project_c);
|
||||
|
||||
// Client C also follows client A.
|
||||
workspace_c
|
||||
.update(cx_c, |workspace, cx| {
|
||||
workspace
|
||||
.toggle_follow(&ToggleFollow(peer_id_a), cx)
|
||||
.unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx_d.foreground().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);
|
||||
active_call_d
|
||||
.update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
drop(project_d);
|
||||
|
||||
// All clients see that clients B and C are following client A.
|
||||
cx_c.foreground().run_until_parked();
|
||||
for (name, active_call, cx) in [
|
||||
("A", &active_call_a, &cx_a),
|
||||
("B", &active_call_b, &cx_b),
|
||||
("C", &active_call_c, &cx_c),
|
||||
("D", &active_call_d, &cx_d),
|
||||
] {
|
||||
active_call.read_with(*cx, |call, cx| {
|
||||
let room = call.room().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
room.followers_for(peer_id_a, project_id),
|
||||
&[peer_id_b, peer_id_c],
|
||||
"checking followers for A as {name}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Client C unfollows client A.
|
||||
workspace_c.update(cx_c, |workspace, cx| {
|
||||
workspace.toggle_follow(&ToggleFollow(peer_id_a), cx);
|
||||
});
|
||||
|
||||
// All clients see that clients B is following client A.
|
||||
cx_c.foreground().run_until_parked();
|
||||
for (name, active_call, cx) in [
|
||||
("A", &active_call_a, &cx_a),
|
||||
("B", &active_call_b, &cx_b),
|
||||
("C", &active_call_c, &cx_c),
|
||||
("D", &active_call_d, &cx_d),
|
||||
] {
|
||||
active_call.read_with(*cx, |call, cx| {
|
||||
let room = call.room().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
room.followers_for(peer_id_a, project_id),
|
||||
&[peer_id_b],
|
||||
"checking followers for A as {name}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Client C re-follows client A.
|
||||
workspace_c.update(cx_c, |workspace, cx| {
|
||||
workspace.toggle_follow(&ToggleFollow(peer_id_a), cx);
|
||||
});
|
||||
|
||||
// All clients see that clients B and C are following client A.
|
||||
cx_c.foreground().run_until_parked();
|
||||
for (name, active_call, cx) in [
|
||||
("A", &active_call_a, &cx_a),
|
||||
("B", &active_call_b, &cx_b),
|
||||
("C", &active_call_c, &cx_c),
|
||||
("D", &active_call_d, &cx_d),
|
||||
] {
|
||||
active_call.read_with(*cx, |call, cx| {
|
||||
let room = call.room().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
room.followers_for(peer_id_a, project_id),
|
||||
&[peer_id_b, peer_id_c],
|
||||
"checking followers for A as {name}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Client D follows client C.
|
||||
workspace_d
|
||||
.update(cx_d, |workspace, cx| {
|
||||
workspace
|
||||
.toggle_follow(&ToggleFollow(peer_id_c), cx)
|
||||
.unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// All clients see that D is following C
|
||||
cx_d.foreground().run_until_parked();
|
||||
for (name, active_call, cx) in [
|
||||
("A", &active_call_a, &cx_a),
|
||||
("B", &active_call_b, &cx_b),
|
||||
("C", &active_call_c, &cx_c),
|
||||
("D", &active_call_d, &cx_d),
|
||||
] {
|
||||
active_call.read_with(*cx, |call, cx| {
|
||||
let room = call.room().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
room.followers_for(peer_id_c, project_id),
|
||||
&[peer_id_d],
|
||||
"checking followers for C as {name}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Client C closes the project.
|
||||
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();
|
||||
for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
|
||||
active_call.read_with(*cx, |call, cx| {
|
||||
let room = call.room().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
room.followers_for(peer_id_a, project_id),
|
||||
&[peer_id_b],
|
||||
"checking followers for A as {name}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// All clients see that no-one is following C
|
||||
for (name, active_call, cx) in [
|
||||
("A", &active_call_a, &cx_a),
|
||||
("B", &active_call_b, &cx_b),
|
||||
("C", &active_call_c, &cx_c),
|
||||
("D", &active_call_d, &cx_d),
|
||||
] {
|
||||
active_call.read_with(*cx, |call, cx| {
|
||||
let room = call.room().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
room.followers_for(peer_id_c, project_id),
|
||||
&[],
|
||||
"checking followers for C as {name}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
@@ -5770,14 +6208,14 @@ async fn test_following(
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace
|
||||
.toggle_follow(&ToggleFollow(client_b_id), cx)
|
||||
.toggle_follow(&ToggleFollow(peer_id_b), cx)
|
||||
.unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||
Some(client_b_id)
|
||||
Some(peer_id_b)
|
||||
);
|
||||
assert_eq!(
|
||||
workspace_a.read_with(cx_a, |workspace, cx| workspace
|
||||
@@ -5849,7 +6287,7 @@ async fn test_following(
|
||||
);
|
||||
|
||||
// Following interrupts when client B disconnects.
|
||||
client_b.disconnect(&cx_b.to_async()).unwrap();
|
||||
client_b.disconnect(&cx_b.to_async());
|
||||
deterministic.advance_clock(RECONNECT_TIMEOUT);
|
||||
assert_eq!(
|
||||
workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||
|
||||
@@ -1064,15 +1064,16 @@ async fn randomly_query_and_mutate_buffers(
|
||||
}
|
||||
}
|
||||
30..=39 if buffer.read_with(cx, |buffer, _| buffer.is_dirty()) => {
|
||||
let (requested_version, save) = buffer.update(cx, |buffer, cx| {
|
||||
let requested_version = buffer.update(cx, |buffer, cx| {
|
||||
log::info!(
|
||||
"{}: saving buffer {} ({:?})",
|
||||
client.username,
|
||||
buffer.remote_id(),
|
||||
buffer.file().unwrap().full_path(cx)
|
||||
);
|
||||
(buffer.version(), buffer.save(cx))
|
||||
buffer.version()
|
||||
});
|
||||
let save = project.update(cx, |project, cx| project.save_buffer(buffer, cx));
|
||||
let save = cx.background().spawn(async move {
|
||||
let (saved_version, _, _) = save
|
||||
.await
|
||||
|
||||
@@ -22,10 +22,12 @@ test-support = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
auto_update = { path = "../auto_update" }
|
||||
call = { path = "../call" }
|
||||
client = { path = "../client" }
|
||||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections" }
|
||||
context_menu = { path = "../context_menu" }
|
||||
editor = { path = "../editor" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
gpui = { path = "../gpui" }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,24 @@
|
||||
mod collab_titlebar_item;
|
||||
mod collaborator_list_popover;
|
||||
mod contact_finder;
|
||||
mod contact_list;
|
||||
mod contact_notification;
|
||||
mod contacts_popover;
|
||||
mod face_pile;
|
||||
mod incoming_call_notification;
|
||||
mod notifications;
|
||||
mod project_shared_notification;
|
||||
mod sharing_status_indicator;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use call::ActiveCall;
|
||||
pub use collab_titlebar_item::{CollabTitlebarItem, ToggleCollaborationMenu};
|
||||
use gpui::MutableAppContext;
|
||||
pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu};
|
||||
use gpui::{actions, MutableAppContext, Task};
|
||||
use std::sync::Arc;
|
||||
use workspace::{AppState, JoinProject, ToggleFollow, Workspace};
|
||||
|
||||
actions!(collab, [ToggleScreenSharing]);
|
||||
|
||||
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
collab_titlebar_item::init(cx);
|
||||
contact_notification::init(cx);
|
||||
@@ -22,89 +27,107 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
contacts_popover::init(cx);
|
||||
incoming_call_notification::init(cx);
|
||||
project_shared_notification::init(cx);
|
||||
sharing_status_indicator::init(cx);
|
||||
|
||||
cx.add_global_action(toggle_screen_sharing);
|
||||
cx.add_global_action(move |action: &JoinProject, cx| {
|
||||
let project_id = action.project_id;
|
||||
let follow_user_id = action.follow_user_id;
|
||||
let app_state = app_state.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let existing_workspace = cx.update(|cx| {
|
||||
cx.window_ids()
|
||||
.filter_map(|window_id| cx.root_view::<Workspace>(window_id))
|
||||
.find(|workspace| {
|
||||
workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
|
||||
})
|
||||
});
|
||||
|
||||
let workspace = if let Some(existing_workspace) = existing_workspace {
|
||||
existing_workspace
|
||||
} else {
|
||||
let active_call = cx.read(ActiveCall::global);
|
||||
let room = active_call
|
||||
.read_with(&cx, |call, _| call.room().cloned())
|
||||
.ok_or_else(|| anyhow!("not in a call"))?;
|
||||
let project = room
|
||||
.update(&mut cx, |room, cx| {
|
||||
room.join_project(
|
||||
project_id,
|
||||
app_state.languages.clone(),
|
||||
app_state.fs.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let (_, workspace) = cx.add_window(
|
||||
(app_state.build_window_options)(None, None, cx.platform().as_ref()),
|
||||
|cx| {
|
||||
let mut workspace = Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project,
|
||||
app_state.dock_default_item_factory,
|
||||
cx,
|
||||
);
|
||||
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
|
||||
workspace
|
||||
},
|
||||
);
|
||||
workspace
|
||||
};
|
||||
|
||||
cx.activate_window(workspace.window_id());
|
||||
cx.platform().activate(true);
|
||||
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||
let follow_peer_id = room
|
||||
.read(cx)
|
||||
.remote_participants()
|
||||
.iter()
|
||||
.find(|(_, participant)| participant.user.id == follow_user_id)
|
||||
.map(|(_, p)| p.peer_id)
|
||||
.or_else(|| {
|
||||
// If we couldn't follow the given user, follow the host instead.
|
||||
let collaborator = workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.collaborators()
|
||||
.values()
|
||||
.find(|collaborator| collaborator.replica_id == 0)?;
|
||||
Some(collaborator.peer_id)
|
||||
});
|
||||
|
||||
if let Some(follow_peer_id) = follow_peer_id {
|
||||
if !workspace.is_following(follow_peer_id) {
|
||||
workspace
|
||||
.toggle_follow(&ToggleFollow(follow_peer_id), cx)
|
||||
.map(|follow| follow.detach_and_log_err(cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
join_project(action, app_state.clone(), cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut MutableAppContext) {
|
||||
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||
let toggle_screen_sharing = room.update(cx, |room, cx| {
|
||||
if room.is_screen_sharing() {
|
||||
Task::ready(room.unshare_screen(cx))
|
||||
} else {
|
||||
room.share_screen(cx)
|
||||
}
|
||||
});
|
||||
toggle_screen_sharing.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn join_project(action: &JoinProject, app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
let project_id = action.project_id;
|
||||
let follow_user_id = action.follow_user_id;
|
||||
cx.spawn(|mut cx| async move {
|
||||
let existing_workspace = cx.update(|cx| {
|
||||
cx.window_ids()
|
||||
.filter_map(|window_id| cx.root_view::<Workspace>(window_id))
|
||||
.find(|workspace| {
|
||||
workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
|
||||
})
|
||||
});
|
||||
|
||||
let workspace = if let Some(existing_workspace) = existing_workspace {
|
||||
existing_workspace
|
||||
} else {
|
||||
let active_call = cx.read(ActiveCall::global);
|
||||
let room = active_call
|
||||
.read_with(&cx, |call, _| call.room().cloned())
|
||||
.ok_or_else(|| anyhow!("not in a call"))?;
|
||||
let project = room
|
||||
.update(&mut cx, |room, cx| {
|
||||
room.join_project(
|
||||
project_id,
|
||||
app_state.languages.clone(),
|
||||
app_state.fs.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let (_, workspace) = cx.add_window(
|
||||
(app_state.build_window_options)(None, None, cx.platform().as_ref()),
|
||||
|cx| {
|
||||
let mut workspace = Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project,
|
||||
app_state.dock_default_item_factory,
|
||||
cx,
|
||||
);
|
||||
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
|
||||
workspace
|
||||
},
|
||||
);
|
||||
workspace
|
||||
};
|
||||
|
||||
cx.activate_window(workspace.window_id());
|
||||
cx.platform().activate(true);
|
||||
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||
let follow_peer_id = room
|
||||
.read(cx)
|
||||
.remote_participants()
|
||||
.iter()
|
||||
.find(|(_, participant)| participant.user.id == follow_user_id)
|
||||
.map(|(_, p)| p.peer_id)
|
||||
.or_else(|| {
|
||||
// If we couldn't follow the given user, follow the host instead.
|
||||
let collaborator = workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.collaborators()
|
||||
.values()
|
||||
.find(|collaborator| collaborator.replica_id == 0)?;
|
||||
Some(collaborator.peer_id)
|
||||
});
|
||||
|
||||
if let Some(follow_peer_id) = follow_peer_id {
|
||||
if !workspace.is_being_followed(follow_peer_id) {
|
||||
workspace
|
||||
.toggle_follow(&ToggleFollow(follow_peer_id), cx)
|
||||
.map(|follow| follow.detach_and_log_err(cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
165
crates/collab_ui/src/collaborator_list_popover.rs
Normal file
165
crates/collab_ui/src/collaborator_list_popover.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
use call::ActiveCall;
|
||||
use client::UserStore;
|
||||
use gpui::Action;
|
||||
use gpui::{
|
||||
actions, elements::*, Entity, ModelHandle, MouseButton, RenderContext, View, ViewContext,
|
||||
};
|
||||
use settings::Settings;
|
||||
|
||||
use crate::collab_titlebar_item::ToggleCollaboratorList;
|
||||
|
||||
pub(crate) enum Event {
|
||||
Dismissed,
|
||||
}
|
||||
|
||||
enum Collaborator {
|
||||
SelfUser { username: String },
|
||||
RemoteUser { username: String },
|
||||
}
|
||||
|
||||
actions!(collaborator_list_popover, [NoOp]);
|
||||
|
||||
pub(crate) struct CollaboratorListPopover {
|
||||
list_state: ListState,
|
||||
}
|
||||
|
||||
impl Entity for CollaboratorListPopover {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for CollaboratorListPopover {
|
||||
fn ui_name() -> &'static str {
|
||||
"CollaboratorListPopover"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
|
||||
MouseEventHandler::<Self>::new(0, cx, |_, _| {
|
||||
List::new(self.list_state.clone())
|
||||
.contained()
|
||||
.with_style(theme.contacts_popover.container) //TODO: Change the name of this theme key
|
||||
.constrained()
|
||||
.with_width(theme.contacts_popover.width)
|
||||
.with_height(theme.contacts_popover.height)
|
||||
.boxed()
|
||||
})
|
||||
.on_down_out(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleCollaboratorList);
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(Event::Dismissed);
|
||||
}
|
||||
}
|
||||
|
||||
impl CollaboratorListPopover {
|
||||
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
|
||||
let mut collaborators = user_store
|
||||
.read(cx)
|
||||
.current_user()
|
||||
.map(|u| Collaborator::SelfUser {
|
||||
username: u.github_login.clone(),
|
||||
})
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
//TODO: What should the canonical sort here look like, consult contacts list implementation
|
||||
if let Some(room) = active_call.read(cx).room() {
|
||||
for participant in room.read(cx).remote_participants() {
|
||||
collaborators.push(Collaborator::RemoteUser {
|
||||
username: participant.1.user.github_login.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
list_state: ListState::new(
|
||||
collaborators.len(),
|
||||
Orientation::Top,
|
||||
0.,
|
||||
cx,
|
||||
move |_, index, cx| match &collaborators[index] {
|
||||
Collaborator::SelfUser { username } => render_collaborator_list_entry(
|
||||
index,
|
||||
username,
|
||||
None::<NoOp>,
|
||||
None,
|
||||
Svg::new("icons/chevron_right_12.svg"),
|
||||
NoOp,
|
||||
"Leave call".to_owned(),
|
||||
cx,
|
||||
),
|
||||
|
||||
Collaborator::RemoteUser { username } => render_collaborator_list_entry(
|
||||
index,
|
||||
username,
|
||||
Some(NoOp),
|
||||
Some(format!("Follow {username}")),
|
||||
Svg::new("icons/x_mark_12.svg"),
|
||||
NoOp,
|
||||
format!("Remove {username} from call"),
|
||||
cx,
|
||||
),
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_collaborator_list_entry<UA: Action + Clone, IA: Action + Clone>(
|
||||
index: usize,
|
||||
username: &str,
|
||||
username_action: Option<UA>,
|
||||
username_tooltip: Option<String>,
|
||||
icon: Svg,
|
||||
icon_action: IA,
|
||||
icon_tooltip: String,
|
||||
cx: &mut RenderContext<CollaboratorListPopover>,
|
||||
) -> ElementBox {
|
||||
enum Username {}
|
||||
enum UsernameTooltip {}
|
||||
enum Icon {}
|
||||
enum IconTooltip {}
|
||||
|
||||
let theme = &cx.global::<Settings>().theme;
|
||||
let username_theme = theme.contact_list.contact_username.text.clone();
|
||||
let tooltip_theme = theme.tooltip.clone();
|
||||
|
||||
let username = MouseEventHandler::<Username>::new(index, cx, |_, _| {
|
||||
Label::new(username.to_owned(), username_theme.clone()).boxed()
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
if let Some(username_action) = username_action.clone() {
|
||||
cx.dispatch_action(username_action);
|
||||
}
|
||||
});
|
||||
|
||||
Flex::row()
|
||||
.with_child(if let Some(username_tooltip) = username_tooltip {
|
||||
username
|
||||
.with_tooltip::<UsernameTooltip, _>(
|
||||
index,
|
||||
username_tooltip,
|
||||
None,
|
||||
tooltip_theme.clone(),
|
||||
cx,
|
||||
)
|
||||
.boxed()
|
||||
} else {
|
||||
username.boxed()
|
||||
})
|
||||
.with_child(
|
||||
MouseEventHandler::<Icon>::new(index, cx, |_, _| icon.boxed())
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(icon_action.clone())
|
||||
})
|
||||
.with_tooltip::<IconTooltip, _>(index, icon_tooltip, None, tooltip_theme, cx)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::collab_titlebar_item::LeaveCall;
|
||||
use crate::contacts_popover;
|
||||
use call::ActiveCall;
|
||||
use client::{proto::PeerId, Contact, User, UserStore};
|
||||
@@ -18,22 +19,20 @@ use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use std::{mem, sync::Arc};
|
||||
use theme::IconButton;
|
||||
use util::ResultExt;
|
||||
use workspace::{JoinProject, OpenSharedScreen};
|
||||
|
||||
impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]);
|
||||
impl_internal_actions!(contact_list, [ToggleExpanded, Call, LeaveCall]);
|
||||
impl_internal_actions!(contact_list, [ToggleExpanded, Call]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(ContactList::remove_contact);
|
||||
cx.add_action(ContactList::respond_to_contact_request);
|
||||
cx.add_action(ContactList::clear_filter);
|
||||
cx.add_action(ContactList::cancel);
|
||||
cx.add_action(ContactList::select_next);
|
||||
cx.add_action(ContactList::select_prev);
|
||||
cx.add_action(ContactList::confirm);
|
||||
cx.add_action(ContactList::toggle_expanded);
|
||||
cx.add_action(ContactList::call);
|
||||
cx.add_action(ContactList::leave_call);
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
@@ -45,9 +44,6 @@ struct Call {
|
||||
initial_project: Option<ModelHandle<Project>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
struct LeaveCall;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
|
||||
enum Section {
|
||||
ActiveCall,
|
||||
@@ -326,7 +322,7 @@ impl ContactList {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
let did_clear = self.filter_editor.update(cx, |editor, cx| {
|
||||
if editor.buffer().read(cx).len(cx) > 0 {
|
||||
editor.set_text("", cx);
|
||||
@@ -335,6 +331,7 @@ impl ContactList {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if !did_clear {
|
||||
cx.emit(Event::Dismissed);
|
||||
}
|
||||
@@ -749,7 +746,7 @@ impl ContactList {
|
||||
)
|
||||
.with_children(if is_pending {
|
||||
Some(
|
||||
Label::new("Calling".to_string(), theme.calling_indicator.text.clone())
|
||||
Label::new("Calling", theme.calling_indicator.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.calling_indicator.container)
|
||||
.aligned()
|
||||
@@ -950,7 +947,7 @@ impl ContactList {
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Label::new("Screen".into(), row.name.text.clone())
|
||||
Label::new("Screen", row.name.text.clone())
|
||||
.aligned()
|
||||
.left()
|
||||
.contained()
|
||||
@@ -980,6 +977,7 @@ impl ContactList {
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
enum Header {}
|
||||
enum LeaveCallContactList {}
|
||||
|
||||
let header_style = theme
|
||||
.header_row
|
||||
@@ -992,9 +990,9 @@ impl ContactList {
|
||||
};
|
||||
let leave_call = if section == Section::ActiveCall {
|
||||
Some(
|
||||
MouseEventHandler::<LeaveCall>::new(0, cx, |state, _| {
|
||||
MouseEventHandler::<LeaveCallContactList>::new(0, cx, |state, _| {
|
||||
let style = theme.leave_call.style_for(state, false);
|
||||
Label::new("Leave Session".into(), style.text.clone())
|
||||
Label::new("Leave Call", style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
@@ -1026,7 +1024,7 @@ impl ContactList {
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(text.to_string(), header_style.text.clone())
|
||||
Label::new(text, header_style.text.clone())
|
||||
.aligned()
|
||||
.left()
|
||||
.contained()
|
||||
@@ -1126,7 +1124,7 @@ impl ContactList {
|
||||
)
|
||||
.with_children(if calling {
|
||||
Some(
|
||||
Label::new("Calling".to_string(), theme.calling_indicator.text.clone())
|
||||
Label::new("Calling", theme.calling_indicator.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.calling_indicator.container)
|
||||
.aligned()
|
||||
@@ -1283,12 +1281,6 @@ impl ContactList {
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| call.hang_up(cx))
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ContactList {
|
||||
@@ -1302,7 +1294,7 @@ impl View for ContactList {
|
||||
|
||||
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||
let mut cx = Self::default_keymap_context();
|
||||
cx.set.insert("menu".into());
|
||||
cx.add_identifier("menu");
|
||||
cx
|
||||
}
|
||||
|
||||
@@ -1334,7 +1326,7 @@ impl View for ContactList {
|
||||
})
|
||||
.with_tooltip::<AddContact, _>(
|
||||
0,
|
||||
"Add contact".into(),
|
||||
"Search for new contact".into(),
|
||||
None,
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleCollaborationMenu};
|
||||
use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleContactsMenu};
|
||||
use client::UserStore;
|
||||
use gpui::{
|
||||
actions, elements::*, ClipboardItem, CursorStyle, Entity, ModelHandle, MouseButton,
|
||||
@@ -155,7 +155,7 @@ impl View for ContactsPopover {
|
||||
.boxed()
|
||||
})
|
||||
.on_down_out(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleCollaborationMenu);
|
||||
cx.dispatch_action(ToggleContactsMenu);
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
101
crates/collab_ui/src/face_pile.rs
Normal file
101
crates/collab_ui/src/face_pile.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::{
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
json::ToJson,
|
||||
serde_json::{self, json},
|
||||
Axis, DebugContext, Element, ElementBox, MeasurementContext, PaintContext,
|
||||
};
|
||||
|
||||
pub(crate) struct FacePile {
|
||||
overlap: f32,
|
||||
faces: Vec<ElementBox>,
|
||||
}
|
||||
|
||||
impl FacePile {
|
||||
pub fn new(overlap: f32) -> FacePile {
|
||||
FacePile {
|
||||
overlap,
|
||||
faces: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for FacePile {
|
||||
type LayoutState = ();
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: gpui::SizeConstraint,
|
||||
cx: &mut gpui::LayoutContext,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
|
||||
|
||||
let mut width = 0.;
|
||||
for face in &mut self.faces {
|
||||
width += face.layout(constraint, cx).x();
|
||||
}
|
||||
width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
|
||||
|
||||
(Vector2F::new(width, constraint.max.y()), ())
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: RectF,
|
||||
visible_bounds: RectF,
|
||||
_layout: &mut Self::LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) -> 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();
|
||||
cx.paint_layer(None, |cx| {
|
||||
face.paint(vec2f(origin_x, origin_y), visible_bounds, cx);
|
||||
});
|
||||
origin_x += self.overlap;
|
||||
}
|
||||
|
||||
()
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
_: Range<usize>,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
_: &MeasurementContext,
|
||||
) -> Option<RectF> {
|
||||
None
|
||||
}
|
||||
|
||||
fn debug(
|
||||
&self,
|
||||
bounds: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
_: &DebugContext,
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "FacePile",
|
||||
"bounds": bounds.to_json()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Extend<ElementBox> for FacePile {
|
||||
fn extend<T: IntoIterator<Item = ElementBox>>(&mut self, children: T) {
|
||||
self.faces.extend(children);
|
||||
}
|
||||
}
|
||||
@@ -172,7 +172,7 @@ impl IncomingCallNotification {
|
||||
.with_child(
|
||||
MouseEventHandler::<Accept>::new(0, cx, |_, cx| {
|
||||
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
|
||||
Label::new("Accept".to_string(), theme.accept_button.text.clone())
|
||||
Label::new("Accept", theme.accept_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.accept_button.container)
|
||||
@@ -188,7 +188,7 @@ impl IncomingCallNotification {
|
||||
.with_child(
|
||||
MouseEventHandler::<Decline>::new(0, cx, |_, cx| {
|
||||
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
|
||||
Label::new("Decline".to_string(), theme.decline_button.text.clone())
|
||||
Label::new("Decline", theme.decline_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.decline_button.container)
|
||||
|
||||
@@ -11,8 +11,8 @@ enum Button {}
|
||||
|
||||
pub fn render_user_notification<V: View, A: Action + Clone>(
|
||||
user: Arc<User>,
|
||||
title: &str,
|
||||
body: Option<&str>,
|
||||
title: &'static str,
|
||||
body: Option<&'static str>,
|
||||
dismiss_action: A,
|
||||
buttons: Vec<(&'static str, Box<dyn Action>)>,
|
||||
cx: &mut RenderContext<V>,
|
||||
@@ -83,7 +83,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
||||
.named("contact notification header"),
|
||||
)
|
||||
.with_children(body.map(|body| {
|
||||
Label::new(body.to_string(), theme.body_message.text.clone())
|
||||
Label::new(body, theme.body_message.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.body_message.container)
|
||||
.boxed()
|
||||
@@ -97,7 +97,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
||||
|(ix, (message, action))| {
|
||||
MouseEventHandler::<Button>::new(ix, cx, |state, _| {
|
||||
let button = theme.button.style_for(state, false);
|
||||
Label::new(message.to_string(), button.text.clone())
|
||||
Label::new(message, button.text.clone())
|
||||
.contained()
|
||||
.with_style(button.container)
|
||||
.boxed()
|
||||
|
||||
@@ -175,7 +175,7 @@ impl ProjectSharedNotification {
|
||||
.with_child(
|
||||
MouseEventHandler::<Open>::new(0, cx, |_, cx| {
|
||||
let theme = &cx.global::<Settings>().theme.project_shared_notification;
|
||||
Label::new("Open".to_string(), theme.open_button.text.clone())
|
||||
Label::new("Open", theme.open_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.open_button.container)
|
||||
@@ -194,7 +194,7 @@ impl ProjectSharedNotification {
|
||||
.with_child(
|
||||
MouseEventHandler::<Dismiss>::new(0, cx, |_, cx| {
|
||||
let theme = &cx.global::<Settings>().theme.project_shared_notification;
|
||||
Label::new("Dismiss".to_string(), theme.dismiss_button.text.clone())
|
||||
Label::new("Dismiss", theme.dismiss_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.dismiss_button.container)
|
||||
|
||||
59
crates/collab_ui/src/sharing_status_indicator.rs
Normal file
59
crates/collab_ui/src/sharing_status_indicator.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use call::ActiveCall;
|
||||
use gpui::{
|
||||
color::Color,
|
||||
elements::{MouseEventHandler, Svg},
|
||||
Appearance, Element, ElementBox, Entity, MouseButton, MutableAppContext, RenderContext, View,
|
||||
};
|
||||
use settings::Settings;
|
||||
|
||||
use crate::ToggleScreenSharing;
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
|
||||
let mut status_indicator = None;
|
||||
cx.observe(&active_call, move |call, cx| {
|
||||
if let Some(room) = call.read(cx).room() {
|
||||
if room.read(cx).is_screen_sharing() {
|
||||
if status_indicator.is_none() && cx.global::<Settings>().show_call_status_icon {
|
||||
status_indicator = Some(cx.add_status_bar_item(|_| SharingStatusIndicator));
|
||||
}
|
||||
} else if let Some((window_id, _)) = status_indicator.take() {
|
||||
cx.remove_status_bar_item(window_id);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub struct SharingStatusIndicator;
|
||||
|
||||
impl Entity for SharingStatusIndicator {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for SharingStatusIndicator {
|
||||
fn ui_name() -> &'static str {
|
||||
"SharingStatusIndicator"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
|
||||
let color = match cx.appearance {
|
||||
Appearance::Light | Appearance::VibrantLight => Color::black(),
|
||||
Appearance::Dark | Appearance::VibrantDark => Color::white(),
|
||||
};
|
||||
|
||||
MouseEventHandler::<Self>::new(0, cx, |_, _| {
|
||||
Svg::new("icons/disable_screen_sharing_12.svg")
|
||||
.with_color(color)
|
||||
.constrained()
|
||||
.with_width(18.)
|
||||
.aligned()
|
||||
.boxed()
|
||||
})
|
||||
.on_click(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(ToggleScreenSharing);
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ impl CommandPalette {
|
||||
action,
|
||||
keystrokes: bindings
|
||||
.iter()
|
||||
.filter_map(|binding| binding.keystrokes())
|
||||
.map(|binding| binding.keystrokes())
|
||||
.last()
|
||||
.map_or(Vec::new(), |keystrokes| keystrokes.to_vec()),
|
||||
})
|
||||
@@ -257,7 +257,7 @@ impl PickerDelegate for CommandPalette {
|
||||
.filter_map(|(modifier, label)| {
|
||||
if modifier {
|
||||
Some(
|
||||
Label::new(label.into(), key_style.label.clone())
|
||||
Label::new(label, key_style.label.clone())
|
||||
.contained()
|
||||
.with_style(key_style.container)
|
||||
.boxed(),
|
||||
|
||||
@@ -5,7 +5,9 @@ use gpui::{
|
||||
};
|
||||
use menu::*;
|
||||
use settings::Settings;
|
||||
use std::{any::TypeId, time::Duration};
|
||||
use std::{any::TypeId, borrow::Cow, time::Duration};
|
||||
|
||||
pub type StaticItem = Box<dyn Fn(&mut MutableAppContext) -> ElementBox>;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
struct Clicked;
|
||||
@@ -24,16 +26,17 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
|
||||
pub enum ContextMenuItem {
|
||||
Item {
|
||||
label: String,
|
||||
label: Cow<'static, str>,
|
||||
action: Box<dyn Action>,
|
||||
},
|
||||
Static(StaticItem),
|
||||
Separator,
|
||||
}
|
||||
|
||||
impl ContextMenuItem {
|
||||
pub fn item(label: impl ToString, action: impl 'static + Action) -> Self {
|
||||
pub fn item(label: impl Into<Cow<'static, str>>, action: impl 'static + Action) -> Self {
|
||||
Self::Item {
|
||||
label: label.to_string(),
|
||||
label: label.into(),
|
||||
action: Box::new(action),
|
||||
}
|
||||
}
|
||||
@@ -42,14 +45,14 @@ impl ContextMenuItem {
|
||||
Self::Separator
|
||||
}
|
||||
|
||||
fn is_separator(&self) -> bool {
|
||||
matches!(self, Self::Separator)
|
||||
fn is_action(&self) -> bool {
|
||||
matches!(self, Self::Item { .. })
|
||||
}
|
||||
|
||||
fn action_id(&self) -> Option<TypeId> {
|
||||
match self {
|
||||
ContextMenuItem::Item { action, .. } => Some(action.id()),
|
||||
ContextMenuItem::Separator => None,
|
||||
ContextMenuItem::Static(..) | ContextMenuItem::Separator => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,11 +61,13 @@ pub struct ContextMenu {
|
||||
show_count: usize,
|
||||
anchor_position: Vector2F,
|
||||
anchor_corner: AnchorCorner,
|
||||
position_mode: OverlayPositionMode,
|
||||
items: Vec<ContextMenuItem>,
|
||||
selected_index: Option<usize>,
|
||||
visible: bool,
|
||||
previously_focused_view_id: Option<usize>,
|
||||
clicked: bool,
|
||||
parent_view_id: usize,
|
||||
_actions_observation: Subscription,
|
||||
}
|
||||
|
||||
@@ -77,7 +82,7 @@ impl View for ContextMenu {
|
||||
|
||||
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||
let mut cx = Self::default_keymap_context();
|
||||
cx.set.insert("menu".into());
|
||||
cx.add_identifier("menu");
|
||||
cx
|
||||
}
|
||||
|
||||
@@ -104,6 +109,7 @@ impl View for ContextMenu {
|
||||
.with_fit_mode(OverlayFitMode::SnapToWindow)
|
||||
.with_anchor_position(self.anchor_position)
|
||||
.with_anchor_corner(self.anchor_corner)
|
||||
.with_position_mode(self.position_mode)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
@@ -114,15 +120,19 @@ impl View for ContextMenu {
|
||||
|
||||
impl ContextMenu {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
let parent_view_id = cx.parent().unwrap();
|
||||
|
||||
Self {
|
||||
show_count: 0,
|
||||
anchor_position: Default::default(),
|
||||
anchor_corner: AnchorCorner::TopLeft,
|
||||
position_mode: OverlayPositionMode::Window,
|
||||
items: Default::default(),
|
||||
selected_index: Default::default(),
|
||||
visible: Default::default(),
|
||||
previously_focused_view_id: Default::default(),
|
||||
clicked: false,
|
||||
parent_view_id,
|
||||
_actions_observation: cx.observe_actions(Self::action_dispatched),
|
||||
}
|
||||
}
|
||||
@@ -184,13 +194,13 @@ impl ContextMenu {
|
||||
}
|
||||
|
||||
fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
|
||||
self.selected_index = self.items.iter().position(|item| !item.is_separator());
|
||||
self.selected_index = self.items.iter().position(|item| item.is_action());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
|
||||
for (ix, item) in self.items.iter().enumerate().rev() {
|
||||
if !item.is_separator() {
|
||||
if item.is_action() {
|
||||
self.selected_index = Some(ix);
|
||||
cx.notify();
|
||||
break;
|
||||
@@ -201,7 +211,7 @@ impl ContextMenu {
|
||||
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
|
||||
if let Some(ix) = self.selected_index {
|
||||
for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
|
||||
if !item.is_separator() {
|
||||
if item.is_action() {
|
||||
self.selected_index = Some(ix);
|
||||
cx.notify();
|
||||
break;
|
||||
@@ -215,7 +225,7 @@ impl ContextMenu {
|
||||
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
|
||||
if let Some(ix) = self.selected_index {
|
||||
for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
|
||||
if !item.is_separator() {
|
||||
if item.is_action() {
|
||||
self.selected_index = Some(ix);
|
||||
cx.notify();
|
||||
break;
|
||||
@@ -230,7 +240,7 @@ impl ContextMenu {
|
||||
&mut self,
|
||||
anchor_position: Vector2F,
|
||||
anchor_corner: AnchorCorner,
|
||||
items: impl IntoIterator<Item = ContextMenuItem>,
|
||||
items: Vec<ContextMenuItem>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let mut items = items.into_iter().peekable();
|
||||
@@ -250,7 +260,12 @@ impl ContextMenu {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_position_mode(&mut self, mode: OverlayPositionMode) {
|
||||
self.position_mode = mode;
|
||||
}
|
||||
|
||||
fn render_menu_for_measurement(&self, cx: &mut RenderContext<Self>) -> impl Element {
|
||||
let window_id = cx.window_id();
|
||||
let style = cx.global::<Settings>().theme.context_menu.clone();
|
||||
Flex::row()
|
||||
.with_child(
|
||||
@@ -268,6 +283,9 @@ impl ContextMenu {
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
ContextMenuItem::Static(f) => f(cx),
|
||||
|
||||
ContextMenuItem::Separator => Empty::new()
|
||||
.collapsed()
|
||||
.contained()
|
||||
@@ -289,12 +307,17 @@ impl ContextMenu {
|
||||
Some(ix) == self.selected_index,
|
||||
);
|
||||
KeystrokeLabel::new(
|
||||
window_id,
|
||||
self.parent_view_id,
|
||||
action.boxed_clone(),
|
||||
style.keystroke.container,
|
||||
style.keystroke.text.clone(),
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
ContextMenuItem::Static(_) => Empty::new().boxed(),
|
||||
|
||||
ContextMenuItem::Separator => Empty::new()
|
||||
.collapsed()
|
||||
.constrained()
|
||||
@@ -318,6 +341,7 @@ impl ContextMenu {
|
||||
|
||||
let style = cx.global::<Settings>().theme.context_menu.clone();
|
||||
|
||||
let window_id = cx.window_id();
|
||||
MouseEventHandler::<Menu>::new(0, cx, |_, cx| {
|
||||
Flex::column()
|
||||
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
|
||||
@@ -331,12 +355,14 @@ impl ContextMenu {
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(label.to_string(), style.label.clone())
|
||||
Label::new(label.clone(), style.label.clone())
|
||||
.contained()
|
||||
.boxed(),
|
||||
)
|
||||
.with_child({
|
||||
KeystrokeLabel::new(
|
||||
window_id,
|
||||
self.parent_view_id,
|
||||
action.boxed_clone(),
|
||||
style.keystroke.container,
|
||||
style.keystroke.text.clone(),
|
||||
@@ -356,6 +382,9 @@ impl ContextMenu {
|
||||
.on_drag(MouseButton::Left, |_, _| {})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
ContextMenuItem::Static(f) => f(cx),
|
||||
|
||||
ContextMenuItem::Separator => Empty::new()
|
||||
.constrained()
|
||||
.with_height(1.)
|
||||
|
||||
@@ -21,6 +21,7 @@ use language::{
|
||||
use project::{DiagnosticSummary, Project, ProjectPath};
|
||||
use serde_json::json;
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cmp::Ordering,
|
||||
@@ -89,14 +90,11 @@ impl View for ProjectDiagnosticsEditor {
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
if self.path_states.is_empty() {
|
||||
let theme = &cx.global::<Settings>().theme.project_diagnostics;
|
||||
Label::new(
|
||||
"No problems in workspace".to_string(),
|
||||
theme.empty_message.clone(),
|
||||
)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.container)
|
||||
.boxed()
|
||||
Label::new("No problems in workspace", theme.empty_message.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.container)
|
||||
.boxed()
|
||||
} else {
|
||||
ChildView::new(&self.editor, cx).boxed()
|
||||
}
|
||||
@@ -579,7 +577,7 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
.update(cx, |editor, cx| editor.git_diff_recalc(project, cx))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
|
||||
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
|
||||
Editor::to_item_events(event)
|
||||
}
|
||||
|
||||
@@ -696,7 +694,7 @@ pub(crate) fn render_summary(
|
||||
theme: &theme::ProjectDiagnostics,
|
||||
) -> ElementBox {
|
||||
if summary.error_count == 0 && summary.warning_count == 0 {
|
||||
Label::new("No problems".to_string(), text_style.clone()).boxed()
|
||||
Label::new("No problems", text_style.clone()).boxed()
|
||||
} else {
|
||||
let icon_width = theme.tab_icon_width;
|
||||
let icon_spacing = theme.tab_icon_spacing;
|
||||
|
||||
@@ -178,14 +178,11 @@ impl View for DiagnosticIndicator {
|
||||
|
||||
if in_progress {
|
||||
element.add_child(
|
||||
Label::new(
|
||||
"Checking…".into(),
|
||||
style.diagnostic_message.default.text.clone(),
|
||||
)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(item_spacing)
|
||||
.boxed(),
|
||||
Label::new("Checking…", style.diagnostic_message.default.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(item_spacing)
|
||||
.boxed(),
|
||||
);
|
||||
} else if let Some(diagnostic) = &self.current_diagnostic {
|
||||
let message_style = style.diagnostic_message.clone();
|
||||
|
||||
@@ -17,7 +17,8 @@ test-support = [
|
||||
"project/test-support",
|
||||
"util/test-support",
|
||||
"workspace/test-support",
|
||||
"tree-sitter-rust"
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript"
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
@@ -58,6 +59,7 @@ smol = "1.2"
|
||||
tree-sitter-rust = { version = "*", optional = true }
|
||||
tree-sitter-html = { version = "*", optional = true }
|
||||
tree-sitter-javascript = { version = "*", optional = true }
|
||||
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
text = { path = "../text", features = ["test-support"] }
|
||||
@@ -75,4 +77,5 @@ unindent = "0.1.7"
|
||||
tree-sitter = "0.20"
|
||||
tree-sitter-rust = "0.20"
|
||||
tree-sitter-html = "0.19"
|
||||
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" }
|
||||
tree-sitter-javascript = "0.20"
|
||||
|
||||
@@ -8,6 +8,7 @@ use block_map::{BlockMap, BlockPoint};
|
||||
use collections::{HashMap, HashSet};
|
||||
use fold_map::FoldMap;
|
||||
use gpui::{
|
||||
color::Color,
|
||||
fonts::{FontId, HighlightStyle},
|
||||
Entity, ModelContext, ModelHandle,
|
||||
};
|
||||
@@ -23,6 +24,12 @@ pub use block_map::{
|
||||
BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum FoldStatus {
|
||||
Folded,
|
||||
Foldable,
|
||||
}
|
||||
|
||||
pub trait ToDisplayPoint {
|
||||
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
|
||||
}
|
||||
@@ -212,6 +219,10 @@ impl DisplayMap {
|
||||
.update(cx, |map, cx| map.set_font(font_id, font_size, cx))
|
||||
}
|
||||
|
||||
pub fn set_fold_ellipses_color(&mut self, color: Color) -> bool {
|
||||
self.fold_map.set_ellipses_color(color)
|
||||
}
|
||||
|
||||
pub fn set_wrap_width(&self, width: Option<f32>, cx: &mut ModelContext<Self>) -> bool {
|
||||
self.wrap_map
|
||||
.update(cx, |map, cx| map.set_wrap_width(width, cx))
|
||||
@@ -337,7 +348,7 @@ impl DisplaySnapshot {
|
||||
.map(|h| h.text)
|
||||
}
|
||||
|
||||
// Returns text chunks starting at the end of the given display row in reverse until the start of the file
|
||||
/// Returns text chunks starting at the end of the given display row in reverse until the start of the file
|
||||
pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
|
||||
(0..=display_row).into_iter().rev().flat_map(|row| {
|
||||
self.blocks_snapshot
|
||||
@@ -411,6 +422,67 @@ impl DisplaySnapshot {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns an iterator of the start positions of the occurances of `target` in the `self` after `from`
|
||||
/// Stops if `condition` returns false for any of the character position pairs observed.
|
||||
pub fn find_while<'a>(
|
||||
&'a self,
|
||||
from: DisplayPoint,
|
||||
target: &str,
|
||||
condition: impl FnMut(char, DisplayPoint) -> bool + 'a,
|
||||
) -> impl Iterator<Item = DisplayPoint> + 'a {
|
||||
Self::find_internal(self.chars_at(from), target.chars().collect(), condition)
|
||||
}
|
||||
|
||||
/// Returns an iterator of the end positions of the occurances of `target` in the `self` before `from`
|
||||
/// Stops if `condition` returns false for any of the character position pairs observed.
|
||||
pub fn reverse_find_while<'a>(
|
||||
&'a self,
|
||||
from: DisplayPoint,
|
||||
target: &str,
|
||||
condition: impl FnMut(char, DisplayPoint) -> bool + 'a,
|
||||
) -> impl Iterator<Item = DisplayPoint> + 'a {
|
||||
Self::find_internal(
|
||||
self.reverse_chars_at(from),
|
||||
target.chars().rev().collect(),
|
||||
condition,
|
||||
)
|
||||
}
|
||||
|
||||
fn find_internal<'a>(
|
||||
iterator: impl Iterator<Item = (char, DisplayPoint)> + 'a,
|
||||
target: Vec<char>,
|
||||
mut condition: impl FnMut(char, DisplayPoint) -> bool + 'a,
|
||||
) -> impl Iterator<Item = DisplayPoint> + 'a {
|
||||
// List of partial matches with the index of the last seen character in target and the starting point of the match
|
||||
let mut partial_matches: Vec<(usize, DisplayPoint)> = Vec::new();
|
||||
iterator
|
||||
.take_while(move |(ch, point)| condition(*ch, *point))
|
||||
.filter_map(move |(ch, point)| {
|
||||
if Some(&ch) == target.get(0) {
|
||||
partial_matches.push((0, point));
|
||||
}
|
||||
|
||||
let mut found = None;
|
||||
// Keep partial matches that have the correct next character
|
||||
partial_matches.retain_mut(|(match_position, match_start)| {
|
||||
if target.get(*match_position) == Some(&ch) {
|
||||
*match_position += 1;
|
||||
if *match_position == target.len() {
|
||||
found = Some(match_start.clone());
|
||||
// This match is completed. No need to keep tracking it
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
found
|
||||
})
|
||||
}
|
||||
|
||||
pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
|
||||
let mut count = 0;
|
||||
let mut column = 0;
|
||||
@@ -530,6 +602,59 @@ impl DisplaySnapshot {
|
||||
self.blocks_snapshot.longest_row()
|
||||
}
|
||||
|
||||
pub fn fold_for_line(self: &Self, display_row: u32) -> Option<FoldStatus> {
|
||||
if self.is_foldable(display_row) {
|
||||
Some(FoldStatus::Foldable)
|
||||
} else if self.is_line_folded(display_row) {
|
||||
Some(FoldStatus::Folded)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_foldable(self: &Self, row: u32) -> bool {
|
||||
let max_point = self.max_point();
|
||||
if row >= max_point.row() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let (start_indent, is_blank) = self.line_indent(row);
|
||||
if is_blank {
|
||||
return false;
|
||||
}
|
||||
|
||||
for display_row in next_rows(row, self) {
|
||||
let (indent, is_blank) = self.line_indent(display_row);
|
||||
if !is_blank {
|
||||
return indent > start_indent;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn foldable_range(self: &Self, row: u32) -> Option<Range<DisplayPoint>> {
|
||||
let start = DisplayPoint::new(row, self.line_len(row));
|
||||
|
||||
if self.is_foldable(row) && !self.is_line_folded(start.row()) {
|
||||
let (start_indent, _) = self.line_indent(row);
|
||||
let max_point = self.max_point();
|
||||
let mut end = None;
|
||||
|
||||
for row in next_rows(row, self) {
|
||||
let (indent, is_blank) = self.line_indent(row);
|
||||
if !is_blank && indent <= start_indent {
|
||||
end = Some(DisplayPoint::new(row - 1, self.line_len(row - 1)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
let end = end.unwrap_or(max_point);
|
||||
Some(start..end)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn highlight_ranges<Tag: ?Sized + 'static>(
|
||||
&self,
|
||||
@@ -617,6 +742,24 @@ impl ToDisplayPoint for Anchor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterator<Item = u32> {
|
||||
let max_row = display_map.max_point().row();
|
||||
let start_row = display_row + 1;
|
||||
let mut current = None;
|
||||
std::iter::from_fn(move || {
|
||||
if current == None {
|
||||
current = Some(start_row);
|
||||
} else {
|
||||
current = Some(current.unwrap() + 1)
|
||||
}
|
||||
if current.unwrap() > max_row {
|
||||
None
|
||||
} else {
|
||||
current
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
@@ -627,7 +770,7 @@ pub mod tests {
|
||||
use smol::stream::StreamExt;
|
||||
use std::{env, sync::Arc};
|
||||
use theme::SyntaxTheme;
|
||||
use util::test::{marked_text_ranges, sample_text};
|
||||
use util::test::{marked_text_offsets, marked_text_ranges, sample_text};
|
||||
use Bias::*;
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
@@ -1106,7 +1249,7 @@ pub mod tests {
|
||||
vec![
|
||||
("fn ".to_string(), None),
|
||||
("out".to_string(), Some(Color::blue())),
|
||||
("…".to_string(), None),
|
||||
("⋯".to_string(), None),
|
||||
(" fn ".to_string(), Some(Color::red())),
|
||||
("inner".to_string(), Some(Color::blue())),
|
||||
("() {}\n}".to_string(), Some(Color::red())),
|
||||
@@ -1187,7 +1330,7 @@ pub mod tests {
|
||||
cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)),
|
||||
[
|
||||
("out".to_string(), Some(Color::blue())),
|
||||
("…\n".to_string(), None),
|
||||
("⋯\n".to_string(), None),
|
||||
(" \nfn ".to_string(), Some(Color::red())),
|
||||
("i\n".to_string(), Some(Color::blue()))
|
||||
]
|
||||
@@ -1418,6 +1561,32 @@ pub mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_internal() {
|
||||
assert("This is a ˇtest of find internal", "test");
|
||||
assert("Some text ˇaˇaˇaa with repeated characters", "aa");
|
||||
|
||||
fn assert(marked_text: &str, target: &str) {
|
||||
let (text, expected_offsets) = marked_text_offsets(marked_text);
|
||||
|
||||
let chars = text
|
||||
.chars()
|
||||
.enumerate()
|
||||
.map(|(index, ch)| (ch, DisplayPoint::new(0, index as u32)));
|
||||
let target = target.chars();
|
||||
|
||||
assert_eq!(
|
||||
expected_offsets
|
||||
.into_iter()
|
||||
.map(|offset| offset as u32)
|
||||
.collect::<Vec<_>>(),
|
||||
DisplaySnapshot::find_internal(chars, target.collect(), |_, _| true)
|
||||
.map(|point| point.column())
|
||||
.collect::<Vec<_>>()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn syntax_chunks<'a>(
|
||||
rows: Range<u32>,
|
||||
map: &ModelHandle<DisplayMap>,
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
ToOffset,
|
||||
};
|
||||
use collections::BTreeMap;
|
||||
use gpui::fonts::HighlightStyle;
|
||||
use gpui::{color::Color, fonts::HighlightStyle};
|
||||
use language::{Chunk, Edit, Point, TextSummary};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
@@ -133,6 +133,7 @@ impl<'a> FoldMapWriter<'a> {
|
||||
folds: self.0.folds.clone(),
|
||||
buffer_snapshot: buffer,
|
||||
version: self.0.version.load(SeqCst),
|
||||
ellipses_color: self.0.ellipses_color,
|
||||
};
|
||||
(snapshot, edits)
|
||||
}
|
||||
@@ -182,6 +183,7 @@ impl<'a> FoldMapWriter<'a> {
|
||||
folds: self.0.folds.clone(),
|
||||
buffer_snapshot: buffer,
|
||||
version: self.0.version.load(SeqCst),
|
||||
ellipses_color: self.0.ellipses_color,
|
||||
};
|
||||
(snapshot, edits)
|
||||
}
|
||||
@@ -192,6 +194,7 @@ pub struct FoldMap {
|
||||
transforms: Mutex<SumTree<Transform>>,
|
||||
folds: SumTree<Fold>,
|
||||
version: AtomicUsize,
|
||||
ellipses_color: Option<Color>,
|
||||
}
|
||||
|
||||
impl FoldMap {
|
||||
@@ -209,6 +212,7 @@ impl FoldMap {
|
||||
},
|
||||
&(),
|
||||
)),
|
||||
ellipses_color: None,
|
||||
version: Default::default(),
|
||||
};
|
||||
|
||||
@@ -217,6 +221,7 @@ impl FoldMap {
|
||||
folds: this.folds.clone(),
|
||||
buffer_snapshot: this.buffer.lock().clone(),
|
||||
version: this.version.load(SeqCst),
|
||||
ellipses_color: None,
|
||||
};
|
||||
(this, snapshot)
|
||||
}
|
||||
@@ -233,6 +238,7 @@ impl FoldMap {
|
||||
folds: self.folds.clone(),
|
||||
buffer_snapshot: self.buffer.lock().clone(),
|
||||
version: self.version.load(SeqCst),
|
||||
ellipses_color: self.ellipses_color,
|
||||
};
|
||||
(snapshot, edits)
|
||||
}
|
||||
@@ -246,6 +252,15 @@ impl FoldMap {
|
||||
(FoldMapWriter(self), snapshot, edits)
|
||||
}
|
||||
|
||||
pub fn set_ellipses_color(&mut self, color: Color) -> bool {
|
||||
if self.ellipses_color != Some(color) {
|
||||
self.ellipses_color = Some(color);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn check_invariants(&self) {
|
||||
if cfg!(test) {
|
||||
assert_eq!(
|
||||
@@ -370,7 +385,7 @@ impl FoldMap {
|
||||
}
|
||||
|
||||
if fold.end > fold.start {
|
||||
let output_text = "…";
|
||||
let output_text = "⋯";
|
||||
new_transforms.push(
|
||||
Transform {
|
||||
summary: TransformSummary {
|
||||
@@ -477,6 +492,7 @@ pub struct FoldSnapshot {
|
||||
folds: SumTree<Fold>,
|
||||
buffer_snapshot: MultiBufferSnapshot,
|
||||
pub version: usize,
|
||||
pub ellipses_color: Option<Color>,
|
||||
}
|
||||
|
||||
impl FoldSnapshot {
|
||||
@@ -739,6 +755,7 @@ impl FoldSnapshot {
|
||||
max_output_offset: range.end.0,
|
||||
highlight_endpoints: highlight_endpoints.into_iter().peekable(),
|
||||
active_highlights: Default::default(),
|
||||
ellipses_color: self.ellipses_color,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1029,6 +1046,7 @@ pub struct FoldChunks<'a> {
|
||||
max_output_offset: usize,
|
||||
highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
|
||||
active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
|
||||
ellipses_color: Option<Color>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for FoldChunks<'a> {
|
||||
@@ -1058,7 +1076,10 @@ impl<'a> Iterator for FoldChunks<'a> {
|
||||
return Some(Chunk {
|
||||
text: output_text,
|
||||
syntax_highlight_id: None,
|
||||
highlight_style: None,
|
||||
highlight_style: self.ellipses_color.map(|color| HighlightStyle {
|
||||
color: Some(color),
|
||||
..Default::default()
|
||||
}),
|
||||
diagnostic_severity: None,
|
||||
is_unnecessary: false,
|
||||
});
|
||||
@@ -1214,7 +1235,7 @@ mod tests {
|
||||
Point::new(0, 2)..Point::new(2, 2),
|
||||
Point::new(2, 4)..Point::new(4, 1),
|
||||
]);
|
||||
assert_eq!(snapshot2.text(), "aa…cc…eeeee");
|
||||
assert_eq!(snapshot2.text(), "aa⋯cc⋯eeeee");
|
||||
assert_eq!(
|
||||
edits,
|
||||
&[
|
||||
@@ -1241,7 +1262,7 @@ mod tests {
|
||||
buffer.snapshot(cx)
|
||||
});
|
||||
let (snapshot3, edits) = map.read(buffer_snapshot, subscription.consume().into_inner());
|
||||
assert_eq!(snapshot3.text(), "123a…c123c…eeeee");
|
||||
assert_eq!(snapshot3.text(), "123a⋯c123c⋯eeeee");
|
||||
assert_eq!(
|
||||
edits,
|
||||
&[
|
||||
@@ -1261,12 +1282,12 @@ mod tests {
|
||||
buffer.snapshot(cx)
|
||||
});
|
||||
let (snapshot4, _) = map.read(buffer_snapshot.clone(), subscription.consume().into_inner());
|
||||
assert_eq!(snapshot4.text(), "123a…c123456eee");
|
||||
assert_eq!(snapshot4.text(), "123a⋯c123456eee");
|
||||
|
||||
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
|
||||
writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), false);
|
||||
let (snapshot5, _) = map.read(buffer_snapshot.clone(), vec![]);
|
||||
assert_eq!(snapshot5.text(), "123a…c123456eee");
|
||||
assert_eq!(snapshot5.text(), "123a⋯c123456eee");
|
||||
|
||||
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
|
||||
writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), true);
|
||||
@@ -1287,19 +1308,19 @@ mod tests {
|
||||
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
|
||||
writer.fold(vec![5..8]);
|
||||
let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]);
|
||||
assert_eq!(snapshot.text(), "abcde…ijkl");
|
||||
assert_eq!(snapshot.text(), "abcde⋯ijkl");
|
||||
|
||||
// Create an fold adjacent to the start of the first fold.
|
||||
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
|
||||
writer.fold(vec![0..1, 2..5]);
|
||||
let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]);
|
||||
assert_eq!(snapshot.text(), "…b…ijkl");
|
||||
assert_eq!(snapshot.text(), "⋯b⋯ijkl");
|
||||
|
||||
// Create an fold adjacent to the end of the first fold.
|
||||
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
|
||||
writer.fold(vec![11..11, 8..10]);
|
||||
let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]);
|
||||
assert_eq!(snapshot.text(), "…b…kl");
|
||||
assert_eq!(snapshot.text(), "⋯b⋯kl");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -1309,7 +1330,7 @@ mod tests {
|
||||
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
|
||||
writer.fold(vec![0..2, 2..5]);
|
||||
let (snapshot, _) = map.read(buffer_snapshot, vec![]);
|
||||
assert_eq!(snapshot.text(), "…fghijkl");
|
||||
assert_eq!(snapshot.text(), "⋯fghijkl");
|
||||
|
||||
// Edit within one of the folds.
|
||||
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
|
||||
@@ -1317,7 +1338,7 @@ mod tests {
|
||||
buffer.snapshot(cx)
|
||||
});
|
||||
let (snapshot, _) = map.read(buffer_snapshot, subscription.consume().into_inner());
|
||||
assert_eq!(snapshot.text(), "12345…fghijkl");
|
||||
assert_eq!(snapshot.text(), "12345⋯fghijkl");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1334,7 +1355,7 @@ mod tests {
|
||||
Point::new(3, 1)..Point::new(4, 1),
|
||||
]);
|
||||
let (snapshot, _) = map.read(buffer_snapshot, vec![]);
|
||||
assert_eq!(snapshot.text(), "aa…eeeee");
|
||||
assert_eq!(snapshot.text(), "aa⋯eeeee");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1351,14 +1372,14 @@ mod tests {
|
||||
Point::new(3, 1)..Point::new(4, 1),
|
||||
]);
|
||||
let (snapshot, _) = map.read(buffer_snapshot, vec![]);
|
||||
assert_eq!(snapshot.text(), "aa…cccc\nd…eeeee");
|
||||
assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee");
|
||||
|
||||
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(Point::new(2, 2)..Point::new(3, 1), "")], None, cx);
|
||||
buffer.snapshot(cx)
|
||||
});
|
||||
let (snapshot, _) = map.read(buffer_snapshot, subscription.consume().into_inner());
|
||||
assert_eq!(snapshot.text(), "aa…eeeee");
|
||||
assert_eq!(snapshot.text(), "aa⋯eeeee");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1450,7 +1471,7 @@ mod tests {
|
||||
|
||||
let mut expected_text: String = buffer_snapshot.text().to_string();
|
||||
for fold_range in map.merged_fold_ranges().into_iter().rev() {
|
||||
expected_text.replace_range(fold_range.start..fold_range.end, "…");
|
||||
expected_text.replace_range(fold_range.start..fold_range.end, "⋯");
|
||||
}
|
||||
|
||||
assert_eq!(snapshot.text(), expected_text);
|
||||
@@ -1655,7 +1676,7 @@ mod tests {
|
||||
]);
|
||||
|
||||
let (snapshot, _) = map.read(buffer_snapshot, vec![]);
|
||||
assert_eq!(snapshot.text(), "aa…cccc\nd…eeeee\nffffff\n");
|
||||
assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee\nffffff\n");
|
||||
assert_eq!(
|
||||
snapshot.buffer_rows(0).collect::<Vec<_>>(),
|
||||
[Some(0), Some(3), Some(5), Some(6)]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod blink_manager;
|
||||
pub mod display_map;
|
||||
mod element;
|
||||
|
||||
mod git;
|
||||
mod highlight_matching_bracket;
|
||||
mod hover_popover;
|
||||
@@ -77,14 +78,14 @@ use std::{
|
||||
cmp::{self, Ordering, Reverse},
|
||||
mem,
|
||||
num::NonZeroU32,
|
||||
ops::{Deref, DerefMut, Range, RangeInclusive},
|
||||
ops::{Deref, DerefMut, Range},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
pub use sum_tree::Bias;
|
||||
use theme::{DiagnosticStyle, Theme};
|
||||
use util::{post_inc, ResultExt, TryFutureExt};
|
||||
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
|
||||
use workspace::{ItemNavHistory, ViewId, Workspace, WorkspaceId};
|
||||
|
||||
use crate::git::diff_hunk_to_display;
|
||||
@@ -154,6 +155,27 @@ pub struct ConfirmCodeAction {
|
||||
pub item_ix: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize, PartialEq)]
|
||||
pub struct ToggleComments {
|
||||
#[serde(default)]
|
||||
pub advance_downwards: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize, PartialEq)]
|
||||
pub struct FoldAt {
|
||||
pub display_row: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize, PartialEq)]
|
||||
pub struct UnfoldAt {
|
||||
pub display_row: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize, PartialEq)]
|
||||
pub struct GutterHover {
|
||||
pub hovered: bool,
|
||||
}
|
||||
|
||||
actions!(
|
||||
editor,
|
||||
[
|
||||
@@ -216,7 +238,6 @@ actions!(
|
||||
AddSelectionBelow,
|
||||
Tab,
|
||||
TabPrev,
|
||||
ToggleComments,
|
||||
ShowCharacterPalette,
|
||||
SelectLargerSyntaxNode,
|
||||
SelectSmallerSyntaxNode,
|
||||
@@ -236,6 +257,8 @@ actions!(
|
||||
RestartLanguageServer,
|
||||
Hover,
|
||||
Format,
|
||||
ToggleSoftWrap,
|
||||
RevealInFinder
|
||||
]
|
||||
);
|
||||
|
||||
@@ -250,6 +273,10 @@ impl_actions!(
|
||||
MovePageDown,
|
||||
ConfirmCompletion,
|
||||
ConfirmCodeAction,
|
||||
ToggleComments,
|
||||
FoldAt,
|
||||
UnfoldAt,
|
||||
GutterHover
|
||||
]
|
||||
);
|
||||
|
||||
@@ -340,12 +367,17 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(Editor::go_to_definition);
|
||||
cx.add_action(Editor::go_to_type_definition);
|
||||
cx.add_action(Editor::fold);
|
||||
cx.add_action(Editor::fold_at);
|
||||
cx.add_action(Editor::unfold_lines);
|
||||
cx.add_action(Editor::unfold_at);
|
||||
cx.add_action(Editor::gutter_hover);
|
||||
cx.add_action(Editor::fold_selected_ranges);
|
||||
cx.add_action(Editor::show_completions);
|
||||
cx.add_action(Editor::toggle_code_actions);
|
||||
cx.add_action(Editor::open_excerpts);
|
||||
cx.add_action(Editor::jump);
|
||||
cx.add_action(Editor::toggle_soft_wrap);
|
||||
cx.add_action(Editor::reveal_in_finder);
|
||||
cx.add_async_action(Editor::format);
|
||||
cx.add_action(Editor::restart_language_server);
|
||||
cx.add_action(Editor::show_character_palette);
|
||||
@@ -400,7 +432,7 @@ pub enum SelectMode {
|
||||
All,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub enum EditorMode {
|
||||
SingleLine,
|
||||
AutoHeight { max_lines: usize },
|
||||
@@ -470,6 +502,7 @@ pub struct Editor {
|
||||
leader_replica_id: Option<u16>,
|
||||
remote_id: Option<ViewId>,
|
||||
hover_state: HoverState,
|
||||
gutter_hovered: bool,
|
||||
link_go_to_definition_state: LinkGoToDefinitionState,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
@@ -810,7 +843,7 @@ impl CompletionsMenu {
|
||||
fuzzy::match_strings(
|
||||
&self.match_candidates,
|
||||
query,
|
||||
false,
|
||||
query.chars().any(|c| c.is_uppercase()),
|
||||
100,
|
||||
&Default::default(),
|
||||
executor,
|
||||
@@ -1141,6 +1174,7 @@ impl Editor {
|
||||
remote_id: None,
|
||||
hover_state: Default::default(),
|
||||
link_go_to_definition_state: Default::default(),
|
||||
gutter_hovered: false,
|
||||
_subscriptions: vec![
|
||||
cx.observe(&buffer, Self::on_buffer_changed),
|
||||
cx.subscribe(&buffer, Self::on_buffer_event),
|
||||
@@ -1732,11 +1766,13 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
|
||||
let text: Arc<str> = text.into();
|
||||
|
||||
if !self.input_enabled {
|
||||
cx.emit(Event::InputIgnored { text });
|
||||
return;
|
||||
}
|
||||
|
||||
let text: Arc<str> = text.into();
|
||||
let selections = self.selections.all_adjusted(cx);
|
||||
let mut edits = Vec::new();
|
||||
let mut new_selections = Vec::with_capacity(selections.len());
|
||||
@@ -1751,8 +1787,8 @@ impl Editor {
|
||||
// bracket of any of this language's bracket pairs.
|
||||
let mut bracket_pair = None;
|
||||
let mut is_bracket_pair_start = false;
|
||||
for pair in language.brackets() {
|
||||
if pair.close && pair.start.ends_with(text.as_ref()) {
|
||||
for (pair, enabled) in language.brackets() {
|
||||
if enabled && pair.close && pair.start.ends_with(text.as_ref()) {
|
||||
bracket_pair = Some(pair.clone());
|
||||
is_bracket_pair_start = true;
|
||||
break;
|
||||
@@ -1814,9 +1850,9 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
}
|
||||
// If an opening bracket is typed while text is selected, then
|
||||
// surround that text with the bracket pair.
|
||||
else if is_bracket_pair_start {
|
||||
// If an opening bracket is 1 character long and is typed while
|
||||
// text is selected, then surround that text with the bracket pair.
|
||||
else if is_bracket_pair_start && bracket_pair.start.chars().count() == 1 {
|
||||
edits.push((selection.start..selection.start, text.clone()));
|
||||
edits.push((
|
||||
selection.end..selection.end,
|
||||
@@ -1920,11 +1956,12 @@ impl Editor {
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>();
|
||||
|
||||
insert_extra_newline = language.brackets().iter().any(|pair| {
|
||||
insert_extra_newline = language.brackets().any(|(pair, enabled)| {
|
||||
let pair_start = pair.start.trim_end();
|
||||
let pair_end = pair.end.trim_start();
|
||||
|
||||
pair.newline
|
||||
enabled
|
||||
&& pair.newline
|
||||
&& buffer
|
||||
.contains_str_at(end + trailing_whitespace_len, pair_end)
|
||||
&& buffer.contains_str_at(
|
||||
@@ -2632,14 +2669,15 @@ impl Editor {
|
||||
pub fn render_code_actions_indicator(
|
||||
&self,
|
||||
style: &EditorStyle,
|
||||
active: bool,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> Option<ElementBox> {
|
||||
if self.available_code_actions.is_some() {
|
||||
enum Tag {}
|
||||
enum CodeActions {}
|
||||
Some(
|
||||
MouseEventHandler::<Tag>::new(0, cx, |_, _| {
|
||||
MouseEventHandler::<CodeActions>::new(0, cx, |state, _| {
|
||||
Svg::new("icons/bolt_8.svg")
|
||||
.with_color(style.code_actions.indicator)
|
||||
.with_color(style.code_actions.indicator.style_for(state, active).color)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
@@ -2656,6 +2694,80 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_fold_indicators(
|
||||
&self,
|
||||
fold_data: Option<Vec<(u32, FoldStatus)>>,
|
||||
active_rows: &BTreeMap<u32, bool>,
|
||||
style: &EditorStyle,
|
||||
gutter_hovered: bool,
|
||||
line_height: f32,
|
||||
gutter_margin: f32,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> Option<Vec<(u32, ElementBox)>> {
|
||||
enum FoldIndicators {}
|
||||
|
||||
let style = style.folds.clone();
|
||||
|
||||
fold_data.map(|fold_data| {
|
||||
fold_data
|
||||
.iter()
|
||||
.copied()
|
||||
.filter_map(|(fold_location, fold_status)| {
|
||||
(gutter_hovered
|
||||
|| fold_status == FoldStatus::Folded
|
||||
|| !*active_rows.get(&fold_location).unwrap_or(&true))
|
||||
.then(|| {
|
||||
(
|
||||
fold_location,
|
||||
MouseEventHandler::<FoldIndicators>::new(
|
||||
fold_location as usize,
|
||||
cx,
|
||||
|mouse_state, _| -> ElementBox {
|
||||
Svg::new(match fold_status {
|
||||
FoldStatus::Folded => style.folded_icon.clone(),
|
||||
FoldStatus::Foldable => style.foldable_icon.clone(),
|
||||
})
|
||||
.with_color(
|
||||
style
|
||||
.indicator
|
||||
.style_for(
|
||||
mouse_state,
|
||||
fold_status == FoldStatus::Folded,
|
||||
)
|
||||
.color,
|
||||
)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_height(line_height)
|
||||
.with_width(gutter_margin)
|
||||
.aligned()
|
||||
.boxed()
|
||||
},
|
||||
)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_padding(Padding::uniform(3.))
|
||||
.on_click(MouseButton::Left, {
|
||||
move |_, cx| {
|
||||
cx.dispatch_any_action(match fold_status {
|
||||
FoldStatus::Folded => Box::new(UnfoldAt {
|
||||
display_row: fold_location,
|
||||
}),
|
||||
FoldStatus::Foldable => Box::new(FoldAt {
|
||||
display_row: fold_location,
|
||||
}),
|
||||
});
|
||||
}
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn context_menu_visible(&self) -> bool {
|
||||
self.context_menu
|
||||
.as_ref()
|
||||
@@ -3238,26 +3350,12 @@ impl Editor {
|
||||
|
||||
while let Some(selection) = selections.next() {
|
||||
// Find all the selections that span a contiguous row range
|
||||
contiguous_row_selections.push(selection.clone());
|
||||
let start_row = selection.start.row;
|
||||
let mut end_row = if selection.end.column > 0 || selection.is_empty() {
|
||||
display_map.next_line_boundary(selection.end).0.row + 1
|
||||
} else {
|
||||
selection.end.row
|
||||
};
|
||||
|
||||
while let Some(next_selection) = selections.peek() {
|
||||
if next_selection.start.row <= end_row {
|
||||
end_row = if next_selection.end.column > 0 || next_selection.is_empty() {
|
||||
display_map.next_line_boundary(next_selection.end).0.row + 1
|
||||
} else {
|
||||
next_selection.end.row
|
||||
};
|
||||
contiguous_row_selections.push(selections.next().unwrap().clone());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let (start_row, end_row) = consume_contiguous_rows(
|
||||
&mut contiguous_row_selections,
|
||||
selection,
|
||||
&display_map,
|
||||
&mut selections,
|
||||
);
|
||||
|
||||
// Move the text spanned by the row range to be before the line preceding the row range
|
||||
if start_row > 0 {
|
||||
@@ -3322,13 +3420,13 @@ impl Editor {
|
||||
}
|
||||
|
||||
self.transact(cx, |this, cx| {
|
||||
this.unfold_ranges(unfold_ranges, true, cx);
|
||||
this.unfold_ranges(unfold_ranges, true, true, cx);
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
for (range, text) in edits {
|
||||
buffer.edit([(range, text)], None, cx);
|
||||
}
|
||||
});
|
||||
this.fold_ranges(refold_ranges, cx);
|
||||
this.fold_ranges(refold_ranges, true, cx);
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select(new_selections);
|
||||
})
|
||||
@@ -3350,26 +3448,12 @@ impl Editor {
|
||||
|
||||
while let Some(selection) = selections.next() {
|
||||
// Find all the selections that span a contiguous row range
|
||||
contiguous_row_selections.push(selection.clone());
|
||||
let start_row = selection.start.row;
|
||||
let mut end_row = if selection.end.column > 0 || selection.is_empty() {
|
||||
display_map.next_line_boundary(selection.end).0.row + 1
|
||||
} else {
|
||||
selection.end.row
|
||||
};
|
||||
|
||||
while let Some(next_selection) = selections.peek() {
|
||||
if next_selection.start.row <= end_row {
|
||||
end_row = if next_selection.end.column > 0 || next_selection.is_empty() {
|
||||
display_map.next_line_boundary(next_selection.end).0.row + 1
|
||||
} else {
|
||||
next_selection.end.row
|
||||
};
|
||||
contiguous_row_selections.push(selections.next().unwrap().clone());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let (start_row, end_row) = consume_contiguous_rows(
|
||||
&mut contiguous_row_selections,
|
||||
selection,
|
||||
&display_map,
|
||||
&mut selections,
|
||||
);
|
||||
|
||||
// Move the text spanned by the row range to be after the last line of the row range
|
||||
if end_row <= buffer.max_point().row {
|
||||
@@ -3427,13 +3511,13 @@ impl Editor {
|
||||
}
|
||||
|
||||
self.transact(cx, |this, cx| {
|
||||
this.unfold_ranges(unfold_ranges, true, cx);
|
||||
this.unfold_ranges(unfold_ranges, true, true, cx);
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
for (range, text) in edits {
|
||||
buffer.edit([(range, text)], None, cx);
|
||||
}
|
||||
});
|
||||
this.fold_ranges(refold_ranges, cx);
|
||||
this.fold_ranges(refold_ranges, true, cx);
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
||||
});
|
||||
}
|
||||
@@ -3800,7 +3884,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
if self.mode == EditorMode::SingleLine {
|
||||
cx.propagate_action();
|
||||
return;
|
||||
}
|
||||
@@ -4261,7 +4345,7 @@ impl Editor {
|
||||
to_unfold.push(selection.start..selection.end);
|
||||
}
|
||||
}
|
||||
self.unfold_ranges(to_unfold, true, cx);
|
||||
self.unfold_ranges(to_unfold, true, true, cx);
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges(new_selection_ranges);
|
||||
});
|
||||
@@ -4410,7 +4494,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
if let Some(next_selected_range) = next_selected_range {
|
||||
self.unfold_ranges([next_selected_range.clone()], false, cx);
|
||||
self.unfold_ranges([next_selected_range.clone()], false, true, cx);
|
||||
self.change_selections(Some(Autoscroll::newest()), cx, |s| {
|
||||
if action.replace_newest {
|
||||
s.delete(s.newest_anchor().id);
|
||||
@@ -4443,7 +4527,7 @@ impl Editor {
|
||||
wordwise: true,
|
||||
done: false,
|
||||
};
|
||||
self.unfold_ranges([selection.start..selection.end], false, cx);
|
||||
self.unfold_ranges([selection.start..selection.end], false, true, cx);
|
||||
self.change_selections(Some(Autoscroll::newest()), cx, |s| {
|
||||
s.select(selections);
|
||||
});
|
||||
@@ -4462,7 +4546,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext<Self>) {
|
||||
pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext<Self>) {
|
||||
self.transact(cx, |this, cx| {
|
||||
let mut selections = this.selections.all::<Point>(cx);
|
||||
let mut edits = Vec::new();
|
||||
@@ -4681,6 +4765,34 @@ impl Editor {
|
||||
|
||||
drop(snapshot);
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
||||
|
||||
let selections = this.selections.all::<Point>(cx);
|
||||
let selections_on_single_row = selections.windows(2).all(|selections| {
|
||||
selections[0].start.row == selections[1].start.row
|
||||
&& selections[0].end.row == selections[1].end.row
|
||||
&& selections[0].start.row == selections[0].end.row
|
||||
});
|
||||
let selections_selecting = selections
|
||||
.iter()
|
||||
.any(|selection| selection.start != selection.end);
|
||||
let advance_downwards = action.advance_downwards
|
||||
&& selections_on_single_row
|
||||
&& !selections_selecting
|
||||
&& this.mode != EditorMode::SingleLine;
|
||||
|
||||
if advance_downwards {
|
||||
let snapshot = this.buffer.read(cx).snapshot(cx);
|
||||
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|display_snapshot, display_point, _| {
|
||||
let mut point = display_point.to_point(display_snapshot);
|
||||
point.row += 1;
|
||||
point = snapshot.clip_point(point, Bias::Left);
|
||||
let display_point = point.to_display_point(display_snapshot);
|
||||
(display_point, SelectionGoal::Column(display_point.column()))
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4750,27 +4862,54 @@ impl Editor {
|
||||
_: &MoveToEnclosingBracket,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||
let mut selections = self.selections.all::<usize>(cx);
|
||||
for selection in &mut selections {
|
||||
if let Some((open_range, close_range)) =
|
||||
buffer.enclosing_bracket_ranges(selection.start..selection.end)
|
||||
{
|
||||
let close_range = close_range.to_inclusive();
|
||||
let destination = if close_range.contains(&selection.start)
|
||||
&& close_range.contains(&selection.end)
|
||||
{
|
||||
open_range.end
|
||||
} else {
|
||||
*close_range.start()
|
||||
};
|
||||
selection.start = destination;
|
||||
selection.end = destination;
|
||||
}
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select(selections);
|
||||
s.move_offsets_with(|snapshot, selection| {
|
||||
let Some(enclosing_bracket_ranges) = snapshot.enclosing_bracket_ranges(selection.start..selection.end) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut best_length = usize::MAX;
|
||||
let mut best_inside = false;
|
||||
let mut best_in_bracket_range = false;
|
||||
let mut best_destination = None;
|
||||
for (open, close) in enclosing_bracket_ranges {
|
||||
let close = close.to_inclusive();
|
||||
let length = close.end() - open.start;
|
||||
let inside = selection.start >= open.end && selection.end <= *close.start();
|
||||
let in_bracket_range = open.to_inclusive().contains(&selection.head()) || close.contains(&selection.head());
|
||||
|
||||
// If best is next to a bracket and current isn't, skip
|
||||
if !in_bracket_range && best_in_bracket_range {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prefer smaller lengths unless best is inside and current isn't
|
||||
if length > best_length && (best_inside || !inside) {
|
||||
continue;
|
||||
}
|
||||
|
||||
best_length = length;
|
||||
best_inside = inside;
|
||||
best_in_bracket_range = in_bracket_range;
|
||||
best_destination = Some(if close.contains(&selection.start) && close.contains(&selection.end) {
|
||||
if inside {
|
||||
open.end
|
||||
} else {
|
||||
open.start
|
||||
}
|
||||
} else {
|
||||
if inside {
|
||||
*close.start()
|
||||
} else {
|
||||
*close.end()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(destination) = best_destination {
|
||||
selection.collapse_to(destination, SelectionGoal::None);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5003,7 +5142,7 @@ impl Editor {
|
||||
GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx),
|
||||
});
|
||||
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
cx.spawn_labeled("Fetching Definition...", |workspace, mut cx| async move {
|
||||
let definitions = definitions.await?;
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
Editor::navigate_to_definitions(workspace, editor_handle, definitions, cx);
|
||||
@@ -5083,31 +5222,36 @@ impl Editor {
|
||||
|
||||
let project = workspace.project().clone();
|
||||
let references = project.update(cx, |project, cx| project.references(&buffer, head, cx));
|
||||
Some(cx.spawn(|workspace, mut cx| async move {
|
||||
let locations = references.await?;
|
||||
if locations.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
Some(cx.spawn_labeled(
|
||||
"Finding All References...",
|
||||
|workspace, mut cx| async move {
|
||||
let locations = references.await?;
|
||||
if locations.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let title = locations
|
||||
.first()
|
||||
.as_ref()
|
||||
.map(|location| {
|
||||
let buffer = location.buffer.read(cx);
|
||||
format!(
|
||||
"References to `{}`",
|
||||
buffer
|
||||
.text_for_range(location.range.clone())
|
||||
.collect::<String>()
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
Self::open_locations_in_multibuffer(workspace, locations, replica_id, title, cx);
|
||||
});
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let title = locations
|
||||
.first()
|
||||
.as_ref()
|
||||
.map(|location| {
|
||||
let buffer = location.buffer.read(cx);
|
||||
format!(
|
||||
"References to `{}`",
|
||||
buffer
|
||||
.text_for_range(location.range.clone())
|
||||
.collect::<String>()
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
Self::open_locations_in_multibuffer(
|
||||
workspace, locations, replica_id, title, cx,
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}))
|
||||
Ok(())
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// Opens a multibuffer with the given project locations in it
|
||||
@@ -5386,21 +5530,20 @@ impl Editor {
|
||||
None => return None,
|
||||
};
|
||||
|
||||
Some(self.perform_format(project, cx))
|
||||
Some(self.perform_format(project, FormatTrigger::Manual, cx))
|
||||
}
|
||||
|
||||
fn perform_format(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
trigger: FormatTrigger,
|
||||
cx: &mut ViewContext<'_, Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let buffer = self.buffer().clone();
|
||||
let buffers = buffer.read(cx).all_buffers();
|
||||
|
||||
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
|
||||
let format = project.update(cx, |project, cx| {
|
||||
project.format(buffers, true, FormatTrigger::Manual, cx)
|
||||
});
|
||||
let format = project.update(cx, |project, cx| project.format(buffers, true, trigger, cx));
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let transaction = futures::select_biased! {
|
||||
@@ -5604,14 +5747,18 @@ impl Editor {
|
||||
let mut fold_ranges = Vec::new();
|
||||
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
let selections = self.selections.all::<Point>(cx);
|
||||
for selection in selections {
|
||||
let range = selection.display_range(&display_map).sorted();
|
||||
let buffer_start_row = range.start.to_point(&display_map).row;
|
||||
|
||||
for row in (0..=range.end.row()).rev() {
|
||||
if self.is_line_foldable(&display_map, row) && !display_map.is_line_folded(row) {
|
||||
let fold_range = self.foldable_range_for_line(&display_map, row);
|
||||
let fold_range = display_map.foldable_range(row).map(|range| {
|
||||
range.start.to_point(&display_map)..range.end.to_point(&display_map)
|
||||
});
|
||||
|
||||
if let Some(fold_range) = fold_range {
|
||||
if fold_range.end.row >= buffer_start_row {
|
||||
fold_ranges.push(fold_range);
|
||||
if row <= range.start.row() {
|
||||
@@ -5622,7 +5769,26 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
self.fold_ranges(fold_ranges, cx);
|
||||
self.fold_ranges(fold_ranges, true, cx);
|
||||
}
|
||||
|
||||
pub fn fold_at(&mut self, fold_at: &FoldAt, cx: &mut ViewContext<Self>) {
|
||||
let display_row = fold_at.display_row;
|
||||
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
if let Some(fold_range) = display_map.foldable_range(display_row) {
|
||||
let autoscroll = self
|
||||
.selections
|
||||
.all::<Point>(cx)
|
||||
.iter()
|
||||
.any(|selection| fold_range.overlaps(&selection.display_range(&display_map)));
|
||||
|
||||
let fold_range =
|
||||
fold_range.start.to_point(&display_map)..fold_range.end.to_point(&display_map);
|
||||
|
||||
self.fold_ranges(std::iter::once(fold_range), autoscroll, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unfold_lines(&mut self, _: &UnfoldLines, cx: &mut ViewContext<Self>) {
|
||||
@@ -5640,85 +5806,86 @@ impl Editor {
|
||||
start..end
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
self.unfold_ranges(ranges, true, cx);
|
||||
|
||||
self.unfold_ranges(ranges, true, true, cx);
|
||||
}
|
||||
|
||||
fn is_line_foldable(&self, display_map: &DisplaySnapshot, display_row: u32) -> bool {
|
||||
let max_point = display_map.max_point();
|
||||
if display_row >= max_point.row() {
|
||||
false
|
||||
} else {
|
||||
let (start_indent, is_blank) = display_map.line_indent(display_row);
|
||||
if is_blank {
|
||||
false
|
||||
} else {
|
||||
for display_row in display_row + 1..=max_point.row() {
|
||||
let (indent, is_blank) = display_map.line_indent(display_row);
|
||||
if !is_blank {
|
||||
return indent > start_indent;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn unfold_at(&mut self, unfold_at: &UnfoldAt, cx: &mut ViewContext<Self>) {
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
fn foldable_range_for_line(
|
||||
&self,
|
||||
display_map: &DisplaySnapshot,
|
||||
start_row: u32,
|
||||
) -> Range<Point> {
|
||||
let max_point = display_map.max_point();
|
||||
let intersection_range = DisplayPoint::new(unfold_at.display_row, 0)
|
||||
..DisplayPoint::new(
|
||||
unfold_at.display_row,
|
||||
display_map.line_len(unfold_at.display_row),
|
||||
);
|
||||
|
||||
let (start_indent, _) = display_map.line_indent(start_row);
|
||||
let start = DisplayPoint::new(start_row, display_map.line_len(start_row));
|
||||
let mut end = None;
|
||||
for row in start_row + 1..=max_point.row() {
|
||||
let (indent, is_blank) = display_map.line_indent(row);
|
||||
if !is_blank && indent <= start_indent {
|
||||
end = Some(DisplayPoint::new(row - 1, display_map.line_len(row - 1)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
let autoscroll =
|
||||
self.selections.all::<Point>(cx).iter().any(|selection| {
|
||||
intersection_range.overlaps(&selection.display_range(&display_map))
|
||||
});
|
||||
|
||||
let end = end.unwrap_or(max_point);
|
||||
start.to_point(display_map)..end.to_point(display_map)
|
||||
let display_point = DisplayPoint::new(unfold_at.display_row, 0).to_point(&display_map);
|
||||
|
||||
let mut point_range = display_point..display_point;
|
||||
|
||||
point_range.start.column = 0;
|
||||
point_range.end.column = display_map.buffer_snapshot.line_len(point_range.end.row);
|
||||
|
||||
self.unfold_ranges(std::iter::once(point_range), true, autoscroll, cx)
|
||||
}
|
||||
|
||||
pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext<Self>) {
|
||||
let selections = self.selections.all::<Point>(cx);
|
||||
let ranges = selections.into_iter().map(|s| s.start..s.end);
|
||||
self.fold_ranges(ranges, cx);
|
||||
self.fold_ranges(ranges, true, cx);
|
||||
}
|
||||
|
||||
pub fn fold_ranges<T: ToOffset>(
|
||||
pub fn fold_ranges<T: ToOffset + Clone>(
|
||||
&mut self,
|
||||
ranges: impl IntoIterator<Item = Range<T>>,
|
||||
auto_scroll: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let mut ranges = ranges.into_iter().peekable();
|
||||
if ranges.peek().is_some() {
|
||||
self.display_map.update(cx, |map, cx| map.fold(ranges, cx));
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
|
||||
if auto_scroll {
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unfold_ranges<T: ToOffset>(
|
||||
pub fn unfold_ranges<T: ToOffset + Clone>(
|
||||
&mut self,
|
||||
ranges: impl IntoIterator<Item = Range<T>>,
|
||||
inclusive: bool,
|
||||
auto_scroll: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let mut ranges = ranges.into_iter().peekable();
|
||||
if ranges.peek().is_some() {
|
||||
self.display_map
|
||||
.update(cx, |map, cx| map.unfold(ranges, inclusive, cx));
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
if auto_scroll {
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gutter_hover(
|
||||
&mut self,
|
||||
GutterHover { hovered }: &GutterHover,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.gutter_hovered = *hovered;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn insert_blocks(
|
||||
&mut self,
|
||||
blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
|
||||
@@ -5810,6 +5977,27 @@ impl Editor {
|
||||
.update(cx, |map, cx| map.set_wrap_width(width, cx))
|
||||
}
|
||||
|
||||
pub fn toggle_soft_wrap(&mut self, _: &ToggleSoftWrap, cx: &mut ViewContext<Self>) {
|
||||
if self.soft_wrap_mode_override.is_some() {
|
||||
self.soft_wrap_mode_override.take();
|
||||
} else {
|
||||
let soft_wrap = match self.soft_wrap_mode(cx) {
|
||||
SoftWrap::None => settings::SoftWrap::EditorWidth,
|
||||
SoftWrap::EditorWidth | SoftWrap::Column(_) => settings::SoftWrap::None,
|
||||
};
|
||||
self.soft_wrap_mode_override = Some(soft_wrap);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
|
||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
|
||||
cx.reveal_path(&file.abs_path(cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn highlight_rows(&mut self, rows: Option<Range<u32>>) {
|
||||
self.highlighted_rows = rows;
|
||||
}
|
||||
@@ -6159,6 +6347,35 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn consume_contiguous_rows(
|
||||
contiguous_row_selections: &mut Vec<Selection<Point>>,
|
||||
selection: &Selection<Point>,
|
||||
display_map: &DisplaySnapshot,
|
||||
selections: &mut std::iter::Peekable<std::slice::Iter<Selection<Point>>>,
|
||||
) -> (u32, u32) {
|
||||
contiguous_row_selections.push(selection.clone());
|
||||
let start_row = selection.start.row;
|
||||
let mut end_row = ending_row(selection, display_map);
|
||||
|
||||
while let Some(next_selection) = selections.peek() {
|
||||
if next_selection.start.row <= end_row {
|
||||
end_row = ending_row(next_selection, display_map);
|
||||
contiguous_row_selections.push(selections.next().unwrap().clone());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
(start_row, end_row)
|
||||
}
|
||||
|
||||
fn ending_row(next_selection: &Selection<Point>, display_map: &DisplaySnapshot) -> u32 {
|
||||
if next_selection.end.column > 0 || next_selection.is_empty() {
|
||||
display_map.next_line_boundary(next_selection.end).0.row + 1
|
||||
} else {
|
||||
next_selection.end.row
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorSnapshot {
|
||||
pub fn language_at<T: ToOffset>(&self, position: T) -> Option<&Arc<Language>> {
|
||||
self.display_snapshot.buffer_snapshot.language_at(position)
|
||||
@@ -6187,6 +6404,9 @@ impl Deref for EditorSnapshot {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Event {
|
||||
InputIgnored {
|
||||
text: Arc<str>,
|
||||
},
|
||||
ExcerptsAdded {
|
||||
buffer: ModelHandle<Buffer>,
|
||||
predecessor: ExcerptId,
|
||||
@@ -6227,6 +6447,7 @@ impl View for Editor {
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let style = self.style(cx);
|
||||
let font_changed = self.display_map.update(cx, |map, cx| {
|
||||
map.set_fold_ellipses_color(style.folds.ellipses.text_color);
|
||||
map.set_font(style.text.font_id, style.text.font_size, cx)
|
||||
});
|
||||
|
||||
@@ -6253,8 +6474,10 @@ impl View for Editor {
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
let focused_event = EditorFocused(cx.handle());
|
||||
cx.emit_global(focused_event);
|
||||
if cx.is_self_focused() {
|
||||
let focused_event = EditorFocused(cx.handle());
|
||||
cx.emit_global(focused_event);
|
||||
}
|
||||
if let Some(rename) = self.pending_rename.as_ref() {
|
||||
cx.focus(&rename.editor);
|
||||
} else {
|
||||
@@ -6334,17 +6557,13 @@ impl View for Editor {
|
||||
EditorMode::AutoHeight { .. } => "auto_height",
|
||||
EditorMode::Full => "full",
|
||||
};
|
||||
context.map.insert("mode".into(), mode.into());
|
||||
context.add_key("mode", mode);
|
||||
if self.pending_rename.is_some() {
|
||||
context.set.insert("renaming".into());
|
||||
context.add_identifier("renaming");
|
||||
}
|
||||
match self.context_menu.as_ref() {
|
||||
Some(ContextMenu::Completions(_)) => {
|
||||
context.set.insert("showing_completions".into());
|
||||
}
|
||||
Some(ContextMenu::CodeActions(_)) => {
|
||||
context.set.insert("showing_code_actions".into());
|
||||
}
|
||||
Some(ContextMenu::Completions(_)) => context.add_identifier("showing_completions"),
|
||||
Some(ContextMenu::CodeActions(_)) => context.add_identifier("showing_code_actions"),
|
||||
None => {}
|
||||
}
|
||||
|
||||
@@ -6393,26 +6612,29 @@ impl View for Editor {
|
||||
text: &str,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.transact(cx, |this, cx| {
|
||||
if this.input_enabled {
|
||||
let new_selected_ranges = if let Some(range_utf16) = range_utf16 {
|
||||
let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
|
||||
Some(this.selection_replacement_ranges(range_utf16, cx))
|
||||
} else {
|
||||
this.marked_text_ranges(cx)
|
||||
};
|
||||
|
||||
if let Some(new_selected_ranges) = new_selected_ranges {
|
||||
this.change_selections(None, cx, |selections| {
|
||||
selections.select_ranges(new_selected_ranges)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.handle_input(text, cx);
|
||||
});
|
||||
|
||||
if !self.input_enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
self.transact(cx, |this, cx| {
|
||||
let new_selected_ranges = if let Some(range_utf16) = range_utf16 {
|
||||
let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
|
||||
Some(this.selection_replacement_ranges(range_utf16, cx))
|
||||
} else {
|
||||
this.marked_text_ranges(cx)
|
||||
};
|
||||
|
||||
if let Some(new_selected_ranges) = new_selected_ranges {
|
||||
this.change_selections(None, cx, |selections| {
|
||||
selections.select_ranges(new_selected_ranges)
|
||||
});
|
||||
}
|
||||
this.handle_input(text, cx);
|
||||
});
|
||||
|
||||
if let Some(transaction) = self.ime_transaction {
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
buffer.group_until_transaction(transaction, cx);
|
||||
@@ -6909,21 +7131,6 @@ pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator<Item = &'a str
|
||||
.flat_map(|word| word.split_inclusive('_'))
|
||||
}
|
||||
|
||||
trait RangeExt<T> {
|
||||
fn sorted(&self) -> Range<T>;
|
||||
fn to_inclusive(&self) -> RangeInclusive<T>;
|
||||
}
|
||||
|
||||
impl<T: Ord + Clone> RangeExt<T> for Range<T> {
|
||||
fn sorted(&self) -> Self {
|
||||
cmp::min(&self.start, &self.end).clone()..cmp::max(&self.start, &self.end).clone()
|
||||
}
|
||||
|
||||
fn to_inclusive(&self) -> RangeInclusive<T> {
|
||||
self.start.clone()..=self.end.clone()
|
||||
}
|
||||
}
|
||||
|
||||
trait RangeToAnchorExt {
|
||||
fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range<Anchor>;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ use super::{
|
||||
ToPoint, MAX_LINE_LEN,
|
||||
};
|
||||
use crate::{
|
||||
display_map::{BlockStyle, DisplaySnapshot, TransformBlock},
|
||||
display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock},
|
||||
git::{diff_hunk_to_display, DisplayDiffHunk},
|
||||
hover_popover::{
|
||||
HideHover, HoverAt, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
|
||||
@@ -14,7 +14,7 @@ use crate::{
|
||||
},
|
||||
mouse_context_menu::DeployMouseContextMenu,
|
||||
scroll::actions::Scroll,
|
||||
EditorStyle,
|
||||
EditorStyle, GutterHover, UnfoldAt,
|
||||
};
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, HashMap};
|
||||
@@ -48,6 +48,9 @@ use std::{
|
||||
ops::{DerefMut, Range},
|
||||
sync::Arc,
|
||||
};
|
||||
use workspace::item::Item;
|
||||
|
||||
enum FoldMarkers {}
|
||||
|
||||
struct SelectionLayout {
|
||||
head: DisplayPoint,
|
||||
@@ -212,6 +215,17 @@ impl EditorElement {
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
enum GutterHandlers {}
|
||||
cx.scene.push_mouse_region(
|
||||
MouseRegion::new::<GutterHandlers>(view.id(), view.id() + 1, gutter_bounds).on_hover(
|
||||
|hover, cx| {
|
||||
cx.dispatch_action(GutterHover {
|
||||
hovered: hover.started,
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn mouse_down(
|
||||
@@ -400,16 +414,7 @@ impl EditorElement {
|
||||
) -> bool {
|
||||
// This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
|
||||
// Don't trigger hover popover if mouse is hovering over context menu
|
||||
let point = if text_bounds.contains_point(position) {
|
||||
let (point, target_point) = position_map.point_for_position(text_bounds, position);
|
||||
if point == target_point {
|
||||
Some(point)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let point = position_to_display_point(position, text_bounds, position_map);
|
||||
|
||||
cx.dispatch_action(UpdateGoToDefinitionLink {
|
||||
point,
|
||||
@@ -418,6 +423,7 @@ impl EditorElement {
|
||||
});
|
||||
|
||||
cx.dispatch_action(HoverAt { point });
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
@@ -569,12 +575,25 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
|
||||
let mut x = bounds.width() - layout.gutter_padding;
|
||||
let mut x = 0.;
|
||||
let mut y = *row as f32 * line_height - scroll_top;
|
||||
x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
|
||||
y += (line_height - indicator.size().y()) / 2.;
|
||||
indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
|
||||
}
|
||||
|
||||
layout.fold_indicators.as_mut().map(|fold_indicators| {
|
||||
for (line, fold_indicator) in fold_indicators.iter_mut() {
|
||||
let mut x = bounds.width() - layout.gutter_padding;
|
||||
let mut y = *line as f32 * line_height - scroll_top;
|
||||
|
||||
x += ((layout.gutter_padding + layout.gutter_margin) - fold_indicator.size().x())
|
||||
/ 2.;
|
||||
y += (line_height - fold_indicator.size().y()) / 2.;
|
||||
|
||||
fold_indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn paint_diff_hunks(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
|
||||
@@ -676,6 +695,7 @@ impl EditorElement {
|
||||
let max_glyph_width = layout.position_map.em_width;
|
||||
let scroll_left = scroll_position.x() * max_glyph_width;
|
||||
let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.);
|
||||
let line_end_overshoot = 0.15 * layout.position_map.line_height;
|
||||
|
||||
cx.scene.push_layer(Some(bounds));
|
||||
|
||||
@@ -688,12 +708,54 @@ impl EditorElement {
|
||||
},
|
||||
});
|
||||
|
||||
let fold_corner_radius =
|
||||
self.style.folds.ellipses.corner_radius_factor * layout.position_map.line_height;
|
||||
for (id, range, color) in layout.fold_ranges.iter() {
|
||||
self.paint_highlighted_range(
|
||||
range.clone(),
|
||||
*color,
|
||||
fold_corner_radius,
|
||||
fold_corner_radius * 2.,
|
||||
layout,
|
||||
content_origin,
|
||||
scroll_top,
|
||||
scroll_left,
|
||||
bounds,
|
||||
cx,
|
||||
);
|
||||
|
||||
for bound in range_to_bounds(
|
||||
&range,
|
||||
content_origin,
|
||||
scroll_left,
|
||||
scroll_top,
|
||||
&layout.visible_display_row_range,
|
||||
line_end_overshoot,
|
||||
&layout.position_map,
|
||||
) {
|
||||
cx.scene.push_cursor_region(CursorRegion {
|
||||
bounds: bound,
|
||||
style: CursorStyle::PointingHand,
|
||||
});
|
||||
|
||||
let display_row = range.start.row();
|
||||
cx.scene.push_mouse_region(
|
||||
MouseRegion::new::<FoldMarkers>(self.view.id(), *id as usize, bound)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(UnfoldAt { display_row })
|
||||
})
|
||||
.with_notify_on_hover(true)
|
||||
.with_notify_on_click(true),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (range, color) in &layout.highlighted_ranges {
|
||||
self.paint_highlighted_range(
|
||||
range.clone(),
|
||||
*color,
|
||||
0.,
|
||||
0.15 * layout.position_map.line_height,
|
||||
line_end_overshoot,
|
||||
layout,
|
||||
content_origin,
|
||||
scroll_top,
|
||||
@@ -704,9 +766,10 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
let mut cursors = SmallVec::<[Cursor; 32]>::new();
|
||||
let corner_radius = 0.15 * layout.position_map.line_height;
|
||||
|
||||
for (replica_id, selections) in &layout.selections {
|
||||
let selection_style = style.replica_selection_style(*replica_id);
|
||||
let corner_radius = 0.15 * layout.position_map.line_height;
|
||||
|
||||
for selection in selections {
|
||||
self.paint_highlighted_range(
|
||||
@@ -1118,6 +1181,24 @@ impl EditorElement {
|
||||
.width()
|
||||
}
|
||||
|
||||
fn get_fold_indicators(
|
||||
&self,
|
||||
is_singleton: bool,
|
||||
display_rows: Range<u32>,
|
||||
snapshot: &EditorSnapshot,
|
||||
) -> Option<Vec<(u32, FoldStatus)>> {
|
||||
is_singleton.then(|| {
|
||||
display_rows
|
||||
.into_iter()
|
||||
.filter_map(|display_row| {
|
||||
snapshot
|
||||
.fold_for_line(display_row)
|
||||
.map(|fold_status| (display_row, fold_status))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
//Folds contained in a hunk are ignored apart from shrinking visual size
|
||||
//If a fold contains any hunks then that fold line is marked as modified
|
||||
fn layout_git_gutters(
|
||||
@@ -1438,7 +1519,7 @@ impl EditorElement {
|
||||
} else {
|
||||
let text_style = self.style.text.clone();
|
||||
Flex::row()
|
||||
.with_child(Label::new("…".to_string(), text_style).boxed())
|
||||
.with_child(Label::new("⋯", text_style).boxed())
|
||||
.with_children(jump_icon)
|
||||
.contained()
|
||||
.with_padding_left(gutter_padding)
|
||||
@@ -1534,15 +1615,14 @@ impl Element for EditorElement {
|
||||
let snapshot = self.update_view(cx.app, |view, cx| {
|
||||
view.set_visible_line_count(size.y() / line_height);
|
||||
|
||||
let editor_width = text_width - gutter_margin - overscroll.x() - em_width;
|
||||
let wrap_width = match view.soft_wrap_mode(cx) {
|
||||
SoftWrap::None => Some((MAX_LINE_LEN / 2) as f32 * em_advance),
|
||||
SoftWrap::EditorWidth => {
|
||||
Some(text_width - gutter_margin - overscroll.x() - em_width)
|
||||
}
|
||||
SoftWrap::Column(column) => Some(column as f32 * em_advance),
|
||||
SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance,
|
||||
SoftWrap::EditorWidth => editor_width,
|
||||
SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance),
|
||||
};
|
||||
|
||||
if view.set_wrap_width(wrap_width, cx) {
|
||||
if view.set_wrap_width(Some(wrap_width), cx) {
|
||||
view.snapshot(cx)
|
||||
} else {
|
||||
snapshot
|
||||
@@ -1607,9 +1687,13 @@ impl Element for EditorElement {
|
||||
let mut active_rows = BTreeMap::new();
|
||||
let mut highlighted_rows = None;
|
||||
let mut highlighted_ranges = Vec::new();
|
||||
let mut fold_ranges = Vec::new();
|
||||
let mut show_scrollbars = false;
|
||||
let mut include_root = false;
|
||||
let mut is_singleton = false;
|
||||
self.update_view(cx.app, |view, cx| {
|
||||
is_singleton = view.is_singleton(cx);
|
||||
|
||||
let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
highlighted_rows = view.highlighted_rows();
|
||||
@@ -1617,6 +1701,19 @@ impl Element for EditorElement {
|
||||
highlighted_ranges =
|
||||
view.background_highlights_in_range(start_anchor..end_anchor, &display_map, theme);
|
||||
|
||||
fold_ranges.extend(
|
||||
snapshot
|
||||
.folds_in_range(start_anchor..end_anchor)
|
||||
.map(|anchor| {
|
||||
let start = anchor.start.to_point(&snapshot.buffer_snapshot);
|
||||
(
|
||||
start.row,
|
||||
start.to_display_point(&snapshot.display_snapshot)
|
||||
..anchor.end.to_display_point(&snapshot),
|
||||
)
|
||||
}),
|
||||
);
|
||||
|
||||
let mut remote_selections = HashMap::default();
|
||||
for (replica_id, line_mode, cursor_shape, selection) in display_map
|
||||
.buffer_snapshot
|
||||
@@ -1685,11 +1782,28 @@ impl Element for EditorElement {
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
let fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)> = fold_ranges
|
||||
.into_iter()
|
||||
.map(|(id, fold)| {
|
||||
let color = self
|
||||
.style
|
||||
.folds
|
||||
.ellipses
|
||||
.background
|
||||
.style_for(&mut cx.mouse_state::<FoldMarkers>(id as usize), false)
|
||||
.color;
|
||||
|
||||
(id, fold, color)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let line_number_layouts =
|
||||
self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx);
|
||||
|
||||
let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot);
|
||||
|
||||
let folds = self.get_fold_indicators(is_singleton, start_row..end_row, &snapshot);
|
||||
|
||||
let scrollbar_row_range = scroll_position.y()..(scroll_position.y() + height_in_lines);
|
||||
|
||||
let mut max_visible_line_width = 0.0;
|
||||
@@ -1756,7 +1870,7 @@ impl Element for EditorElement {
|
||||
let mut code_actions_indicator = None;
|
||||
let mut hover = None;
|
||||
let mut mode = EditorMode::Full;
|
||||
cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| {
|
||||
let mut fold_indicators = cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| {
|
||||
let newest_selection_head = view
|
||||
.selections
|
||||
.newest::<usize>(cx)
|
||||
@@ -1770,14 +1884,26 @@ impl Element for EditorElement {
|
||||
view.render_context_menu(newest_selection_head, style.clone(), cx);
|
||||
}
|
||||
|
||||
let active = matches!(view.context_menu, Some(crate::ContextMenu::CodeActions(_)));
|
||||
|
||||
code_actions_indicator = view
|
||||
.render_code_actions_indicator(&style, cx)
|
||||
.render_code_actions_indicator(&style, active, cx)
|
||||
.map(|indicator| (newest_selection_head.row(), indicator));
|
||||
}
|
||||
|
||||
let visible_rows = start_row..start_row + line_layouts.len() as u32;
|
||||
hover = view.hover_state.render(&snapshot, &style, visible_rows, cx);
|
||||
mode = view.mode;
|
||||
|
||||
view.render_fold_indicators(
|
||||
folds,
|
||||
&active_rows,
|
||||
&style,
|
||||
view.gutter_hovered,
|
||||
line_height,
|
||||
gutter_margin,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
if let Some((_, context_menu)) = context_menu.as_mut() {
|
||||
@@ -1803,6 +1929,18 @@ impl Element for EditorElement {
|
||||
);
|
||||
}
|
||||
|
||||
fold_indicators.as_mut().map(|fold_indicators| {
|
||||
for (_, indicator) in fold_indicators.iter_mut() {
|
||||
indicator.layout(
|
||||
SizeConstraint::strict_along(
|
||||
Axis::Vertical,
|
||||
line_height * style.code_actions.vertical_scale,
|
||||
),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if let Some((_, hover_popovers)) = hover.as_mut() {
|
||||
for hover_popover in hover_popovers.iter_mut() {
|
||||
hover_popover.layout(
|
||||
@@ -1846,12 +1984,14 @@ impl Element for EditorElement {
|
||||
active_rows,
|
||||
highlighted_rows,
|
||||
highlighted_ranges,
|
||||
fold_ranges,
|
||||
line_number_layouts,
|
||||
display_hunks,
|
||||
blocks,
|
||||
selections,
|
||||
context_menu,
|
||||
code_actions_indicator,
|
||||
fold_indicators,
|
||||
hover_popovers: hover,
|
||||
},
|
||||
)
|
||||
@@ -1959,6 +2099,8 @@ impl Element for EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
type BufferRow = u32;
|
||||
|
||||
pub struct LayoutState {
|
||||
position_map: Arc<PositionMap>,
|
||||
gutter_size: Vector2F,
|
||||
@@ -1973,6 +2115,7 @@ pub struct LayoutState {
|
||||
display_hunks: Vec<DisplayDiffHunk>,
|
||||
blocks: Vec<BlockLayout>,
|
||||
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
||||
fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)>,
|
||||
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
|
||||
scrollbar_row_range: Range<f32>,
|
||||
show_scrollbars: bool,
|
||||
@@ -1980,6 +2123,7 @@ pub struct LayoutState {
|
||||
context_menu: Option<(DisplayPoint, ElementBox)>,
|
||||
code_actions_indicator: Option<(u32, ElementBox)>,
|
||||
hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>,
|
||||
fold_indicators: Option<Vec<(u32, ElementBox)>>,
|
||||
}
|
||||
|
||||
pub struct PositionMap {
|
||||
@@ -2278,6 +2422,75 @@ impl HighlightedRange {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn position_to_display_point(
|
||||
position: Vector2F,
|
||||
text_bounds: RectF,
|
||||
position_map: &PositionMap,
|
||||
) -> Option<DisplayPoint> {
|
||||
if text_bounds.contains_point(position) {
|
||||
let (point, target_point) = position_map.point_for_position(text_bounds, position);
|
||||
if point == target_point {
|
||||
Some(point)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn range_to_bounds(
|
||||
range: &Range<DisplayPoint>,
|
||||
content_origin: Vector2F,
|
||||
scroll_left: f32,
|
||||
scroll_top: f32,
|
||||
visible_row_range: &Range<u32>,
|
||||
line_end_overshoot: f32,
|
||||
position_map: &PositionMap,
|
||||
) -> impl Iterator<Item = RectF> {
|
||||
let mut bounds: SmallVec<[RectF; 1]> = SmallVec::new();
|
||||
|
||||
if range.start == range.end {
|
||||
return bounds.into_iter();
|
||||
}
|
||||
|
||||
let start_row = visible_row_range.start;
|
||||
let end_row = visible_row_range.end;
|
||||
|
||||
let row_range = if range.end.column() == 0 {
|
||||
cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
|
||||
} else {
|
||||
cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row)
|
||||
};
|
||||
|
||||
let first_y =
|
||||
content_origin.y() + row_range.start as f32 * position_map.line_height - scroll_top;
|
||||
|
||||
for (idx, row) in row_range.enumerate() {
|
||||
let line_layout = &position_map.line_layouts[(row - start_row) as usize];
|
||||
|
||||
let start_x = if row == range.start.row() {
|
||||
content_origin.x() + line_layout.x_for_index(range.start.column() as usize)
|
||||
- scroll_left
|
||||
} else {
|
||||
content_origin.x() - scroll_left
|
||||
};
|
||||
|
||||
let end_x = if row == range.end.row() {
|
||||
content_origin.x() + line_layout.x_for_index(range.end.column() as usize) - scroll_left
|
||||
} else {
|
||||
content_origin.x() + line_layout.width() + line_end_overshoot - scroll_left
|
||||
};
|
||||
|
||||
bounds.push(RectF::from_points(
|
||||
vec2f(start_x, first_y + position_map.line_height * idx as f32),
|
||||
vec2f(end_x, first_y + position_map.line_height * (idx + 1) as f32),
|
||||
))
|
||||
}
|
||||
|
||||
bounds.into_iter()
|
||||
}
|
||||
|
||||
pub fn scale_vertical_mouse_autoscroll_delta(delta: f32) -> f32 {
|
||||
delta.powf(1.5) / 100.0
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
|
||||
let snapshot = editor.snapshot(cx);
|
||||
if let Some((opening_range, closing_range)) = snapshot
|
||||
.buffer_snapshot
|
||||
.enclosing_bracket_ranges(head..head)
|
||||
.innermost_enclosing_bracket_ranges(head..head)
|
||||
{
|
||||
editor.highlight_background::<MatchingBracketHighlight>(
|
||||
vec![
|
||||
@@ -32,11 +32,10 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::test::editor_lsp_test_context::EditorLspTestContext;
|
||||
|
||||
use super::*;
|
||||
use crate::test::editor_lsp_test_context::EditorLspTestContext;
|
||||
use indoc::indoc;
|
||||
use language::{BracketPair, Language, LanguageConfig};
|
||||
use language::{BracketPair, BracketPairConfig, Language, LanguageConfig};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) {
|
||||
@@ -45,20 +44,23 @@ mod tests {
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
brackets: vec![
|
||||
BracketPair {
|
||||
start: "{".to_string(),
|
||||
end: "}".to_string(),
|
||||
close: false,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "(".to_string(),
|
||||
end: ")".to_string(),
|
||||
close: false,
|
||||
newline: true,
|
||||
},
|
||||
],
|
||||
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()),
|
||||
|
||||
@@ -2,12 +2,10 @@ use crate::{
|
||||
display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
|
||||
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
|
||||
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
|
||||
FORMAT_TIMEOUT,
|
||||
};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use collections::HashSet;
|
||||
use futures::future::try_join_all;
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
|
||||
RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
@@ -19,6 +17,7 @@ use language::{
|
||||
use project::{FormatTrigger, Item as _, Project, ProjectPath};
|
||||
use rpc::proto::{self, update_view};
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::{self, Ordering},
|
||||
@@ -530,7 +529,7 @@ impl Item for Editor {
|
||||
) -> ElementBox {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(self.title(cx).into(), style.label.clone())
|
||||
Label::new(self.title(cx).to_string(), style.label.clone())
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
@@ -609,32 +608,12 @@ impl Item for Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.report_event("save editor", cx);
|
||||
|
||||
let buffer = self.buffer().clone();
|
||||
let buffers = buffer.read(cx).all_buffers();
|
||||
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
|
||||
let format = project.update(cx, |project, cx| {
|
||||
project.format(buffers, true, FormatTrigger::Save, cx)
|
||||
});
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let transaction = futures::select_biased! {
|
||||
_ = timeout => {
|
||||
log::warn!("timed out waiting for formatting");
|
||||
None
|
||||
}
|
||||
transaction = format.log_err().fuse() => transaction,
|
||||
};
|
||||
|
||||
buffer
|
||||
.update(&mut cx, |buffer, cx| {
|
||||
if let Some(transaction) = transaction {
|
||||
if !buffer.is_singleton() {
|
||||
buffer.push_transaction(&transaction.0);
|
||||
}
|
||||
}
|
||||
|
||||
buffer.save(cx)
|
||||
})
|
||||
let format = self.perform_format(project.clone(), FormatTrigger::Save, cx);
|
||||
let buffers = self.buffer().clone().read(cx).all_buffers();
|
||||
cx.as_mut().spawn(|mut cx| async move {
|
||||
format.await?;
|
||||
project
|
||||
.update(&mut cx, |project, cx| project.save_buffers(buffers, cx))
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
@@ -693,8 +672,8 @@ impl Item for Editor {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
|
||||
let mut result = Vec::new();
|
||||
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
|
||||
let mut result = SmallVec::new();
|
||||
match event {
|
||||
Event::Closed => result.push(ItemEvent::CloseItem),
|
||||
Event::Saved | Event::TitleChanged => {
|
||||
@@ -907,7 +886,7 @@ impl SearchableItem for Editor {
|
||||
matches: Vec<Range<Anchor>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.unfold_ranges([matches[index].clone()], false, cx);
|
||||
self.unfold_ranges([matches[index].clone()], false, true, cx);
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges([matches[index].clone()])
|
||||
});
|
||||
@@ -1158,7 +1137,6 @@ fn path_for_file<'a>(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::MutableAppContext;
|
||||
use language::RopeFingerprint;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
@@ -1204,17 +1182,6 @@ mod tests {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn save(
|
||||
&self,
|
||||
_: u64,
|
||||
_: language::Rope,
|
||||
_: clock::Global,
|
||||
_: project::LineEnding,
|
||||
_: &mut MutableAppContext,
|
||||
) -> gpui::Task<anyhow::Result<(clock::Global, RopeFingerprint, SystemTime)>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
todo!()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use gpui::{
|
||||
|
||||
use crate::{
|
||||
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition,
|
||||
Rename, SelectMode, ToggleCodeActions,
|
||||
Rename, RevealInFinder, SelectMode, ToggleCodeActions,
|
||||
};
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
@@ -52,8 +52,8 @@ pub fn deploy_context_menu(
|
||||
AnchorCorner::TopLeft,
|
||||
vec![
|
||||
ContextMenuItem::item("Rename Symbol", Rename),
|
||||
ContextMenuItem::item("Go To Definition", GoToDefinition),
|
||||
ContextMenuItem::item("Go To Type Definition", GoToTypeDefinition),
|
||||
ContextMenuItem::item("Go to Definition", GoToDefinition),
|
||||
ContextMenuItem::item("Go to Type Definition", GoToTypeDefinition),
|
||||
ContextMenuItem::item("Find All References", FindAllReferences),
|
||||
ContextMenuItem::item(
|
||||
"Code Actions",
|
||||
@@ -61,6 +61,8 @@ pub fn deploy_context_menu(
|
||||
deployed_from_indicator: false,
|
||||
},
|
||||
),
|
||||
ContextMenuItem::Separator,
|
||||
ContextMenuItem::item("Reveal in Finder", RevealInFinder),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
mod anchor;
|
||||
|
||||
pub use anchor::{Anchor, AnchorRangeExt};
|
||||
use anyhow::Result;
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, Bound, HashMap, HashSet};
|
||||
use futures::{channel::mpsc, SinkExt};
|
||||
@@ -385,9 +384,13 @@ impl MultiBuffer {
|
||||
_ => Default::default(),
|
||||
};
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
let mut buffer_edits: HashMap<usize, Vec<(Range<usize>, Arc<str>, bool, u32)>> =
|
||||
Default::default();
|
||||
struct BufferEdit {
|
||||
range: Range<usize>,
|
||||
new_text: Arc<str>,
|
||||
is_insertion: bool,
|
||||
original_indent_column: u32,
|
||||
}
|
||||
let mut buffer_edits: HashMap<usize, Vec<BufferEdit>> = Default::default();
|
||||
let mut cursor = snapshot.excerpts.cursor::<usize>();
|
||||
for (ix, (range, new_text)) in edits.enumerate() {
|
||||
let new_text: Arc<str> = new_text.into();
|
||||
@@ -422,12 +425,12 @@ impl MultiBuffer {
|
||||
buffer_edits
|
||||
.entry(start_excerpt.buffer_id)
|
||||
.or_insert(Vec::new())
|
||||
.push((
|
||||
buffer_start..buffer_end,
|
||||
.push(BufferEdit {
|
||||
range: buffer_start..buffer_end,
|
||||
new_text,
|
||||
true,
|
||||
is_insertion: true,
|
||||
original_indent_column,
|
||||
));
|
||||
});
|
||||
} else {
|
||||
let start_excerpt_range = buffer_start
|
||||
..start_excerpt
|
||||
@@ -444,21 +447,21 @@ impl MultiBuffer {
|
||||
buffer_edits
|
||||
.entry(start_excerpt.buffer_id)
|
||||
.or_insert(Vec::new())
|
||||
.push((
|
||||
start_excerpt_range,
|
||||
new_text.clone(),
|
||||
true,
|
||||
.push(BufferEdit {
|
||||
range: start_excerpt_range,
|
||||
new_text: new_text.clone(),
|
||||
is_insertion: true,
|
||||
original_indent_column,
|
||||
));
|
||||
});
|
||||
buffer_edits
|
||||
.entry(end_excerpt.buffer_id)
|
||||
.or_insert(Vec::new())
|
||||
.push((
|
||||
end_excerpt_range,
|
||||
new_text.clone(),
|
||||
false,
|
||||
.push(BufferEdit {
|
||||
range: end_excerpt_range,
|
||||
new_text: new_text.clone(),
|
||||
is_insertion: false,
|
||||
original_indent_column,
|
||||
));
|
||||
});
|
||||
|
||||
cursor.seek(&range.start, Bias::Right, &());
|
||||
cursor.next(&());
|
||||
@@ -469,19 +472,19 @@ impl MultiBuffer {
|
||||
buffer_edits
|
||||
.entry(excerpt.buffer_id)
|
||||
.or_insert(Vec::new())
|
||||
.push((
|
||||
excerpt.range.context.to_offset(&excerpt.buffer),
|
||||
new_text.clone(),
|
||||
false,
|
||||
.push(BufferEdit {
|
||||
range: excerpt.range.context.to_offset(&excerpt.buffer),
|
||||
new_text: new_text.clone(),
|
||||
is_insertion: false,
|
||||
original_indent_column,
|
||||
));
|
||||
});
|
||||
cursor.next(&());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (buffer_id, mut edits) in buffer_edits {
|
||||
edits.sort_unstable_by_key(|(range, _, _, _)| range.start);
|
||||
edits.sort_unstable_by_key(|edit| edit.range.start);
|
||||
self.buffers.borrow()[&buffer_id]
|
||||
.buffer
|
||||
.update(cx, |buffer, cx| {
|
||||
@@ -490,14 +493,19 @@ impl MultiBuffer {
|
||||
let mut original_indent_columns = Vec::new();
|
||||
let mut deletions = Vec::new();
|
||||
let empty_str: Arc<str> = "".into();
|
||||
while let Some((
|
||||
while let Some(BufferEdit {
|
||||
mut range,
|
||||
new_text,
|
||||
mut is_insertion,
|
||||
original_indent_column,
|
||||
)) = edits.next()
|
||||
}) = edits.next()
|
||||
{
|
||||
while let Some((next_range, _, next_is_insertion, _)) = edits.peek() {
|
||||
while let Some(BufferEdit {
|
||||
range: next_range,
|
||||
is_insertion: next_is_insertion,
|
||||
..
|
||||
}) = edits.peek()
|
||||
{
|
||||
if range.end >= next_range.start {
|
||||
range.end = cmp::max(next_range.end, range.end);
|
||||
is_insertion |= *next_is_insertion;
|
||||
@@ -1279,20 +1287,6 @@ impl MultiBuffer {
|
||||
.map(|state| state.buffer.clone())
|
||||
}
|
||||
|
||||
pub fn save(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
let mut save_tasks = Vec::new();
|
||||
for BufferState { buffer, .. } in self.buffers.borrow().values() {
|
||||
save_tasks.push(buffer.update(cx, |buffer, cx| buffer.save(cx)));
|
||||
}
|
||||
|
||||
cx.spawn(|_, _| async move {
|
||||
for save in save_tasks {
|
||||
save.await?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_completion_trigger<T>(&self, position: T, text: &str, cx: &AppContext) -> bool
|
||||
where
|
||||
T: ToOffset,
|
||||
@@ -2621,57 +2615,89 @@ impl MultiBufferSnapshot {
|
||||
self.parse_count
|
||||
}
|
||||
|
||||
pub fn enclosing_bracket_ranges<T: ToOffset>(
|
||||
/// Returns the smallest enclosing bracket ranges containing the given range or
|
||||
/// None if no brackets contain range or the range is not contained in a single
|
||||
/// excerpt
|
||||
pub fn innermost_enclosing_bracket_ranges<T: ToOffset>(
|
||||
&self,
|
||||
range: Range<T>,
|
||||
) -> Option<(Range<usize>, Range<usize>)> {
|
||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||
|
||||
let mut cursor = self.excerpts.cursor::<usize>();
|
||||
cursor.seek(&range.start, Bias::Right, &());
|
||||
let start_excerpt = cursor.item();
|
||||
// Get the ranges of the innermost pair of brackets.
|
||||
let mut result: Option<(Range<usize>, Range<usize>)> = None;
|
||||
|
||||
cursor.seek(&range.end, Bias::Right, &());
|
||||
let end_excerpt = cursor.item();
|
||||
let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else { return None; };
|
||||
|
||||
start_excerpt
|
||||
.zip(end_excerpt)
|
||||
.and_then(|(start_excerpt, end_excerpt)| {
|
||||
if start_excerpt.id != end_excerpt.id {
|
||||
return None;
|
||||
for (open, close) in enclosing_bracket_ranges {
|
||||
let len = close.end - open.start;
|
||||
|
||||
if let Some((existing_open, existing_close)) = &result {
|
||||
let existing_len = existing_close.end - existing_open.start;
|
||||
if len > existing_len {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let excerpt_buffer_start = start_excerpt
|
||||
.range
|
||||
.context
|
||||
.start
|
||||
.to_offset(&start_excerpt.buffer);
|
||||
let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.len;
|
||||
result = Some((open, close));
|
||||
}
|
||||
|
||||
let start_in_buffer =
|
||||
excerpt_buffer_start + range.start.saturating_sub(*cursor.start());
|
||||
let end_in_buffer =
|
||||
excerpt_buffer_start + range.end.saturating_sub(*cursor.start());
|
||||
let (mut start_bracket_range, mut end_bracket_range) = start_excerpt
|
||||
.buffer
|
||||
.enclosing_bracket_ranges(start_in_buffer..end_in_buffer)?;
|
||||
result
|
||||
}
|
||||
|
||||
if start_bracket_range.start >= excerpt_buffer_start
|
||||
&& end_bracket_range.end <= excerpt_buffer_end
|
||||
{
|
||||
/// Returns enclosing bracket ranges containing the given range or returns None if the range is
|
||||
/// not contained in a single excerpt
|
||||
pub fn enclosing_bracket_ranges<'a, T: ToOffset>(
|
||||
&'a self,
|
||||
range: Range<T>,
|
||||
) -> Option<impl Iterator<Item = (Range<usize>, Range<usize>)> + 'a> {
|
||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||
|
||||
self.bracket_ranges(range.clone()).map(|range_pairs| {
|
||||
range_pairs
|
||||
.filter(move |(open, close)| open.start <= range.start && close.end >= range.end)
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns bracket range pairs overlapping the given `range` or returns None if the `range` is
|
||||
/// not contained in a single excerpt
|
||||
pub fn bracket_ranges<'a, T: ToOffset>(
|
||||
&'a self,
|
||||
range: Range<T>,
|
||||
) -> Option<impl Iterator<Item = (Range<usize>, Range<usize>)> + 'a> {
|
||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||
let excerpt = self.excerpt_containing(range.clone());
|
||||
excerpt.map(|(excerpt, excerpt_offset)| {
|
||||
let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
|
||||
let excerpt_buffer_end = excerpt_buffer_start + excerpt.text_summary.len;
|
||||
|
||||
let start_in_buffer = excerpt_buffer_start + range.start.saturating_sub(excerpt_offset);
|
||||
let end_in_buffer = excerpt_buffer_start + range.end.saturating_sub(excerpt_offset);
|
||||
|
||||
excerpt
|
||||
.buffer
|
||||
.bracket_ranges(start_in_buffer..end_in_buffer)
|
||||
.filter_map(move |(start_bracket_range, end_bracket_range)| {
|
||||
if start_bracket_range.start < excerpt_buffer_start
|
||||
|| end_bracket_range.end > excerpt_buffer_end
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut start_bracket_range = start_bracket_range.clone();
|
||||
start_bracket_range.start =
|
||||
cursor.start() + (start_bracket_range.start - excerpt_buffer_start);
|
||||
excerpt_offset + (start_bracket_range.start - excerpt_buffer_start);
|
||||
start_bracket_range.end =
|
||||
cursor.start() + (start_bracket_range.end - excerpt_buffer_start);
|
||||
excerpt_offset + (start_bracket_range.end - excerpt_buffer_start);
|
||||
|
||||
let mut end_bracket_range = end_bracket_range.clone();
|
||||
end_bracket_range.start =
|
||||
cursor.start() + (end_bracket_range.start - excerpt_buffer_start);
|
||||
excerpt_offset + (end_bracket_range.start - excerpt_buffer_start);
|
||||
end_bracket_range.end =
|
||||
cursor.start() + (end_bracket_range.end - excerpt_buffer_start);
|
||||
excerpt_offset + (end_bracket_range.end - excerpt_buffer_start);
|
||||
Some((start_bracket_range, end_bracket_range))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn diagnostics_update_count(&self) -> usize {
|
||||
@@ -2812,40 +2838,23 @@ impl MultiBufferSnapshot {
|
||||
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
|
||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||
|
||||
let mut cursor = self.excerpts.cursor::<usize>();
|
||||
cursor.seek(&range.start, Bias::Right, &());
|
||||
let start_excerpt = cursor.item();
|
||||
|
||||
cursor.seek(&range.end, Bias::Right, &());
|
||||
let end_excerpt = cursor.item();
|
||||
|
||||
start_excerpt
|
||||
.zip(end_excerpt)
|
||||
.and_then(|(start_excerpt, end_excerpt)| {
|
||||
if start_excerpt.id != end_excerpt.id {
|
||||
return None;
|
||||
}
|
||||
|
||||
let excerpt_buffer_start = start_excerpt
|
||||
.range
|
||||
.context
|
||||
.start
|
||||
.to_offset(&start_excerpt.buffer);
|
||||
let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.len;
|
||||
self.excerpt_containing(range.clone())
|
||||
.and_then(|(excerpt, excerpt_offset)| {
|
||||
let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
|
||||
let excerpt_buffer_end = excerpt_buffer_start + excerpt.text_summary.len;
|
||||
|
||||
let start_in_buffer =
|
||||
excerpt_buffer_start + range.start.saturating_sub(*cursor.start());
|
||||
let end_in_buffer =
|
||||
excerpt_buffer_start + range.end.saturating_sub(*cursor.start());
|
||||
let mut ancestor_buffer_range = start_excerpt
|
||||
excerpt_buffer_start + range.start.saturating_sub(excerpt_offset);
|
||||
let end_in_buffer = excerpt_buffer_start + range.end.saturating_sub(excerpt_offset);
|
||||
let mut ancestor_buffer_range = excerpt
|
||||
.buffer
|
||||
.range_for_syntax_ancestor(start_in_buffer..end_in_buffer)?;
|
||||
ancestor_buffer_range.start =
|
||||
cmp::max(ancestor_buffer_range.start, excerpt_buffer_start);
|
||||
ancestor_buffer_range.end = cmp::min(ancestor_buffer_range.end, excerpt_buffer_end);
|
||||
|
||||
let start = cursor.start() + (ancestor_buffer_range.start - excerpt_buffer_start);
|
||||
let end = cursor.start() + (ancestor_buffer_range.end - excerpt_buffer_start);
|
||||
let start = excerpt_offset + (ancestor_buffer_range.start - excerpt_buffer_start);
|
||||
let end = excerpt_offset + (ancestor_buffer_range.end - excerpt_buffer_start);
|
||||
Some(start..end)
|
||||
})
|
||||
}
|
||||
@@ -2929,6 +2938,35 @@ impl MultiBufferSnapshot {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the excerpt containing range and its offset start within the multibuffer or none if `range` spans multiple excerpts
|
||||
fn excerpt_containing<'a, T: ToOffset>(
|
||||
&'a self,
|
||||
range: Range<T>,
|
||||
) -> Option<(&'a Excerpt, usize)> {
|
||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||
|
||||
let mut cursor = self.excerpts.cursor::<usize>();
|
||||
cursor.seek(&range.start, Bias::Right, &());
|
||||
let start_excerpt = cursor.item();
|
||||
|
||||
if range.start == range.end {
|
||||
return start_excerpt.map(|excerpt| (excerpt, *cursor.start()));
|
||||
}
|
||||
|
||||
cursor.seek(&range.end, Bias::Right, &());
|
||||
let end_excerpt = cursor.item();
|
||||
|
||||
start_excerpt
|
||||
.zip(end_excerpt)
|
||||
.and_then(|(start_excerpt, end_excerpt)| {
|
||||
if start_excerpt.id != end_excerpt.id {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((start_excerpt, *cursor.start()))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remote_selections_in_range<'a>(
|
||||
&'a self,
|
||||
range: &'a Range<Anchor>,
|
||||
|
||||
@@ -659,6 +659,31 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_offsets_with(
|
||||
&mut self,
|
||||
mut move_selection: impl FnMut(&MultiBufferSnapshot, &mut Selection<usize>),
|
||||
) {
|
||||
let mut changed = false;
|
||||
let snapshot = self.buffer().clone();
|
||||
let selections = self
|
||||
.all::<usize>(self.cx)
|
||||
.into_iter()
|
||||
.map(|selection| {
|
||||
let mut moved_selection = selection.clone();
|
||||
move_selection(&snapshot, &mut moved_selection);
|
||||
if selection != moved_selection {
|
||||
changed = true;
|
||||
}
|
||||
moved_selection
|
||||
})
|
||||
.collect();
|
||||
drop(snapshot);
|
||||
|
||||
if changed {
|
||||
self.select(selections)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_heads_with(
|
||||
&mut self,
|
||||
mut update_head: impl FnMut(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
ops::{Deref, DerefMut, Range},
|
||||
sync::Arc,
|
||||
};
|
||||
@@ -7,7 +8,8 @@ use anyhow::Result;
|
||||
|
||||
use futures::Future;
|
||||
use gpui::{json, ViewContext, ViewHandle};
|
||||
use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig};
|
||||
use indoc::indoc;
|
||||
use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries};
|
||||
use lsp::{notification, request};
|
||||
use project::Project;
|
||||
use smol::stream::StreamExt;
|
||||
@@ -37,7 +39,7 @@ impl<'a> EditorLspTestContext<'a> {
|
||||
pane::init(cx);
|
||||
});
|
||||
|
||||
let params = cx.update(AppState::test);
|
||||
let app_state = cx.update(AppState::test);
|
||||
|
||||
let file_name = format!(
|
||||
"file.{}",
|
||||
@@ -54,13 +56,13 @@ impl<'a> EditorLspTestContext<'a> {
|
||||
}))
|
||||
.await;
|
||||
|
||||
let project = Project::test(params.fs.clone(), [], cx).await;
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
|
||||
|
||||
params
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree("/root", json!({ "dir": { file_name: "" }}))
|
||||
.insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))
|
||||
.await;
|
||||
|
||||
let (window_id, workspace) = cx.add_window(|cx| {
|
||||
@@ -105,7 +107,7 @@ impl<'a> EditorLspTestContext<'a> {
|
||||
},
|
||||
lsp,
|
||||
workspace,
|
||||
buffer_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
|
||||
buffer_lsp_url: lsp::Url::from_file_path(format!("/root/dir/{file_name}")).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +122,59 @@ impl<'a> EditorLspTestContext<'a> {
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
)
|
||||
.with_queries(LanguageQueries {
|
||||
indents: Some(Cow::from(indoc! {r#"
|
||||
[
|
||||
((where_clause) _ @end)
|
||||
(field_expression)
|
||||
(call_expression)
|
||||
(assignment_expression)
|
||||
(let_declaration)
|
||||
(let_chain)
|
||||
(await_expression)
|
||||
] @indent
|
||||
|
||||
(_ "[" "]" @end) @indent
|
||||
(_ "<" ">" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
(_ "(" ")" @end) @indent"#})),
|
||||
brackets: Some(Cow::from(indoc! {r#"
|
||||
("(" @open ")" @close)
|
||||
("[" @open "]" @close)
|
||||
("{" @open "}" @close)
|
||||
("<" @open ">" @close)
|
||||
("\"" @open "\"" @close)
|
||||
(closure_parameters "|" @open "|" @close)"#})),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("Could not parse queries");
|
||||
|
||||
Self::new(language, capabilities, cx).await
|
||||
}
|
||||
|
||||
pub async fn new_typescript(
|
||||
capabilities: lsp::ServerCapabilities,
|
||||
cx: &'a mut gpui::TestAppContext,
|
||||
) -> EditorLspTestContext<'a> {
|
||||
let language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Typescript".into(),
|
||||
path_suffixes: vec!["ts".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_typescript::language_typescript()),
|
||||
)
|
||||
.with_queries(LanguageQueries {
|
||||
brackets: Some(Cow::from(indoc! {r#"
|
||||
("(" @open ")" @close)
|
||||
("[" @open "]" @close)
|
||||
("{" @open "}" @close)
|
||||
("<" @open ">" @close)
|
||||
("\"" @open "\"" @close)"#})),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("Could not parse queries");
|
||||
|
||||
Self::new(language, capabilities, cx).await
|
||||
}
|
||||
|
||||
@@ -162,10 +162,13 @@ impl<'a> EditorTestContext<'a> {
|
||||
/// embedded range markers that represent the ranges and directions of
|
||||
/// each selection.
|
||||
///
|
||||
/// Returns a context handle so that assertion failures can print what
|
||||
/// editor state was needed to cause the failure.
|
||||
///
|
||||
/// See the `util::test::marked_text_ranges` function for more information.
|
||||
pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
|
||||
let _state_context = self.add_assertion_context(format!(
|
||||
"Editor State: \"{}\"",
|
||||
"Initial Editor State: \"{}\"",
|
||||
marked_text.escape_debug().to_string()
|
||||
));
|
||||
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
|
||||
@@ -182,6 +185,7 @@ impl<'a> EditorTestContext<'a> {
|
||||
/// of its selections using a string containing embedded range markers.
|
||||
///
|
||||
/// See the `util::test::marked_text_ranges` function for more information.
|
||||
#[track_caller]
|
||||
pub fn assert_editor_state(&mut self, marked_text: &str) {
|
||||
let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
|
||||
let buffer_text = self.buffer_text();
|
||||
|
||||
40
crates/feedback/src/deploy_feedback_button.rs
Normal file
40
crates/feedback/src/deploy_feedback_button.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use gpui::{
|
||||
elements::{MouseEventHandler, ParentElement, Stack, Text},
|
||||
CursorStyle, Element, ElementBox, Entity, MouseButton, RenderContext, View, ViewContext,
|
||||
};
|
||||
use settings::Settings;
|
||||
use workspace::{item::ItemHandle, StatusItemView};
|
||||
|
||||
use crate::feedback_editor::GiveFeedback;
|
||||
|
||||
pub struct DeployFeedbackButton;
|
||||
|
||||
impl Entity for DeployFeedbackButton {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for DeployFeedbackButton {
|
||||
fn ui_name() -> &'static str {
|
||||
"DeployFeedbackButton"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<Self>::new(0, cx, |state, cx| {
|
||||
let theme = &cx.global::<Settings>().theme;
|
||||
let theme = &theme.workspace.status_bar.feedback;
|
||||
|
||||
Text::new("Give Feedback", theme.style_for(state, true).clone()).boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(GiveFeedback))
|
||||
.boxed(),
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for DeployFeedbackButton {
|
||||
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
pub mod deploy_feedback_button;
|
||||
pub mod feedback_editor;
|
||||
pub mod feedback_info_text;
|
||||
pub mod submit_feedback_button;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub mod feedback_editor;
|
||||
mod system_specs;
|
||||
use gpui::{actions, impl_actions, ClipboardItem, MutableAppContext, PromptLevel, ViewContext};
|
||||
use serde::Deserialize;
|
||||
@@ -16,7 +20,12 @@ impl_actions!(zed, [OpenBrowser]);
|
||||
|
||||
actions!(
|
||||
zed,
|
||||
[CopySystemSpecsIntoClipboard, FileBugReport, RequestFeature]
|
||||
[
|
||||
CopySystemSpecsIntoClipboard,
|
||||
FileBugReport,
|
||||
RequestFeature,
|
||||
OpenZedCommunityRepo
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
@@ -28,7 +37,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
|
||||
|
||||
let url = format!(
|
||||
"https://github.com/zed-industries/feedback/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}",
|
||||
"https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}",
|
||||
urlencoding::encode(&system_specs_text)
|
||||
);
|
||||
|
||||
@@ -48,7 +57,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
|
||||
cx.add_action(
|
||||
|_: &mut Workspace, _: &RequestFeature, cx: &mut ViewContext<Workspace>| {
|
||||
let url = "https://github.com/zed-industries/feedback/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml";
|
||||
let url = "https://github.com/zed-industries/community/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml";
|
||||
cx.dispatch_action(OpenBrowser {
|
||||
url: url.into(),
|
||||
});
|
||||
@@ -62,4 +71,11 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
cx.add_action(
|
||||
|_: &mut Workspace, _: &OpenZedCommunityRepo, cx: &mut ViewContext<Workspace>| {
|
||||
let url = "https://github.com/zed-industries/community";
|
||||
cx.dispatch_action(OpenBrowser { url: url.into() });
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,10 +10,9 @@ use editor::{Anchor, Editor};
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{ChildView, Flex, Label, MouseEventHandler, ParentElement, Stack, Text},
|
||||
serde_json, AnyViewHandle, AppContext, CursorStyle, Element, ElementBox, Entity, ModelHandle,
|
||||
MouseButton, MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext,
|
||||
ViewHandle, WeakViewHandle,
|
||||
elements::{ChildView, Flex, Label, ParentElement},
|
||||
serde_json, AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle,
|
||||
MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use isahc::Request;
|
||||
use language::Buffer;
|
||||
@@ -21,21 +20,19 @@ use postage::prelude::Stream;
|
||||
|
||||
use project::Project;
|
||||
use serde::Serialize;
|
||||
use settings::Settings;
|
||||
use workspace::{
|
||||
item::{Item, ItemHandle},
|
||||
searchable::{SearchableItem, SearchableItemHandle},
|
||||
AppState, StatusItemView, Workspace,
|
||||
AppState, Workspace,
|
||||
};
|
||||
|
||||
use crate::system_specs::SystemSpecs;
|
||||
use crate::{submit_feedback_button::SubmitFeedbackButton, system_specs::SystemSpecs};
|
||||
|
||||
const FEEDBACK_CHAR_LIMIT: RangeInclusive<usize> = 10..=5000;
|
||||
const FEEDBACK_PLACEHOLDER_TEXT: &str = "Thanks for spending time with Zed. Enter your feedback here as Markdown. Save the tab to submit your feedback.";
|
||||
const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
|
||||
"Feedback failed to submit, see error log for details.";
|
||||
|
||||
actions!(feedback, [SubmitFeedback, GiveFeedback, DeployFeedback]);
|
||||
actions!(feedback, [GiveFeedback, SubmitFeedback]);
|
||||
|
||||
pub fn init(system_specs: SystemSpecs, app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
cx.add_action({
|
||||
@@ -43,42 +40,16 @@ pub fn init(system_specs: SystemSpecs, app_state: Arc<AppState>, cx: &mut Mutabl
|
||||
FeedbackEditor::deploy(system_specs.clone(), workspace, app_state.clone(), cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub struct FeedbackButton;
|
||||
|
||||
impl Entity for FeedbackButton {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for FeedbackButton {
|
||||
fn ui_name() -> &'static str {
|
||||
"FeedbackButton"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<Self>::new(0, cx, |state, cx| {
|
||||
let theme = &cx.global::<Settings>().theme;
|
||||
let theme = &theme.workspace.status_bar.feedback;
|
||||
|
||||
Text::new(
|
||||
"Give Feedback".to_string(),
|
||||
theme.style_for(state, true).clone(),
|
||||
)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(GiveFeedback))
|
||||
.boxed(),
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for FeedbackButton {
|
||||
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
|
||||
cx.add_async_action(
|
||||
|submit_feedback_button: &mut SubmitFeedbackButton, _: &SubmitFeedback, cx| {
|
||||
if let Some(active_item) = submit_feedback_button.active_item.as_ref() {
|
||||
Some(active_item.update(cx, |feedback_editor, cx| feedback_editor.handle_save(cx)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -86,11 +57,12 @@ struct FeedbackRequestBody<'a> {
|
||||
feedback_text: &'a str,
|
||||
metrics_id: Option<Arc<str>>,
|
||||
system_specs: SystemSpecs,
|
||||
is_staff: bool,
|
||||
token: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct FeedbackEditor {
|
||||
pub(crate) struct FeedbackEditor {
|
||||
system_specs: SystemSpecs,
|
||||
editor: ViewHandle<Editor>,
|
||||
project: ModelHandle<Project>,
|
||||
@@ -106,7 +78,6 @@ impl FeedbackEditor {
|
||||
let editor = cx.add_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
|
||||
editor.set_vertical_scroll_margin(5, cx);
|
||||
editor.set_placeholder_text(FEEDBACK_PLACEHOLDER_TEXT, cx);
|
||||
editor
|
||||
});
|
||||
|
||||
@@ -120,12 +91,10 @@ impl FeedbackEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_save(
|
||||
&mut self,
|
||||
_: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
let feedback_char_count = self.editor.read(cx).text(cx).chars().count();
|
||||
fn handle_save(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
|
||||
let feedback_text = self.editor.read(cx).text(cx);
|
||||
let feedback_char_count = feedback_text.chars().count();
|
||||
let feedback_text = feedback_text.trim().to_string();
|
||||
|
||||
let error = if feedback_char_count < *FEEDBACK_CHAR_LIMIT.start() {
|
||||
Some(format!(
|
||||
@@ -154,7 +123,6 @@ impl FeedbackEditor {
|
||||
|
||||
let this = cx.handle();
|
||||
let client = cx.global::<Arc<Client>>().clone();
|
||||
let feedback_text = self.editor.read(cx).text(cx);
|
||||
let specs = self.system_specs.clone();
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
@@ -198,12 +166,14 @@ impl FeedbackEditor {
|
||||
let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL);
|
||||
|
||||
let metrics_id = zed_client.metrics_id();
|
||||
let is_staff = zed_client.is_staff();
|
||||
let http_client = zed_client.http_client();
|
||||
|
||||
let request = FeedbackRequestBody {
|
||||
feedback_text: &feedback_text,
|
||||
metrics_id,
|
||||
system_specs,
|
||||
is_staff: is_staff.unwrap_or(false),
|
||||
token: ZED_SECRET_CLIENT_TOKEN,
|
||||
};
|
||||
|
||||
@@ -275,7 +245,7 @@ impl Item for FeedbackEditor {
|
||||
fn tab_content(&self, _: Option<usize>, style: &theme::Tab, _: &AppContext) -> ElementBox {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new("Feedback".to_string(), style.label.clone())
|
||||
Label::new("Feedback", style.label.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.boxed(),
|
||||
@@ -287,35 +257,29 @@ impl Item for FeedbackEditor {
|
||||
self.editor.for_each_project_item(cx, f)
|
||||
}
|
||||
|
||||
fn to_item_events(_: &Self::Event) -> Vec<workspace::item::ItemEvent> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn is_singleton(&self, _: &AppContext) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
|
||||
|
||||
fn can_save(&self, _: &AppContext) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn save(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
_: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.handle_save(project, cx)
|
||||
self.handle_save(cx)
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
_: ModelHandle<Project>,
|
||||
_: std::path::PathBuf,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.handle_save(project, cx)
|
||||
self.handle_save(cx)
|
||||
}
|
||||
|
||||
fn reload(
|
||||
@@ -323,7 +287,7 @@ impl Item for FeedbackEditor {
|
||||
_: ModelHandle<Project>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
unreachable!("reload should not have been called")
|
||||
Task::Ready(Some(Ok(())))
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
@@ -350,20 +314,6 @@ impl Item for FeedbackEditor {
|
||||
))
|
||||
}
|
||||
|
||||
fn serialized_item_kind() -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
_: ModelHandle<Project>,
|
||||
_: WeakViewHandle<Workspace>,
|
||||
_: workspace::WorkspaceId,
|
||||
_: workspace::ItemId,
|
||||
_: &mut ViewContext<workspace::Pane>,
|
||||
) -> Task<anyhow::Result<ViewHandle<Self>>> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(handle.clone()))
|
||||
}
|
||||
|
||||
97
crates/feedback/src/feedback_info_text.rs
Normal file
97
crates/feedback/src/feedback_info_text.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use gpui::{
|
||||
elements::{Flex, Label, MouseEventHandler, ParentElement, Text},
|
||||
CursorStyle, Element, ElementBox, Entity, MouseButton, RenderContext, View, ViewContext,
|
||||
ViewHandle,
|
||||
};
|
||||
use settings::Settings;
|
||||
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
|
||||
|
||||
use crate::{feedback_editor::FeedbackEditor, OpenZedCommunityRepo};
|
||||
|
||||
pub struct FeedbackInfoText {
|
||||
active_item: Option<ViewHandle<FeedbackEditor>>,
|
||||
}
|
||||
|
||||
impl FeedbackInfoText {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
active_item: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for FeedbackInfoText {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for FeedbackInfoText {
|
||||
fn ui_name() -> &'static str {
|
||||
"FeedbackInfoText"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Text::new(
|
||||
"We read whatever you submit here. For issues and discussions, visit the ",
|
||||
theme.feedback.info_text_default.text.clone(),
|
||||
)
|
||||
.with_soft_wrap(false)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::<OpenZedCommunityRepo>::new(0, cx, |state, _| {
|
||||
let contained_text = if state.hovered() {
|
||||
&theme.feedback.link_text_hover
|
||||
} else {
|
||||
&theme.feedback.link_text_default
|
||||
};
|
||||
|
||||
Label::new("community repo", contained_text.text.clone())
|
||||
.contained()
|
||||
.aligned()
|
||||
.left()
|
||||
.clipped()
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(OpenZedCommunityRepo)
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Text::new(" on GitHub.", theme.feedback.info_text_default.text.clone())
|
||||
.with_soft_wrap(false)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
.aligned()
|
||||
.left()
|
||||
.clipped()
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolbarItemView for FeedbackInfoText {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> workspace::ToolbarItemLocation {
|
||||
cx.notify();
|
||||
if let Some(feedback_editor) = active_pane_item.and_then(|i| i.downcast::<FeedbackEditor>())
|
||||
{
|
||||
self.active_item = Some(feedback_editor);
|
||||
ToolbarItemLocation::PrimaryLeft {
|
||||
flex: Some((1., false)),
|
||||
}
|
||||
} else {
|
||||
self.active_item = None;
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
76
crates/feedback/src/submit_feedback_button.rs
Normal file
76
crates/feedback/src/submit_feedback_button.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use gpui::{
|
||||
elements::{Label, MouseEventHandler},
|
||||
CursorStyle, Element, ElementBox, Entity, MouseButton, RenderContext, View, ViewContext,
|
||||
ViewHandle,
|
||||
};
|
||||
use settings::Settings;
|
||||
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
|
||||
|
||||
use crate::feedback_editor::{FeedbackEditor, SubmitFeedback};
|
||||
|
||||
pub struct SubmitFeedbackButton {
|
||||
pub(crate) active_item: Option<ViewHandle<FeedbackEditor>>,
|
||||
}
|
||||
|
||||
impl SubmitFeedbackButton {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
active_item: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for SubmitFeedbackButton {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for SubmitFeedbackButton {
|
||||
fn ui_name() -> &'static str {
|
||||
"SubmitFeedbackButton"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
enum SubmitFeedbackButton {}
|
||||
MouseEventHandler::<SubmitFeedbackButton>::new(0, cx, |state, _| {
|
||||
let style = theme.feedback.submit_button.style_for(state, false);
|
||||
Label::new("Submit as Markdown", style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(SubmitFeedback)
|
||||
})
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(theme.feedback.button_margin)
|
||||
.with_tooltip::<Self, _>(
|
||||
0,
|
||||
"cmd-s".into(),
|
||||
Some(Box::new(SubmitFeedback)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolbarItemView for SubmitFeedbackButton {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> workspace::ToolbarItemLocation {
|
||||
cx.notify();
|
||||
if let Some(feedback_editor) = active_pane_item.and_then(|i| i.downcast::<FeedbackEditor>())
|
||||
{
|
||||
self.active_item = Some(feedback_editor);
|
||||
ToolbarItemLocation::PrimaryRight { flex: None }
|
||||
} else {
|
||||
self.active_item = None;
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ pub struct FileFinder {
|
||||
latest_search_id: usize,
|
||||
latest_search_did_cancel: bool,
|
||||
latest_search_query: String,
|
||||
relative_to: Option<Arc<Path>>,
|
||||
matches: Vec<PathMatch>,
|
||||
selected: Option<(usize, Arc<Path>)>,
|
||||
cancel_flag: Arc<AtomicBool>,
|
||||
@@ -90,7 +91,11 @@ impl FileFinder {
|
||||
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||
workspace.toggle_modal(cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let finder = cx.add_view(|cx| Self::new(project, cx));
|
||||
let relative_to = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.project_path(cx))
|
||||
.map(|project_path| project_path.path.clone());
|
||||
let finder = cx.add_view(|cx| Self::new(project, relative_to, cx));
|
||||
cx.subscribe(&finder, Self::on_event).detach();
|
||||
finder
|
||||
});
|
||||
@@ -115,7 +120,11 @@ impl FileFinder {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
|
||||
pub fn new(
|
||||
project: ModelHandle<Project>,
|
||||
relative_to: Option<Arc<Path>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let handle = cx.weak_handle();
|
||||
cx.observe(&project, Self::project_updated).detach();
|
||||
Self {
|
||||
@@ -125,6 +134,7 @@ impl FileFinder {
|
||||
latest_search_id: 0,
|
||||
latest_search_did_cancel: false,
|
||||
latest_search_query: String::new(),
|
||||
relative_to,
|
||||
matches: Vec::new(),
|
||||
selected: None,
|
||||
cancel_flag: Arc::new(AtomicBool::new(false)),
|
||||
@@ -137,6 +147,7 @@ impl FileFinder {
|
||||
}
|
||||
|
||||
fn spawn_search(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
|
||||
let relative_to = self.relative_to.clone();
|
||||
let worktrees = self
|
||||
.project
|
||||
.read(cx)
|
||||
@@ -165,6 +176,7 @@ impl FileFinder {
|
||||
let matches = fuzzy::match_path_sets(
|
||||
candidate_sets.as_slice(),
|
||||
&query,
|
||||
relative_to,
|
||||
false,
|
||||
100,
|
||||
&cancel_flag,
|
||||
@@ -377,7 +389,7 @@ mod tests {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
||||
|
||||
let query = "hi".to_string();
|
||||
finder
|
||||
@@ -453,7 +465,7 @@ mod tests {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
||||
finder
|
||||
.update(cx, |f, cx| f.spawn_search("hi".into(), cx))
|
||||
.await;
|
||||
@@ -479,7 +491,7 @@ mod tests {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
||||
|
||||
// Even though there is only one worktree, that worktree's filename
|
||||
// is included in the matching, because the worktree is a single file.
|
||||
@@ -532,8 +544,9 @@ mod tests {
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
||||
|
||||
// Run a search that matches two files with the same relative path.
|
||||
finder
|
||||
@@ -551,6 +564,48 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_path_distance_ordering(cx: &mut gpui::TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
let app_state = cx.update(AppState::test);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"dir1": { "a.txt": "" },
|
||||
"dir2": {
|
||||
"a.txt": "",
|
||||
"b.txt": ""
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
|
||||
// When workspace has an active item, sort items which are closer to that item
|
||||
// first when they have the same name. In this case, b.txt is closer to dir2's a.txt
|
||||
// so that one should be sorted earlier
|
||||
let b_path = Some(Arc::from(Path::new("/root/dir2/b.txt")));
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), b_path, cx));
|
||||
|
||||
finder
|
||||
.update(cx, |f, cx| f.spawn_search("a.txt".into(), cx))
|
||||
.await;
|
||||
|
||||
finder.read_with(cx, |f, _| {
|
||||
assert_eq!(f.matches[0].path.as_ref(), Path::new("dir2/a.txt"));
|
||||
assert_eq!(f.matches[1].path.as_ref(), Path::new("dir1/a.txt"));
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) {
|
||||
let app_state = cx.update(AppState::test);
|
||||
@@ -573,7 +628,7 @@ mod tests {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
||||
finder
|
||||
.update(cx, |f, cx| f.spawn_search("dir".into(), cx))
|
||||
.await;
|
||||
|
||||
@@ -443,6 +443,7 @@ mod tests {
|
||||
positions: Vec::new(),
|
||||
path: candidate.path.clone(),
|
||||
path_prefix: "".into(),
|
||||
distance_to_relative_ancestor: usize::MAX,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ pub struct PathMatch {
|
||||
pub worktree_id: usize,
|
||||
pub path: Arc<Path>,
|
||||
pub path_prefix: Arc<str>,
|
||||
/// Number of steps removed from a shared parent with the relative path
|
||||
/// Used to order closer paths first in the search list
|
||||
pub distance_to_relative_ancestor: usize,
|
||||
}
|
||||
|
||||
pub trait PathMatchCandidateSet<'a>: Send + Sync {
|
||||
@@ -78,6 +81,11 @@ impl Ord for PathMatch {
|
||||
.partial_cmp(&other.score)
|
||||
.unwrap_or(Ordering::Equal)
|
||||
.then_with(|| self.worktree_id.cmp(&other.worktree_id))
|
||||
.then_with(|| {
|
||||
other
|
||||
.distance_to_relative_ancestor
|
||||
.cmp(&self.distance_to_relative_ancestor)
|
||||
})
|
||||
.then_with(|| self.path.cmp(&other.path))
|
||||
}
|
||||
}
|
||||
@@ -85,6 +93,7 @@ impl Ord for PathMatch {
|
||||
pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
|
||||
candidate_sets: &'a [Set],
|
||||
query: &str,
|
||||
relative_to: Option<Arc<Path>>,
|
||||
smart_case: bool,
|
||||
max_results: usize,
|
||||
cancel_flag: &AtomicBool,
|
||||
@@ -111,6 +120,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
|
||||
background
|
||||
.scoped(|scope| {
|
||||
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
|
||||
let relative_to = relative_to.clone();
|
||||
scope.spawn(async move {
|
||||
let segment_start = segment_idx * segment_size;
|
||||
let segment_end = segment_start + segment_size;
|
||||
@@ -149,6 +159,15 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
|
||||
positions: Vec::new(),
|
||||
path: candidate.path.clone(),
|
||||
path_prefix: candidate_set.prefix(),
|
||||
distance_to_relative_ancestor: relative_to.as_ref().map_or(
|
||||
usize::MAX,
|
||||
|relative_to| {
|
||||
distance_between_paths(
|
||||
candidate.path.as_ref(),
|
||||
relative_to.as_ref(),
|
||||
)
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -172,3 +191,30 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
/// Compute the distance from a given path to some other path
|
||||
/// If there is no shared path, returns usize::MAX
|
||||
fn distance_between_paths(path: &Path, relative_to: &Path) -> usize {
|
||||
let mut path_components = path.components();
|
||||
let mut relative_components = relative_to.components();
|
||||
|
||||
while path_components
|
||||
.next()
|
||||
.zip(relative_components.next())
|
||||
.map(|(path_component, relative_component)| path_component == relative_component)
|
||||
.unwrap_or_default()
|
||||
{}
|
||||
path_components.count() + relative_components.count() + 1
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use super::distance_between_paths;
|
||||
|
||||
#[test]
|
||||
fn test_distance_between_paths_empty() {
|
||||
distance_between_paths(Path::new(""), Path::new(""));
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,14 @@ pub trait Action: 'static {
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for dyn Action {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("dyn Action")
|
||||
.field("namespace", &self.namespace())
|
||||
.field("name", &self.name())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
/// Define a set of unit struct types that all implement the `Action` trait.
|
||||
///
|
||||
/// The first argument is a namespace that will be associated with each of
|
||||
|
||||
89
crates/gpui/src/app/menu.rs
Normal file
89
crates/gpui/src/app/menu.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use crate::{Action, App, ForegroundPlatform, MutableAppContext};
|
||||
|
||||
pub struct Menu<'a> {
|
||||
pub name: &'a str,
|
||||
pub items: Vec<MenuItem<'a>>,
|
||||
}
|
||||
|
||||
pub enum MenuItem<'a> {
|
||||
Separator,
|
||||
Submenu(Menu<'a>),
|
||||
Action {
|
||||
name: &'a str,
|
||||
action: Box<dyn Action>,
|
||||
os_action: Option<OsAction>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> MenuItem<'a> {
|
||||
pub fn separator() -> Self {
|
||||
Self::Separator
|
||||
}
|
||||
|
||||
pub fn submenu(menu: Menu<'a>) -> Self {
|
||||
Self::Submenu(menu)
|
||||
}
|
||||
|
||||
pub fn action(name: &'a str, action: impl Action) -> Self {
|
||||
Self::Action {
|
||||
name,
|
||||
action: Box::new(action),
|
||||
os_action: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn os_action(name: &'a str, action: impl Action, os_action: OsAction) -> Self {
|
||||
Self::Action {
|
||||
name,
|
||||
action: Box::new(action),
|
||||
os_action: Some(os_action),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub enum OsAction {
|
||||
Cut,
|
||||
Copy,
|
||||
Paste,
|
||||
SelectAll,
|
||||
Undo,
|
||||
Redo,
|
||||
}
|
||||
|
||||
impl MutableAppContext {
|
||||
pub fn set_menus(&mut self, menus: Vec<Menu>) {
|
||||
self.foreground_platform
|
||||
.set_menus(menus, &self.keystroke_matcher);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn setup_menu_handlers(foreground_platform: &dyn ForegroundPlatform, app: &App) {
|
||||
foreground_platform.on_will_open_menu(Box::new({
|
||||
let cx = app.0.clone();
|
||||
move || {
|
||||
let mut cx = cx.borrow_mut();
|
||||
cx.keystroke_matcher.clear_pending();
|
||||
}
|
||||
}));
|
||||
foreground_platform.on_validate_menu_command(Box::new({
|
||||
let cx = app.0.clone();
|
||||
move |action| {
|
||||
let cx = cx.borrow_mut();
|
||||
!cx.keystroke_matcher.has_pending_keystrokes() && cx.is_action_available(action)
|
||||
}
|
||||
}));
|
||||
foreground_platform.on_menu_command(Box::new({
|
||||
let cx = app.0.clone();
|
||||
move |action| {
|
||||
let mut cx = cx.borrow_mut();
|
||||
if let Some(key_window_id) = cx.cx.platform.key_window_id() {
|
||||
if let Some(view_id) = cx.focused_view_id(key_window_id) {
|
||||
cx.handle_dispatch_action_from_effect(key_window_id, Some(view_id), action);
|
||||
return;
|
||||
}
|
||||
}
|
||||
cx.dispatch_global_action_any(action);
|
||||
}
|
||||
}));
|
||||
}
|
||||
220
crates/gpui/src/app/ref_counts.rs
Normal file
220
crates/gpui/src/app/ref_counts.rs
Normal file
@@ -0,0 +1,220 @@
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use std::sync::Arc;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use collections::{hash_map::Entry, HashMap, HashSet};
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use crate::util::post_inc;
|
||||
use crate::ElementStateId;
|
||||
|
||||
lazy_static! {
|
||||
static ref LEAK_BACKTRACE: bool =
|
||||
std::env::var("LEAK_BACKTRACE").map_or(false, |b| !b.is_empty());
|
||||
}
|
||||
|
||||
struct ElementStateRefCount {
|
||||
ref_count: usize,
|
||||
frame_id: usize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RefCounts {
|
||||
entity_counts: HashMap<usize, usize>,
|
||||
element_state_counts: HashMap<ElementStateId, ElementStateRefCount>,
|
||||
dropped_models: HashSet<usize>,
|
||||
dropped_views: HashSet<(usize, usize)>,
|
||||
dropped_element_states: HashSet<ElementStateId>,
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub leak_detector: Arc<Mutex<LeakDetector>>,
|
||||
}
|
||||
|
||||
impl RefCounts {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn new(leak_detector: Arc<Mutex<LeakDetector>>) -> Self {
|
||||
Self {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
leak_detector,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inc_model(&mut self, model_id: usize) {
|
||||
match self.entity_counts.entry(model_id) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
*entry.get_mut() += 1;
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(1);
|
||||
self.dropped_models.remove(&model_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inc_view(&mut self, window_id: usize, view_id: usize) {
|
||||
match self.entity_counts.entry(view_id) {
|
||||
Entry::Occupied(mut entry) => *entry.get_mut() += 1,
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(1);
|
||||
self.dropped_views.remove(&(window_id, view_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inc_element_state(&mut self, id: ElementStateId, frame_id: usize) {
|
||||
match self.element_state_counts.entry(id) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
let entry = entry.get_mut();
|
||||
if entry.frame_id == frame_id || entry.ref_count >= 2 {
|
||||
panic!("used the same element state more than once in the same frame");
|
||||
}
|
||||
entry.ref_count += 1;
|
||||
entry.frame_id = frame_id;
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(ElementStateRefCount {
|
||||
ref_count: 1,
|
||||
frame_id,
|
||||
});
|
||||
self.dropped_element_states.remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dec_model(&mut self, model_id: usize) {
|
||||
let count = self.entity_counts.get_mut(&model_id).unwrap();
|
||||
*count -= 1;
|
||||
if *count == 0 {
|
||||
self.entity_counts.remove(&model_id);
|
||||
self.dropped_models.insert(model_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dec_view(&mut self, window_id: usize, view_id: usize) {
|
||||
let count = self.entity_counts.get_mut(&view_id).unwrap();
|
||||
*count -= 1;
|
||||
if *count == 0 {
|
||||
self.entity_counts.remove(&view_id);
|
||||
self.dropped_views.insert((window_id, view_id));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dec_element_state(&mut self, id: ElementStateId) {
|
||||
let entry = self.element_state_counts.get_mut(&id).unwrap();
|
||||
entry.ref_count -= 1;
|
||||
if entry.ref_count == 0 {
|
||||
self.element_state_counts.remove(&id);
|
||||
self.dropped_element_states.insert(id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_entity_alive(&self, entity_id: usize) -> bool {
|
||||
self.entity_counts.contains_key(&entity_id)
|
||||
}
|
||||
|
||||
pub fn take_dropped(
|
||||
&mut self,
|
||||
) -> (
|
||||
HashSet<usize>,
|
||||
HashSet<(usize, usize)>,
|
||||
HashSet<ElementStateId>,
|
||||
) {
|
||||
(
|
||||
std::mem::take(&mut self.dropped_models),
|
||||
std::mem::take(&mut self.dropped_views),
|
||||
std::mem::take(&mut self.dropped_element_states),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[derive(Default)]
|
||||
pub struct LeakDetector {
|
||||
next_handle_id: usize,
|
||||
#[allow(clippy::type_complexity)]
|
||||
handle_backtraces: HashMap<
|
||||
usize,
|
||||
(
|
||||
Option<&'static str>,
|
||||
HashMap<usize, Option<backtrace::Backtrace>>,
|
||||
),
|
||||
>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl LeakDetector {
|
||||
pub fn handle_created(&mut self, type_name: Option<&'static str>, entity_id: usize) -> usize {
|
||||
let handle_id = post_inc(&mut self.next_handle_id);
|
||||
let entry = self.handle_backtraces.entry(entity_id).or_default();
|
||||
let backtrace = if *LEAK_BACKTRACE {
|
||||
Some(backtrace::Backtrace::new_unresolved())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(type_name) = type_name {
|
||||
entry.0.get_or_insert(type_name);
|
||||
}
|
||||
entry.1.insert(handle_id, backtrace);
|
||||
handle_id
|
||||
}
|
||||
|
||||
pub fn handle_dropped(&mut self, entity_id: usize, handle_id: usize) {
|
||||
if let Some((_, backtraces)) = self.handle_backtraces.get_mut(&entity_id) {
|
||||
assert!(backtraces.remove(&handle_id).is_some());
|
||||
if backtraces.is_empty() {
|
||||
self.handle_backtraces.remove(&entity_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assert_dropped(&mut self, entity_id: usize) {
|
||||
if let Some((type_name, backtraces)) = self.handle_backtraces.get_mut(&entity_id) {
|
||||
for trace in backtraces.values_mut().flatten() {
|
||||
trace.resolve();
|
||||
eprintln!("{:?}", crate::util::CwdBacktrace(trace));
|
||||
}
|
||||
|
||||
let hint = if *LEAK_BACKTRACE {
|
||||
""
|
||||
} else {
|
||||
" – set LEAK_BACKTRACE=1 for more information"
|
||||
};
|
||||
|
||||
panic!(
|
||||
"{} handles to {} {} still exist{}",
|
||||
backtraces.len(),
|
||||
type_name.unwrap_or("entity"),
|
||||
entity_id,
|
||||
hint
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn detect(&mut self) {
|
||||
let mut found_leaks = false;
|
||||
for (id, (type_name, backtraces)) in self.handle_backtraces.iter_mut() {
|
||||
eprintln!(
|
||||
"leaked {} handles to {} {}",
|
||||
backtraces.len(),
|
||||
type_name.unwrap_or("entity"),
|
||||
id
|
||||
);
|
||||
for trace in backtraces.values_mut().flatten() {
|
||||
trace.resolve();
|
||||
eprintln!("{:?}", crate::util::CwdBacktrace(trace));
|
||||
}
|
||||
found_leaks = true;
|
||||
}
|
||||
|
||||
let hint = if *LEAK_BACKTRACE {
|
||||
""
|
||||
} else {
|
||||
" – set LEAK_BACKTRACE=1 for more information"
|
||||
};
|
||||
assert!(!found_leaks, "detected leaked handles{}", hint);
|
||||
}
|
||||
}
|
||||
@@ -18,14 +18,16 @@ use smol::stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
executor, geometry::vector::Vector2F, keymap_matcher::Keystroke, platform, Action,
|
||||
AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent,
|
||||
LeakDetector, ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith,
|
||||
AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, Handle, InputHandler,
|
||||
KeyDownEvent, ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith,
|
||||
ReadViewWith, RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle,
|
||||
WeakHandle, WindowInputHandler,
|
||||
WeakHandle,
|
||||
};
|
||||
use collections::BTreeMap;
|
||||
|
||||
use super::{AsyncAppContext, RefCounts};
|
||||
use super::{
|
||||
ref_counts::LeakDetector, window_input_handler::WindowInputHandler, AsyncAppContext, RefCounts,
|
||||
};
|
||||
|
||||
pub struct TestAppContext {
|
||||
cx: Rc<RefCell<MutableAppContext>>,
|
||||
@@ -52,11 +54,7 @@ impl TestAppContext {
|
||||
platform,
|
||||
foreground_platform.clone(),
|
||||
font_cache,
|
||||
RefCounts {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
leak_detector,
|
||||
..Default::default()
|
||||
},
|
||||
RefCounts::new(leak_detector),
|
||||
(),
|
||||
);
|
||||
cx.next_entity_id = first_entity_id;
|
||||
@@ -332,6 +330,14 @@ impl TestAppContext {
|
||||
.assert_dropped(handle.id())
|
||||
}
|
||||
|
||||
/// Drop a handle, assuming it is the last. If it is not the last, panic with debug information about
|
||||
/// where the stray handles were created.
|
||||
pub fn drop_last<T, W: WeakHandle, H: Handle<T, Weak = W>>(&mut self, handle: H) {
|
||||
let weak = handle.downgrade();
|
||||
self.update(|_| drop(handle));
|
||||
self.assert_dropped(weak);
|
||||
}
|
||||
|
||||
fn window_mut(&self, window_id: usize) -> std::cell::RefMut<platform::test::Window> {
|
||||
std::cell::RefMut::map(self.cx.borrow_mut(), |state| {
|
||||
let (_, window) = state
|
||||
@@ -624,6 +630,8 @@ impl<T: View> ViewHandle<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks string context to be printed when assertions fail.
|
||||
/// Often this is done by storing a context string in the manager and returning the handle.
|
||||
#[derive(Clone)]
|
||||
pub struct AssertionContextManager {
|
||||
id: Arc<AtomicUsize>,
|
||||
@@ -654,6 +662,9 @@ impl AssertionContextManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails.
|
||||
/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails,
|
||||
/// the state that was set initially for the failure can be printed in the error message
|
||||
pub struct ContextHandle {
|
||||
id: usize,
|
||||
manager: AssertionContextManager,
|
||||
|
||||
98
crates/gpui/src/app/window_input_handler.rs
Normal file
98
crates/gpui/src/app/window_input_handler.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
use std::{cell::RefCell, ops::Range, rc::Rc};
|
||||
|
||||
use pathfinder_geometry::rect::RectF;
|
||||
|
||||
use crate::{AnyView, AppContext, InputHandler, MutableAppContext};
|
||||
|
||||
pub struct WindowInputHandler {
|
||||
pub app: Rc<RefCell<MutableAppContext>>,
|
||||
pub window_id: usize,
|
||||
}
|
||||
|
||||
impl WindowInputHandler {
|
||||
fn read_focused_view<T, F>(&self, f: F) -> Option<T>
|
||||
where
|
||||
F: FnOnce(&dyn AnyView, &AppContext) -> T,
|
||||
{
|
||||
// Input-related application hooks are sometimes called by the OS during
|
||||
// a call to a window-manipulation API, like prompting the user for file
|
||||
// paths. In that case, the AppContext will already be borrowed, so any
|
||||
// InputHandler methods need to fail gracefully.
|
||||
//
|
||||
// See https://github.com/zed-industries/community/issues/444
|
||||
let app = self.app.try_borrow().ok()?;
|
||||
|
||||
let view_id = app.focused_view_id(self.window_id)?;
|
||||
let view = app.cx.views.get(&(self.window_id, view_id))?;
|
||||
let result = f(view.as_ref(), &app);
|
||||
Some(result)
|
||||
}
|
||||
|
||||
fn update_focused_view<T, F>(&mut self, f: F) -> Option<T>
|
||||
where
|
||||
F: FnOnce(usize, usize, &mut dyn AnyView, &mut MutableAppContext) -> T,
|
||||
{
|
||||
let mut app = self.app.try_borrow_mut().ok()?;
|
||||
app.update(|app| {
|
||||
let view_id = app.focused_view_id(self.window_id)?;
|
||||
let mut view = app.cx.views.remove(&(self.window_id, view_id))?;
|
||||
let result = f(self.window_id, view_id, view.as_mut(), &mut *app);
|
||||
app.cx.views.insert((self.window_id, view_id), view);
|
||||
Some(result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl InputHandler for WindowInputHandler {
|
||||
fn text_for_range(&self, range: Range<usize>) -> Option<String> {
|
||||
self.read_focused_view(|view, cx| view.text_for_range(range.clone(), cx))
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn selected_text_range(&self) -> Option<Range<usize>> {
|
||||
self.read_focused_view(|view, cx| view.selected_text_range(cx))
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn replace_text_in_range(&mut self, range: Option<Range<usize>>, text: &str) {
|
||||
self.update_focused_view(|window_id, view_id, view, cx| {
|
||||
view.replace_text_in_range(range, text, cx, window_id, view_id);
|
||||
});
|
||||
}
|
||||
|
||||
fn marked_text_range(&self) -> Option<Range<usize>> {
|
||||
self.read_focused_view(|view, cx| view.marked_text_range(cx))
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn unmark_text(&mut self) {
|
||||
self.update_focused_view(|window_id, view_id, view, cx| {
|
||||
view.unmark_text(cx, window_id, view_id);
|
||||
});
|
||||
}
|
||||
|
||||
fn replace_and_mark_text_in_range(
|
||||
&mut self,
|
||||
range: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
new_selected_range: Option<Range<usize>>,
|
||||
) {
|
||||
self.update_focused_view(|window_id, view_id, view, cx| {
|
||||
view.replace_and_mark_text_in_range(
|
||||
range,
|
||||
new_text,
|
||||
new_selected_range,
|
||||
cx,
|
||||
window_id,
|
||||
view_id,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn rect_for_range(&self, range_utf16: Range<usize>) -> Option<RectF> {
|
||||
let app = self.app.borrow();
|
||||
let (presenter, _) = app.presenters_and_platform_windows.get(&self.window_id)?;
|
||||
let presenter = presenter.borrow();
|
||||
presenter.rect_for_text_range(range_utf16, &app)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
mod align;
|
||||
mod canvas;
|
||||
mod clipped;
|
||||
mod constrained_box;
|
||||
mod container;
|
||||
mod empty;
|
||||
@@ -19,12 +20,12 @@ mod text;
|
||||
mod tooltip;
|
||||
mod uniform_list;
|
||||
|
||||
use self::expanded::Expanded;
|
||||
pub use self::{
|
||||
align::*, canvas::*, constrained_box::*, container::*, empty::*, flex::*, hook::*, image::*,
|
||||
keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*, resizable::*,
|
||||
stack::*, svg::*, text::*, tooltip::*, uniform_list::*,
|
||||
};
|
||||
use self::{clipped::Clipped, expanded::Expanded};
|
||||
pub use crate::presenter::ChildView;
|
||||
use crate::{
|
||||
geometry::{
|
||||
@@ -135,6 +136,13 @@ pub trait Element {
|
||||
Align::new(self.boxed())
|
||||
}
|
||||
|
||||
fn clipped(self) -> Clipped
|
||||
where
|
||||
Self: 'static + Sized,
|
||||
{
|
||||
Clipped::new(self.boxed())
|
||||
}
|
||||
|
||||
fn contained(self) -> Container
|
||||
where
|
||||
Self: 'static + Sized,
|
||||
@@ -288,7 +296,10 @@ impl<T: Element> AnyElement for Lifecycle<T> {
|
||||
paint,
|
||||
}
|
||||
}
|
||||
_ => panic!("invalid element lifecycle state"),
|
||||
Lifecycle::Empty => panic!("invalid element lifecycle state"),
|
||||
Lifecycle::Init { .. } => {
|
||||
panic!("invalid element lifecycle state, paint called before layout")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,6 +366,7 @@ impl<T: Element> AnyElement for Lifecycle<T> {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
_ => panic!("invalid element lifecycle state"),
|
||||
}
|
||||
}
|
||||
|
||||
69
crates/gpui/src/elements/clipped.rs
Normal file
69
crates/gpui/src/elements/clipped.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
json, DebugContext, Element, ElementBox, LayoutContext, MeasurementContext, PaintContext,
|
||||
SizeConstraint,
|
||||
};
|
||||
|
||||
pub struct Clipped {
|
||||
child: ElementBox,
|
||||
}
|
||||
|
||||
impl Clipped {
|
||||
pub fn new(child: ElementBox) -> Self {
|
||||
Self { child }
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for Clipped {
|
||||
type LayoutState = ();
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
cx: &mut LayoutContext,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
(self.child.layout(constraint, cx), ())
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: RectF,
|
||||
visible_bounds: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) -> Self::PaintState {
|
||||
cx.scene.push_layer(Some(bounds));
|
||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||
cx.scene.pop_layer();
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
range_utf16: Range<usize>,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
cx: &MeasurementContext,
|
||||
) -> Option<RectF> {
|
||||
self.child.rect_for_text_range(range_utf16, cx)
|
||||
}
|
||||
|
||||
fn debug(
|
||||
&self,
|
||||
_: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
cx: &DebugContext,
|
||||
) -> json::Value {
|
||||
json!({
|
||||
"type": "Clipped",
|
||||
"child": self.child.debug(cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -308,7 +308,9 @@ impl Element for Flex {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
child.paint(child_origin, visible_bounds, cx);
|
||||
|
||||
match self.axis {
|
||||
Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
|
||||
Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
|
||||
|
||||
@@ -12,15 +12,21 @@ pub struct KeystrokeLabel {
|
||||
action: Box<dyn Action>,
|
||||
container_style: ContainerStyle,
|
||||
text_style: TextStyle,
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
}
|
||||
|
||||
impl KeystrokeLabel {
|
||||
pub fn new(
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
action: Box<dyn Action>,
|
||||
container_style: ContainerStyle,
|
||||
text_style: TextStyle,
|
||||
) -> Self {
|
||||
Self {
|
||||
window_id,
|
||||
view_id,
|
||||
action,
|
||||
container_style,
|
||||
text_style,
|
||||
@@ -37,7 +43,10 @@ impl Element for KeystrokeLabel {
|
||||
constraint: SizeConstraint,
|
||||
cx: &mut LayoutContext,
|
||||
) -> (Vector2F, ElementBox) {
|
||||
let mut element = if let Some(keystrokes) = cx.keystrokes_for_action(self.action.as_ref()) {
|
||||
let mut element = if let Some(keystrokes) =
|
||||
cx.app
|
||||
.keystrokes_for_action(self.window_id, self.view_id, self.action.as_ref())
|
||||
{
|
||||
Flex::row()
|
||||
.with_children(keystrokes.iter().map(|keystroke| {
|
||||
Label::new(keystroke.to_string(), self.text_style.clone())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::ops::Range;
|
||||
use std::{borrow::Cow, ops::Range};
|
||||
|
||||
use crate::{
|
||||
fonts::TextStyle,
|
||||
@@ -16,7 +16,7 @@ use serde_json::json;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
|
||||
pub struct Label {
|
||||
text: String,
|
||||
text: Cow<'static, str>,
|
||||
style: LabelStyle,
|
||||
highlight_indices: Vec<usize>,
|
||||
}
|
||||
@@ -44,9 +44,9 @@ impl LabelStyle {
|
||||
}
|
||||
|
||||
impl Label {
|
||||
pub fn new(text: String, style: impl Into<LabelStyle>) -> Self {
|
||||
pub fn new<I: Into<Cow<'static, str>>>(text: I, style: impl Into<LabelStyle>) -> Self {
|
||||
Self {
|
||||
text,
|
||||
text: text.into(),
|
||||
highlight_indices: Default::default(),
|
||||
style: style.into(),
|
||||
}
|
||||
@@ -138,11 +138,9 @@ impl Element for Label {
|
||||
cx: &mut LayoutContext,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let runs = self.compute_runs();
|
||||
let line = cx.text_layout_cache.layout_str(
|
||||
self.text.as_str(),
|
||||
self.style.text.font_size,
|
||||
runs.as_slice(),
|
||||
);
|
||||
let line =
|
||||
cx.text_layout_cache
|
||||
.layout_str(&self.text, self.style.text.font_size, runs.as_slice());
|
||||
|
||||
let size = vec2f(
|
||||
line.width()
|
||||
|
||||
@@ -15,7 +15,7 @@ use serde_json::json;
|
||||
use std::{borrow::Cow, ops::Range, sync::Arc};
|
||||
|
||||
pub struct Text {
|
||||
text: String,
|
||||
text: Cow<'static, str>,
|
||||
style: TextStyle,
|
||||
soft_wrap: bool,
|
||||
highlights: Vec<(Range<usize>, HighlightStyle)>,
|
||||
@@ -28,9 +28,9 @@ pub struct LayoutState {
|
||||
}
|
||||
|
||||
impl Text {
|
||||
pub fn new(text: String, style: TextStyle) -> Self {
|
||||
pub fn new<I: Into<Cow<'static, str>>>(text: I, style: TextStyle) -> Self {
|
||||
Self {
|
||||
text,
|
||||
text: text.into(),
|
||||
style,
|
||||
soft_wrap: true,
|
||||
highlights: Vec::new(),
|
||||
@@ -280,7 +280,7 @@ mod tests {
|
||||
let (window_id, _) = cx.add_window(Default::default(), |_| TestView);
|
||||
let mut presenter = cx.build_presenter(window_id, Default::default(), Default::default());
|
||||
fonts::with_font_cache(cx.font_cache().clone(), || {
|
||||
let mut text = Text::new("Hello\r\n".into(), Default::default()).with_soft_wrap(true);
|
||||
let mut text = Text::new("Hello\r\n", Default::default()).with_soft_wrap(true);
|
||||
let (_, state) = text.layout(
|
||||
SizeConstraint::new(Default::default(), vec2f(f32::INFINITY, f32::INFINITY)),
|
||||
&mut presenter.build_layout_context(Default::default(), false, cx),
|
||||
|
||||
@@ -61,11 +61,14 @@ impl Tooltip {
|
||||
) -> Self {
|
||||
struct ElementState<Tag>(Tag);
|
||||
struct MouseEventHandlerState<Tag>(Tag);
|
||||
let focused_view_id = cx.focused_view_id(cx.window_id);
|
||||
|
||||
let state_handle = cx.default_element_state::<ElementState<Tag>, Rc<TooltipState>>(id);
|
||||
let state = state_handle.read(cx).clone();
|
||||
let tooltip = if state.visible.get() {
|
||||
let mut collapsed_tooltip = Self::render_tooltip(
|
||||
cx.window_id,
|
||||
focused_view_id,
|
||||
text.clone(),
|
||||
style.clone(),
|
||||
action.as_ref().map(|a| a.boxed_clone()),
|
||||
@@ -74,7 +77,7 @@ impl Tooltip {
|
||||
.boxed();
|
||||
Some(
|
||||
Overlay::new(
|
||||
Self::render_tooltip(text, style, action, false)
|
||||
Self::render_tooltip(cx.window_id, focused_view_id, text, style, action, false)
|
||||
.constrained()
|
||||
.dynamically(move |constraint, cx| {
|
||||
SizeConstraint::strict_along(
|
||||
@@ -128,6 +131,8 @@ impl Tooltip {
|
||||
}
|
||||
|
||||
pub fn render_tooltip(
|
||||
window_id: usize,
|
||||
focused_view_id: Option<usize>,
|
||||
text: String,
|
||||
style: TooltipStyle,
|
||||
action: Option<Box<dyn Action>>,
|
||||
@@ -144,13 +149,18 @@ impl Tooltip {
|
||||
text.flex(1., false).aligned().boxed()
|
||||
}
|
||||
})
|
||||
.with_children(action.map(|action| {
|
||||
let keystroke_label =
|
||||
KeystrokeLabel::new(action, style.keystroke.container, style.keystroke.text);
|
||||
.with_children(action.and_then(|action| {
|
||||
let keystroke_label = KeystrokeLabel::new(
|
||||
window_id,
|
||||
focused_view_id?,
|
||||
action,
|
||||
style.keystroke.container,
|
||||
style.keystroke.text,
|
||||
);
|
||||
if measure {
|
||||
keystroke_label.boxed()
|
||||
Some(keystroke_label.boxed())
|
||||
} else {
|
||||
keystroke_label.aligned().boxed()
|
||||
Some(keystroke_label.aligned().boxed())
|
||||
}
|
||||
}))
|
||||
.contained()
|
||||
|
||||
@@ -6,24 +6,15 @@ mod keystroke;
|
||||
use std::{any::TypeId, fmt::Debug};
|
||||
|
||||
use collections::HashMap;
|
||||
use serde::Deserialize;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{impl_actions, Action};
|
||||
use crate::Action;
|
||||
|
||||
pub use binding::{Binding, BindingMatchResult};
|
||||
pub use keymap::Keymap;
|
||||
pub use keymap_context::{KeymapContext, KeymapContextPredicate};
|
||||
pub use keystroke::Keystroke;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
|
||||
pub struct KeyPressed {
|
||||
#[serde(default)]
|
||||
pub keystroke: Keystroke,
|
||||
}
|
||||
|
||||
impl_actions!(gpui, [KeyPressed]);
|
||||
|
||||
pub struct KeymapMatcher {
|
||||
pub contexts: Vec<KeymapContext>,
|
||||
pending_views: HashMap<usize, KeymapContext>,
|
||||
@@ -69,13 +60,27 @@ impl KeymapMatcher {
|
||||
!self.pending_keystrokes.is_empty()
|
||||
}
|
||||
|
||||
/// Pushes a keystroke onto the matcher.
|
||||
/// The result of the new keystroke is returned:
|
||||
/// MatchResult::None =>
|
||||
/// No match is valid for this key given any pending keystrokes.
|
||||
/// MatchResult::Pending =>
|
||||
/// There exist bindings which are still waiting for more keys.
|
||||
/// MatchResult::Complete(matches) =>
|
||||
/// 1 or more bindings have recieved the necessary key presses.
|
||||
/// The order of the matched actions is by position of the matching first,
|
||||
// and order in the keymap second.
|
||||
pub fn push_keystroke(
|
||||
&mut self,
|
||||
keystroke: Keystroke,
|
||||
mut dispatch_path: Vec<(usize, KeymapContext)>,
|
||||
) -> MatchResult {
|
||||
let mut any_pending = false;
|
||||
let mut matched_bindings: Vec<(usize, Box<dyn Action>)> = Vec::new();
|
||||
// Collect matched bindings into an ordered list using the position in the matching binding first,
|
||||
// and then the order the binding matched in the view tree second.
|
||||
// The key is the reverse position of the binding in the bindings list so that later bindings
|
||||
// match before earlier ones in the user's config
|
||||
let mut matched_bindings: Vec<(usize, Box<dyn Action>)> = Default::default();
|
||||
|
||||
let first_keystroke = self.pending_keystrokes.is_empty();
|
||||
self.pending_keystrokes.push(keystroke.clone());
|
||||
@@ -84,35 +89,30 @@ impl KeymapMatcher {
|
||||
self.contexts
|
||||
.extend(dispatch_path.iter_mut().map(|e| std::mem::take(&mut e.1)));
|
||||
|
||||
for (i, (view_id, _)) in dispatch_path.into_iter().enumerate() {
|
||||
// Find the bindings which map the pending keystrokes and current context
|
||||
for (i, (view_id, _)) in dispatch_path.iter().enumerate() {
|
||||
// Don't require pending view entry if there are no pending keystrokes
|
||||
if !first_keystroke && !self.pending_views.contains_key(&view_id) {
|
||||
if !first_keystroke && !self.pending_views.contains_key(view_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If there is a previous view context, invalidate that view if it
|
||||
// has changed
|
||||
if let Some(previous_view_context) = self.pending_views.remove(&view_id) {
|
||||
if let Some(previous_view_context) = self.pending_views.remove(view_id) {
|
||||
if previous_view_context != self.contexts[i] {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the bindings which map the pending keystrokes and current context
|
||||
for binding in self.keymap.bindings().iter().rev() {
|
||||
match binding.match_keys_and_context(&self.pending_keystrokes, &self.contexts[i..])
|
||||
{
|
||||
BindingMatchResult::Complete(mut action) => {
|
||||
// Swap in keystroke for special KeyPressed action
|
||||
if action.name() == "KeyPressed" && action.namespace() == "gpui" {
|
||||
action = Box::new(KeyPressed {
|
||||
keystroke: keystroke.clone(),
|
||||
});
|
||||
}
|
||||
matched_bindings.push((view_id, action))
|
||||
BindingMatchResult::Complete(action) => {
|
||||
matched_bindings.push((*view_id, action));
|
||||
}
|
||||
BindingMatchResult::Partial => {
|
||||
self.pending_views.insert(view_id, self.contexts[i].clone());
|
||||
self.pending_views
|
||||
.insert(*view_id, self.contexts[i].clone());
|
||||
any_pending = true;
|
||||
}
|
||||
_ => {}
|
||||
@@ -125,6 +125,8 @@ impl KeymapMatcher {
|
||||
}
|
||||
|
||||
if !matched_bindings.is_empty() {
|
||||
// Collect the sorted matched bindings into the final vec for ease of use
|
||||
// Matched bindings are in order by precedence
|
||||
MatchResult::Matches(matched_bindings)
|
||||
} else if any_pending {
|
||||
MatchResult::Pending
|
||||
@@ -219,15 +221,47 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_keymap_and_view_ordering() -> Result<()> {
|
||||
actions!(test, [EditorAction, ProjectPanelAction]);
|
||||
|
||||
let mut editor = KeymapContext::default();
|
||||
editor.add_identifier("Editor");
|
||||
|
||||
let mut project_panel = KeymapContext::default();
|
||||
project_panel.add_identifier("ProjectPanel");
|
||||
|
||||
// Editor 'deeper' in than project panel
|
||||
let dispatch_path = vec![(2, editor), (1, project_panel)];
|
||||
|
||||
// But editor actions 'higher' up in keymap
|
||||
let keymap = Keymap::new(vec![
|
||||
Binding::new("left", EditorAction, Some("Editor")),
|
||||
Binding::new("left", ProjectPanelAction, Some("ProjectPanel")),
|
||||
]);
|
||||
|
||||
let mut matcher = KeymapMatcher::new(keymap);
|
||||
|
||||
assert_eq!(
|
||||
matcher.push_keystroke(Keystroke::parse("left")?, dispatch_path.clone()),
|
||||
MatchResult::Matches(vec![
|
||||
(2, Box::new(EditorAction)),
|
||||
(1, Box::new(ProjectPanelAction)),
|
||||
]),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_keystroke() -> Result<()> {
|
||||
actions!(test, [B, AB, C, D, DA]);
|
||||
actions!(test, [B, AB, C, D, DA, E, EF]);
|
||||
|
||||
let mut context1 = KeymapContext::default();
|
||||
context1.set.insert("1".into());
|
||||
context1.add_identifier("1");
|
||||
|
||||
let mut context2 = KeymapContext::default();
|
||||
context2.set.insert("2".into());
|
||||
context2.add_identifier("2");
|
||||
|
||||
let dispatch_path = vec![(2, context2), (1, context1)];
|
||||
|
||||
@@ -280,6 +314,7 @@ mod tests {
|
||||
matcher.push_keystroke(Keystroke::parse("d")?, dispatch_path.clone()),
|
||||
MatchResult::Matches(vec![(2, Box::new(D)), (1, Box::new(D))]),
|
||||
);
|
||||
|
||||
// If none of the d action handlers consume the binding, a pending
|
||||
// binding may then be used
|
||||
assert_eq!(
|
||||
@@ -360,22 +395,22 @@ mod tests {
|
||||
let predicate = KeymapContextPredicate::parse("a && b || c == d").unwrap();
|
||||
|
||||
let mut context = KeymapContext::default();
|
||||
context.set.insert("a".into());
|
||||
context.add_identifier("a");
|
||||
assert!(!predicate.eval(&[context]));
|
||||
|
||||
let mut context = KeymapContext::default();
|
||||
context.set.insert("a".into());
|
||||
context.set.insert("b".into());
|
||||
context.add_identifier("a");
|
||||
context.add_identifier("b");
|
||||
assert!(predicate.eval(&[context]));
|
||||
|
||||
let mut context = KeymapContext::default();
|
||||
context.set.insert("a".into());
|
||||
context.map.insert("c".into(), "x".into());
|
||||
context.add_identifier("a");
|
||||
context.add_key("c", "x");
|
||||
assert!(!predicate.eval(&[context]));
|
||||
|
||||
let mut context = KeymapContext::default();
|
||||
context.set.insert("a".into());
|
||||
context.map.insert("c".into(), "d".into());
|
||||
context.add_identifier("a");
|
||||
context.add_key("c", "d");
|
||||
assert!(predicate.eval(&[context]));
|
||||
|
||||
let predicate = KeymapContextPredicate::parse("!a").unwrap();
|
||||
@@ -415,10 +450,11 @@ mod tests {
|
||||
assert!(!predicate.eval(&contexts[6..]));
|
||||
|
||||
fn context_set(names: &[&str]) -> KeymapContext {
|
||||
KeymapContext {
|
||||
set: names.iter().copied().map(str::to_string).collect(),
|
||||
..Default::default()
|
||||
}
|
||||
let mut keymap = KeymapContext::new();
|
||||
names
|
||||
.iter()
|
||||
.for_each(|name| keymap.add_identifier(name.to_string()));
|
||||
keymap
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,10 +477,10 @@ mod tests {
|
||||
]);
|
||||
|
||||
let mut context_a = KeymapContext::default();
|
||||
context_a.set.insert("a".into());
|
||||
context_a.add_identifier("a");
|
||||
|
||||
let mut context_b = KeymapContext::default();
|
||||
context_b.set.insert("b".into());
|
||||
context_b.add_identifier("b");
|
||||
|
||||
let mut matcher = KeymapMatcher::new(keymap);
|
||||
|
||||
@@ -489,7 +525,7 @@ mod tests {
|
||||
matcher.clear_pending();
|
||||
|
||||
let mut context_c = KeymapContext::default();
|
||||
context_c.set.insert("c".into());
|
||||
context_c.add_identifier("c");
|
||||
|
||||
// Pending keystrokes are maintained per-view
|
||||
assert_eq!(
|
||||
|
||||
@@ -7,7 +7,7 @@ use super::{KeymapContext, KeymapContextPredicate, Keystroke};
|
||||
|
||||
pub struct Binding {
|
||||
action: Box<dyn Action>,
|
||||
keystrokes: Option<SmallVec<[Keystroke; 2]>>,
|
||||
keystrokes: SmallVec<[Keystroke; 2]>,
|
||||
context_predicate: Option<KeymapContextPredicate>,
|
||||
}
|
||||
|
||||
@@ -23,16 +23,10 @@ impl Binding {
|
||||
None
|
||||
};
|
||||
|
||||
let keystrokes = if keystrokes == "*" {
|
||||
None // Catch all context
|
||||
} else {
|
||||
Some(
|
||||
keystrokes
|
||||
.split_whitespace()
|
||||
.map(Keystroke::parse)
|
||||
.collect::<Result<_>>()?,
|
||||
)
|
||||
};
|
||||
let keystrokes = keystrokes
|
||||
.split_whitespace()
|
||||
.map(Keystroke::parse)
|
||||
.collect::<Result<_>>()?;
|
||||
|
||||
Ok(Self {
|
||||
keystrokes,
|
||||
@@ -41,7 +35,7 @@ impl Binding {
|
||||
})
|
||||
}
|
||||
|
||||
fn match_context(&self, contexts: &[KeymapContext]) -> bool {
|
||||
pub fn match_context(&self, contexts: &[KeymapContext]) -> bool {
|
||||
self.context_predicate
|
||||
.as_ref()
|
||||
.map(|predicate| predicate.eval(contexts))
|
||||
@@ -53,20 +47,10 @@ impl Binding {
|
||||
pending_keystrokes: &Vec<Keystroke>,
|
||||
contexts: &[KeymapContext],
|
||||
) -> BindingMatchResult {
|
||||
if self
|
||||
.keystrokes
|
||||
.as_ref()
|
||||
.map(|keystrokes| keystrokes.starts_with(&pending_keystrokes))
|
||||
.unwrap_or(true)
|
||||
&& self.match_context(contexts)
|
||||
if self.keystrokes.as_ref().starts_with(&pending_keystrokes) && self.match_context(contexts)
|
||||
{
|
||||
// If the binding is completed, push it onto the matches list
|
||||
if self
|
||||
.keystrokes
|
||||
.as_ref()
|
||||
.map(|keystrokes| keystrokes.len() == pending_keystrokes.len())
|
||||
.unwrap_or(true)
|
||||
{
|
||||
if self.keystrokes.as_ref().len() == pending_keystrokes.len() {
|
||||
BindingMatchResult::Complete(self.action.boxed_clone())
|
||||
} else {
|
||||
BindingMatchResult::Partial
|
||||
@@ -82,14 +66,14 @@ impl Binding {
|
||||
contexts: &[KeymapContext],
|
||||
) -> Option<SmallVec<[Keystroke; 2]>> {
|
||||
if self.action.eq(action) && self.match_context(contexts) {
|
||||
self.keystrokes.clone()
|
||||
Some(self.keystrokes.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn keystrokes(&self) -> Option<&[Keystroke]> {
|
||||
self.keystrokes.as_deref()
|
||||
pub fn keystrokes(&self) -> &[Keystroke] {
|
||||
self.keystrokes.as_slice()
|
||||
}
|
||||
|
||||
pub fn action(&self) -> &dyn Action {
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::{HashMap, HashSet};
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct KeymapContext {
|
||||
pub set: HashSet<String>,
|
||||
pub map: HashMap<String, String>,
|
||||
set: HashSet<Cow<'static, str>>,
|
||||
map: HashMap<Cow<'static, str>, Cow<'static, str>>,
|
||||
}
|
||||
|
||||
impl KeymapContext {
|
||||
pub fn new() -> Self {
|
||||
KeymapContext {
|
||||
set: HashSet::default(),
|
||||
map: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, other: &Self) {
|
||||
for v in &other.set {
|
||||
self.set.insert(v.clone());
|
||||
@@ -16,6 +25,18 @@ impl KeymapContext {
|
||||
self.map.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_identifier<I: Into<Cow<'static, str>>>(&mut self, identifier: I) {
|
||||
self.set.insert(identifier.into());
|
||||
}
|
||||
|
||||
pub fn add_key<S1: Into<Cow<'static, str>>, S2: Into<Cow<'static, str>>>(
|
||||
&mut self,
|
||||
key: S1,
|
||||
value: S2,
|
||||
) {
|
||||
self.map.insert(key.into(), value.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
@@ -43,15 +64,15 @@ impl KeymapContextPredicate {
|
||||
pub fn eval(&self, contexts: &[KeymapContext]) -> bool {
|
||||
let Some(context) = contexts.first() else { return false };
|
||||
match self {
|
||||
Self::Identifier(name) => context.set.contains(name.as_str()),
|
||||
Self::Identifier(name) => (&context.set).contains(name.as_str()),
|
||||
Self::Equal(left, right) => context
|
||||
.map
|
||||
.get(left)
|
||||
.get(left.as_str())
|
||||
.map(|value| value == right)
|
||||
.unwrap_or(false),
|
||||
Self::NotEqual(left, right) => context
|
||||
.map
|
||||
.get(left)
|
||||
.get(left.as_str())
|
||||
.map(|value| value != right)
|
||||
.unwrap_or(true),
|
||||
Self::Not(pred) => !pred.eval(contexts),
|
||||
|
||||
@@ -80,6 +80,7 @@ pub trait Platform: Send + Sync {
|
||||
fn app_version(&self) -> Result<AppVersion>;
|
||||
fn os_name(&self) -> &'static str;
|
||||
fn os_version(&self) -> Result<AppVersion>;
|
||||
fn restart(&self);
|
||||
}
|
||||
|
||||
pub(crate) trait ForegroundPlatform {
|
||||
@@ -99,6 +100,7 @@ pub(crate) trait ForegroundPlatform {
|
||||
options: PathPromptOptions,
|
||||
) -> oneshot::Receiver<Option<Vec<PathBuf>>>;
|
||||
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>>;
|
||||
fn reveal_path(&self, path: &Path);
|
||||
}
|
||||
|
||||
pub trait Dispatcher: Send + Sync {
|
||||
@@ -124,7 +126,7 @@ pub trait InputHandler {
|
||||
pub trait Screen: Debug {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn bounds(&self) -> RectF;
|
||||
fn display_uuid(&self) -> Uuid;
|
||||
fn display_uuid(&self) -> Option<Uuid>;
|
||||
}
|
||||
|
||||
pub trait Window {
|
||||
|
||||
@@ -23,12 +23,16 @@ pub use renderer::Surface;
|
||||
use std::{ops::Range, rc::Rc, sync::Arc};
|
||||
use window::Window;
|
||||
|
||||
use crate::executor;
|
||||
|
||||
pub(crate) fn platform() -> Arc<dyn super::Platform> {
|
||||
Arc::new(MacPlatform::new())
|
||||
}
|
||||
|
||||
pub(crate) fn foreground_platform() -> Rc<dyn super::ForegroundPlatform> {
|
||||
Rc::new(MacForegroundPlatform::default())
|
||||
pub(crate) fn foreground_platform(
|
||||
foreground: Rc<executor::Foreground>,
|
||||
) -> Rc<dyn super::ForegroundPlatform> {
|
||||
Rc::new(MacForegroundPlatform::new(foreground))
|
||||
}
|
||||
|
||||
trait BoolExt {
|
||||
|
||||
@@ -14,12 +14,12 @@ use pathfinder_geometry::{
|
||||
|
||||
pub trait Vector2FExt {
|
||||
/// Converts self to an NSPoint with y axis pointing up.
|
||||
fn to_screen_ns_point(&self, native_window: id) -> NSPoint;
|
||||
fn to_screen_ns_point(&self, native_window: id, window_height: f64) -> NSPoint;
|
||||
}
|
||||
impl Vector2FExt for Vector2F {
|
||||
fn to_screen_ns_point(&self, native_window: id) -> NSPoint {
|
||||
fn to_screen_ns_point(&self, native_window: id, window_height: f64) -> NSPoint {
|
||||
unsafe {
|
||||
let point = NSPoint::new(self.x() as f64, -self.y() as f64);
|
||||
let point = NSPoint::new(self.x() as f64, window_height - self.y() as f64);
|
||||
msg_send![native_window, convertPointToScreen: point]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use cocoa::{
|
||||
NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
|
||||
NSPasteboardTypeString, NSSavePanel, NSWindow,
|
||||
},
|
||||
base::{id, nil, selector, YES},
|
||||
base::{id, nil, selector, BOOL, YES},
|
||||
foundation::{
|
||||
NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSString,
|
||||
NSUInteger, NSURL,
|
||||
@@ -45,6 +45,7 @@ use std::{
|
||||
ffi::{c_void, CStr, OsStr},
|
||||
os::{raw::c_char, unix::ffi::OsStrExt},
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
ptr,
|
||||
rc::Rc,
|
||||
slice, str,
|
||||
@@ -97,6 +98,31 @@ unsafe fn build_classes() {
|
||||
sel!(handleGPUIMenuItem:),
|
||||
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
// Add menu item handlers so that OS save panels have the correct key commands
|
||||
decl.add_method(
|
||||
sel!(cut:),
|
||||
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(copy:),
|
||||
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(paste:),
|
||||
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(selectAll:),
|
||||
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(undo:),
|
||||
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(redo:),
|
||||
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(validateMenuItem:),
|
||||
validate_menu_item as extern "C" fn(&mut Object, Sel, id) -> bool,
|
||||
@@ -113,10 +139,8 @@ unsafe fn build_classes() {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MacForegroundPlatform(RefCell<MacForegroundPlatformState>);
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MacForegroundPlatformState {
|
||||
become_active: Option<Box<dyn FnMut()>>,
|
||||
resign_active: Option<Box<dyn FnMut()>>,
|
||||
@@ -128,9 +152,26 @@ pub struct MacForegroundPlatformState {
|
||||
open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
|
||||
finish_launching: Option<Box<dyn FnOnce()>>,
|
||||
menu_actions: Vec<Box<dyn Action>>,
|
||||
foreground: Rc<executor::Foreground>,
|
||||
}
|
||||
|
||||
impl MacForegroundPlatform {
|
||||
pub fn new(foreground: Rc<executor::Foreground>) -> Self {
|
||||
Self(RefCell::new(MacForegroundPlatformState {
|
||||
become_active: Default::default(),
|
||||
resign_active: Default::default(),
|
||||
quit: Default::default(),
|
||||
event: Default::default(),
|
||||
menu_command: Default::default(),
|
||||
validate_menu_command: Default::default(),
|
||||
will_open_menu: Default::default(),
|
||||
open_urls: Default::default(),
|
||||
finish_launching: Default::default(),
|
||||
menu_actions: Default::default(),
|
||||
foreground,
|
||||
}))
|
||||
}
|
||||
|
||||
unsafe fn create_menu_bar(
|
||||
&self,
|
||||
menus: Vec<Menu>,
|
||||
@@ -177,14 +218,28 @@ impl MacForegroundPlatform {
|
||||
) -> id {
|
||||
match item {
|
||||
MenuItem::Separator => NSMenuItem::separatorItem(nil),
|
||||
MenuItem::Action { name, action } => {
|
||||
MenuItem::Action {
|
||||
name,
|
||||
action,
|
||||
os_action,
|
||||
} => {
|
||||
// TODO
|
||||
let keystrokes = keystroke_matcher
|
||||
.bindings_for_action_type(action.as_any().type_id())
|
||||
.find(|binding| binding.action().eq(action.as_ref()))
|
||||
.map(|binding| binding.keystrokes());
|
||||
let selector = match os_action {
|
||||
Some(crate::OsAction::Cut) => selector("cut:"),
|
||||
Some(crate::OsAction::Copy) => selector("copy:"),
|
||||
Some(crate::OsAction::Paste) => selector("paste:"),
|
||||
Some(crate::OsAction::SelectAll) => selector("selectAll:"),
|
||||
Some(crate::OsAction::Undo) => selector("undo:"),
|
||||
Some(crate::OsAction::Redo) => selector("redo:"),
|
||||
None => selector("handleGPUIMenuItem:"),
|
||||
};
|
||||
|
||||
let item;
|
||||
if let Some(keystrokes) = keystrokes.flatten() {
|
||||
if let Some(keystrokes) = keystrokes {
|
||||
if keystrokes.len() == 1 {
|
||||
let keystroke = &keystrokes[0];
|
||||
let mut mask = NSEventModifierFlags::empty();
|
||||
@@ -202,7 +257,7 @@ impl MacForegroundPlatform {
|
||||
item = NSMenuItem::alloc(nil)
|
||||
.initWithTitle_action_keyEquivalent_(
|
||||
ns_string(name),
|
||||
selector("handleGPUIMenuItem:"),
|
||||
selector,
|
||||
ns_string(key_to_native(&keystroke.key).as_ref()),
|
||||
)
|
||||
.autorelease();
|
||||
@@ -224,7 +279,7 @@ impl MacForegroundPlatform {
|
||||
item = NSMenuItem::alloc(nil)
|
||||
.initWithTitle_action_keyEquivalent_(
|
||||
ns_string(&name),
|
||||
selector("handleGPUIMenuItem:"),
|
||||
selector,
|
||||
ns_string(""),
|
||||
)
|
||||
.autorelease();
|
||||
@@ -233,7 +288,7 @@ impl MacForegroundPlatform {
|
||||
item = NSMenuItem::alloc(nil)
|
||||
.initWithTitle_action_keyEquivalent_(
|
||||
ns_string(name),
|
||||
selector("handleGPUIMenuItem:"),
|
||||
selector,
|
||||
ns_string(""),
|
||||
)
|
||||
.autorelease();
|
||||
@@ -398,6 +453,26 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
|
||||
done_rx
|
||||
}
|
||||
}
|
||||
|
||||
fn reveal_path(&self, path: &Path) {
|
||||
unsafe {
|
||||
let path = path.to_path_buf();
|
||||
self.0
|
||||
.borrow()
|
||||
.foreground
|
||||
.spawn(async move {
|
||||
let full_path = ns_string(path.to_str().unwrap_or(""));
|
||||
let root_full_path = ns_string("");
|
||||
let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
|
||||
let _: BOOL = msg_send![
|
||||
workspace,
|
||||
selectFile: full_path
|
||||
inFileViewerRootedAtPath: root_full_path
|
||||
];
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MacPlatform {
|
||||
@@ -790,6 +865,28 @@ impl platform::Platform for MacPlatform {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn restart(&self) {
|
||||
#[cfg(debug_assertions)]
|
||||
let path = std::env::current_exe().unwrap();
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
let path = self
|
||||
.app_path()
|
||||
.unwrap_or_else(|_| std::env::current_exe().unwrap());
|
||||
|
||||
let script = r#"lsof -p "$0" +r 1 &>/dev/null && open "$1""#;
|
||||
|
||||
Command::new("/bin/bash")
|
||||
.arg("-c")
|
||||
.arg(script)
|
||||
.arg(std::process::id().to_string())
|
||||
.arg(path)
|
||||
.spawn()
|
||||
.ok();
|
||||
|
||||
self.quit();
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn path_from_objc(path: id) -> PathBuf {
|
||||
|
||||
@@ -35,7 +35,7 @@ impl Screen {
|
||||
.map(|ix| Screen {
|
||||
native_screen: native_screens.objectAtIndex(ix),
|
||||
})
|
||||
.find(|screen| platform::Screen::display_uuid(screen) == uuid)
|
||||
.find(|screen| platform::Screen::display_uuid(screen) == Some(uuid))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ impl platform::Screen for Screen {
|
||||
self
|
||||
}
|
||||
|
||||
fn display_uuid(&self) -> uuid::Uuid {
|
||||
fn display_uuid(&self) -> Option<uuid::Uuid> {
|
||||
unsafe {
|
||||
// Screen ids are not stable. Further, the default device id is also unstable across restarts.
|
||||
// CGDisplayCreateUUIDFromDisplayID is stable but not exposed in the bindings we use.
|
||||
@@ -74,8 +74,12 @@ impl platform::Screen for Screen {
|
||||
(&mut device_id) as *mut _ as *mut c_void,
|
||||
);
|
||||
let cfuuid = CGDisplayCreateUUIDFromDisplayID(device_id as CGDirectDisplayID);
|
||||
if cfuuid.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let bytes = CFUUIDGetUUIDBytes(cfuuid);
|
||||
Uuid::from_bytes([
|
||||
Some(Uuid::from_bytes([
|
||||
bytes.byte0,
|
||||
bytes.byte1,
|
||||
bytes.byte2,
|
||||
@@ -92,7 +96,7 @@ impl platform::Screen for Screen {
|
||||
bytes.byte13,
|
||||
bytes.byte14,
|
||||
bytes.byte15,
|
||||
])
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,16 +85,12 @@ impl SpriteCache {
|
||||
) -> Option<GlyphSprite> {
|
||||
const SUBPIXEL_VARIANTS: u8 = 4;
|
||||
|
||||
let scale_factor = self.scale_factor;
|
||||
let target_position = target_position * scale_factor;
|
||||
let fonts = &self.fonts;
|
||||
let atlases = &mut self.atlases;
|
||||
let target_position = target_position * self.scale_factor;
|
||||
let subpixel_variant = (
|
||||
(target_position.x().fract() * SUBPIXEL_VARIANTS as f32).round() as u8
|
||||
% SUBPIXEL_VARIANTS,
|
||||
(target_position.y().fract() * SUBPIXEL_VARIANTS as f32).round() as u8
|
||||
% SUBPIXEL_VARIANTS,
|
||||
(target_position.x().fract() * SUBPIXEL_VARIANTS as f32).floor() as u8,
|
||||
(target_position.y().fract() * SUBPIXEL_VARIANTS as f32).floor() as u8,
|
||||
);
|
||||
|
||||
self.glyphs
|
||||
.entry(GlyphDescriptor {
|
||||
font_id,
|
||||
@@ -107,16 +103,17 @@ impl SpriteCache {
|
||||
subpixel_variant.0 as f32 / SUBPIXEL_VARIANTS as f32,
|
||||
subpixel_variant.1 as f32 / SUBPIXEL_VARIANTS as f32,
|
||||
);
|
||||
let (glyph_bounds, mask) = fonts.rasterize_glyph(
|
||||
let (glyph_bounds, mask) = self.fonts.rasterize_glyph(
|
||||
font_id,
|
||||
font_size,
|
||||
glyph_id,
|
||||
subpixel_shift,
|
||||
scale_factor,
|
||||
self.scale_factor,
|
||||
RasterizationOptions::Alpha,
|
||||
)?;
|
||||
|
||||
let (alloc_id, atlas_bounds) = atlases
|
||||
let (alloc_id, atlas_bounds) = self
|
||||
.atlases
|
||||
.upload(glyph_bounds.size(), &mask)
|
||||
.expect("could not upload glyph");
|
||||
Some(GlyphSprite {
|
||||
|
||||
@@ -19,7 +19,7 @@ use cocoa::{
|
||||
appkit::{
|
||||
CGPoint, NSApplication, NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable,
|
||||
NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior,
|
||||
NSWindowStyleMask,
|
||||
NSWindowStyleMask, NSWindowTitleVisibility,
|
||||
},
|
||||
base::{id, nil},
|
||||
foundation::{NSAutoreleasePool, NSInteger, NSPoint, NSRect, NSSize, NSString, NSUInteger},
|
||||
@@ -483,6 +483,7 @@ impl Window {
|
||||
|
||||
let native_view: id = msg_send![VIEW_CLASS, alloc];
|
||||
let native_view = NSView::init(native_view);
|
||||
|
||||
assert!(!native_view.is_null());
|
||||
|
||||
let window = Self(Rc::new(RefCell::new(WindowState {
|
||||
@@ -535,6 +536,7 @@ impl Window {
|
||||
.map_or(true, |titlebar| titlebar.appears_transparent)
|
||||
{
|
||||
native_window.setTitlebarAppearsTransparent_(YES);
|
||||
native_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden);
|
||||
}
|
||||
|
||||
native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable);
|
||||
@@ -733,7 +735,9 @@ impl platform::Window for Window {
|
||||
let app = NSApplication::sharedApplication(nil);
|
||||
let window = self.0.borrow().native_window;
|
||||
let title = ns_string(title);
|
||||
msg_send![app, changeWindowsItem:window title:title filename:false]
|
||||
let _: () = msg_send![app, changeWindowsItem:window title:title filename:false];
|
||||
let _: () = msg_send![window, setTitle: title];
|
||||
self.0.borrow().move_traffic_light();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -828,12 +832,14 @@ impl platform::Window for Window {
|
||||
let self_id = self_borrow.id;
|
||||
|
||||
unsafe {
|
||||
let window_frame = self_borrow.frame();
|
||||
let app = NSApplication::sharedApplication(nil);
|
||||
|
||||
// Convert back to screen coordinates
|
||||
let screen_point =
|
||||
(position + window_frame.origin()).to_screen_ns_point(self_borrow.native_window);
|
||||
let screen_point = position.to_screen_ns_point(
|
||||
self_borrow.native_window,
|
||||
self_borrow.content_size().y() as f64,
|
||||
);
|
||||
|
||||
let window_number: NSInteger = msg_send![class!(NSWindow), windowNumberAtPoint:screen_point belowWindowWithWindowNumber:0];
|
||||
let top_most_window: id = msg_send![app, windowWithWindowNumber: window_number];
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
keymap_matcher::KeymapMatcher,
|
||||
Action, ClipboardItem,
|
||||
Action, ClipboardItem, Menu,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::VecDeque;
|
||||
@@ -77,7 +77,7 @@ impl super::ForegroundPlatform for ForegroundPlatform {
|
||||
fn on_menu_command(&self, _: Box<dyn FnMut(&dyn Action)>) {}
|
||||
fn on_validate_menu_command(&self, _: Box<dyn FnMut(&dyn Action) -> bool>) {}
|
||||
fn on_will_open_menu(&self, _: Box<dyn FnMut()>) {}
|
||||
fn set_menus(&self, _: Vec<crate::Menu>, _: &KeymapMatcher) {}
|
||||
fn set_menus(&self, _: Vec<Menu>, _: &KeymapMatcher) {}
|
||||
|
||||
fn prompt_for_paths(
|
||||
&self,
|
||||
@@ -92,6 +92,8 @@ impl super::ForegroundPlatform for ForegroundPlatform {
|
||||
*self.last_prompt_for_new_path_args.borrow_mut() = Some((path.to_path_buf(), done_tx));
|
||||
done_rx
|
||||
}
|
||||
|
||||
fn reveal_path(&self, _: &Path) {}
|
||||
}
|
||||
|
||||
pub fn platform() -> Platform {
|
||||
@@ -224,6 +226,8 @@ impl super::Platform for Platform {
|
||||
patch: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn restart(&self) {}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -238,8 +242,8 @@ impl super::Screen for Screen {
|
||||
RectF::new(Vector2F::zero(), Vector2F::new(1920., 1080.))
|
||||
}
|
||||
|
||||
fn display_uuid(&self) -> uuid::Uuid {
|
||||
uuid::Uuid::new_v4()
|
||||
fn display_uuid(&self) -> Option<uuid::Uuid> {
|
||||
Some(uuid::Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ use crate::{
|
||||
font_cache::FontCache,
|
||||
geometry::rect::RectF,
|
||||
json::{self, ToJson},
|
||||
keymap_matcher::Keystroke,
|
||||
platform::{CursorStyle, Event},
|
||||
scene::{
|
||||
CursorRegion, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover,
|
||||
@@ -13,9 +12,9 @@ use crate::{
|
||||
text_layout::TextLayoutCache,
|
||||
Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, Appearance,
|
||||
AssetCache, ElementBox, Entity, FontSystem, ModelHandle, MouseButton, MouseMovedEvent,
|
||||
MouseRegion, MouseRegionId, ParentId, ReadModel, ReadView, RenderContext, RenderParams,
|
||||
SceneBuilder, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle,
|
||||
WeakViewHandle,
|
||||
MouseRegion, MouseRegionId, MouseState, ParentId, ReadModel, ReadView, RenderContext,
|
||||
RenderParams, SceneBuilder, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle,
|
||||
WeakModelHandle, WeakViewHandle,
|
||||
};
|
||||
use anyhow::bail;
|
||||
use collections::{HashMap, HashSet};
|
||||
@@ -508,15 +507,18 @@ impl Presenter {
|
||||
}
|
||||
// Handle Down events if the MouseRegion has a Click or Drag handler. This makes the api more intuitive as you would
|
||||
// not expect a MouseRegion to be transparent to Down events if it also has a Click handler.
|
||||
// This behavior can be overridden by adding a Down handler that calls cx.propogate_event
|
||||
// This behavior can be overridden by adding a Down handler
|
||||
if let MouseEvent::Down(e) = &mouse_event {
|
||||
if valid_region
|
||||
let has_click = valid_region
|
||||
.handlers
|
||||
.contains(MouseEvent::click_disc(), Some(e.button))
|
||||
|| valid_region
|
||||
.handlers
|
||||
.contains(MouseEvent::drag_disc(), Some(e.button))
|
||||
{
|
||||
.contains(MouseEvent::click_disc(), Some(e.button));
|
||||
let has_drag = valid_region
|
||||
.handlers
|
||||
.contains(MouseEvent::drag_disc(), Some(e.button));
|
||||
let has_down = valid_region
|
||||
.handlers
|
||||
.contains(MouseEvent::down_disc(), Some(e.button));
|
||||
if !has_down && (has_click || has_drag) {
|
||||
event_cx.handled = true;
|
||||
}
|
||||
}
|
||||
@@ -524,14 +526,13 @@ impl Presenter {
|
||||
// `event_consumed` should only be true if there are any handlers for this event.
|
||||
let mut event_consumed = event_cx.handled;
|
||||
if let Some(callbacks) = valid_region.handlers.get(&mouse_event.handler_key()) {
|
||||
event_consumed = true;
|
||||
for callback in callbacks {
|
||||
event_cx.handled = true;
|
||||
event_cx.with_current_view(valid_region.id().view_id(), {
|
||||
let region_event = mouse_event.clone();
|
||||
|cx| callback(region_event, cx)
|
||||
});
|
||||
event_consumed &= event_cx.handled;
|
||||
event_consumed |= event_cx.handled;
|
||||
any_event_handled |= event_cx.handled;
|
||||
}
|
||||
}
|
||||
@@ -604,12 +605,22 @@ pub struct LayoutContext<'a> {
|
||||
}
|
||||
|
||||
impl<'a> LayoutContext<'a> {
|
||||
pub(crate) fn keystrokes_for_action(
|
||||
&mut self,
|
||||
action: &dyn Action,
|
||||
) -> Option<SmallVec<[Keystroke; 2]>> {
|
||||
self.app
|
||||
.keystrokes_for_action(self.window_id, &self.view_stack, action)
|
||||
pub fn mouse_state<Tag: 'static>(&self, region_id: usize) -> MouseState {
|
||||
let view_id = self.view_stack.last().unwrap();
|
||||
|
||||
let region_id = MouseRegionId::new::<Tag>(*view_id, region_id);
|
||||
MouseState {
|
||||
hovered: self.hovered_region_ids.contains(®ion_id),
|
||||
clicked: self.clicked_region_ids.as_ref().and_then(|(ids, button)| {
|
||||
if ids.contains(®ion_id) {
|
||||
Some(*button)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
accessed_hovered: false,
|
||||
accessed_clicked: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(&mut self, view_id: usize, constraint: SizeConstraint) -> Vector2F {
|
||||
|
||||
@@ -1,14 +1,3 @@
|
||||
use crate::{
|
||||
elements::Empty,
|
||||
executor::{self, ExecutorEvent},
|
||||
platform,
|
||||
util::CwdBacktrace,
|
||||
Element, ElementBox, Entity, FontCache, Handle, LeakDetector, MutableAppContext, Platform,
|
||||
RenderContext, Subscription, TestAppContext, View,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use parking_lot::Mutex;
|
||||
use smol::channel;
|
||||
use std::{
|
||||
fmt::Write,
|
||||
panic::{self, RefUnwindSafe},
|
||||
@@ -19,6 +8,20 @@ use std::{
|
||||
},
|
||||
};
|
||||
|
||||
use futures::StreamExt;
|
||||
use parking_lot::Mutex;
|
||||
use smol::channel;
|
||||
|
||||
use crate::{
|
||||
app::ref_counts::LeakDetector,
|
||||
elements::Empty,
|
||||
executor::{self, ExecutorEvent},
|
||||
platform,
|
||||
util::CwdBacktrace,
|
||||
Element, ElementBox, Entity, FontCache, Handle, MutableAppContext, Platform, RenderContext,
|
||||
Subscription, TestAppContext, View,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
|
||||
@@ -66,6 +66,7 @@ settings = { path = "../settings", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
ctor = "0.1"
|
||||
env_logger = "0.9"
|
||||
indoc = "1.0.4"
|
||||
rand = "0.8.3"
|
||||
tree-sitter-embedded-template = "*"
|
||||
tree-sitter-html = "*"
|
||||
|
||||
@@ -41,7 +41,7 @@ pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, Opera
|
||||
use theme::SyntaxTheme;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use util::RandomCharIter;
|
||||
use util::TryFutureExt as _;
|
||||
use util::{RangeExt, TryFutureExt as _};
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use {tree_sitter_rust, tree_sitter_typescript};
|
||||
@@ -214,15 +214,6 @@ pub trait File: Send + Sync {
|
||||
|
||||
fn is_deleted(&self) -> bool;
|
||||
|
||||
fn save(
|
||||
&self,
|
||||
buffer_id: u64,
|
||||
text: Rope,
|
||||
version: clock::Global,
|
||||
line_ending: LineEnding,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<Result<(clock::Global, RopeFingerprint, SystemTime)>>;
|
||||
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
|
||||
fn to_proto(&self) -> rpc::proto::File;
|
||||
@@ -314,7 +305,7 @@ pub struct Chunk<'a> {
|
||||
}
|
||||
|
||||
pub struct Diff {
|
||||
base_version: clock::Global,
|
||||
pub(crate) base_version: clock::Global,
|
||||
line_ending: LineEnding,
|
||||
edits: Vec<(Range<usize>, Arc<str>)>,
|
||||
}
|
||||
@@ -529,33 +520,6 @@ impl Buffer {
|
||||
self.file.as_ref()
|
||||
}
|
||||
|
||||
pub fn save(
|
||||
&mut self,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<(clock::Global, RopeFingerprint, SystemTime)>> {
|
||||
let file = if let Some(file) = self.file.as_ref() {
|
||||
file
|
||||
} else {
|
||||
return Task::ready(Err(anyhow!("buffer has no file")));
|
||||
};
|
||||
let text = self.as_rope().clone();
|
||||
let version = self.version();
|
||||
let save = file.save(
|
||||
self.remote_id(),
|
||||
text,
|
||||
version,
|
||||
self.line_ending(),
|
||||
cx.as_mut(),
|
||||
);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let (version, fingerprint, mtime) = save.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.did_save(version.clone(), fingerprint, mtime, None, cx);
|
||||
});
|
||||
Ok((version, fingerprint, mtime))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn saved_version(&self) -> &clock::Global {
|
||||
&self.saved_version
|
||||
}
|
||||
@@ -585,16 +549,11 @@ impl Buffer {
|
||||
version: clock::Global,
|
||||
fingerprint: RopeFingerprint,
|
||||
mtime: SystemTime,
|
||||
new_file: Option<Arc<dyn File>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.saved_version = version;
|
||||
self.saved_version_fingerprint = fingerprint;
|
||||
self.saved_mtime = mtime;
|
||||
if let Some(new_file) = new_file {
|
||||
self.file = Some(new_file);
|
||||
self.file_update_count += 1;
|
||||
}
|
||||
cx.emit(Event::Saved);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -610,18 +569,21 @@ impl Buffer {
|
||||
.read_with(&cx, |this, cx| this.diff(new_text, cx))
|
||||
.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(transaction) = this.apply_diff(diff, cx).cloned() {
|
||||
this.did_reload(
|
||||
this.version(),
|
||||
this.as_rope().fingerprint(),
|
||||
this.line_ending(),
|
||||
new_mtime,
|
||||
cx,
|
||||
);
|
||||
Ok(Some(transaction))
|
||||
} else {
|
||||
Ok(None)
|
||||
if this.version() == diff.base_version {
|
||||
this.finalize_last_transaction();
|
||||
this.apply_diff(diff, cx);
|
||||
if let Some(transaction) = this.finalize_last_transaction().cloned() {
|
||||
this.did_reload(
|
||||
this.version(),
|
||||
this.as_rope().fingerprint(),
|
||||
this.line_ending(),
|
||||
new_mtime,
|
||||
cx,
|
||||
);
|
||||
return Ok(Some(transaction));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
})
|
||||
} else {
|
||||
Ok(None)
|
||||
@@ -661,36 +623,35 @@ impl Buffer {
|
||||
new_file: Arc<dyn File>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<()> {
|
||||
let old_file = if let Some(file) = self.file.as_ref() {
|
||||
file
|
||||
} else {
|
||||
return Task::ready(());
|
||||
};
|
||||
let mut file_changed = false;
|
||||
let mut task = Task::ready(());
|
||||
|
||||
if new_file.path() != old_file.path() {
|
||||
file_changed = true;
|
||||
}
|
||||
|
||||
if new_file.is_deleted() {
|
||||
if !old_file.is_deleted() {
|
||||
if let Some(old_file) = self.file.as_ref() {
|
||||
if new_file.path() != old_file.path() {
|
||||
file_changed = true;
|
||||
if !self.is_dirty() {
|
||||
cx.emit(Event::DirtyChanged);
|
||||
}
|
||||
|
||||
if new_file.is_deleted() {
|
||||
if !old_file.is_deleted() {
|
||||
file_changed = true;
|
||||
if !self.is_dirty() {
|
||||
cx.emit(Event::DirtyChanged);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let new_mtime = new_file.mtime();
|
||||
if new_mtime != old_file.mtime() {
|
||||
file_changed = true;
|
||||
|
||||
if !self.is_dirty() {
|
||||
let reload = self.reload(cx).log_err().map(drop);
|
||||
task = cx.foreground().spawn(reload);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let new_mtime = new_file.mtime();
|
||||
if new_mtime != old_file.mtime() {
|
||||
file_changed = true;
|
||||
|
||||
if !self.is_dirty() {
|
||||
let reload = self.reload(cx).log_err().map(drop);
|
||||
task = cx.foreground().spawn(reload);
|
||||
}
|
||||
}
|
||||
}
|
||||
file_changed = true;
|
||||
};
|
||||
|
||||
if file_changed {
|
||||
self.file_update_count += 1;
|
||||
@@ -1196,20 +1157,84 @@ impl Buffer {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn apply_diff(&mut self, diff: Diff, cx: &mut ModelContext<Self>) -> Option<&Transaction> {
|
||||
if self.version == diff.base_version {
|
||||
self.finalize_last_transaction();
|
||||
self.start_transaction();
|
||||
self.text.set_line_ending(diff.line_ending);
|
||||
self.edit(diff.edits, None, cx);
|
||||
if self.end_transaction(cx).is_some() {
|
||||
self.finalize_last_transaction()
|
||||
} else {
|
||||
None
|
||||
/// Spawn a background task that searches the buffer for any whitespace
|
||||
/// at the ends of a lines, and returns a `Diff` that removes that whitespace.
|
||||
pub fn remove_trailing_whitespace(&self, cx: &AppContext) -> Task<Diff> {
|
||||
let old_text = self.as_rope().clone();
|
||||
let line_ending = self.line_ending();
|
||||
let base_version = self.version();
|
||||
cx.background().spawn(async move {
|
||||
let ranges = trailing_whitespace_ranges(&old_text);
|
||||
let empty = Arc::<str>::from("");
|
||||
Diff {
|
||||
base_version,
|
||||
line_ending,
|
||||
edits: ranges
|
||||
.into_iter()
|
||||
.map(|range| (range, empty.clone()))
|
||||
.collect(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Ensure that the buffer ends with a single newline character, and
|
||||
/// no other whitespace.
|
||||
pub fn ensure_final_newline(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let len = self.len();
|
||||
let mut offset = len;
|
||||
for chunk in self.as_rope().reversed_chunks_in_range(0..len) {
|
||||
let non_whitespace_len = chunk
|
||||
.trim_end_matches(|c: char| c.is_ascii_whitespace())
|
||||
.len();
|
||||
offset -= chunk.len();
|
||||
offset += non_whitespace_len;
|
||||
if non_whitespace_len != 0 {
|
||||
if offset == len - 1 && chunk.get(non_whitespace_len..) == Some("\n") {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
self.edit([(offset..len, "\n")], None, cx);
|
||||
}
|
||||
|
||||
/// Apply a diff to the buffer. If the buffer has changed since the given diff was
|
||||
/// calculated, then adjust the diff to account for those changes, and discard any
|
||||
/// parts of the diff that conflict with those changes.
|
||||
pub fn apply_diff(&mut self, diff: Diff, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
|
||||
// Check for any edits to the buffer that have occurred since this diff
|
||||
// was computed.
|
||||
let snapshot = self.snapshot();
|
||||
let mut edits_since = snapshot.edits_since::<usize>(&diff.base_version).peekable();
|
||||
let mut delta = 0;
|
||||
let adjusted_edits = diff.edits.into_iter().filter_map(|(range, new_text)| {
|
||||
while let Some(edit_since) = edits_since.peek() {
|
||||
// If the edit occurs after a diff hunk, then it does not
|
||||
// affect that hunk.
|
||||
if edit_since.old.start > range.end {
|
||||
break;
|
||||
}
|
||||
// If the edit precedes the diff hunk, then adjust the hunk
|
||||
// to reflect the edit.
|
||||
else if edit_since.old.end < range.start {
|
||||
delta += edit_since.new_len() as i64 - edit_since.old_len() as i64;
|
||||
edits_since.next();
|
||||
}
|
||||
// If the edit intersects a diff hunk, then discard that hunk.
|
||||
else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let start = (range.start as i64 + delta) as usize;
|
||||
let end = (range.end as i64 + delta) as usize;
|
||||
Some((start..end, new_text))
|
||||
});
|
||||
|
||||
self.start_transaction();
|
||||
self.text.set_line_ending(diff.line_ending);
|
||||
self.edit(adjusted_edits, None, cx);
|
||||
self.end_transaction(cx)
|
||||
}
|
||||
|
||||
pub fn is_dirty(&self) -> bool {
|
||||
@@ -1389,12 +1414,12 @@ impl Buffer {
|
||||
.enumerate()
|
||||
.zip(&edit_operation.as_edit().unwrap().new_text)
|
||||
.map(|((ix, (range, _)), new_text)| {
|
||||
let new_text_len = new_text.len();
|
||||
let new_text_length = new_text.len();
|
||||
let old_start = range.start.to_point(&before_edit);
|
||||
let new_start = (delta + range.start as isize) as usize;
|
||||
delta += new_text_len as isize - (range.end as isize - range.start as isize);
|
||||
delta += new_text_length as isize - (range.end as isize - range.start as isize);
|
||||
|
||||
let mut range_of_insertion_to_indent = 0..new_text_len;
|
||||
let mut range_of_insertion_to_indent = 0..new_text_length;
|
||||
let mut first_line_is_new = false;
|
||||
let mut original_indent_column = None;
|
||||
|
||||
@@ -2247,7 +2272,6 @@ impl BufferSnapshot {
|
||||
.map(|g| g.outline_config.as_ref().unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut chunks = self.chunks(0..self.len(), true);
|
||||
let mut stack = Vec::<Range<usize>>::new();
|
||||
let mut items = Vec::new();
|
||||
while let Some(mat) = matches.peek() {
|
||||
@@ -2266,9 +2290,7 @@ impl BufferSnapshot {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut text = String::new();
|
||||
let mut name_ranges = Vec::new();
|
||||
let mut highlight_ranges = Vec::new();
|
||||
let mut buffer_ranges = Vec::new();
|
||||
for capture in mat.captures {
|
||||
let node_is_name;
|
||||
if capture.index == config.name_capture_ix {
|
||||
@@ -2286,12 +2308,27 @@ impl BufferSnapshot {
|
||||
range.start + self.line_len(start.row as u32) as usize - start.column;
|
||||
}
|
||||
|
||||
buffer_ranges.push((range, node_is_name));
|
||||
}
|
||||
|
||||
if buffer_ranges.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut text = String::new();
|
||||
let mut highlight_ranges = Vec::new();
|
||||
let mut name_ranges = Vec::new();
|
||||
let mut chunks = self.chunks(
|
||||
buffer_ranges.first().unwrap().0.start..buffer_ranges.last().unwrap().0.end,
|
||||
true,
|
||||
);
|
||||
for (buffer_range, is_name) in buffer_ranges {
|
||||
if !text.is_empty() {
|
||||
text.push(' ');
|
||||
}
|
||||
if node_is_name {
|
||||
if is_name {
|
||||
let mut start = text.len();
|
||||
let end = start + range.len();
|
||||
let end = start + buffer_range.len();
|
||||
|
||||
// When multiple names are captured, then the matcheable text
|
||||
// includes the whitespace in between the names.
|
||||
@@ -2302,12 +2339,12 @@ impl BufferSnapshot {
|
||||
name_ranges.push(start..end);
|
||||
}
|
||||
|
||||
let mut offset = range.start;
|
||||
let mut offset = buffer_range.start;
|
||||
chunks.seek(offset);
|
||||
for mut chunk in chunks.by_ref() {
|
||||
if chunk.text.len() > range.end - offset {
|
||||
chunk.text = &chunk.text[0..(range.end - offset)];
|
||||
offset = range.end;
|
||||
if chunk.text.len() > buffer_range.end - offset {
|
||||
chunk.text = &chunk.text[0..(buffer_range.end - offset)];
|
||||
offset = buffer_range.end;
|
||||
} else {
|
||||
offset += chunk.text.len();
|
||||
}
|
||||
@@ -2321,7 +2358,7 @@ impl BufferSnapshot {
|
||||
highlight_ranges.push((start..end, style));
|
||||
}
|
||||
text.push_str(chunk.text);
|
||||
if offset >= range.end {
|
||||
if offset >= buffer_range.end {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -2346,56 +2383,50 @@ impl BufferSnapshot {
|
||||
Some(items)
|
||||
}
|
||||
|
||||
pub fn enclosing_bracket_ranges<T: ToOffset>(
|
||||
&self,
|
||||
/// Returns bracket range pairs overlapping or adjacent to `range`
|
||||
pub fn bracket_ranges<'a, T: ToOffset>(
|
||||
&'a self,
|
||||
range: Range<T>,
|
||||
) -> Option<(Range<usize>, Range<usize>)> {
|
||||
) -> impl Iterator<Item = (Range<usize>, Range<usize>)> + 'a {
|
||||
// Find bracket pairs that *inclusively* contain the given range.
|
||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||
let mut matches = self.syntax.matches(
|
||||
range.start.saturating_sub(1)..self.len().min(range.end + 1),
|
||||
&self.text,
|
||||
|grammar| grammar.brackets_config.as_ref().map(|c| &c.query),
|
||||
);
|
||||
let range = range.start.to_offset(self).saturating_sub(1)
|
||||
..self.len().min(range.end.to_offset(self) + 1);
|
||||
|
||||
let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
|
||||
grammar.brackets_config.as_ref().map(|c| &c.query)
|
||||
});
|
||||
let configs = matches
|
||||
.grammars()
|
||||
.iter()
|
||||
.map(|grammar| grammar.brackets_config.as_ref().unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Get the ranges of the innermost pair of brackets.
|
||||
let mut result: Option<(Range<usize>, Range<usize>)> = None;
|
||||
while let Some(mat) = matches.peek() {
|
||||
let mut open = None;
|
||||
let mut close = None;
|
||||
let config = &configs[mat.grammar_index];
|
||||
for capture in mat.captures {
|
||||
if capture.index == config.open_capture_ix {
|
||||
open = Some(capture.node.byte_range());
|
||||
} else if capture.index == config.close_capture_ix {
|
||||
close = Some(capture.node.byte_range());
|
||||
iter::from_fn(move || {
|
||||
while let Some(mat) = matches.peek() {
|
||||
let mut open = None;
|
||||
let mut close = None;
|
||||
let config = &configs[mat.grammar_index];
|
||||
for capture in mat.captures {
|
||||
if capture.index == config.open_capture_ix {
|
||||
open = Some(capture.node.byte_range());
|
||||
} else if capture.index == config.close_capture_ix {
|
||||
close = Some(capture.node.byte_range());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matches.advance();
|
||||
matches.advance();
|
||||
|
||||
let Some((open, close)) = open.zip(close) else { continue };
|
||||
if open.start > range.start || close.end < range.end {
|
||||
continue;
|
||||
}
|
||||
let len = close.end - open.start;
|
||||
let Some((open, close)) = open.zip(close) else { continue };
|
||||
|
||||
if let Some((existing_open, existing_close)) = &result {
|
||||
let existing_len = existing_close.end - existing_open.start;
|
||||
if len > existing_len {
|
||||
let bracket_range = open.start..=close.end;
|
||||
if !bracket_range.overlaps(&range) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return Some((open, close));
|
||||
}
|
||||
|
||||
result = Some((open, close));
|
||||
}
|
||||
|
||||
result
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
@@ -2876,3 +2907,42 @@ pub fn char_kind(c: char) -> CharKind {
|
||||
CharKind::Punctuation
|
||||
}
|
||||
}
|
||||
|
||||
/// Find all of the ranges of whitespace that occur at the ends of lines
|
||||
/// in the given rope.
|
||||
///
|
||||
/// This could also be done with a regex search, but this implementation
|
||||
/// avoids copying text.
|
||||
pub fn trailing_whitespace_ranges(rope: &Rope) -> Vec<Range<usize>> {
|
||||
let mut ranges = Vec::new();
|
||||
|
||||
let mut offset = 0;
|
||||
let mut prev_chunk_trailing_whitespace_range = 0..0;
|
||||
for chunk in rope.chunks() {
|
||||
let mut prev_line_trailing_whitespace_range = 0..0;
|
||||
for (i, line) in chunk.split('\n').enumerate() {
|
||||
let line_end_offset = offset + line.len();
|
||||
let trimmed_line_len = line.trim_end_matches(|c| matches!(c, ' ' | '\t')).len();
|
||||
let mut trailing_whitespace_range = (offset + trimmed_line_len)..line_end_offset;
|
||||
|
||||
if i == 0 && trimmed_line_len == 0 {
|
||||
trailing_whitespace_range.start = prev_chunk_trailing_whitespace_range.start;
|
||||
}
|
||||
if !prev_line_trailing_whitespace_range.is_empty() {
|
||||
ranges.push(prev_line_trailing_whitespace_range);
|
||||
}
|
||||
|
||||
offset = line_end_offset + 1;
|
||||
prev_line_trailing_whitespace_range = trailing_whitespace_range;
|
||||
}
|
||||
|
||||
offset -= 1;
|
||||
prev_chunk_trailing_whitespace_range = prev_line_trailing_whitespace_range;
|
||||
}
|
||||
|
||||
if !prev_chunk_trailing_whitespace_range.is_empty() {
|
||||
ranges.push(prev_chunk_trailing_whitespace_range);
|
||||
}
|
||||
|
||||
ranges
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ use clock::ReplicaId;
|
||||
use collections::BTreeMap;
|
||||
use fs::LineEnding;
|
||||
use gpui::{ModelHandle, MutableAppContext};
|
||||
use indoc::indoc;
|
||||
use proto::deserialize_operation;
|
||||
use rand::prelude::*;
|
||||
use regex::RegexBuilder;
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
@@ -15,7 +17,14 @@ use std::{
|
||||
};
|
||||
use text::network::Network;
|
||||
use unindent::Unindent as _;
|
||||
use util::{post_inc, test::marked_text_ranges, RandomCharIter};
|
||||
use util::{assert_set_eq, post_inc, test::marked_text_ranges, RandomCharIter};
|
||||
|
||||
lazy_static! {
|
||||
static ref TRAILING_WHITESPACE_REGEX: Regex = RegexBuilder::new("[ \t]+$")
|
||||
.multi_line(true)
|
||||
.build()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
@@ -210,6 +219,79 @@ async fn test_apply_diff(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) {
|
||||
let text = [
|
||||
"zero", //
|
||||
"one ", // 2 trailing spaces
|
||||
"two", //
|
||||
"three ", // 3 trailing spaces
|
||||
"four", //
|
||||
"five ", // 4 trailing spaces
|
||||
]
|
||||
.join("\n");
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
|
||||
|
||||
// Spawn a task to format the buffer's whitespace.
|
||||
// Pause so that the foratting task starts running.
|
||||
let format = buffer.read_with(cx, |buffer, cx| buffer.remove_trailing_whitespace(cx));
|
||||
smol::future::yield_now().await;
|
||||
|
||||
// Edit the buffer while the normalization task is running.
|
||||
let version_before_edit = buffer.read_with(cx, |buffer, _| buffer.version());
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
[
|
||||
(Point::new(0, 1)..Point::new(0, 1), "EE"),
|
||||
(Point::new(3, 5)..Point::new(3, 5), "EEE"),
|
||||
],
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let format_diff = format.await;
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let version_before_format = format_diff.base_version.clone();
|
||||
buffer.apply_diff(format_diff, cx);
|
||||
|
||||
// The outcome depends on the order of concurrent taks.
|
||||
//
|
||||
// If the edit occurred while searching for trailing whitespace ranges,
|
||||
// then the trailing whitespace region touched by the edit is left intact.
|
||||
if version_before_format == version_before_edit {
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
[
|
||||
"zEEero", //
|
||||
"one", //
|
||||
"two", //
|
||||
"threeEEE ", //
|
||||
"four", //
|
||||
"five", //
|
||||
]
|
||||
.join("\n")
|
||||
);
|
||||
}
|
||||
// Otherwise, all trailing whitespace is removed.
|
||||
else {
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
[
|
||||
"zEEero", //
|
||||
"one", //
|
||||
"two", //
|
||||
"threeEEE", //
|
||||
"four", //
|
||||
"five", //
|
||||
]
|
||||
.join("\n")
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_reparse(cx: &mut gpui::TestAppContext) {
|
||||
let text = "fn a() {}";
|
||||
@@ -576,53 +658,117 @@ async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
let buffer = cx.add_model(|cx| {
|
||||
let text = "
|
||||
mod x {
|
||||
mod y {
|
||||
let mut assert = |selection_text, range_markers| {
|
||||
assert_bracket_pairs(selection_text, range_markers, rust_lang(), cx)
|
||||
};
|
||||
|
||||
assert(
|
||||
indoc! {"
|
||||
mod x {
|
||||
moˇd y {
|
||||
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent();
|
||||
Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)
|
||||
});
|
||||
let buffer = buffer.read(cx);
|
||||
assert_eq!(
|
||||
buffer.enclosing_bracket_point_ranges(Point::new(1, 6)..Point::new(1, 6)),
|
||||
Some((
|
||||
Point::new(0, 6)..Point::new(0, 7),
|
||||
Point::new(4, 0)..Point::new(4, 1)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.enclosing_bracket_point_ranges(Point::new(1, 10)..Point::new(1, 10)),
|
||||
Some((
|
||||
Point::new(1, 10)..Point::new(1, 11),
|
||||
Point::new(3, 4)..Point::new(3, 5)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.enclosing_bracket_point_ranges(Point::new(3, 5)..Point::new(3, 5)),
|
||||
Some((
|
||||
Point::new(1, 10)..Point::new(1, 11),
|
||||
Point::new(3, 4)..Point::new(3, 5)
|
||||
))
|
||||
let foo = 1;"},
|
||||
vec![indoc! {"
|
||||
mod x «{»
|
||||
mod y {
|
||||
|
||||
}
|
||||
«}»
|
||||
let foo = 1;"}],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
buffer.enclosing_bracket_point_ranges(Point::new(4, 1)..Point::new(4, 1)),
|
||||
Some((
|
||||
Point::new(0, 6)..Point::new(0, 7),
|
||||
Point::new(4, 0)..Point::new(4, 1)
|
||||
))
|
||||
assert(
|
||||
indoc! {"
|
||||
mod x {
|
||||
mod y ˇ{
|
||||
|
||||
}
|
||||
}
|
||||
let foo = 1;"},
|
||||
vec![
|
||||
indoc! {"
|
||||
mod x «{»
|
||||
mod y {
|
||||
|
||||
}
|
||||
«}»
|
||||
let foo = 1;"},
|
||||
indoc! {"
|
||||
mod x {
|
||||
mod y «{»
|
||||
|
||||
«}»
|
||||
}
|
||||
let foo = 1;"},
|
||||
],
|
||||
);
|
||||
|
||||
assert(
|
||||
indoc! {"
|
||||
mod x {
|
||||
mod y {
|
||||
|
||||
}ˇ
|
||||
}
|
||||
let foo = 1;"},
|
||||
vec![
|
||||
indoc! {"
|
||||
mod x «{»
|
||||
mod y {
|
||||
|
||||
}
|
||||
«}»
|
||||
let foo = 1;"},
|
||||
indoc! {"
|
||||
mod x {
|
||||
mod y «{»
|
||||
|
||||
«}»
|
||||
}
|
||||
let foo = 1;"},
|
||||
],
|
||||
);
|
||||
|
||||
assert(
|
||||
indoc! {"
|
||||
mod x {
|
||||
mod y {
|
||||
|
||||
}
|
||||
ˇ}
|
||||
let foo = 1;"},
|
||||
vec![indoc! {"
|
||||
mod x «{»
|
||||
mod y {
|
||||
|
||||
}
|
||||
«}»
|
||||
let foo = 1;"}],
|
||||
);
|
||||
|
||||
assert(
|
||||
indoc! {"
|
||||
mod x {
|
||||
mod y {
|
||||
|
||||
}
|
||||
}
|
||||
let fˇoo = 1;"},
|
||||
vec![],
|
||||
);
|
||||
|
||||
// Regression test: avoid crash when querying at the end of the buffer.
|
||||
assert_eq!(
|
||||
buffer.enclosing_bracket_point_ranges(Point::new(4, 1)..Point::new(5, 0)),
|
||||
None
|
||||
assert(
|
||||
indoc! {"
|
||||
mod x {
|
||||
mod y {
|
||||
|
||||
}
|
||||
}
|
||||
let foo = 1;ˇ"},
|
||||
vec![],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -630,52 +776,34 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||
fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(
|
||||
cx: &mut MutableAppContext,
|
||||
) {
|
||||
let javascript_language = Arc::new(
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "JavaScript".into(),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_javascript::language()),
|
||||
)
|
||||
.with_brackets_query(
|
||||
r#"
|
||||
("{" @open "}" @close)
|
||||
("(" @open ")" @close)
|
||||
"#,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
cx.set_global(Settings::test(cx));
|
||||
let buffer = cx.add_model(|cx| {
|
||||
let text = "
|
||||
for (const a in b) {
|
||||
// a comment that's longer than the for-loop header
|
||||
}
|
||||
"
|
||||
.unindent();
|
||||
Buffer::new(0, text, cx).with_language(javascript_language, cx)
|
||||
});
|
||||
|
||||
let buffer = buffer.read(cx);
|
||||
assert_eq!(
|
||||
buffer.enclosing_bracket_point_ranges(Point::new(0, 18)..Point::new(0, 18)),
|
||||
Some((
|
||||
Point::new(0, 4)..Point::new(0, 5),
|
||||
Point::new(0, 17)..Point::new(0, 18)
|
||||
))
|
||||
let mut assert = |selection_text, bracket_pair_texts| {
|
||||
assert_bracket_pairs(selection_text, bracket_pair_texts, javascript_lang(), cx)
|
||||
};
|
||||
|
||||
assert(
|
||||
indoc! {"
|
||||
for (const a in b)ˇ {
|
||||
// a comment that's longer than the for-loop header
|
||||
}"},
|
||||
vec![indoc! {"
|
||||
for «(»const a in b«)» {
|
||||
// a comment that's longer than the for-loop header
|
||||
}"}],
|
||||
);
|
||||
|
||||
eprintln!("-----------------------");
|
||||
// Regression test: even though the parent node of the parentheses (the for loop) does
|
||||
// intersect the given range, the parentheses themselves do not contain the range, so
|
||||
// they should not be returned. Only the curly braces contain the range.
|
||||
assert_eq!(
|
||||
buffer.enclosing_bracket_point_ranges(Point::new(0, 20)..Point::new(0, 20)),
|
||||
Some((
|
||||
Point::new(0, 19)..Point::new(0, 20),
|
||||
Point::new(2, 0)..Point::new(2, 1)
|
||||
))
|
||||
assert(
|
||||
indoc! {"
|
||||
for (const a in b) {ˇ
|
||||
// a comment that's longer than the for-loop header
|
||||
}"},
|
||||
vec![indoc! {"
|
||||
for (const a in b) «{»
|
||||
// a comment that's longer than the for-loop header
|
||||
«}»"}],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1480,42 +1608,34 @@ fn test_language_config_at(cx: &mut MutableAppContext) {
|
||||
LanguageConfig {
|
||||
name: "JavaScript".into(),
|
||||
line_comment: Some("// ".into()),
|
||||
brackets: vec![
|
||||
BracketPair {
|
||||
start: "{".into(),
|
||||
end: "}".into(),
|
||||
close: true,
|
||||
newline: false,
|
||||
},
|
||||
BracketPair {
|
||||
start: "'".into(),
|
||||
end: "'".into(),
|
||||
close: true,
|
||||
newline: false,
|
||||
},
|
||||
],
|
||||
overrides: [
|
||||
(
|
||||
"element".into(),
|
||||
LanguageConfigOverride {
|
||||
line_comment: Override::Remove { remove: true },
|
||||
block_comment: Override::Set(("{/*".into(), "*/}".into())),
|
||||
..Default::default()
|
||||
brackets: BracketPairConfig {
|
||||
pairs: vec![
|
||||
BracketPair {
|
||||
start: "{".into(),
|
||||
end: "}".into(),
|
||||
close: true,
|
||||
newline: false,
|
||||
},
|
||||
),
|
||||
(
|
||||
"string".into(),
|
||||
LanguageConfigOverride {
|
||||
brackets: Override::Set(vec![BracketPair {
|
||||
start: "{".into(),
|
||||
end: "}".into(),
|
||||
close: true,
|
||||
newline: false,
|
||||
}]),
|
||||
..Default::default()
|
||||
BracketPair {
|
||||
start: "'".into(),
|
||||
end: "'".into(),
|
||||
close: true,
|
||||
newline: false,
|
||||
},
|
||||
),
|
||||
]
|
||||
],
|
||||
disabled_scopes_by_bracket_ix: vec![
|
||||
Vec::new(), //
|
||||
vec!["string".into()],
|
||||
],
|
||||
},
|
||||
overrides: [(
|
||||
"element".into(),
|
||||
LanguageConfigOverride {
|
||||
line_comment: Override::Remove { remove: true },
|
||||
block_comment: Override::Set(("{/*".into(), "*/}".into())),
|
||||
..Default::default()
|
||||
},
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
@@ -1537,11 +1657,19 @@ fn test_language_config_at(cx: &mut MutableAppContext) {
|
||||
|
||||
let config = snapshot.language_scope_at(0).unwrap();
|
||||
assert_eq!(config.line_comment_prefix().unwrap().as_ref(), "// ");
|
||||
assert_eq!(config.brackets().len(), 2);
|
||||
// Both bracket pairs are enabled
|
||||
assert_eq!(
|
||||
config.brackets().map(|e| e.1).collect::<Vec<_>>(),
|
||||
&[true, true]
|
||||
);
|
||||
|
||||
let string_config = snapshot.language_scope_at(3).unwrap();
|
||||
assert_eq!(config.line_comment_prefix().unwrap().as_ref(), "// ");
|
||||
assert_eq!(string_config.brackets().len(), 1);
|
||||
assert_eq!(string_config.line_comment_prefix().unwrap().as_ref(), "// ");
|
||||
// Second bracket pair is disabled
|
||||
assert_eq!(
|
||||
string_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
|
||||
&[true, false]
|
||||
);
|
||||
|
||||
let element_config = snapshot.language_scope_at(10).unwrap();
|
||||
assert_eq!(element_config.line_comment_prefix(), None);
|
||||
@@ -1549,7 +1677,11 @@ fn test_language_config_at(cx: &mut MutableAppContext) {
|
||||
element_config.block_comment_delimiters(),
|
||||
Some((&"{/*".into(), &"*/}".into()))
|
||||
);
|
||||
assert_eq!(element_config.brackets().len(), 2);
|
||||
// Both bracket pairs are enabled
|
||||
assert_eq!(
|
||||
element_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
|
||||
&[true, true]
|
||||
);
|
||||
|
||||
buffer
|
||||
});
|
||||
@@ -1892,19 +2024,43 @@ fn test_contiguous_ranges() {
|
||||
);
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
pub fn enclosing_bracket_point_ranges<T: ToOffset>(
|
||||
&self,
|
||||
range: Range<T>,
|
||||
) -> Option<(Range<Point>, Range<Point>)> {
|
||||
self.snapshot()
|
||||
.enclosing_bracket_ranges(range)
|
||||
.map(|(start, end)| {
|
||||
let point_start = start.start.to_point(self)..start.end.to_point(self);
|
||||
let point_end = end.start.to_point(self)..end.end.to_point(self);
|
||||
(point_start, point_end)
|
||||
})
|
||||
#[gpui::test(iterations = 500)]
|
||||
fn test_trailing_whitespace_ranges(mut rng: StdRng) {
|
||||
// Generate a random multi-line string containing
|
||||
// some lines with trailing whitespace.
|
||||
let mut text = String::new();
|
||||
for _ in 0..rng.gen_range(0..16) {
|
||||
for _ in 0..rng.gen_range(0..36) {
|
||||
text.push(match rng.gen_range(0..10) {
|
||||
0..=1 => ' ',
|
||||
3 => '\t',
|
||||
_ => rng.gen_range('a'..'z'),
|
||||
});
|
||||
}
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
match rng.gen_range(0..10) {
|
||||
// sometimes remove the last newline
|
||||
0..=1 => drop(text.pop()), //
|
||||
|
||||
// sometimes add extra newlines
|
||||
2..=3 => text.push_str(&"\n".repeat(rng.gen_range(1..5))),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let rope = Rope::from(text.as_str());
|
||||
let actual_ranges = trailing_whitespace_ranges(&rope);
|
||||
let expected_ranges = TRAILING_WHITESPACE_REGEX
|
||||
.find_iter(&text)
|
||||
.map(|m| m.range())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
actual_ranges,
|
||||
expected_ranges,
|
||||
"wrong ranges for text lines:\n{:?}",
|
||||
text.split("\n").collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
fn ruby_lang() -> Language {
|
||||
@@ -1990,6 +2146,23 @@ fn json_lang() -> Language {
|
||||
)
|
||||
}
|
||||
|
||||
fn javascript_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "JavaScript".into(),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_javascript::language()),
|
||||
)
|
||||
.with_brackets_query(
|
||||
r#"
|
||||
("{" @open "}" @close)
|
||||
("(" @open ")" @close)
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> String {
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
let snapshot = buffer.snapshot();
|
||||
@@ -1997,3 +2170,34 @@ fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> Str
|
||||
layers[0].node.to_sexp()
|
||||
})
|
||||
}
|
||||
|
||||
// Assert that the enclosing bracket ranges around the selection match the pairs indicated by the marked text in `range_markers`
|
||||
fn assert_bracket_pairs(
|
||||
selection_text: &'static str,
|
||||
bracket_pair_texts: Vec<&'static str>,
|
||||
language: Language,
|
||||
cx: &mut MutableAppContext,
|
||||
) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
let (expected_text, selection_ranges) = marked_text_ranges(selection_text, false);
|
||||
let buffer = cx.add_model(|cx| {
|
||||
Buffer::new(0, expected_text.clone(), cx).with_language(Arc::new(language), cx)
|
||||
});
|
||||
let buffer = buffer.update(cx, |buffer, _cx| buffer.snapshot());
|
||||
|
||||
let selection_range = selection_ranges[0].clone();
|
||||
|
||||
let bracket_pairs = bracket_pair_texts
|
||||
.into_iter()
|
||||
.map(|pair_text| {
|
||||
let (bracket_text, ranges) = marked_text_ranges(pair_text, false);
|
||||
assert_eq!(bracket_text, expected_text);
|
||||
(ranges[0].clone(), ranges[1].clone())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_set_eq!(
|
||||
buffer.bracket_ranges(selection_range).collect::<Vec<_>>(),
|
||||
bracket_pairs
|
||||
);
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ pub struct CodeLabel {
|
||||
pub struct LanguageConfig {
|
||||
pub name: Arc<str>,
|
||||
pub path_suffixes: Vec<String>,
|
||||
pub brackets: Vec<BracketPair>,
|
||||
pub brackets: BracketPairConfig,
|
||||
#[serde(default = "auto_indent_using_last_non_empty_line_default")]
|
||||
pub auto_indent_using_last_non_empty_line: bool,
|
||||
#[serde(default, deserialize_with = "deserialize_regex")]
|
||||
@@ -258,7 +258,7 @@ pub struct LanguageQueries {
|
||||
pub overrides: Option<Cow<'static, str>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LanguageScope {
|
||||
language: Arc<Language>,
|
||||
override_id: Option<u32>,
|
||||
@@ -270,8 +270,8 @@ pub struct LanguageConfigOverride {
|
||||
pub line_comment: Override<Arc<str>>,
|
||||
#[serde(default)]
|
||||
pub block_comment: Override<(Arc<str>, Arc<str>)>,
|
||||
#[serde(default)]
|
||||
pub brackets: Override<Vec<BracketPair>>,
|
||||
#[serde(skip_deserializing)]
|
||||
pub disabled_bracket_ixs: Vec<u16>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -336,7 +336,41 @@ pub struct FakeLspAdapter {
|
||||
pub disk_based_diagnostics_sources: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct BracketPairConfig {
|
||||
pub pairs: Vec<BracketPair>,
|
||||
pub disabled_scopes_by_bracket_ix: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for BracketPairConfig {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
pub struct Entry {
|
||||
#[serde(flatten)]
|
||||
pub bracket_pair: BracketPair,
|
||||
#[serde(default)]
|
||||
pub not_in: Vec<String>,
|
||||
}
|
||||
|
||||
let result = Vec::<Entry>::deserialize(deserializer)?;
|
||||
let mut brackets = Vec::with_capacity(result.len());
|
||||
let mut disabled_scopes_by_bracket_ix = Vec::with_capacity(result.len());
|
||||
for entry in result {
|
||||
brackets.push(entry.bracket_pair);
|
||||
disabled_scopes_by_bracket_ix.push(entry.not_in);
|
||||
}
|
||||
|
||||
Ok(BracketPairConfig {
|
||||
pairs: brackets,
|
||||
disabled_scopes_by_bracket_ix,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
|
||||
pub struct BracketPair {
|
||||
pub start: String,
|
||||
pub end: String,
|
||||
@@ -393,7 +427,7 @@ struct InjectionConfig {
|
||||
|
||||
struct OverrideConfig {
|
||||
query: Query,
|
||||
values: HashMap<u32, LanguageConfigOverride>,
|
||||
values: HashMap<u32, (String, LanguageConfigOverride)>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
@@ -967,16 +1001,11 @@ impl Language {
|
||||
pub fn with_override_query(mut self, source: &str) -> Result<Self> {
|
||||
let query = Query::new(self.grammar_mut().ts_language, source)?;
|
||||
|
||||
let mut values = HashMap::default();
|
||||
let mut override_configs_by_id = HashMap::default();
|
||||
for (ix, name) in query.capture_names().iter().enumerate() {
|
||||
if !name.starts_with('_') {
|
||||
let value = self.config.overrides.remove(name).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"language {:?} has override in query but not in config: {name:?}",
|
||||
self.config.name
|
||||
)
|
||||
})?;
|
||||
values.insert(ix as u32, value);
|
||||
let value = self.config.overrides.remove(name).unwrap_or_default();
|
||||
override_configs_by_id.insert(ix as u32, (name.clone(), value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -988,7 +1017,46 @@ impl Language {
|
||||
))?;
|
||||
}
|
||||
|
||||
self.grammar_mut().override_config = Some(OverrideConfig { query, values });
|
||||
for disabled_scope_name in self
|
||||
.config
|
||||
.brackets
|
||||
.disabled_scopes_by_bracket_ix
|
||||
.iter()
|
||||
.flatten()
|
||||
{
|
||||
if !override_configs_by_id
|
||||
.values()
|
||||
.any(|(scope_name, _)| scope_name == disabled_scope_name)
|
||||
{
|
||||
Err(anyhow!(
|
||||
"language {:?} has overrides in config not in query: {disabled_scope_name:?}",
|
||||
self.config.name
|
||||
))?;
|
||||
}
|
||||
}
|
||||
|
||||
for (name, override_config) in override_configs_by_id.values_mut() {
|
||||
override_config.disabled_bracket_ixs = self
|
||||
.config
|
||||
.brackets
|
||||
.disabled_scopes_by_bracket_ix
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(ix, disabled_scope_names)| {
|
||||
if disabled_scope_names.contains(name) {
|
||||
Some(ix as u16)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
self.config.brackets.disabled_scopes_by_bracket_ix.clear();
|
||||
self.grammar_mut().override_config = Some(OverrideConfig {
|
||||
query,
|
||||
values: override_configs_by_id,
|
||||
});
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
@@ -1132,12 +1200,26 @@ impl LanguageScope {
|
||||
.map(|e| (&e.0, &e.1))
|
||||
}
|
||||
|
||||
pub fn brackets(&self) -> &[BracketPair] {
|
||||
Override::as_option(
|
||||
self.config_override().map(|o| &o.brackets),
|
||||
Some(&self.language.config.brackets),
|
||||
)
|
||||
.map_or(&[], Vec::as_slice)
|
||||
pub fn brackets(&self) -> impl Iterator<Item = (&BracketPair, bool)> {
|
||||
let mut disabled_ids = self
|
||||
.config_override()
|
||||
.map_or(&[] as _, |o| o.disabled_bracket_ixs.as_slice());
|
||||
self.language
|
||||
.config
|
||||
.brackets
|
||||
.pairs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(move |(ix, bracket)| {
|
||||
let mut is_enabled = true;
|
||||
if let Some(next_disabled_ix) = disabled_ids.first() {
|
||||
if ix == *next_disabled_ix as usize {
|
||||
disabled_ids = &disabled_ids[1..];
|
||||
is_enabled = false;
|
||||
}
|
||||
}
|
||||
(bracket, is_enabled)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn should_autoclose_before(&self, c: char) -> bool {
|
||||
@@ -1148,7 +1230,7 @@ impl LanguageScope {
|
||||
let id = self.override_id?;
|
||||
let grammar = self.language.grammar.as_ref()?;
|
||||
let override_config = grammar.override_config.as_ref()?;
|
||||
override_config.values.get(&id)
|
||||
override_config.values.get(&id).map(|e| &e.1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +165,7 @@ struct ParseStep {
|
||||
mode: ParseMode,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ParseStepLanguage {
|
||||
Loaded { language: Arc<Language> },
|
||||
Pending { name: Arc<str> },
|
||||
@@ -514,15 +515,32 @@ impl SyntaxSnapshot {
|
||||
let Some(grammar) = language.grammar() else { continue };
|
||||
let tree;
|
||||
let changed_ranges;
|
||||
|
||||
let mut included_ranges = step.included_ranges;
|
||||
for range in &mut included_ranges {
|
||||
range.start_byte -= step_start_byte;
|
||||
range.end_byte -= step_start_byte;
|
||||
range.start_point = (Point::from_ts_point(range.start_point)
|
||||
- step_start_point)
|
||||
.to_ts_point();
|
||||
range.end_point = (Point::from_ts_point(range.end_point)
|
||||
- step_start_point)
|
||||
.to_ts_point();
|
||||
}
|
||||
|
||||
if let Some(SyntaxLayerContent::Parsed { tree: old_tree, .. }) =
|
||||
old_layer.map(|layer| &layer.content)
|
||||
{
|
||||
if let ParseMode::Combined {
|
||||
parent_layer_changed_ranges,
|
||||
mut parent_layer_changed_ranges,
|
||||
..
|
||||
} = step.mode
|
||||
{
|
||||
for range in &mut parent_layer_changed_ranges {
|
||||
range.start -= step_start_byte;
|
||||
range.end -= step_start_byte;
|
||||
}
|
||||
|
||||
included_ranges = splice_included_ranges(
|
||||
old_tree.included_ranges(),
|
||||
&parent_layer_changed_ranges,
|
||||
@@ -534,7 +552,6 @@ impl SyntaxSnapshot {
|
||||
grammar,
|
||||
text.as_rope(),
|
||||
step_start_byte,
|
||||
step_start_point,
|
||||
included_ranges,
|
||||
Some(old_tree.clone()),
|
||||
);
|
||||
@@ -551,7 +568,6 @@ impl SyntaxSnapshot {
|
||||
grammar,
|
||||
text.as_rope(),
|
||||
step_start_byte,
|
||||
step_start_point,
|
||||
included_ranges,
|
||||
None,
|
||||
);
|
||||
@@ -608,6 +624,31 @@ impl SyntaxSnapshot {
|
||||
self.layers = layers;
|
||||
self.interpolated_version = text.version.clone();
|
||||
self.parsed_version = text.version.clone();
|
||||
#[cfg(debug_assertions)]
|
||||
self.check_invariants(text);
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn check_invariants(&self, text: &BufferSnapshot) {
|
||||
let mut max_depth = 0;
|
||||
let mut prev_range: Option<Range<Anchor>> = None;
|
||||
for layer in self.layers.iter() {
|
||||
if layer.depth == max_depth {
|
||||
if let Some(prev_range) = prev_range {
|
||||
match layer.range.start.cmp(&prev_range.start, text) {
|
||||
Ordering::Less => panic!("layers out of order"),
|
||||
Ordering::Equal => {
|
||||
assert!(layer.range.end.cmp(&prev_range.end, text).is_ge())
|
||||
}
|
||||
Ordering::Greater => {}
|
||||
}
|
||||
}
|
||||
} else if layer.depth < max_depth {
|
||||
panic!("layers out of order")
|
||||
}
|
||||
max_depth = layer.depth;
|
||||
prev_range = Some(layer.range.clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn single_tree_captures<'a>(
|
||||
@@ -1035,17 +1076,9 @@ fn parse_text(
|
||||
grammar: &Grammar,
|
||||
text: &Rope,
|
||||
start_byte: usize,
|
||||
start_point: Point,
|
||||
mut ranges: Vec<tree_sitter::Range>,
|
||||
ranges: Vec<tree_sitter::Range>,
|
||||
old_tree: Option<Tree>,
|
||||
) -> Tree {
|
||||
for range in &mut ranges {
|
||||
range.start_byte -= start_byte;
|
||||
range.end_byte -= start_byte;
|
||||
range.start_point = (Point::from_ts_point(range.start_point) - start_point).to_ts_point();
|
||||
range.end_point = (Point::from_ts_point(range.end_point) - start_point).to_ts_point();
|
||||
}
|
||||
|
||||
PARSER.with(|parser| {
|
||||
let mut parser = parser.borrow_mut();
|
||||
let mut chunks = text.chunks_in_range(start_byte..text.len());
|
||||
@@ -1419,7 +1452,7 @@ impl sum_tree::Summary for SyntaxLayerSummary {
|
||||
self.max_depth = other.max_depth;
|
||||
self.range = other.range.clone();
|
||||
} else {
|
||||
if other.range.start.cmp(&self.range.start, buffer).is_lt() {
|
||||
if self.range == (Anchor::MAX..Anchor::MAX) {
|
||||
self.range.start = other.range.start;
|
||||
}
|
||||
if other.range.end.cmp(&self.range.end, buffer).is_gt() {
|
||||
@@ -2183,6 +2216,37 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_combined_injections_inside_injections() {
|
||||
let (_buffer, _syntax_map) = test_edit_sequence(
|
||||
"Markdown",
|
||||
&[
|
||||
r#"
|
||||
here is some ERB code:
|
||||
|
||||
```erb
|
||||
<ul>
|
||||
<% people.each do |person| %>
|
||||
<li><%= person.name %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
```
|
||||
"#,
|
||||
r#"
|
||||
here is some ERB code:
|
||||
|
||||
```erb
|
||||
<ul>
|
||||
<% people«2».each do |person| %>
|
||||
<li><%= person.name %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
```
|
||||
"#,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 50)]
|
||||
fn test_random_syntax_map_edits(mut rng: StdRng) {
|
||||
let operations = env::var("OPERATIONS")
|
||||
|
||||
@@ -20,6 +20,7 @@ fn main() {
|
||||
items: vec![MenuItem::Action {
|
||||
name: "Quit",
|
||||
action: Box::new(Quit),
|
||||
os_action: None,
|
||||
}],
|
||||
}]);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use log::warn;
|
||||
pub use lsp_types::request::*;
|
||||
pub use lsp_types::*;
|
||||
|
||||
@@ -64,6 +65,7 @@ struct Request<'a, T> {
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct AnyResponse<'a> {
|
||||
jsonrpc: &'a str,
|
||||
id: usize,
|
||||
#[serde(default)]
|
||||
error: Option<Error>,
|
||||
@@ -203,8 +205,9 @@ impl LanguageServer {
|
||||
} else {
|
||||
on_unhandled_notification(msg);
|
||||
}
|
||||
} else if let Ok(AnyResponse { id, error, result }) =
|
||||
serde_json::from_slice(&buffer)
|
||||
} else if let Ok(AnyResponse {
|
||||
id, error, result, ..
|
||||
}) = serde_json::from_slice(&buffer)
|
||||
{
|
||||
if let Some(handler) = response_handlers
|
||||
.lock()
|
||||
@@ -220,10 +223,10 @@ impl LanguageServer {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow!(
|
||||
"failed to deserialize message:\n{}",
|
||||
warn!(
|
||||
"Failed to deserialize message:\n{}",
|
||||
std::str::from_utf8(&buffer)?
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
// Don't starve the main thread when receiving lots of messages at once.
|
||||
@@ -419,6 +422,10 @@ impl LanguageServer {
|
||||
self.notification_handlers.lock().remove(T::METHOD);
|
||||
}
|
||||
|
||||
pub fn remove_notification_handler<T: notification::Notification>(&self) {
|
||||
self.notification_handlers.lock().remove(T::METHOD);
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn on_custom_notification<Params, F>(&self, method: &'static str, mut f: F) -> Subscription
|
||||
where
|
||||
@@ -460,35 +467,57 @@ impl LanguageServer {
|
||||
method,
|
||||
Box::new(move |id, params, cx| {
|
||||
if let Some(id) = id {
|
||||
if let Some(params) = serde_json::from_str(params).log_err() {
|
||||
let response = f(params, cx.clone());
|
||||
cx.foreground()
|
||||
.spawn({
|
||||
let outbound_tx = outbound_tx.clone();
|
||||
async move {
|
||||
let response = match response.await {
|
||||
Ok(result) => Response {
|
||||
jsonrpc: JSON_RPC_VERSION,
|
||||
id,
|
||||
result: Some(result),
|
||||
error: None,
|
||||
},
|
||||
Err(error) => Response {
|
||||
jsonrpc: JSON_RPC_VERSION,
|
||||
id,
|
||||
result: None,
|
||||
error: Some(Error {
|
||||
message: error.to_string(),
|
||||
}),
|
||||
},
|
||||
};
|
||||
if let Some(response) = serde_json::to_vec(&response).log_err()
|
||||
{
|
||||
outbound_tx.try_send(response).ok();
|
||||
match serde_json::from_str(params) {
|
||||
Ok(params) => {
|
||||
let response = f(params, cx.clone());
|
||||
cx.foreground()
|
||||
.spawn({
|
||||
let outbound_tx = outbound_tx.clone();
|
||||
async move {
|
||||
let response = match response.await {
|
||||
Ok(result) => Response {
|
||||
jsonrpc: JSON_RPC_VERSION,
|
||||
id,
|
||||
result: Some(result),
|
||||
error: None,
|
||||
},
|
||||
Err(error) => Response {
|
||||
jsonrpc: JSON_RPC_VERSION,
|
||||
id,
|
||||
result: None,
|
||||
error: Some(Error {
|
||||
message: error.to_string(),
|
||||
}),
|
||||
},
|
||||
};
|
||||
if let Some(response) =
|
||||
serde_json::to_vec(&response).log_err()
|
||||
{
|
||||
outbound_tx.try_send(response).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!(
|
||||
"error deserializing {} request: {:?}, message: {:?}",
|
||||
method,
|
||||
error,
|
||||
params
|
||||
);
|
||||
let response = AnyResponse {
|
||||
jsonrpc: JSON_RPC_VERSION,
|
||||
id,
|
||||
result: None,
|
||||
error: Some(Error {
|
||||
message: error.to_string(),
|
||||
}),
|
||||
};
|
||||
if let Some(response) = serde_json::to_vec(&response).log_err() {
|
||||
outbound_tx.try_send(response).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -755,6 +784,26 @@ impl FakeLanguageServer {
|
||||
responded_rx
|
||||
}
|
||||
|
||||
pub fn handle_notification<T, F>(
|
||||
&self,
|
||||
mut handler: F,
|
||||
) -> futures::channel::mpsc::UnboundedReceiver<()>
|
||||
where
|
||||
T: 'static + notification::Notification,
|
||||
T::Params: 'static + Send,
|
||||
F: 'static + Send + FnMut(T::Params, gpui::AsyncAppContext),
|
||||
{
|
||||
let (handled_tx, handled_rx) = futures::channel::mpsc::unbounded();
|
||||
self.server.remove_notification_handler::<T>();
|
||||
self.server
|
||||
.on_notification::<T, _>(move |params, cx| {
|
||||
handler(params, cx.clone());
|
||||
handled_tx.unbounded_send(()).ok();
|
||||
})
|
||||
.detach();
|
||||
handled_rx
|
||||
}
|
||||
|
||||
pub fn remove_request_handler<T>(&mut self)
|
||||
where
|
||||
T: 'static + request::Request,
|
||||
|
||||
21
crates/pando/Cargo.toml
Normal file
21
crates/pando/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "pando"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/pando.rs"
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.38"
|
||||
client = { path = "../client" }
|
||||
gpui = { path = "../gpui" }
|
||||
settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
workspace = { path = "../workspace" }
|
||||
sqlez = { path = "../sqlez" }
|
||||
sqlez_macros = { path = "../sqlez_macros" }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user