Compare commits
381 Commits
v0.75.2-pr
...
v0.78.1-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3b714a4ab | ||
|
|
03d5eebf08 | ||
|
|
8ea16a11b0 | ||
|
|
2042188f5a | ||
|
|
0bbb4b22c6 | ||
|
|
75901f1c33 | ||
|
|
a6ebc9bd26 | ||
|
|
9e3085b0c4 | ||
|
|
7af9dda869 | ||
|
|
2a5ac4f203 | ||
|
|
d8a3f16891 | ||
|
|
99257a8213 | ||
|
|
0f429243d7 | ||
|
|
cba41ef7c5 | ||
|
|
2ba38b2fca | ||
|
|
e7f78c4f74 | ||
|
|
8980df1f5d | ||
|
|
2db8ac4a6f | ||
|
|
818a514110 | ||
|
|
1b4f783b97 | ||
|
|
88599add56 | ||
|
|
05f6747132 | ||
|
|
1096720b41 | ||
|
|
5c7c4dd4dd | ||
|
|
da35202bbf | ||
|
|
f5c4a2a0dd | ||
|
|
77a63c6598 | ||
|
|
edd925f77b | ||
|
|
6d0f8290a4 | ||
|
|
6497ca8ccb | ||
|
|
e60dea7049 | ||
|
|
e64fe6d660 | ||
|
|
f6f09e8661 | ||
|
|
ef7d8f46df | ||
|
|
7df2440757 | ||
|
|
6fd4e28813 | ||
|
|
bca1acf6d3 | ||
|
|
097a768725 | ||
|
|
404dd43c30 | ||
|
|
c6f27903cc | ||
|
|
f6b0c56a47 | ||
|
|
e45d680126 | ||
|
|
e993d32900 | ||
|
|
09911d43bc | ||
|
|
c8696149b8 | ||
|
|
db73d831c7 | ||
|
|
d2411a6c86 | ||
|
|
726c8eb43f | ||
|
|
c59dafab7e | ||
|
|
e272a1a18f | ||
|
|
90bca1b94a | ||
|
|
06ad3a7f7b | ||
|
|
c18f1b6246 | ||
|
|
46efb844af | ||
|
|
29f0078084 | ||
|
|
432aeeac56 | ||
|
|
8440a98850 | ||
|
|
bccc34c61a | ||
|
|
e8b3d4e0fa | ||
|
|
ff1c7db38f | ||
|
|
30a08467b0 | ||
|
|
c8de738972 | ||
|
|
87ac409e51 | ||
|
|
badfe70a93 | ||
|
|
11d8394af2 | ||
|
|
c24194156e | ||
|
|
ece2af1e22 | ||
|
|
adf94a1681 | ||
|
|
09d306df85 | ||
|
|
0a5cf4b831 | ||
|
|
9398de6a57 | ||
|
|
e45104a1c0 | ||
|
|
74b10e4ba5 | ||
|
|
ddbffd2c41 | ||
|
|
00a38e4c3b | ||
|
|
37d01c7fb3 | ||
|
|
281ff92236 | ||
|
|
bb721a08f5 | ||
|
|
f50b51bdad | ||
|
|
693172854c | ||
|
|
b3c7526fb5 | ||
|
|
6e37ff880f | ||
|
|
f08685f65f | ||
|
|
ce828d55d5 | ||
|
|
f28806d09b | ||
|
|
686f5439ad | ||
|
|
b402f27d50 | ||
|
|
d39c761de5 | ||
|
|
7a600e7a65 | ||
|
|
221bb54e48 | ||
|
|
431e11a033 | ||
|
|
8b7273e46e | ||
|
|
9dc608dc4b | ||
|
|
648f0e5b7b | ||
|
|
b40ea4df14 | ||
|
|
01e3173ed0 | ||
|
|
8ee25be7b9 | ||
|
|
0384456e7d | ||
|
|
20064b5629 | ||
|
|
daed75096e | ||
|
|
718052bb72 | ||
|
|
4eb75f058f | ||
|
|
6c68a3e709 | ||
|
|
e7af3f223a | ||
|
|
baff428de5 | ||
|
|
3952e98320 | ||
|
|
51be0efa1f | ||
|
|
5bfd5e35b3 | ||
|
|
d53c18cc57 | ||
|
|
5b7d0ee6fe | ||
|
|
e2bdd261a1 | ||
|
|
9187863d0e | ||
|
|
3daeabc1d6 | ||
|
|
bebfe53e89 | ||
|
|
9328bb0153 | ||
|
|
89c283ecf0 | ||
|
|
a00ce3f286 | ||
|
|
4ce51c8138 | ||
|
|
f626920af1 | ||
|
|
325827699e | ||
|
|
a3b1980a5e | ||
|
|
709c101834 | ||
|
|
943ea61452 | ||
|
|
981b3a459f | ||
|
|
a65dd0fd98 | ||
|
|
cf6ea6d698 | ||
|
|
152755b043 | ||
|
|
dad66eb3fb | ||
|
|
f62e0b502a | ||
|
|
344f59adf7 | ||
|
|
cc33f83e4e | ||
|
|
9842b7ad1a | ||
|
|
14497027d4 | ||
|
|
ae510c80db | ||
|
|
ad7e49ed06 | ||
|
|
b687aec9d9 | ||
|
|
a435dc1339 | ||
|
|
b4561b848d | ||
|
|
baa9e271d5 | ||
|
|
350ddf2025 | ||
|
|
3594243644 | ||
|
|
904993dfc9 | ||
|
|
4179ed66a6 | ||
|
|
d173b1d412 | ||
|
|
ab4b3293d1 | ||
|
|
5892f16602 | ||
|
|
84aefb9dcb | ||
|
|
4e81513af1 | ||
|
|
90296667b0 | ||
|
|
e0f9b2b40f | ||
|
|
477453c396 | ||
|
|
19fc143209 | ||
|
|
ca03d871a6 | ||
|
|
0a3f0c5252 | ||
|
|
c80942ea00 | ||
|
|
caa6a75238 | ||
|
|
4f4af55329 | ||
|
|
1e5aff9e51 | ||
|
|
ee154feda4 | ||
|
|
3b31f10c6f | ||
|
|
8db7e17ac5 | ||
|
|
1f6bd0ea77 | ||
|
|
ba652fc033 | ||
|
|
7163ba429b | ||
|
|
c832e4406e | ||
|
|
515724821e | ||
|
|
0867162c87 | ||
|
|
aba2914a31 | ||
|
|
246a6adab7 | ||
|
|
020a0965b0 | ||
|
|
b74553455f | ||
|
|
4a8527478d | ||
|
|
4c179875ab | ||
|
|
f89f33347d | ||
|
|
9dee2ca2be | ||
|
|
62aeb6b8b3 | ||
|
|
5210be95fe | ||
|
|
7d7053b990 | ||
|
|
118435a348 | ||
|
|
86e2101592 | ||
|
|
50586812ec | ||
|
|
416c793076 | ||
|
|
a0637a769c | ||
|
|
9401ef223d | ||
|
|
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 | ||
|
|
3d6c81584f | ||
|
|
81ece4fd44 | ||
|
|
2ec5c88f98 | ||
|
|
8dd249a7cd | ||
|
|
5ce147a2ad | ||
|
|
a32c0d1c9b | ||
|
|
e65c0810ba | ||
|
|
1fcfa5d272 | ||
|
|
addfcdc1f4 | ||
|
|
4501a5a7ee | ||
|
|
a120996f0d | ||
|
|
187fac1579 | ||
|
|
0acb820f04 | ||
|
|
dda0febf39 | ||
|
|
0e238210bb | ||
|
|
76685406ed | ||
|
|
70eedbb48e | ||
|
|
42b5fa1fa3 | ||
|
|
f787f6054b | ||
|
|
6f342bb2c6 | ||
|
|
0ba44c6dc4 | ||
|
|
2ff82732b9 | ||
|
|
cbfdfa8124 |
4
.github/workflows/ci.yml
vendored
@@ -19,7 +19,9 @@ env:
|
||||
jobs:
|
||||
rustfmt:
|
||||
name: Check formatting
|
||||
runs-on: self-hosted
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
|
||||
63
Cargo.lock
generated
@@ -794,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",
|
||||
@@ -1188,7 +1188,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.5.4"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-tungstenite",
|
||||
@@ -1257,7 +1257,9 @@ dependencies = [
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"context_menu",
|
||||
"editor",
|
||||
"feedback",
|
||||
"futures 0.3.25",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
@@ -3017,6 +3019,17 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3"
|
||||
|
||||
[[package]]
|
||||
name = "install_cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gpui",
|
||||
"log",
|
||||
"smol",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
@@ -3156,6 +3169,7 @@ dependencies = [
|
||||
name = "journal"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"dirs 4.0.0",
|
||||
"editor",
|
||||
@@ -3284,6 +3298,22 @@ dependencies = [
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "language_selector"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"editor",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
"picker",
|
||||
"project",
|
||||
"settings",
|
||||
"theme",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
@@ -4591,6 +4621,7 @@ dependencies = [
|
||||
"lsp",
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"pulldown-cmark",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
@@ -8011,6 +8042,26 @@ version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
|
||||
|
||||
[[package]]
|
||||
name = "welcome"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"db",
|
||||
"editor",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"install_cli",
|
||||
"log",
|
||||
"picker",
|
||||
"project",
|
||||
"settings",
|
||||
"theme",
|
||||
"theme_selector",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wepoll-ffi"
|
||||
version = "0.1.2"
|
||||
@@ -8286,6 +8337,7 @@ dependencies = [
|
||||
"futures 0.3.25",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"install_cli",
|
||||
"language",
|
||||
"lazy_static",
|
||||
"log",
|
||||
@@ -8297,6 +8349,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"terminal",
|
||||
"theme",
|
||||
"util",
|
||||
"uuid 1.2.2",
|
||||
@@ -8356,7 +8409,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.75.0"
|
||||
version = "0.78.1"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -8378,6 +8431,7 @@ dependencies = [
|
||||
"command_palette",
|
||||
"context_menu",
|
||||
"ctor",
|
||||
"db",
|
||||
"diagnostics",
|
||||
"easy-parallel",
|
||||
"editor",
|
||||
@@ -8393,9 +8447,11 @@ dependencies = [
|
||||
"ignore",
|
||||
"image",
|
||||
"indexmap",
|
||||
"install_cli",
|
||||
"isahc",
|
||||
"journal",
|
||||
"language",
|
||||
"language_selector",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -8457,6 +8513,7 @@ dependencies = [
|
||||
"util",
|
||||
"uuid 1.2.2",
|
||||
"vim",
|
||||
"welcome",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
|
||||
@@ -26,8 +26,10 @@ members = [
|
||||
"crates/go_to_line",
|
||||
"crates/gpui",
|
||||
"crates/gpui_macros",
|
||||
"crates/install_cli",
|
||||
"crates/journal",
|
||||
"crates/language",
|
||||
"crates/language_selector",
|
||||
"crates/live_kit_client",
|
||||
"crates/live_kit_server",
|
||||
"crates/lsp",
|
||||
@@ -58,6 +60,7 @@ members = [
|
||||
"crates/util",
|
||||
"crates/vim",
|
||||
"crates/workspace",
|
||||
"crates/welcome",
|
||||
"crates/zed",
|
||||
]
|
||||
default-members = ["crates/zed"]
|
||||
|
||||
12
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
|
||||
|
||||
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/logo_96.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 6C7.34315 6 6 7.34315 6 9V75H0V9C0 4.02944 4.02944 0 9 0H89.3787C93.3878 0 95.3955 4.84715 92.5607 7.68198L43.0551 57.1875H57V51H63V58.6875C63 61.1728 60.9853 63.1875 58.5 63.1875H37.0551L26.7426 73.5H73.5V36H79.5V73.5C79.5 76.8137 76.8137 79.5 73.5 79.5H20.7426L10.2426 90H87C88.6569 90 90 88.6569 90 87V21H96V87C96 91.9706 91.9706 96 87 96H6.62132C2.61224 96 0.604504 91.1529 3.43934 88.318L52.7574 39H39V45H33V37.5C33 35.0147 35.0147 33 37.5 33H58.7574L69.2574 22.5H22.5V60H16.5V22.5C16.5 19.1863 19.1863 16.5 22.5 16.5H75.2574L85.7574 6H9Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 715 B |
3
assets/icons/speech_bubble_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 d="M10.6667 0.400196H1.33346C0.819658 0.400196 0.399658 0.820196 0.399658 1.3326V10.6658C0.399658 11.181 0.816998 11.5982 1.33206 11.5982C1.58966 11.5982 1.82206 11.4932 1.99146 11.3238L4.51706 8.79684H10.6639C11.1763 8.79684 11.5963 8.37544 11.5963 7.86304V1.3298C11.5963 0.815996 11.1749 0.395996 10.6625 0.395996L10.6667 0.400196ZM2.2667 2.2664H6.00008V3.1988H2.26628V2.265L2.2667 2.2664ZM7.8667 6.93316H2.2667V5.99936H7.8667V6.93176V6.93316ZM9.7329 5.06556H2.26488V4.13176H9.73164V5.06416L9.7329 5.06556Z" fill="#282C34"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 636 B |
@@ -1,3 +1,5 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.2 6.00001C5.52563 6.00001 6.6 4.92545 6.6 3.60001C6.6 2.27457 5.52563 1.20001 4.2 1.20001C2.87438 1.20001 1.8 2.27457 1.8 3.60001C1.8 4.92545 2.87438 6.00001 4.2 6.00001ZM5.15063 6.90001H3.24938C1.45519 6.90001 0 8.35501 0 10.1494C0 10.5094 0.291 10.8 0.649875 10.8H7.7505C8.10938 10.8 8.4 10.5094 8.4 10.1494C8.4 8.35501 6.945 6.90001 5.15063 6.90001ZM11.55 4.95001H10.65V4.05001C10.65 3.80251 10.4494 3.60001 10.2 3.60001C9.95063 3.60001 9.75 3.80157 9.75 4.05001V4.95001H8.85C8.6025 4.95001 8.4 5.15251 8.4 5.40001C8.4 5.64751 8.60156 5.85001 8.85 5.85001H9.75V6.75001C9.75 6.99939 9.9525 7.20001 10.2 7.20001C10.4475 7.20001 10.65 6.99845 10.65 6.75001V5.85001H11.55C11.7994 5.85001 12 5.64939 12 5.40001C12 5.15064 11.7994 4.95001 11.55 4.95001Z" fill="white"/>
|
||||
<path d="M5.75062 7.09998H3.24938C1.45519 7.09998 0 8.55498 0 10.3493C0 10.7093 0.291 11 0.649875 11H8.3505C8.70938 11 9 10.7093 9 10.3493C9 8.55498 7.545 7.09998 5.75062 7.09998Z" fill="white"/>
|
||||
<path d="M7 3.5C7 4.82544 5.82562 6 4.5 6C3.17438 6 2 4.82544 2 3.5C2 2.17456 3.17438 1 4.5 1C5.82562 1 7 2.17456 7 3.5Z" fill="white"/>
|
||||
<path d="M9.5 3.75V5.5M9.5 7.25V5.5M9.5 5.5H11.25M9.5 5.5H7.75" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 882 B After Width: | Height: | Size: 564 B |
@@ -1,3 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.9 8.00002C7.44656 8.00002 8.7 6.74637 8.7 5.20002C8.7 3.65368 7.44656 2.40002 5.9 2.40002C4.35344 2.40002 3.1 3.65368 3.1 5.20002C3.1 6.74637 4.35344 8.00002 5.9 8.00002ZM7.00906 9.05002H4.79094C2.69772 9.05002 1 10.7475 1 12.841C1 13.261 1.3395 13.6 1.75819 13.6H10.0422C10.4609 13.6 10.8 13.261 10.8 12.841C10.8 10.7475 9.1025 9.05002 7.00906 9.05002ZM14.475 6.77502H13.425V5.72502C13.425 5.43627 13.1909 5.20002 12.9 5.20002C12.6091 5.20002 12.375 5.43518 12.375 5.72502V6.77502H11.325C11.0363 6.77502 10.8 7.01127 10.8 7.30002C10.8 7.58877 11.0352 7.82502 11.325 7.82502H12.375V8.87502C12.375 9.16596 12.6112 9.40002 12.9 9.40002C13.1887 9.40002 13.425 9.16487 13.425 8.87502V7.82502H14.475C14.7659 7.82502 15 7.59096 15 7.30002C15 7.00909 14.7659 6.77502 14.475 6.77502Z" fill="white"/>
|
||||
<path d="M7.00906 8.99999H4.79094C2.69772 8.99999 1 11.1475 1 13.2409C1 13.6609 1.3395 14 1.75819 14H10.0422C10.4609 14 10.8 13.6609 10.8 13.2409C10.8 11.1475 9.1025 8.99999 7.00906 8.99999Z" fill="white"/>
|
||||
<path d="M9 5C9 6.54634 7.44657 7.99998 5.90001 7.99998C4.35344 7.99998 3 6.54634 3 5C3 3.45366 4.45344 2 6 2C7.54656 2 9 3.45366 9 5Z" fill="white"/>
|
||||
<path d="M13.025 6H14.475C14.7659 6 15 6.20906 15 6.5C15 6.79094 14.7659 7 14.475 7H13V8.49995C13 8.7898 12.7638 9.02495 12.475 9.02495C12.1863 9.02495 11.95 8.79089 11.95 8.49995V7H10.525C10.2352 7 10 6.78875 10 6.5C10 6.21125 10.2362 6 10.525 6H11.975V4.525C11.975 4.23516 12.2091 4 12.5 4C12.7909 4 13.025 4.23625 13.025 4.525V6Z" fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 907 B After Width: | Height: | Size: 810 B |
68
assets/keymaps/atom.json
Normal file
@@ -0,0 +1,68 @@
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"cmd-k cmd-p": "workspace::ActivatePreviousPane",
|
||||
"cmd-k cmd-n": "workspace::ActivateNextPane"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"cmd-b": "editor::GoToDefinition",
|
||||
"cmd-<": "editor::ScrollCursorCenter",
|
||||
"cmd-g": [
|
||||
"editor::SelectNext",
|
||||
{
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"cmd-r": "outline::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
"cmd-f3": "search::SelectNextMatch",
|
||||
"cmd-shift-f3": "search::SelectPrevMatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"cmd-\\": "workspace::ToggleLeftSidebar",
|
||||
"cmd-k cmd-b": "workspace::ToggleLeftSidebar",
|
||||
"cmd-t": "file_finder::Toggle",
|
||||
"cmd-shift-r": "project_symbols::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"alt-cmd-/": "search::ToggleRegex",
|
||||
"ctrl-0": "project_panel::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
"ctrl-[": "project_panel::CollapseSelectedEntry",
|
||||
"ctrl-b": "project_panel::CollapseSelectedEntry",
|
||||
"h": "project_panel::CollapseSelectedEntry",
|
||||
"ctrl-]": "project_panel::ExpandSelectedEntry",
|
||||
"ctrl-f": "project_panel::ExpandSelectedEntry",
|
||||
"ctrl-shift-c": "project_panel::CopyPath"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"bindings": {}
|
||||
}
|
||||
]
|
||||
@@ -228,6 +228,7 @@
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"cmd-k cmd-i": "editor::Hover",
|
||||
"cmd-/": [
|
||||
"editor::ToggleComments",
|
||||
{
|
||||
@@ -248,7 +249,8 @@
|
||||
"alt-cmd-[": "editor::Fold",
|
||||
"alt-cmd-]": "editor::UnfoldLines",
|
||||
"ctrl-space": "editor::ShowCompletions",
|
||||
"cmd-.": "editor::ToggleCodeActions"
|
||||
"cmd-.": "editor::ToggleCodeActions",
|
||||
"alt-cmd-r": "editor::RevealInFinder"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -352,7 +354,8 @@
|
||||
"cmd-shift-p": "command_palette::Toggle",
|
||||
"cmd-shift-m": "diagnostics::Deploy",
|
||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||
"cmd-alt-s": "workspace::SaveAll"
|
||||
"cmd-alt-s": "workspace::SaveAll",
|
||||
"cmd-k m": "language_selector::Toggle"
|
||||
}
|
||||
},
|
||||
// Bindings from Sublime Text
|
||||
@@ -418,7 +421,7 @@
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
|
||||
"cmd-shift-c": "collab::ToggleCollaborationMenu",
|
||||
"cmd-shift-c": "collab::ToggleContactsMenu",
|
||||
"cmd-alt-i": "zed::DebugElements"
|
||||
}
|
||||
},
|
||||
@@ -456,7 +459,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"context": "Pane && docked",
|
||||
"bindings": {
|
||||
"shift-escape": "dock::HideDock",
|
||||
"cmd-escape": "dock::RemoveTabFromDock"
|
||||
@@ -472,7 +475,8 @@
|
||||
"cmd-v": "project_panel::Paste",
|
||||
"cmd-alt-c": "project_panel::CopyPath",
|
||||
"f2": "project_panel::Rename",
|
||||
"backspace": "project_panel::Delete"
|
||||
"backspace": "project_panel::Delete",
|
||||
"alt-cmd-r": "project_panel::RevealInFinder"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -536,4 +540,4 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
78
assets/keymaps/jetbrains.json
Normal file
@@ -0,0 +1,78 @@
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"cmd-shift-[": "pane::ActivatePrevItem",
|
||||
"cmd-shift-]": "pane::ActivateNextItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl->": "zed::IncreaseBufferFontSize",
|
||||
"ctrl-<": "zed::DecreaseBufferFontSize",
|
||||
"cmd-d": "editor::DuplicateLine",
|
||||
"cmd-pagedown": "editor::MovePageDown",
|
||||
"cmd-pageup": "editor::MovePageUp",
|
||||
"ctrl-alt-shift-b": "editor::SelectToPreviousWordStart",
|
||||
"shift-enter": "editor::NewlineBelow",
|
||||
"cmd--": "editor::Fold",
|
||||
"cmd-=": "editor::UnfoldLines",
|
||||
"alt-shift-g": "editor::SplitSelectionIntoLines",
|
||||
"ctrl-g": [
|
||||
"editor::SelectNext",
|
||||
{
|
||||
"replace_newest": false
|
||||
}
|
||||
],
|
||||
"cmd-/": [
|
||||
"editor::ToggleComments",
|
||||
{
|
||||
"advance_downwards": true
|
||||
}
|
||||
],
|
||||
"shift-alt-up": "editor::MoveLineUp",
|
||||
"shift-alt-down": "editor::MoveLineDown",
|
||||
"cmd-[": "pane::GoBack",
|
||||
"cmd-]": "pane::GoForward",
|
||||
"alt-f7": "editor::FindAllReferences",
|
||||
"cmd-alt-f7": "editor::FindAllReferences",
|
||||
"cmd-b": "editor::GoToDefinition",
|
||||
"cmd-alt-b": "editor::GoToDefinition",
|
||||
"cmd-shift-b": "editor::GoToTypeDefinition",
|
||||
"alt-enter": "editor::ToggleCodeActions",
|
||||
"f2": "editor::GoToDiagnostic",
|
||||
"cmd-f2": "editor::GoToPrevDiagnostic",
|
||||
"ctrl-alt-shift-down": "editor::GoToHunk",
|
||||
"ctrl-alt-shift-up": "editor::GoToPrevHunk",
|
||||
"cmd-home": "editor::MoveToBeginning",
|
||||
"cmd-end": "editor::MoveToEnd",
|
||||
"cmd-shift-home": "editor::SelectToBeginning",
|
||||
"cmd-shift-end": "editor::SelectToEnd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"cmd-f12": "outline::Toggle",
|
||||
"cmd-7": "outline::Toggle",
|
||||
"cmd-shift-o": "file_finder::Toggle",
|
||||
"cmd-l": "go_to_line::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"cmd-shift-a": "command_palette::Toggle",
|
||||
"cmd-alt-o": "project_symbols::Toggle",
|
||||
"cmd-1": "workspace::ToggleLeftSidebar",
|
||||
"cmd-6": "diagnostics::Deploy",
|
||||
"alt-f12": "dock::FocusDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"bindings": {
|
||||
"alt-f12": "dock::HideDock"
|
||||
}
|
||||
}
|
||||
]
|
||||
60
assets/keymaps/sublime_text.json
Normal file
@@ -0,0 +1,60 @@
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"cmd-shift-[": "pane::ActivatePrevItem",
|
||||
"cmd-shift-]": "pane::ActivateNextItem",
|
||||
"ctrl-pagedown": "pane::ActivatePrevItem",
|
||||
"ctrl-pageup": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivateNextItem",
|
||||
"ctrl-tab": "pane::ActivatePrevItem",
|
||||
"cmd-+": "zed::IncreaseBufferFontSize"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
"cmd-shift-space": "editor::SelectAll",
|
||||
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
|
||||
"cmd-shift-a": "editor::SelectLargerSyntaxNode",
|
||||
"shift-f12": "editor::FindAllReferences",
|
||||
"alt-cmd-down": "editor::GoToDefinition",
|
||||
"alt-shift-cmd-down": "editor::FindAllReferences",
|
||||
"ctrl-.": "editor::GoToHunk",
|
||||
"ctrl-,": "editor::GoToPrevHunk",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"cmd-r": "outline::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"f4": "search::SelectNextMatch",
|
||||
"shift-f4": "search::SelectPrevMatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"ctrl-`": "dock::FocusDock",
|
||||
"cmd-k cmd-b": "workspace::ToggleLeftSidebar",
|
||||
"cmd-t": "file_finder::Toggle",
|
||||
"shift-cmd-r": "project_symbols::Toggle",
|
||||
// Currently busted: https://github.com/zed-industries/feedback/issues/898
|
||||
"ctrl-0": "project_panel::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"bindings": {
|
||||
"ctrl-`": "dock::HideDock"
|
||||
}
|
||||
}
|
||||
]
|
||||
90
assets/keymaps/textmate.json
Normal file
@@ -0,0 +1,90 @@
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"cmd-shift-o": "projects::OpenRecent",
|
||||
"cmd-alt-tab": "project_panel::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"cmd-l": "go_to_line::Toggle",
|
||||
"ctrl-shift-d": "editor::DuplicateLine",
|
||||
"cmd-b": "editor::GoToDefinition",
|
||||
"cmd-j": "editor::ScrollCursorCenter",
|
||||
"cmd-enter": "editor::NewlineBelow",
|
||||
"cmd-shift-l": "editor::SelectLine",
|
||||
"cmd-shift-t": "outline::Toggle",
|
||||
"alt-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"alt-shift-backspace": "editor::DeleteToNextWordEnd",
|
||||
"alt-delete": "editor::DeleteToNextWordEnd",
|
||||
"alt-shift-delete": "editor::DeleteToNextWordEnd",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextSubwordEnd",
|
||||
"alt-left": [
|
||||
"editor::MoveToPreviousWordStart",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"alt-right": [
|
||||
"editor::MoveToNextWordEnd",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"ctrl-left": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-right": "editor::MoveToNextSubwordEnd",
|
||||
"cmd-shift-left": "editor::SelectToBeginningOfLine",
|
||||
"cmd-shift-right": "editor::SelectToEndOfLine",
|
||||
"alt-shift-left": [
|
||||
"editor::SelectToBeginningOfLine",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"alt-shift-right": [
|
||||
"editor::SelectToEndOfLine",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"ctrl-shift-left": "editor::SelectToPreviousSubwordStart",
|
||||
"ctrl-shift-right": "editor::SelectToNextSubwordEnd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
"ctrl-s": "search::SelectNextMatch",
|
||||
"ctrl-shift-s": "search::SelectPrevMatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"cmd-alt-ctrl-d": "workspace::ToggleLeftSidebar",
|
||||
"cmd-t": "file_finder::Toggle",
|
||||
"cmd-shift-t": "project_symbols::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"alt-cmd-r": "search::ToggleRegex",
|
||||
"ctrl-tab": "project_panel::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"bindings": {}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -22,11 +22,6 @@
|
||||
"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
|
||||
// setting, projects keep their last online status when you reopen them.
|
||||
"projects_online_by_default": true,
|
||||
// Whether to use language servers to provide code intelligence.
|
||||
"enable_language_server": true,
|
||||
// When to automatically save edited buffers. This setting can
|
||||
@@ -50,7 +45,13 @@
|
||||
// "default_dock_anchor": "right"
|
||||
// 3. Position the dock full screen over the entire workspace"
|
||||
// "default_dock_anchor": "expanded"
|
||||
"default_dock_anchor": "right",
|
||||
"default_dock_anchor": "bottom",
|
||||
// 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:
|
||||
@@ -83,7 +84,7 @@
|
||||
"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,
|
||||
@@ -196,12 +197,6 @@
|
||||
"Plain Text": {
|
||||
"soft_wrap": "preferred_line_length"
|
||||
},
|
||||
"C": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"C++": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Elixir": {
|
||||
"tab_size": 2
|
||||
},
|
||||
@@ -212,9 +207,6 @@
|
||||
"Markdown": {
|
||||
"soft_wrap": "preferred_line_length"
|
||||
},
|
||||
"Rust": {
|
||||
"tab_size": 4
|
||||
},
|
||||
"JavaScript": {
|
||||
"tab_size": 2
|
||||
},
|
||||
|
||||
@@ -264,12 +264,13 @@ impl ActiveCall {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
|
||||
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
cx.notify();
|
||||
if let Some((room, _)) = self.room.take() {
|
||||
room.update(cx, |room, cx| room.leave(cx))?;
|
||||
cx.notify();
|
||||
room.update(cx, |room, cx| room.leave(cx))
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn share_project(
|
||||
|
||||
@@ -17,10 +17,10 @@ use language::LanguageRegistry;
|
||||
use live_kit_client::{LocalTrackPublication, LocalVideoTrack, RemoteVideoTrackUpdate};
|
||||
use postage::stream::Stream;
|
||||
use project::Project;
|
||||
use std::{mem, sync::Arc, time::Duration};
|
||||
use std::{future::Future, mem, pin::Pin, 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,7 +55,7 @@ pub struct Room {
|
||||
leave_when_empty: bool,
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
follows_by_leader_id: HashMap<PeerId, Vec<PeerId>>,
|
||||
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<()>>>,
|
||||
@@ -64,10 +64,27 @@ pub struct Room {
|
||||
impl Entity for Room {
|
||||
type Event = Event;
|
||||
|
||||
fn release(&mut self, _: &mut MutableAppContext) {
|
||||
fn release(&mut self, cx: &mut MutableAppContext) {
|
||||
if self.status.is_online() {
|
||||
log::info!("room was released, sending leave message");
|
||||
let _ = self.client.send(proto::LeaveRoom {});
|
||||
self.leave_internal(cx).detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn app_will_quit(
|
||||
&mut self,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Option<Pin<Box<dyn Future<Output = ()>>>> {
|
||||
if self.status.is_online() {
|
||||
let leave = self.leave_internal(cx);
|
||||
Some(
|
||||
cx.background()
|
||||
.spawn(async move {
|
||||
leave.await.log_err();
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,7 +166,7 @@ impl Room {
|
||||
pending_room_update: None,
|
||||
client,
|
||||
user_store,
|
||||
follows_by_leader_id: Default::default(),
|
||||
follows_by_leader_id_project_id: Default::default(),
|
||||
maintain_connection: Some(maintain_connection),
|
||||
}
|
||||
}
|
||||
@@ -234,13 +251,17 @@ impl Room {
|
||||
&& self.pending_call_count == 0
|
||||
}
|
||||
|
||||
pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
|
||||
if self.status.is_offline() {
|
||||
return Err(anyhow!("room is offline"));
|
||||
}
|
||||
|
||||
pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
cx.notify();
|
||||
cx.emit(Event::Left);
|
||||
self.leave_internal(cx)
|
||||
}
|
||||
|
||||
fn leave_internal(&mut self, cx: &mut MutableAppContext) -> Task<Result<()>> {
|
||||
if self.status.is_offline() {
|
||||
return Task::ready(Err(anyhow!("room is offline")));
|
||||
}
|
||||
|
||||
log::info!("leaving room");
|
||||
|
||||
for project in self.shared_projects.drain() {
|
||||
@@ -266,8 +287,12 @@ impl Room {
|
||||
self.live_kit.take();
|
||||
self.pending_room_update.take();
|
||||
self.maintain_connection.take();
|
||||
self.client.send(proto::LeaveRoom {})?;
|
||||
Ok(())
|
||||
|
||||
let leave_room = self.client.request(proto::LeaveRoom {});
|
||||
cx.background().spawn(async move {
|
||||
leave_room.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
async fn maintain_connection(
|
||||
@@ -277,14 +302,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| {
|
||||
@@ -298,12 +321,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 };
|
||||
@@ -317,7 +335,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
|
||||
}
|
||||
@@ -339,18 +365,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<()>> {
|
||||
@@ -459,9 +487,9 @@ impl Room {
|
||||
self.participant_user_ids.contains(&user_id)
|
||||
}
|
||||
|
||||
pub fn followers_for(&self, leader_id: PeerId) -> &[PeerId] {
|
||||
self.follows_by_leader_id
|
||||
.get(&leader_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())
|
||||
}
|
||||
|
||||
@@ -598,7 +626,7 @@ impl Room {
|
||||
|
||||
if let Some(live_kit) = this.live_kit.as_ref() {
|
||||
let tracks =
|
||||
live_kit.room.remote_video_tracks(&peer_id.to_string());
|
||||
live_kit.room.remote_video_tracks(&user.id.to_string());
|
||||
for track in tracks {
|
||||
this.remote_video_track_updated(
|
||||
RemoteVideoTrackUpdate::Subscribed(track),
|
||||
@@ -631,8 +659,9 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
this.follows_by_leader_id.clear();
|
||||
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),
|
||||
|
||||
@@ -643,8 +672,8 @@ impl Room {
|
||||
};
|
||||
|
||||
let list = this
|
||||
.follows_by_leader_id
|
||||
.entry(leader)
|
||||
.follows_by_leader_id_project_id
|
||||
.entry((leader, project_id))
|
||||
.or_insert(Vec::new());
|
||||
if !list.contains(&follower) {
|
||||
list.push(follower);
|
||||
@@ -754,10 +783,10 @@ impl Room {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.pending_call_count -= 1;
|
||||
if this.should_leave() {
|
||||
this.leave(cx)?;
|
||||
this.leave(cx).detach_and_log_err(cx);
|
||||
}
|
||||
result
|
||||
})?;
|
||||
});
|
||||
result?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -66,12 +66,12 @@ 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, [SignIn, SignOut]);
|
||||
|
||||
pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
|
||||
cx.add_global_action({
|
||||
let client = client.clone();
|
||||
move |_: &Authenticate, cx| {
|
||||
move |_: &SignIn, cx| {
|
||||
let client = client.clone();
|
||||
cx.spawn(
|
||||
|cx| async move { client.authenticate_and_connect(true, &cx).log_err().await },
|
||||
@@ -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> {
|
||||
|
||||
@@ -224,7 +224,7 @@ impl Telemetry {
|
||||
.header("Content-Type", "application/json")
|
||||
.body(json_bytes.into())?;
|
||||
this.http_client.send(request).await?;
|
||||
Ok(())
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
)
|
||||
@@ -320,7 +320,7 @@ impl Telemetry {
|
||||
.header("Content-Type", "application/json")
|
||||
.body(json_bytes.into())?;
|
||||
this.http_client.send(request).await?;
|
||||
Ok(())
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
)
|
||||
|
||||
@@ -183,6 +183,11 @@ impl UserStore {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub fn clear_cache(&mut self) {
|
||||
self.users.clear();
|
||||
}
|
||||
|
||||
async fn handle_update_invite_info(
|
||||
this: ModelHandle<Self>,
|
||||
message: TypedEnvelope<proto::UpdateInviteInfo>,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
DATABASE_URL = "postgres://postgres@localhost/zed"
|
||||
DATABASE_MAX_CONNECTIONS = 5
|
||||
HTTP_PORT = 8080
|
||||
API_TOKEN = "secret"
|
||||
INVITE_LINK_PREFIX = "http://localhost:3000/invites/"
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.5.4"
|
||||
version = "0.8.0"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
ZED_ENVIRONMENT=preview
|
||||
RUST_LOG=info
|
||||
INVITE_LINK_PREFIX=https://zed.dev/invites/
|
||||
DATABASE_MAX_CONNECTIONS=10
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
ZED_ENVIRONMENT=production
|
||||
RUST_LOG=info
|
||||
INVITE_LINK_PREFIX=https://zed.dev/invites/
|
||||
DATABASE_MAX_CONNECTIONS=85
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
ZED_ENVIRONMENT=staging
|
||||
RUST_LOG=info
|
||||
INVITE_LINK_PREFIX=https://staging.zed.dev/invites/
|
||||
DATABASE_MAX_CONNECTIONS=5
|
||||
|
||||
@@ -59,6 +59,13 @@ spec:
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 5
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
@@ -73,6 +80,8 @@ spec:
|
||||
secretKeyRef:
|
||||
name: database
|
||||
key: url
|
||||
- name: DATABASE_MAX_CONNECTIONS
|
||||
value: "${DATABASE_MAX_CONNECTIONS}"
|
||||
- name: API_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
|
||||
@@ -78,6 +78,7 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
|
||||
struct AuthenticatedUserParams {
|
||||
github_user_id: Option<i32>,
|
||||
github_login: String,
|
||||
github_email: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -92,7 +93,11 @@ async fn get_authenticated_user(
|
||||
) -> Result<Json<AuthenticatedUserResponse>> {
|
||||
let user = app
|
||||
.db
|
||||
.get_user_by_github_account(¶ms.github_login, params.github_user_id)
|
||||
.get_or_create_user_by_github_account(
|
||||
¶ms.github_login,
|
||||
params.github_user_id,
|
||||
params.github_email.as_deref(),
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "user not found".into()))?;
|
||||
let metrics_id = app.db.get_user_metrics_id(user.id).await?;
|
||||
@@ -297,11 +302,7 @@ async fn create_access_token(
|
||||
let mut user_id = user.id;
|
||||
if let Some(impersonate) = params.impersonate {
|
||||
if user.admin {
|
||||
if let Some(impersonated_user) = app
|
||||
.db
|
||||
.get_user_by_github_account(&impersonate, None)
|
||||
.await?
|
||||
{
|
||||
if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? {
|
||||
user_id = impersonated_user.id;
|
||||
} else {
|
||||
return Err(Error::Http(
|
||||
|
||||
@@ -59,7 +59,7 @@ async fn main() {
|
||||
|
||||
for (github_user, admin) in zed_users {
|
||||
if db
|
||||
.get_user_by_github_account(&github_user.login, Some(github_user.id))
|
||||
.get_user_by_github_login(&github_user.login)
|
||||
.await
|
||||
.expect("failed to fetch user")
|
||||
.is_none()
|
||||
|
||||
@@ -158,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())
|
||||
@@ -191,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
|
||||
}
|
||||
@@ -294,10 +295,21 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_user_by_github_account(
|
||||
pub async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
|
||||
self.transaction(|tx| async move {
|
||||
Ok(user::Entity::find()
|
||||
.filter(user::Column::GithubLogin.eq(github_login))
|
||||
.one(&*tx)
|
||||
.await?)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_or_create_user_by_github_account(
|
||||
&self,
|
||||
github_login: &str,
|
||||
github_user_id: Option<i32>,
|
||||
github_email: Option<&str>,
|
||||
) -> Result<Option<User>> {
|
||||
self.transaction(|tx| async move {
|
||||
let tx = &*tx;
|
||||
@@ -319,7 +331,19 @@ impl Database {
|
||||
user_by_github_login.github_user_id = ActiveValue::set(Some(github_user_id));
|
||||
Ok(Some(user_by_github_login.update(tx).await?))
|
||||
} else {
|
||||
Ok(None)
|
||||
let user = user::Entity::insert(user::ActiveModel {
|
||||
email_address: ActiveValue::set(github_email.map(|email| email.into())),
|
||||
github_login: ActiveValue::set(github_login.into()),
|
||||
github_user_id: ActiveValue::set(Some(github_user_id)),
|
||||
admin: ActiveValue::set(false),
|
||||
invite_count: ActiveValue::set(0),
|
||||
invite_code: ActiveValue::set(None),
|
||||
metrics_id: ActiveValue::set(Uuid::new_v4()),
|
||||
..Default::default()
|
||||
})
|
||||
.exec_with_returning(&*tx)
|
||||
.await?;
|
||||
Ok(Some(user))
|
||||
}
|
||||
} else {
|
||||
Ok(user::Entity::find()
|
||||
@@ -1130,18 +1154,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(
|
||||
@@ -1158,8 +1180,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
|
||||
}
|
||||
@@ -1172,7 +1194,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),
|
||||
@@ -1191,7 +1213,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
|
||||
}
|
||||
@@ -1201,7 +1223,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
|
||||
@@ -1211,7 +1233,7 @@ impl Database {
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
Ok((room_id, room))
|
||||
Ok(room)
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -1258,7 +1280,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()
|
||||
@@ -1277,14 +1299,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
|
||||
}
|
||||
@@ -1295,7 +1316,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()
|
||||
@@ -1317,7 +1338,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
|
||||
@@ -1329,9 +1350,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()
|
||||
@@ -1550,14 +1571,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
|
||||
}
|
||||
@@ -1724,8 +1742,8 @@ impl Database {
|
||||
leader_connection: ConnectionId,
|
||||
follower_connection: ConnectionId,
|
||||
) -> Result<RoomGuard<proto::Room>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
let room_id = self.room_id_for_project(project_id, &*tx).await?;
|
||||
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),
|
||||
@@ -1742,7 +1760,8 @@ impl Database {
|
||||
.insert(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok((room_id, self.get_room(room_id, &*tx).await?))
|
||||
let room = self.get_room(room_id, &*tx).await?;
|
||||
Ok(room)
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -1753,60 +1772,39 @@ impl Database {
|
||||
leader_connection: ConnectionId,
|
||||
follower_connection: ConnectionId,
|
||||
) -> Result<RoomGuard<proto::Room>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
let room_id = self.room_id_for_project(project_id, &*tx).await?;
|
||||
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)
|
||||
.and(follower::Column::LeaderConnectionId.eq(leader_connection.id)),
|
||||
.eq(leader_connection.owner_id),
|
||||
)
|
||||
.add(follower::Column::LeaderConnectionId.eq(leader_connection.id))
|
||||
.add(
|
||||
follower::Column::FollowerConnectionServerId
|
||||
.eq(follower_connection.owner_id)
|
||||
.and(
|
||||
follower::Column::FollowerConnectionId
|
||||
.eq(follower_connection.id),
|
||||
),
|
||||
),
|
||||
.eq(follower_connection.owner_id),
|
||||
)
|
||||
.add(follower::Column::FollowerConnectionId.eq(follower_connection.id)),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok((room_id, self.get_room(room_id, &*tx).await?))
|
||||
let room = self.get_room(room_id, &*tx).await?;
|
||||
Ok(room)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn room_id_for_project(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<RoomId> {
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryAs {
|
||||
RoomId,
|
||||
}
|
||||
|
||||
Ok(project::Entity::find_by_id(project_id)
|
||||
.select_only()
|
||||
.column(project::Column::RoomId)
|
||||
.into_values::<_, QueryAs>()
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such project"))?)
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -1852,7 +1850,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"))?
|
||||
}
|
||||
@@ -2018,6 +2016,7 @@ impl Database {
|
||||
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(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2058,7 +2057,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()
|
||||
@@ -2119,7 +2118,7 @@ impl Database {
|
||||
.await?;
|
||||
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
Ok((room_id, (project.id, room)))
|
||||
Ok((project.id, room))
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -2129,7 +2128,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)
|
||||
@@ -2137,12 +2137,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"))?
|
||||
}
|
||||
@@ -2156,7 +2155,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()
|
||||
@@ -2174,7 +2174,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
|
||||
}
|
||||
@@ -2219,12 +2219,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))
|
||||
@@ -2235,7 +2235,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 {
|
||||
@@ -2315,7 +2314,7 @@ impl Database {
|
||||
}
|
||||
|
||||
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
|
||||
Ok((room_id, connection_ids))
|
||||
Ok(connection_ids)
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -2325,9 +2324,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()
|
||||
@@ -2369,7 +2369,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
|
||||
}
|
||||
@@ -2379,8 +2379,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()
|
||||
@@ -2414,7 +2415,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
|
||||
}
|
||||
@@ -2424,7 +2425,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()
|
||||
@@ -2550,7 +2552,6 @@ impl Database {
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let room_id = project.room_id;
|
||||
let project = Project {
|
||||
collaborators: collaborators
|
||||
.into_iter()
|
||||
@@ -2570,7 +2571,7 @@ impl Database {
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
Ok((room_id, (project, replica_id as ReplicaId)))
|
||||
Ok((project, replica_id as ReplicaId))
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -2579,8 +2580,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()
|
||||
@@ -2610,13 +2612,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
|
||||
}
|
||||
@@ -2626,11 +2654,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)
|
||||
@@ -2648,7 +2673,7 @@ impl Database {
|
||||
.iter()
|
||||
.any(|collaborator| collaborator.connection_id == connection_id)
|
||||
{
|
||||
Ok((project.room_id, collaborators))
|
||||
Ok(collaborators)
|
||||
} else {
|
||||
Err(anyhow!("no such project"))?
|
||||
}
|
||||
@@ -2661,11 +2686,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)
|
||||
@@ -2678,7 +2700,7 @@ impl Database {
|
||||
}
|
||||
|
||||
if connection_ids.contains(&connection_id) {
|
||||
Ok((project.room_id, connection_ids))
|
||||
Ok(connection_ids)
|
||||
} else {
|
||||
Err(anyhow!("no such project"))?
|
||||
}
|
||||
@@ -2708,6 +2730,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(
|
||||
@@ -2858,21 +2891,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>)>
|
||||
|
||||
@@ -92,8 +92,8 @@ test_both_dbs!(
|
||||
);
|
||||
|
||||
test_both_dbs!(
|
||||
test_get_user_by_github_account_postgres,
|
||||
test_get_user_by_github_account_sqlite,
|
||||
test_get_or_create_user_by_github_account_postgres,
|
||||
test_get_or_create_user_by_github_account_sqlite,
|
||||
db,
|
||||
{
|
||||
let user_id1 = db
|
||||
@@ -124,7 +124,7 @@ test_both_dbs!(
|
||||
.user_id;
|
||||
|
||||
let user = db
|
||||
.get_user_by_github_account("login1", None)
|
||||
.get_or_create_user_by_github_account("login1", None, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
@@ -133,19 +133,28 @@ test_both_dbs!(
|
||||
assert_eq!(user.github_user_id, Some(101));
|
||||
|
||||
assert!(db
|
||||
.get_user_by_github_account("non-existent-login", None)
|
||||
.get_or_create_user_by_github_account("non-existent-login", None, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
let user = db
|
||||
.get_user_by_github_account("the-new-login2", Some(102))
|
||||
.get_or_create_user_by_github_account("the-new-login2", Some(102), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(user.id, user_id2);
|
||||
assert_eq!(&user.github_login, "the-new-login2");
|
||||
assert_eq!(user.github_user_id, Some(102));
|
||||
|
||||
let user = db
|
||||
.get_or_create_user_by_github_account("login3", Some(103), Some("user3@example.com"))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(&user.github_login, "login3");
|
||||
assert_eq!(user.github_user_id, Some(103));
|
||||
assert_eq!(user.email_address, Some("user3@example.com".into()));
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ impl std::error::Error for Error {}
|
||||
pub struct Config {
|
||||
pub http_port: u16,
|
||||
pub database_url: String,
|
||||
pub database_max_connections: u32,
|
||||
pub api_token: String,
|
||||
pub invite_link_prefix: String,
|
||||
pub live_kit_server: Option<String>,
|
||||
@@ -116,7 +117,7 @@ pub struct AppState {
|
||||
impl AppState {
|
||||
pub async fn new(config: Config) -> Result<Arc<Self>> {
|
||||
let mut db_options = db::ConnectOptions::new(config.database_url.clone());
|
||||
db_options.max_connections(5);
|
||||
db_options.max_connections(config.database_max_connections);
|
||||
let db = Database::new(db_options).await?;
|
||||
let live_kit_client = if let Some(((server, key), secret)) = config
|
||||
.live_kit_server
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use anyhow::anyhow;
|
||||
use axum::{routing::get, Router};
|
||||
use axum::{routing::get, Extension, Router};
|
||||
use collab::{db, env, executor::Executor, AppState, Config, MigrateConfig, Result};
|
||||
use db::Database;
|
||||
use std::{
|
||||
env::args,
|
||||
net::{SocketAddr, TcpListener},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::signal::unix::SignalKind;
|
||||
use tracing_log::LogTracer;
|
||||
@@ -66,7 +67,12 @@ async fn main() -> Result<()> {
|
||||
|
||||
let app = collab::api::routes(rpc_server.clone(), state.clone())
|
||||
.merge(collab::rpc::routes(rpc_server.clone()))
|
||||
.merge(Router::new().route("/", get(handle_root)));
|
||||
.merge(
|
||||
Router::new()
|
||||
.route("/", get(handle_root))
|
||||
.route("/healthz", get(handle_liveness_probe))
|
||||
.layer(Extension(state.clone())),
|
||||
);
|
||||
|
||||
axum::Server::from_tcp(listener)?
|
||||
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
|
||||
@@ -95,6 +101,11 @@ async fn handle_root() -> String {
|
||||
format!("collab v{VERSION}")
|
||||
}
|
||||
|
||||
async fn handle_liveness_probe(Extension(state): Extension<Arc<AppState>>) -> Result<String> {
|
||||
state.db.get_all_users(0, 1).await?;
|
||||
Ok("ok".to_string())
|
||||
}
|
||||
|
||||
pub fn init_tracing(config: &Config) -> Option<()> {
|
||||
use std::str::FromStr;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
|
||||
@@ -53,11 +53,11 @@ use std::{
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::sync::watch;
|
||||
use tokio::sync::{watch, Semaphore};
|
||||
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! {
|
||||
@@ -186,7 +186,7 @@ impl Server {
|
||||
.add_request_handler(create_room)
|
||||
.add_request_handler(join_room)
|
||||
.add_request_handler(rejoin_room)
|
||||
.add_message_handler(leave_room)
|
||||
.add_request_handler(leave_room)
|
||||
.add_request_handler(call)
|
||||
.add_request_handler(cancel_call)
|
||||
.add_message_handler(decline_call)
|
||||
@@ -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,
|
||||
@@ -539,8 +542,13 @@ impl Server {
|
||||
// This arrangement ensures we will attempt to process earlier messages first, but fall
|
||||
// back to processing messages arrived later in the spirit of making progress.
|
||||
let mut foreground_message_handlers = FuturesUnordered::new();
|
||||
let concurrent_handlers = Arc::new(Semaphore::new(256));
|
||||
loop {
|
||||
let next_message = incoming_rx.next().fuse();
|
||||
let next_message = async {
|
||||
let permit = concurrent_handlers.clone().acquire_owned().await.unwrap();
|
||||
let message = incoming_rx.next().await;
|
||||
(permit, message)
|
||||
}.fuse();
|
||||
futures::pin_mut!(next_message);
|
||||
futures::select_biased! {
|
||||
_ = teardown.changed().fuse() => return Ok(()),
|
||||
@@ -551,7 +559,8 @@ impl Server {
|
||||
break;
|
||||
}
|
||||
_ = foreground_message_handlers.next() => {}
|
||||
message = next_message => {
|
||||
next_message = next_message => {
|
||||
let (permit, message) = next_message;
|
||||
if let Some(message) = message {
|
||||
let type_name = message.payload_type_name();
|
||||
let span = tracing::info_span!("receive message", %user_id, %login, %connection_id, %address, type_name);
|
||||
@@ -561,7 +570,10 @@ impl Server {
|
||||
let handle_message = (handler)(message, session.clone());
|
||||
drop(span_enter);
|
||||
|
||||
let handle_message = handle_message.instrument(span);
|
||||
let handle_message = async move {
|
||||
handle_message.await;
|
||||
drop(permit);
|
||||
}.instrument(span);
|
||||
if is_background {
|
||||
executor.spawn_detached(handle_message);
|
||||
} else {
|
||||
@@ -1090,8 +1102,14 @@ async fn rejoin_room(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn leave_room(_message: proto::LeaveRoom, session: Session) -> Result<()> {
|
||||
leave_room_for_session(&session).await
|
||||
async fn leave_room(
|
||||
_: proto::LeaveRoom,
|
||||
response: Response<proto::LeaveRoom>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
leave_room_for_session(&session).await?;
|
||||
response.send(proto::Ack {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn call(
|
||||
@@ -1405,7 +1423,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)
|
||||
@@ -1416,7 +1434,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(())
|
||||
}
|
||||
@@ -1855,7 +1875,7 @@ async fn fuzzy_search_users(
|
||||
1 | 2 => session
|
||||
.db()
|
||||
.await
|
||||
.get_user_by_github_account(&query, None)
|
||||
.get_user_by_github_login(&query)
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect(),
|
||||
|
||||
@@ -104,11 +104,7 @@ impl TestServer {
|
||||
});
|
||||
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let user_id = if let Ok(Some(user)) = self
|
||||
.app_state
|
||||
.db
|
||||
.get_user_by_github_account(name, None)
|
||||
.await
|
||||
let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
|
||||
{
|
||||
user.id
|
||||
} else {
|
||||
@@ -197,7 +193,8 @@ impl TestServer {
|
||||
fs: fs.clone(),
|
||||
build_window_options: |_, _, _| Default::default(),
|
||||
initialize_workspace: |_, _, _| unimplemented!(),
|
||||
dock_default_item_factory: |_, _| unimplemented!(),
|
||||
dock_default_item_factory: |_, _| None,
|
||||
background_actions: || &[],
|
||||
});
|
||||
|
||||
Project::init(&client);
|
||||
@@ -434,15 +431,7 @@ impl TestClient {
|
||||
cx: &mut TestAppContext,
|
||||
) -> ViewHandle<Workspace> {
|
||||
let (_, root_view) = cx.add_window(|_| EmptyView);
|
||||
cx.add_view(&root_view, |cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project.clone(),
|
||||
|_, _| unimplemented!(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
cx.add_view(&root_view, |cx| Workspace::test_new(project.clone(), cx))
|
||||
}
|
||||
|
||||
fn create_new_root_dir(&mut self) -> PathBuf {
|
||||
|
||||
@@ -274,10 +274,14 @@ async fn test_basic_calls(
|
||||
}
|
||||
|
||||
// User A leaves the room.
|
||||
active_call_a.update(cx_a, |call, cx| {
|
||||
call.hang_up(cx).unwrap();
|
||||
assert!(call.room().is_none());
|
||||
});
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
let hang_up = call.hang_up(cx);
|
||||
assert!(call.room().is_none());
|
||||
hang_up
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
assert_eq!(
|
||||
room_participants(&room_a, cx_a),
|
||||
@@ -557,6 +561,7 @@ async fn test_room_uniqueness(
|
||||
// Client C can successfully call client B after client B leaves the room.
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.hang_up(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
active_call_c
|
||||
@@ -733,6 +738,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;
|
||||
@@ -753,19 +766,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();
|
||||
@@ -821,7 +834,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 {
|
||||
@@ -928,6 +941,7 @@ async fn test_server_restarts(
|
||||
// User D hangs up.
|
||||
active_call_d
|
||||
.update(cx_d, |call, cx| call.hang_up(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
assert_eq!(
|
||||
@@ -993,7 +1007,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 {
|
||||
@@ -1083,7 +1097,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())
|
||||
@@ -1091,7 +1105,10 @@ async fn test_calls_on_multiple_connections(
|
||||
.unwrap();
|
||||
|
||||
// User B hangs up, and user A calls them again.
|
||||
active_call_b2.update(cx_b2, |call, cx| call.hang_up(cx).unwrap());
|
||||
active_call_b2
|
||||
.update(cx_b2, |call, cx| call.hang_up(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
@@ -1126,7 +1143,10 @@ async fn test_calls_on_multiple_connections(
|
||||
assert!(incoming_call_b2.next().await.unwrap().is_some());
|
||||
|
||||
// User A hangs up, causing both connections to stop ringing.
|
||||
active_call_a.update(cx_a, |call, cx| call.hang_up(cx).unwrap());
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.hang_up(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
assert!(incoming_call_b1.next().await.unwrap().is_none());
|
||||
assert!(incoming_call_b2.next().await.unwrap().is_none());
|
||||
@@ -1363,7 +1383,10 @@ async fn test_unshare_project(
|
||||
.unwrap();
|
||||
|
||||
// When client B leaves the room, the project becomes read-only.
|
||||
active_call_b.update(cx_b, |call, cx| call.hang_up(cx).unwrap());
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.hang_up(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
|
||||
|
||||
@@ -1392,7 +1415,10 @@ async fn test_unshare_project(
|
||||
.unwrap();
|
||||
|
||||
// When client A (the host) leaves the room, the project gets unshared and guests are notified.
|
||||
active_call_a.update(cx_a, |call, cx| call.hang_up(cx).unwrap());
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.hang_up(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
|
||||
project_c2.read_with(cx_c, |project, _| {
|
||||
@@ -1441,15 +1467,7 @@ async fn test_host_disconnect(
|
||||
deterministic.run_until_parked();
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
|
||||
|
||||
let (_, workspace_b) = cx_b.add_window(|cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project_b.clone(),
|
||||
|_, _| unimplemented!(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let (_, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "b.txt"), None, true, cx)
|
||||
@@ -3227,7 +3245,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);
|
||||
@@ -3884,9 +3902,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
|
||||
@@ -4696,15 +4716,7 @@ async fn test_collaborating_with_code_actions(
|
||||
|
||||
// Join the project as client B.
|
||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
let (_window_b, workspace_b) = cx_b.add_window(|cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project_b.clone(),
|
||||
|_, _| unimplemented!(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||
@@ -4927,15 +4939,7 @@ async fn test_collaborating_with_renames(
|
||||
.unwrap();
|
||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
|
||||
let (_window_b, workspace_b) = cx_b.add_window(|cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project_b.clone(),
|
||||
|_, _| unimplemented!(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "one.rs"), None, true, cx)
|
||||
@@ -5469,7 +5473,10 @@ async fn test_contacts(
|
||||
[("user_b".to_string(), "online", "busy")]
|
||||
);
|
||||
|
||||
active_call_a.update(cx_a, |call, cx| call.hang_up(cx).unwrap());
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.hang_up(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
assert_eq!(
|
||||
contacts(&client_a, cx_a),
|
||||
@@ -5772,7 +5779,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())
|
||||
@@ -5782,11 +5789,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);
|
||||
@@ -5796,11 +5804,14 @@ async fn test_following(
|
||||
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)])
|
||||
.await;
|
||||
server
|
||||
.make_contacts(&mut [(&client_a, cx_a), (&client_c, cx_c)])
|
||||
.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);
|
||||
@@ -5867,6 +5878,7 @@ async fn test_following(
|
||||
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();
|
||||
|
||||
// Client A updates their selections in those editors
|
||||
editor_a1.update(cx_a, |editor, cx| {
|
||||
@@ -5886,25 +5898,15 @@ async fn test_following(
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client A invites client C to the call.
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.invite(client_c.current_user_id(cx_c).to_proto(), None, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx_c.foreground().run_until_parked();
|
||||
let active_call_c = cx_c.read(ActiveCall::global);
|
||||
active_call_c
|
||||
.update(cx_c, |call, cx| call.accept_incoming(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
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
|
||||
@@ -5916,17 +5918,28 @@ async fn test_following(
|
||||
.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),
|
||||
room.followers_for(peer_id_a, project_id),
|
||||
&[peer_id_b, peer_id_c],
|
||||
"checking followers for A as {name}"
|
||||
);
|
||||
@@ -5944,17 +5957,102 @@ async fn test_following(
|
||||
("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),
|
||||
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)
|
||||
@@ -6186,7 +6284,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)),
|
||||
@@ -6194,6 +6292,99 @@ async fn test_following(
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_join_call_after_screen_was_shared(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
.make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
// Call users B and C from client A.
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.invite(client_b.user_id().unwrap(), None, cx)
|
||||
})
|
||||
.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 B receives the call.
|
||||
let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
|
||||
let call_b = incoming_call_b.next().await.unwrap().unwrap();
|
||||
assert_eq!(call_b.calling_user.github_login, "user_a");
|
||||
|
||||
// User A shares their screen
|
||||
let display = MacOSDisplay::new();
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.room().unwrap().update(cx, |room, cx| {
|
||||
room.set_display_sources(vec![display.clone()]);
|
||||
room.share_screen(cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
client_b.user_store.update(cx_b, |user_store, _| {
|
||||
user_store.clear_cache();
|
||||
});
|
||||
|
||||
// User B joins the room
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.accept_incoming(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
|
||||
assert!(incoming_call_b.next().await.unwrap().is_none());
|
||||
|
||||
deterministic.run_until_parked();
|
||||
assert_eq!(
|
||||
room_participants(&room_a, cx_a),
|
||||
RoomParticipants {
|
||||
remote: vec!["user_b".to_string()],
|
||||
pending: vec![],
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
room_participants(&room_b, cx_b),
|
||||
RoomParticipants {
|
||||
remote: vec!["user_a".to_string()],
|
||||
pending: vec![],
|
||||
}
|
||||
);
|
||||
|
||||
// Ensure User B sees User A's screenshare.
|
||||
room_b.read_with(cx_b, |room, _| {
|
||||
assert_eq!(
|
||||
room.remote_participants()
|
||||
.get(&client_a.user_id().unwrap())
|
||||
.unwrap()
|
||||
.tracks
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_following_tab_order(
|
||||
deterministic: Arc<Deterministic>,
|
||||
|
||||
@@ -641,7 +641,7 @@ async fn randomly_mutate_active_call(
|
||||
if can_hang_up && active_call.read_with(cx, |call, _| call.room().is_some()) =>
|
||||
{
|
||||
log::info!("{}: hanging up", client.username);
|
||||
active_call.update(cx, |call, cx| call.hang_up(cx))?;
|
||||
active_call.update(cx, |call, cx| call.hang_up(cx)).await?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@ call = { path = "../call" }
|
||||
client = { path = "../client" }
|
||||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections" }
|
||||
context_menu = { path = "../context_menu" }
|
||||
editor = { path = "../editor" }
|
||||
feedback = { path = "../feedback" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
gpui = { path = "../gpui" }
|
||||
menu = { path = "../menu" }
|
||||
|
||||
@@ -4,9 +4,10 @@ use crate::{
|
||||
ToggleScreenSharing,
|
||||
};
|
||||
use call::{ActiveCall, ParticipantLocation, Room};
|
||||
use client::{proto::PeerId, Authenticate, ContactEventKind, User, UserStore};
|
||||
use client::{proto::PeerId, ContactEventKind, SignIn, SignOut, User, UserStore};
|
||||
use clock::ReplicaId;
|
||||
use contacts_popover::ContactsPopover;
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use gpui::{
|
||||
actions,
|
||||
color::Color,
|
||||
@@ -28,8 +29,9 @@ actions!(
|
||||
[
|
||||
ToggleCollaboratorList,
|
||||
ToggleContactsMenu,
|
||||
ToggleUserMenu,
|
||||
ShareProject,
|
||||
UnshareProject
|
||||
UnshareProject,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -38,25 +40,20 @@ impl_internal_actions!(collab, [LeaveCall]);
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
pub(crate) struct LeaveCall;
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
enum ContactsPopoverSide {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(CollabTitlebarItem::toggle_collaborator_list_popover);
|
||||
cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
|
||||
cx.add_action(CollabTitlebarItem::share_project);
|
||||
cx.add_action(CollabTitlebarItem::unshare_project);
|
||||
cx.add_action(CollabTitlebarItem::leave_call);
|
||||
cx.add_action(CollabTitlebarItem::toggle_user_menu);
|
||||
}
|
||||
|
||||
pub struct CollabTitlebarItem {
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
contacts_popover: Option<ViewHandle<ContactsPopover>>,
|
||||
contacts_popover_side: ContactsPopoverSide,
|
||||
user_menu: ViewHandle<ContextMenu>,
|
||||
collaborator_list_popover: Option<ViewHandle<CollaboratorListPopover>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
@@ -90,9 +87,9 @@ impl View for CollabTitlebarItem {
|
||||
}
|
||||
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let user = workspace.read(cx).user_store().read(cx).current_user();
|
||||
|
||||
let mut left_container = Flex::row();
|
||||
let mut right_container = Flex::row();
|
||||
|
||||
left_container.add_child(
|
||||
Label::new(project_title, theme.workspace.titlebar.title.clone())
|
||||
@@ -103,39 +100,29 @@ impl View for CollabTitlebarItem {
|
||||
.boxed(),
|
||||
);
|
||||
|
||||
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||
left_container.add_child(self.render_current_user(&workspace, &theme, &user, cx));
|
||||
left_container.add_children(self.render_collaborators(&workspace, &theme, room, cx));
|
||||
left_container.add_child(self.render_toggle_contacts_button(&theme, cx));
|
||||
}
|
||||
|
||||
let mut right_container = Flex::row();
|
||||
|
||||
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||
right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
|
||||
right_container.add_child(self.render_leave_call_button(&theme, cx));
|
||||
right_container
|
||||
let user = workspace.read(cx).user_store().read(cx).current_user();
|
||||
let peer_id = workspace.read(cx).client().peer_id();
|
||||
if let Some(((user, peer_id), room)) = user
|
||||
.zip(peer_id)
|
||||
.zip(ActiveCall::global(cx).read(cx).room().cloned())
|
||||
{
|
||||
left_container
|
||||
.add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
|
||||
} else {
|
||||
right_container.add_child(self.render_outside_call_share_button(&theme, cx));
|
||||
|
||||
right_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
|
||||
right_container
|
||||
.add_child(self.render_current_user(&workspace, &theme, &user, peer_id, cx));
|
||||
right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
|
||||
}
|
||||
|
||||
right_container.add_children(self.render_connection_status(&workspace, cx));
|
||||
|
||||
if let Some(user) = user {
|
||||
//TODO: Add style
|
||||
right_container.add_child(
|
||||
Label::new(
|
||||
user.github_login.clone(),
|
||||
theme.workspace.titlebar.title.clone(),
|
||||
)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(theme.workspace.titlebar.item_spacing)
|
||||
.boxed(),
|
||||
);
|
||||
let status = workspace.read(cx).client().status();
|
||||
let status = &*status.borrow();
|
||||
if matches!(status, client::Status::Connected { .. }) {
|
||||
right_container.add_child(self.render_toggle_contacts_button(&theme, cx));
|
||||
right_container.add_child(self.render_user_menu_button(&theme, cx));
|
||||
} else {
|
||||
right_container.add_child(Self::render_authenticate(&theme, cx));
|
||||
right_container.add_children(self.render_connection_status(status, cx));
|
||||
right_container.add_child(self.render_sign_in_button(&theme, cx));
|
||||
}
|
||||
|
||||
Stack::new()
|
||||
@@ -186,7 +173,11 @@ impl CollabTitlebarItem {
|
||||
workspace: workspace.downgrade(),
|
||||
user_store: user_store.clone(),
|
||||
contacts_popover: None,
|
||||
contacts_popover_side: ContactsPopoverSide::Right,
|
||||
user_menu: cx.add_view(|cx| {
|
||||
let mut menu = ContextMenu::new(cx);
|
||||
menu.set_position_mode(OverlayPositionMode::Local);
|
||||
menu
|
||||
}),
|
||||
collaborator_list_popover: None,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
@@ -278,12 +269,6 @@ impl CollabTitlebarItem {
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
|
||||
self.contacts_popover_side = match ActiveCall::global(cx).read(cx).room() {
|
||||
Some(_) => ContactsPopoverSide::Left,
|
||||
None => ContactsPopoverSide::Right,
|
||||
};
|
||||
|
||||
self.contacts_popover = Some(view);
|
||||
}
|
||||
}
|
||||
@@ -291,10 +276,73 @@ impl CollabTitlebarItem {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let avatar_style = theme.workspace.titlebar.leader_avatar.clone();
|
||||
let item_style = theme.context_menu.item.disabled_style().clone();
|
||||
self.user_menu.update(cx, |user_menu, cx| {
|
||||
let items = if let Some(user) = self.user_store.read(cx).current_user() {
|
||||
vec![
|
||||
ContextMenuItem::Static(Box::new(move |_| {
|
||||
Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Self::render_face(
|
||||
avatar,
|
||||
avatar_style.clone(),
|
||||
Color::transparent_black(),
|
||||
)
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(user.github_login.clone(), item_style.label.clone())
|
||||
.boxed(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(item_style.container)
|
||||
.boxed()
|
||||
})),
|
||||
ContextMenuItem::Item {
|
||||
label: "Sign out".into(),
|
||||
action: Box::new(SignOut),
|
||||
},
|
||||
ContextMenuItem::Item {
|
||||
label: "Give Feedback".into(),
|
||||
action: Box::new(feedback::feedback_editor::GiveFeedback),
|
||||
},
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
ContextMenuItem::Item {
|
||||
label: "Sign in".into(),
|
||||
action: Box::new(SignIn),
|
||||
},
|
||||
ContextMenuItem::Item {
|
||||
label: "Give Feedback".into(),
|
||||
action: Box::new(feedback::feedback_editor::GiveFeedback),
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
user_menu.show(
|
||||
vec2f(
|
||||
theme
|
||||
.workspace
|
||||
.titlebar
|
||||
.user_menu_button
|
||||
.default
|
||||
.button_width,
|
||||
theme.workspace.titlebar.height,
|
||||
),
|
||||
AnchorCorner::TopRight,
|
||||
items,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| call.hang_up(cx))
|
||||
.log_err();
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn render_toggle_contacts_button(
|
||||
@@ -328,12 +376,10 @@ impl CollabTitlebarItem {
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<ToggleContactsMenu>::new(0, cx, |state, _| {
|
||||
let style = titlebar.toggle_contacts_button.style_for(
|
||||
state,
|
||||
self.contacts_popover.is_some()
|
||||
&& self.contacts_popover_side == ContactsPopoverSide::Left,
|
||||
);
|
||||
Svg::new("icons/plus_8.svg")
|
||||
let style = titlebar
|
||||
.toggle_contacts_button
|
||||
.style_for(state, self.contacts_popover.is_some());
|
||||
Svg::new("icons/user_plus_16.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
@@ -349,15 +395,18 @@ impl CollabTitlebarItem {
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleContactsMenu);
|
||||
})
|
||||
.with_tooltip::<ToggleContactsMenu, _>(
|
||||
0,
|
||||
"Show contacts menu".into(),
|
||||
Some(Box::new(ToggleContactsMenu)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(badge)
|
||||
.with_children(self.render_contacts_popover_host(
|
||||
ContactsPopoverSide::Left,
|
||||
titlebar,
|
||||
cx,
|
||||
))
|
||||
.with_children(self.render_contacts_popover_host(titlebar, cx))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
@@ -407,40 +456,6 @@ impl CollabTitlebarItem {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_leave_call_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let titlebar = &theme.workspace.titlebar;
|
||||
|
||||
MouseEventHandler::<LeaveCall>::new(0, cx, |state, _| {
|
||||
let style = titlebar.call_control.style_for(state, false);
|
||||
Svg::new("icons/leave_12.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(LeaveCall);
|
||||
})
|
||||
.with_tooltip::<LeaveCall, _>(
|
||||
0,
|
||||
"Leave call".to_owned(),
|
||||
Some(Box::new(LeaveCall)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.contained()
|
||||
.with_margin_left(theme.workspace.titlebar.item_spacing)
|
||||
.aligned()
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_in_call_share_unshare_button(
|
||||
&self,
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
@@ -468,11 +483,9 @@ impl CollabTitlebarItem {
|
||||
.with_child(
|
||||
MouseEventHandler::<ShareUnshare>::new(0, cx, |state, _| {
|
||||
//TODO: Ensure this button has consistant width for both text variations
|
||||
let style = titlebar.share_button.style_for(
|
||||
state,
|
||||
self.contacts_popover.is_some()
|
||||
&& self.contacts_popover_side == ContactsPopoverSide::Right,
|
||||
);
|
||||
let style = titlebar
|
||||
.share_button
|
||||
.style_for(state, self.contacts_popover.is_some());
|
||||
Label::new(label, style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
@@ -495,11 +508,6 @@ impl CollabTitlebarItem {
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(self.render_contacts_popover_host(
|
||||
ContactsPopoverSide::Right,
|
||||
titlebar,
|
||||
cx,
|
||||
))
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(theme.workspace.titlebar.item_spacing)
|
||||
@@ -507,94 +515,96 @@ impl CollabTitlebarItem {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_outside_call_share_button(
|
||||
&self,
|
||||
theme: &Theme,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
let tooltip = "Share project with new call";
|
||||
fn render_user_menu_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let titlebar = &theme.workspace.titlebar;
|
||||
|
||||
enum OutsideCallShare {}
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<OutsideCallShare>::new(0, cx, |state, _| {
|
||||
//TODO: Ensure this button has consistant width for both text variations
|
||||
let style = titlebar.share_button.style_for(
|
||||
state,
|
||||
self.contacts_popover.is_some()
|
||||
&& self.contacts_popover_side == ContactsPopoverSide::Right,
|
||||
);
|
||||
Label::new("Share".to_owned(), style.text.clone())
|
||||
MouseEventHandler::<ToggleUserMenu>::new(0, cx, |state, _| {
|
||||
let style = titlebar.call_control.style_for(state, false);
|
||||
Svg::new("icons/ellipsis_14.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleContactsMenu);
|
||||
cx.dispatch_action(ToggleUserMenu);
|
||||
})
|
||||
.with_tooltip::<OutsideCallShare, _>(
|
||||
.with_tooltip::<ToggleUserMenu, _>(
|
||||
0,
|
||||
tooltip.to_owned(),
|
||||
None,
|
||||
"Toggle user menu".to_owned(),
|
||||
Some(Box::new(ToggleUserMenu)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.contained()
|
||||
.with_margin_left(theme.workspace.titlebar.item_spacing)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(self.render_contacts_popover_host(
|
||||
ContactsPopoverSide::Right,
|
||||
titlebar,
|
||||
cx,
|
||||
))
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(theme.workspace.titlebar.item_spacing)
|
||||
.with_child(ChildView::new(&self.user_menu, cx).boxed())
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_sign_in_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let titlebar = &theme.workspace.titlebar;
|
||||
MouseEventHandler::<SignIn>::new(0, cx, |state, _| {
|
||||
let style = titlebar.sign_in_prompt.style_for(state, false);
|
||||
Label::new("Sign In", style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(SignIn);
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_contacts_popover_host<'a>(
|
||||
&'a self,
|
||||
side: ContactsPopoverSide,
|
||||
theme: &'a theme::Titlebar,
|
||||
cx: &'a RenderContext<Self>,
|
||||
) -> impl Iterator<Item = ElementBox> + 'a {
|
||||
self.contacts_popover
|
||||
.iter()
|
||||
.filter(move |_| self.contacts_popover_side == side)
|
||||
.map(|popover| {
|
||||
Overlay::new(
|
||||
ChildView::new(popover, cx)
|
||||
.contained()
|
||||
.with_margin_top(theme.height)
|
||||
.with_margin_left(theme.toggle_contacts_button.default.button_width)
|
||||
.with_margin_right(-theme.toggle_contacts_button.default.button_width)
|
||||
.boxed(),
|
||||
)
|
||||
.with_fit_mode(OverlayFitMode::SwitchAnchor)
|
||||
.with_anchor_corner(AnchorCorner::BottomLeft)
|
||||
.with_z_index(999)
|
||||
.boxed()
|
||||
})
|
||||
) -> Option<ElementBox> {
|
||||
self.contacts_popover.as_ref().map(|popover| {
|
||||
Overlay::new(
|
||||
ChildView::new(popover, cx)
|
||||
.contained()
|
||||
.with_margin_top(theme.height)
|
||||
.with_margin_left(theme.toggle_contacts_button.default.button_width)
|
||||
.with_margin_right(-theme.toggle_contacts_button.default.button_width)
|
||||
.boxed(),
|
||||
)
|
||||
.with_fit_mode(OverlayFitMode::SwitchAnchor)
|
||||
.with_anchor_corner(AnchorCorner::BottomLeft)
|
||||
.with_z_index(999)
|
||||
.boxed()
|
||||
})
|
||||
}
|
||||
|
||||
fn render_collaborators(
|
||||
&self,
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
theme: &Theme,
|
||||
room: ModelHandle<Room>,
|
||||
room: &ModelHandle<Room>,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> Vec<ElementBox> {
|
||||
let project = workspace.read(cx).project().read(cx);
|
||||
|
||||
let mut participants = room
|
||||
.read(cx)
|
||||
.remote_participants()
|
||||
.values()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
participants.sort_by_key(|p| Some(project.collaborators().get(&p.peer_id)?.replica_id));
|
||||
participants.sort_by_cached_key(|p| p.user.github_login.clone());
|
||||
|
||||
participants
|
||||
.into_iter()
|
||||
@@ -615,7 +625,7 @@ impl CollabTitlebarItem {
|
||||
theme,
|
||||
cx,
|
||||
))
|
||||
.with_margin_left(theme.workspace.titlebar.face_pile_spacing)
|
||||
.with_margin_right(theme.workspace.titlebar.face_pile_spacing)
|
||||
.boxed(),
|
||||
)
|
||||
})
|
||||
@@ -626,35 +636,21 @@ impl CollabTitlebarItem {
|
||||
&self,
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
theme: &Theme,
|
||||
user: &Option<Arc<User>>,
|
||||
user: &Arc<User>,
|
||||
peer_id: PeerId,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
let user = user.as_ref().expect("Active call without user");
|
||||
let replica_id = workspace.read(cx).project().read(cx).replica_id();
|
||||
let peer_id = workspace
|
||||
.read(cx)
|
||||
.client()
|
||||
.peer_id()
|
||||
.expect("Active call without peer id");
|
||||
self.render_face_pile(user, Some(replica_id), peer_id, None, workspace, theme, cx)
|
||||
}
|
||||
|
||||
fn render_authenticate(theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
MouseEventHandler::<Authenticate>::new(0, cx, |state, _| {
|
||||
let style = theme
|
||||
.workspace
|
||||
.titlebar
|
||||
.sign_in_prompt
|
||||
.style_for(state, false);
|
||||
Label::new("Sign in", style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.with_margin_left(theme.workspace.titlebar.item_spacing)
|
||||
.boxed()
|
||||
})
|
||||
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.aligned()
|
||||
Container::new(self.render_face_pile(
|
||||
user,
|
||||
Some(replica_id),
|
||||
peer_id,
|
||||
None,
|
||||
workspace,
|
||||
theme,
|
||||
cx,
|
||||
))
|
||||
.with_margin_right(theme.workspace.titlebar.item_spacing)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
@@ -668,33 +664,26 @@ impl CollabTitlebarItem {
|
||||
theme: &Theme,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
let project_id = workspace.read(cx).project().read(cx).remote_id();
|
||||
let room = ActiveCall::global(cx).read(cx).room();
|
||||
let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
|
||||
let followed_by_self = room
|
||||
.map(|room| {
|
||||
is_being_followed
|
||||
&& room
|
||||
.read(cx)
|
||||
.followers_for(peer_id)
|
||||
.iter()
|
||||
.any(|&follower| Some(follower) == workspace.read(cx).client().peer_id())
|
||||
.and_then(|room| {
|
||||
Some(
|
||||
is_being_followed
|
||||
&& room
|
||||
.read(cx)
|
||||
.followers_for(peer_id, project_id?)
|
||||
.iter()
|
||||
.any(|&follower| {
|
||||
Some(follower) == workspace.read(cx).client().peer_id()
|
||||
}),
|
||||
)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
let avatar_style;
|
||||
if let Some(location) = location {
|
||||
if let ParticipantLocation::SharedProject { project_id } = location {
|
||||
if Some(project_id) == workspace.read(cx).project().read(cx).remote_id() {
|
||||
avatar_style = &theme.workspace.titlebar.avatar;
|
||||
} else {
|
||||
avatar_style = &theme.workspace.titlebar.inactive_avatar;
|
||||
}
|
||||
} else {
|
||||
avatar_style = &theme.workspace.titlebar.inactive_avatar;
|
||||
}
|
||||
} else {
|
||||
avatar_style = &theme.workspace.titlebar.avatar;
|
||||
}
|
||||
let leader_style = theme.workspace.titlebar.leader_avatar;
|
||||
let follower_style = theme.workspace.titlebar.follower_avatar;
|
||||
|
||||
let mut background_color = theme
|
||||
.workspace
|
||||
@@ -710,23 +699,26 @@ impl CollabTitlebarItem {
|
||||
}
|
||||
}
|
||||
|
||||
let content = Stack::new()
|
||||
let mut content = Stack::new()
|
||||
.with_children(user.avatar.as_ref().map(|avatar| {
|
||||
let face_pile = FacePile::new(theme.workspace.titlebar.follower_avatar_overlap)
|
||||
.with_child(Self::render_face(
|
||||
avatar.clone(),
|
||||
avatar_style.clone(),
|
||||
Self::location_style(workspace, location, leader_style, cx),
|
||||
background_color,
|
||||
))
|
||||
.with_children(
|
||||
(|| {
|
||||
let project_id = project_id?;
|
||||
let room = room?.read(cx);
|
||||
let followers = room.followers_for(peer_id);
|
||||
let followers = room.followers_for(peer_id, project_id);
|
||||
|
||||
Some(followers.into_iter().flat_map(|&follower| {
|
||||
let avatar = room
|
||||
.remote_participant_for_peer_id(follower)
|
||||
.and_then(|participant| participant.user.avatar.clone())
|
||||
let remote_participant =
|
||||
room.remote_participant_for_peer_id(follower);
|
||||
|
||||
let avatar = remote_participant
|
||||
.and_then(|p| p.user.avatar.clone())
|
||||
.or_else(|| {
|
||||
if follower == workspace.read(cx).client().peer_id()? {
|
||||
workspace
|
||||
@@ -741,9 +733,11 @@ impl CollabTitlebarItem {
|
||||
}
|
||||
})?;
|
||||
|
||||
let location = remote_participant.map(|p| p.location);
|
||||
|
||||
Some(Self::render_face(
|
||||
avatar.clone(),
|
||||
theme.workspace.titlebar.follower_avatar.clone(),
|
||||
Self::location_style(workspace, location, follower_style, cx),
|
||||
background_color,
|
||||
))
|
||||
}))
|
||||
@@ -782,7 +776,10 @@ impl CollabTitlebarItem {
|
||||
|
||||
if let Some(location) = location {
|
||||
if let Some(replica_id) = replica_id {
|
||||
MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| content)
|
||||
content =
|
||||
MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| {
|
||||
content
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleFollow(peer_id))
|
||||
@@ -798,12 +795,14 @@ impl CollabTitlebarItem {
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.boxed()
|
||||
.boxed();
|
||||
} else if let ParticipantLocation::SharedProject { project_id } = location {
|
||||
let user_id = user.id;
|
||||
MouseEventHandler::<JoinProject>::new(peer_id.as_u64() as usize, cx, move |_, _| {
|
||||
content
|
||||
})
|
||||
content = MouseEventHandler::<JoinProject>::new(
|
||||
peer_id.as_u64() as usize,
|
||||
cx,
|
||||
move |_, _| content,
|
||||
)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(JoinProject {
|
||||
@@ -818,13 +817,29 @@ impl CollabTitlebarItem {
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.boxed()
|
||||
} else {
|
||||
content
|
||||
.boxed();
|
||||
}
|
||||
} else {
|
||||
content
|
||||
}
|
||||
content
|
||||
}
|
||||
|
||||
fn location_style(
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
location: Option<ParticipantLocation>,
|
||||
mut style: AvatarStyle,
|
||||
cx: &RenderContext<Self>,
|
||||
) -> AvatarStyle {
|
||||
if let Some(location) = location {
|
||||
if let ParticipantLocation::SharedProject { project_id } = location {
|
||||
if Some(project_id) != workspace.read(cx).project().read(cx).remote_id() {
|
||||
style.image.grayscale = true;
|
||||
}
|
||||
} else {
|
||||
style.image.grayscale = true;
|
||||
}
|
||||
}
|
||||
|
||||
style
|
||||
}
|
||||
|
||||
fn render_face(
|
||||
@@ -832,7 +847,7 @@ impl CollabTitlebarItem {
|
||||
avatar_style: AvatarStyle,
|
||||
background_color: Color,
|
||||
) -> ElementBox {
|
||||
Image::new(avatar)
|
||||
Image::from_data(avatar)
|
||||
.with_style(avatar_style.image)
|
||||
.aligned()
|
||||
.contained()
|
||||
@@ -847,13 +862,13 @@ impl CollabTitlebarItem {
|
||||
|
||||
fn render_connection_status(
|
||||
&self,
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
status: &client::Status,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> Option<ElementBox> {
|
||||
enum ConnectionStatusButton {}
|
||||
|
||||
let theme = &cx.global::<Settings>().theme.clone();
|
||||
match &*workspace.read(cx).client().status().borrow() {
|
||||
match status {
|
||||
client::Status::ConnectionError
|
||||
| client::Status::ConnectionLost
|
||||
| client::Status::Reauthenticating { .. }
|
||||
|
||||
@@ -86,6 +86,7 @@ fn join_project(action: &JoinProject, app_state: Arc<AppState>, cx: &mut Mutable
|
||||
0,
|
||||
project,
|
||||
app_state.dock_default_item_factory,
|
||||
app_state.background_actions,
|
||||
cx,
|
||||
);
|
||||
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use client::{ContactRequestStatus, User, UserStore};
|
||||
use gpui::{
|
||||
elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext, RenderContext,
|
||||
Task, View, ViewContext, ViewHandle,
|
||||
elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MouseState, MutableAppContext,
|
||||
RenderContext, Task, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::Settings;
|
||||
@@ -68,7 +68,7 @@ impl PickerDelegate for ContactFinder {
|
||||
this.potential_contacts = potential_contacts.into();
|
||||
cx.notify();
|
||||
});
|
||||
Ok(())
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
.await;
|
||||
@@ -128,7 +128,7 @@ impl PickerDelegate for ContactFinder {
|
||||
.style_for(mouse_state, selected);
|
||||
Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_finder.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
@@ -178,4 +178,14 @@ impl ContactFinder {
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn editor_text(&self, cx: &AppContext) -> String {
|
||||
self.picker.read(cx).query(cx)
|
||||
}
|
||||
|
||||
pub fn with_editor_text(self, editor_text: String, cx: &mut ViewContext<Self>) -> Self {
|
||||
self.picker
|
||||
.update(cx, |picker, cx| picker.set_query(editor_text, cx));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +141,10 @@ impl PartialEq for ContactEntry {
|
||||
pub struct RequestContact(pub u64);
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
pub struct RemoveContact(pub u64);
|
||||
pub struct RemoveContact {
|
||||
user_id: u64,
|
||||
github_login: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
pub struct RespondToContactRequest {
|
||||
@@ -294,11 +297,25 @@ impl ContactList {
|
||||
this
|
||||
}
|
||||
|
||||
pub fn editor_text(&self, cx: &AppContext) -> String {
|
||||
self.filter_editor.read(cx).text(cx)
|
||||
}
|
||||
|
||||
pub fn with_editor_text(self, editor_text: String, cx: &mut ViewContext<Self>) -> Self {
|
||||
self.filter_editor
|
||||
.update(cx, |picker, cx| picker.set_text(editor_text, cx));
|
||||
self
|
||||
}
|
||||
|
||||
fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext<Self>) {
|
||||
let user_id = request.0;
|
||||
let user_id = request.user_id;
|
||||
let github_login = &request.github_login;
|
||||
let user_store = self.user_store.clone();
|
||||
let prompt_message = "Are you sure you want to remove this contact?";
|
||||
let mut answer = cx.prompt(PromptLevel::Warning, prompt_message, &["Remove", "Cancel"]);
|
||||
let prompt_message = format!(
|
||||
"Are you sure you want to remove \"{}\" from your contacts?",
|
||||
github_login
|
||||
);
|
||||
let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
if answer.next().await == Some(0) {
|
||||
user_store
|
||||
@@ -726,7 +743,7 @@ impl ContactList {
|
||||
) -> ElementBox {
|
||||
Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
@@ -1057,6 +1074,7 @@ impl ContactList {
|
||||
let online = contact.online;
|
||||
let busy = contact.busy || calling;
|
||||
let user_id = contact.user.id;
|
||||
let github_login = contact.user.github_login.clone();
|
||||
let initial_project = project.clone();
|
||||
let mut element =
|
||||
MouseEventHandler::<Contact>::new(contact.user.id as usize, cx, |_, cx| {
|
||||
@@ -1080,7 +1098,7 @@ impl ContactList {
|
||||
};
|
||||
Stack::new()
|
||||
.with_child(
|
||||
Image::new(avatar)
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
@@ -1117,7 +1135,10 @@ impl ContactList {
|
||||
.with_padding(Padding::uniform(2.))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(RemoveContact(user_id))
|
||||
cx.dispatch_action(RemoveContact {
|
||||
user_id,
|
||||
github_login: github_login.clone(),
|
||||
})
|
||||
})
|
||||
.flex_float()
|
||||
.boxed(),
|
||||
@@ -1173,7 +1194,7 @@ impl ContactList {
|
||||
|
||||
let mut row = Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
@@ -1193,6 +1214,7 @@ impl ContactList {
|
||||
);
|
||||
|
||||
let user_id = user.id;
|
||||
let github_login = user.github_login.clone();
|
||||
let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
|
||||
let button_spacing = theme.contact_button_spacing;
|
||||
|
||||
@@ -1254,7 +1276,10 @@ impl ContactList {
|
||||
.with_padding(Padding::uniform(2.))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(RemoveContact(user_id))
|
||||
cx.dispatch_action(RemoveContact {
|
||||
user_id,
|
||||
github_login: github_login.clone(),
|
||||
})
|
||||
})
|
||||
.flex_float()
|
||||
.boxed(),
|
||||
@@ -1294,7 +1319,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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleContactsMenu};
|
||||
use client::UserStore;
|
||||
use gpui::{
|
||||
actions, elements::*, ClipboardItem, CursorStyle, Entity, ModelHandle, MouseButton,
|
||||
MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
|
||||
actions, elements::*, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, View,
|
||||
ViewContext, ViewHandle,
|
||||
};
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
@@ -43,19 +43,23 @@ impl ContactsPopover {
|
||||
user_store,
|
||||
_subscription: None,
|
||||
};
|
||||
this.show_contact_list(cx);
|
||||
this.show_contact_list(String::new(), cx);
|
||||
this
|
||||
}
|
||||
|
||||
fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext<Self>) {
|
||||
match &self.child {
|
||||
Child::ContactList(_) => self.show_contact_finder(cx),
|
||||
Child::ContactFinder(_) => self.show_contact_list(cx),
|
||||
Child::ContactList(list) => self.show_contact_finder(list.read(cx).editor_text(cx), cx),
|
||||
Child::ContactFinder(finder) => {
|
||||
self.show_contact_list(finder.read(cx).editor_text(cx), cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn show_contact_finder(&mut self, cx: &mut ViewContext<ContactsPopover>) {
|
||||
let child = cx.add_view(|cx| ContactFinder::new(self.user_store.clone(), cx));
|
||||
fn show_contact_finder(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
|
||||
let child = cx.add_view(|cx| {
|
||||
ContactFinder::new(self.user_store.clone(), cx).with_editor_text(editor_text, cx)
|
||||
});
|
||||
cx.focus(&child);
|
||||
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
|
||||
crate::contact_finder::Event::Dismissed => cx.emit(Event::Dismissed),
|
||||
@@ -64,9 +68,11 @@ impl ContactsPopover {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn show_contact_list(&mut self, cx: &mut ViewContext<ContactsPopover>) {
|
||||
let child =
|
||||
cx.add_view(|cx| ContactList::new(self.project.clone(), self.user_store.clone(), cx));
|
||||
fn show_contact_list(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
|
||||
let child = cx.add_view(|cx| {
|
||||
ContactList::new(self.project.clone(), self.user_store.clone(), cx)
|
||||
.with_editor_text(editor_text, cx)
|
||||
});
|
||||
cx.focus(&child);
|
||||
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
|
||||
crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
|
||||
@@ -92,61 +98,9 @@ impl View for ContactsPopover {
|
||||
Child::ContactFinder(child) => ChildView::new(child, cx),
|
||||
};
|
||||
|
||||
MouseEventHandler::<ContactsPopover>::new(0, cx, |_, cx| {
|
||||
MouseEventHandler::<ContactsPopover>::new(0, cx, |_, _| {
|
||||
Flex::column()
|
||||
.with_child(child.flex(1., true).boxed())
|
||||
.with_children(
|
||||
self.user_store
|
||||
.read(cx)
|
||||
.invite_info()
|
||||
.cloned()
|
||||
.and_then(|info| {
|
||||
enum InviteLink {}
|
||||
|
||||
if info.count > 0 {
|
||||
Some(
|
||||
MouseEventHandler::<InviteLink>::new(0, cx, |state, cx| {
|
||||
let style = theme
|
||||
.contacts_popover
|
||||
.invite_row
|
||||
.style_for(state, false)
|
||||
.clone();
|
||||
|
||||
let copied =
|
||||
cx.read_from_clipboard().map_or(false, |item| {
|
||||
item.text().as_str() == info.url.as_ref()
|
||||
});
|
||||
|
||||
Label::new(
|
||||
format!(
|
||||
"{} invite link ({} left)",
|
||||
if copied { "Copied" } else { "Copy" },
|
||||
info.count
|
||||
),
|
||||
style.label.clone(),
|
||||
)
|
||||
.aligned()
|
||||
.left()
|
||||
.constrained()
|
||||
.with_height(theme.contacts_popover.invite_row_height)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new(
|
||||
info.url.to_string(),
|
||||
));
|
||||
cx.notify();
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.contacts_popover.container)
|
||||
.constrained()
|
||||
|
||||
@@ -108,7 +108,7 @@ impl IncomingCallNotification {
|
||||
.unwrap_or(&default_project);
|
||||
Flex::row()
|
||||
.with_children(self.call.calling_user.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.caller_avatar)
|
||||
.aligned()
|
||||
.boxed()
|
||||
|
||||
@@ -24,7 +24,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.header_avatar)
|
||||
.aligned()
|
||||
.constrained()
|
||||
|
||||
@@ -108,7 +108,7 @@ impl ProjectSharedNotification {
|
||||
let theme = &cx.global::<Settings>().theme.project_shared_notification;
|
||||
Flex::row()
|
||||
.with_children(self.owner.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.owner_avatar)
|
||||
.aligned()
|
||||
.boxed()
|
||||
|
||||
@@ -352,9 +352,7 @@ mod tests {
|
||||
});
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let editor = cx.add_view(&workspace, |cx| {
|
||||
let mut editor = Editor::single_line(None, cx);
|
||||
editor.set_text("abc", cx);
|
||||
|
||||
@@ -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,6 +61,7 @@ pub struct ContextMenu {
|
||||
show_count: usize,
|
||||
anchor_position: Vector2F,
|
||||
anchor_corner: AnchorCorner,
|
||||
position_mode: OverlayPositionMode,
|
||||
items: Vec<ContextMenuItem>,
|
||||
selected_index: Option<usize>,
|
||||
visible: bool,
|
||||
@@ -78,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
|
||||
}
|
||||
|
||||
@@ -105,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()
|
||||
}
|
||||
|
||||
@@ -121,6 +126,7 @@ impl ContextMenu {
|
||||
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(),
|
||||
@@ -188,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;
|
||||
@@ -205,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;
|
||||
@@ -219,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;
|
||||
@@ -234,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();
|
||||
@@ -254,6 +260,10 @@ 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();
|
||||
@@ -273,6 +283,9 @@ impl ContextMenu {
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
ContextMenuItem::Static(f) => f(cx),
|
||||
|
||||
ContextMenuItem::Separator => Empty::new()
|
||||
.collapsed()
|
||||
.contained()
|
||||
@@ -302,6 +315,9 @@ impl ContextMenu {
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
ContextMenuItem::Static(_) => Empty::new().boxed(),
|
||||
|
||||
ContextMenuItem::Separator => Empty::new()
|
||||
.collapsed()
|
||||
.constrained()
|
||||
@@ -339,7 +355,7 @@ impl ContextMenu {
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(label.to_string(), style.label.clone())
|
||||
Label::new(label.clone(), style.label.clone())
|
||||
.contained()
|
||||
.boxed(),
|
||||
)
|
||||
@@ -366,6 +382,9 @@ impl ContextMenu {
|
||||
.on_drag(MouseButton::Left, |_, _| {})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
ContextMenuItem::Static(f) => f(cx),
|
||||
|
||||
ContextMenuItem::Separator => Empty::new()
|
||||
.constrained()
|
||||
.with_height(1.)
|
||||
|
||||
@@ -4,6 +4,7 @@ pub mod query;
|
||||
// Re-export
|
||||
pub use anyhow;
|
||||
use anyhow::Context;
|
||||
use gpui::MutableAppContext;
|
||||
pub use indoc::indoc;
|
||||
pub use lazy_static;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
@@ -17,6 +18,7 @@ use sqlez::domain::Migrator;
|
||||
use sqlez::thread_safe_connection::ThreadSafeConnection;
|
||||
use sqlez_macros::sql;
|
||||
use std::fs::create_dir_all;
|
||||
use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
@@ -39,6 +41,7 @@ const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB";
|
||||
const DB_FILE_NAME: &'static str = "db.sqlite";
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
// !!!!!!! CHANGE BACK TO DEFAULT FALSE BEFORE SHIPPING
|
||||
static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
|
||||
static ref DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
|
||||
pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
|
||||
@@ -63,11 +66,11 @@ pub async fn open_db<M: Migrator + 'static>(
|
||||
let connection = async_iife!({
|
||||
// Note: This still has a race condition where 1 set of migrations succeeds
|
||||
// (e.g. (Workspace, Editor)) and another fails (e.g. (Workspace, Terminal))
|
||||
// This will cause the first connection to have the database taken out
|
||||
// This will cause the first connection to have the database taken out
|
||||
// from under it. This *should* be fine though. The second dabatase failure will
|
||||
// cause errors in the log and so should be observed by developers while writing
|
||||
// soon-to-be good migrations. If user databases are corrupted, we toss them out
|
||||
// and try again from a blank. As long as running all migrations from start to end
|
||||
// and try again from a blank. As long as running all migrations from start to end
|
||||
// on a blank database is ok, this race condition will never be triggered.
|
||||
//
|
||||
// Basically: Don't ever push invalid migrations to stable or everyone will have
|
||||
@@ -85,7 +88,7 @@ pub async fn open_db<M: Migrator + 'static>(
|
||||
};
|
||||
}
|
||||
|
||||
// Take a lock in the failure case so that we move the db once per process instead
|
||||
// Take a lock in the failure case so that we move the db once per process instead
|
||||
// of potentially multiple times from different threads. This shouldn't happen in the
|
||||
// normal path
|
||||
let _lock = DB_FILE_OPERATIONS.lock();
|
||||
@@ -236,6 +239,15 @@ macro_rules! define_connection {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write_and_log<F>(cx: &mut MutableAppContext, db_write: impl FnOnce() -> F + Send + 'static)
|
||||
where
|
||||
F: Future<Output = anyhow::Result<()>> + Send,
|
||||
{
|
||||
cx.background()
|
||||
.spawn(async move { db_write().await.log_err() })
|
||||
.detach()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs, thread};
|
||||
|
||||
@@ -805,15 +805,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project.clone(),
|
||||
|_, _| unimplemented!(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
// Create some diagnostics
|
||||
project.update(cx, |project, cx| {
|
||||
|
||||
@@ -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))
|
||||
@@ -591,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,
|
||||
@@ -678,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::*;
|
||||
@@ -1167,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())),
|
||||
@@ -1248,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()))
|
||||
]
|
||||
|
||||
@@ -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;
|
||||
@@ -160,6 +161,21 @@ pub struct ToggleComments {
|
||||
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,
|
||||
[
|
||||
@@ -258,6 +274,9 @@ impl_actions!(
|
||||
ConfirmCompletion,
|
||||
ConfirmCodeAction,
|
||||
ToggleComments,
|
||||
FoldAt,
|
||||
UnfoldAt,
|
||||
GutterHover
|
||||
]
|
||||
);
|
||||
|
||||
@@ -348,7 +367,10 @@ 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);
|
||||
@@ -480,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>,
|
||||
}
|
||||
@@ -1151,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),
|
||||
@@ -1230,6 +1254,15 @@ impl Editor {
|
||||
self.buffer.read(cx).language_at(point, cx)
|
||||
}
|
||||
|
||||
pub fn active_excerpt(
|
||||
&self,
|
||||
cx: &AppContext,
|
||||
) -> Option<(ExcerptId, ModelHandle<Buffer>, Range<text::Anchor>)> {
|
||||
self.buffer
|
||||
.read(cx)
|
||||
.excerpt_containing(self.selections.newest_anchor().head(), cx)
|
||||
}
|
||||
|
||||
fn style(&self, cx: &AppContext) -> EditorStyle {
|
||||
build_style(
|
||||
cx.global::<Settings>(),
|
||||
@@ -2645,14 +2678,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)
|
||||
@@ -2669,6 +2703,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(gutter_margin * style.icon_margin_scale)
|
||||
.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()
|
||||
@@ -3251,26 +3359,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 {
|
||||
@@ -3335,13 +3429,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);
|
||||
})
|
||||
@@ -3363,26 +3457,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 {
|
||||
@@ -3440,13 +3520,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));
|
||||
});
|
||||
}
|
||||
@@ -4274,7 +4354,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);
|
||||
});
|
||||
@@ -4423,7 +4503,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);
|
||||
@@ -4456,7 +4536,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);
|
||||
});
|
||||
@@ -5459,21 +5539,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! {
|
||||
@@ -5677,14 +5756,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() {
|
||||
@@ -5695,7 +5778,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>) {
|
||||
@@ -5713,85 +5815,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>>,
|
||||
@@ -6253,6 +6356,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)
|
||||
@@ -6324,6 +6456,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)
|
||||
});
|
||||
|
||||
@@ -6433,17 +6566,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 => {}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
use drag_and_drop::DragAndDrop;
|
||||
use futures::StreamExt;
|
||||
use indoc::indoc;
|
||||
use std::{cell::RefCell, rc::Rc, time::Instant};
|
||||
use unindent::Unindent;
|
||||
|
||||
use super::*;
|
||||
use crate::test::{
|
||||
assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
|
||||
editor_test_context::EditorTestContext, select_ranges,
|
||||
};
|
||||
use drag_and_drop::DragAndDrop;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
executor::Deterministic,
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
platform::{WindowBounds, WindowOptions},
|
||||
serde_json,
|
||||
};
|
||||
use indoc::indoc;
|
||||
use language::{BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point};
|
||||
use parking_lot::Mutex;
|
||||
use project::FakeFs;
|
||||
use settings::EditorSettings;
|
||||
use std::{cell::RefCell, rc::Rc, time::Instant};
|
||||
use unindent::Unindent;
|
||||
use util::{
|
||||
assert_set_eq,
|
||||
test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
|
||||
@@ -447,6 +447,7 @@ fn test_clone(cx: &mut gpui::MutableAppContext) {
|
||||
Point::new(1, 0)..Point::new(2, 0),
|
||||
Point::new(3, 0)..Point::new(4, 0),
|
||||
],
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -483,7 +484,7 @@ fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
cx.set_global(DragAndDrop::<Workspace>::default());
|
||||
use workspace::item::Item;
|
||||
let (_, pane) = cx.add_window(Default::default(), |cx| Pane::new(None, cx));
|
||||
let (_, pane) = cx.add_window(Default::default(), |cx| Pane::new(0, None, || &[], cx));
|
||||
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
|
||||
|
||||
cx.add_view(&pane, |cx| {
|
||||
@@ -669,10 +670,10 @@ fn test_fold(cx: &mut gpui::MutableAppContext) {
|
||||
1
|
||||
}
|
||||
|
||||
fn b() {…
|
||||
fn b() {⋯
|
||||
}
|
||||
|
||||
fn c() {…
|
||||
fn c() {⋯
|
||||
}
|
||||
}
|
||||
"
|
||||
@@ -683,7 +684,7 @@ fn test_fold(cx: &mut gpui::MutableAppContext) {
|
||||
assert_eq!(
|
||||
view.display_text(cx),
|
||||
"
|
||||
impl Foo {…
|
||||
impl Foo {⋯
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
@@ -700,10 +701,10 @@ fn test_fold(cx: &mut gpui::MutableAppContext) {
|
||||
1
|
||||
}
|
||||
|
||||
fn b() {…
|
||||
fn b() {⋯
|
||||
}
|
||||
|
||||
fn c() {…
|
||||
fn c() {⋯
|
||||
}
|
||||
}
|
||||
"
|
||||
@@ -807,9 +808,10 @@ fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) {
|
||||
Point::new(1, 2)..Point::new(1, 4),
|
||||
Point::new(2, 4)..Point::new(2, 8),
|
||||
],
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
assert_eq!(view.display_text(cx), "ⓐⓑ…ⓔ\nab…e\nαβ…ε\n");
|
||||
assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\nαβ⋯ε\n");
|
||||
|
||||
view.move_right(&MoveRight, cx);
|
||||
assert_eq!(
|
||||
@@ -824,13 +826,13 @@ fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) {
|
||||
view.move_right(&MoveRight, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
&[empty_range(0, "ⓐⓑ…".len())]
|
||||
&[empty_range(0, "ⓐⓑ⋯".len())]
|
||||
);
|
||||
|
||||
view.move_down(&MoveDown, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
&[empty_range(1, "ab…".len())]
|
||||
&[empty_range(1, "ab⋯".len())]
|
||||
);
|
||||
view.move_left(&MoveLeft, cx);
|
||||
assert_eq!(
|
||||
@@ -856,28 +858,28 @@ fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) {
|
||||
view.move_right(&MoveRight, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
&[empty_range(2, "αβ…".len())]
|
||||
&[empty_range(2, "αβ⋯".len())]
|
||||
);
|
||||
view.move_right(&MoveRight, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
&[empty_range(2, "αβ…ε".len())]
|
||||
&[empty_range(2, "αβ⋯ε".len())]
|
||||
);
|
||||
|
||||
view.move_up(&MoveUp, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
&[empty_range(1, "ab…e".len())]
|
||||
&[empty_range(1, "ab⋯e".len())]
|
||||
);
|
||||
view.move_up(&MoveUp, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
&[empty_range(0, "ⓐⓑ…ⓔ".len())]
|
||||
&[empty_range(0, "ⓐⓑ⋯ⓔ".len())]
|
||||
);
|
||||
view.move_left(&MoveLeft, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
&[empty_range(0, "ⓐⓑ…".len())]
|
||||
&[empty_range(0, "ⓐⓑ⋯".len())]
|
||||
);
|
||||
view.move_left(&MoveLeft, cx);
|
||||
assert_eq!(
|
||||
@@ -2119,6 +2121,7 @@ fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) {
|
||||
Point::new(2, 3)..Point::new(4, 1),
|
||||
Point::new(7, 0)..Point::new(8, 4),
|
||||
],
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
view.change_selections(None, cx, |s| {
|
||||
@@ -2131,13 +2134,13 @@ fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) {
|
||||
});
|
||||
assert_eq!(
|
||||
view.display_text(cx),
|
||||
"aa…bbb\nccc…eeee\nfffff\nggggg\n…i\njjjjj"
|
||||
"aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
|
||||
);
|
||||
|
||||
view.move_line_up(&MoveLineUp, cx);
|
||||
assert_eq!(
|
||||
view.display_text(cx),
|
||||
"aa…bbb\nccc…eeee\nggggg\n…i\njjjjj\nfffff"
|
||||
"aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
|
||||
);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
@@ -2154,7 +2157,7 @@ fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) {
|
||||
view.move_line_down(&MoveLineDown, cx);
|
||||
assert_eq!(
|
||||
view.display_text(cx),
|
||||
"ccc…eeee\naa…bbb\nfffff\nggggg\n…i\njjjjj"
|
||||
"ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
|
||||
);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
@@ -2171,7 +2174,7 @@ fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) {
|
||||
view.move_line_down(&MoveLineDown, cx);
|
||||
assert_eq!(
|
||||
view.display_text(cx),
|
||||
"ccc…eeee\nfffff\naa…bbb\nggggg\n…i\njjjjj"
|
||||
"ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
|
||||
);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
@@ -2188,7 +2191,7 @@ fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) {
|
||||
view.move_line_up(&MoveLineUp, cx);
|
||||
assert_eq!(
|
||||
view.display_text(cx),
|
||||
"ccc…eeee\naa…bbb\nggggg\n…i\njjjjj\nfffff"
|
||||
"ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
|
||||
);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
@@ -2350,12 +2353,16 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) {
|
||||
e.paste(&Paste, cx);
|
||||
e.handle_input(") ", cx);
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
( one✅
|
||||
three
|
||||
five ) ˇtwo one✅ four three six five ( one✅
|
||||
three
|
||||
five ) ˇ"});
|
||||
cx.assert_editor_state(
|
||||
&([
|
||||
"( one✅ ",
|
||||
"three ",
|
||||
"five ) ˇtwo one✅ four three six five ( one✅ ",
|
||||
"three ",
|
||||
"five ) ˇ",
|
||||
]
|
||||
.join("\n")),
|
||||
);
|
||||
|
||||
// Cut with three selections, one of which is full-line.
|
||||
cx.set_state(indoc! {"
|
||||
@@ -2586,6 +2593,7 @@ fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) {
|
||||
Point::new(2, 3)..Point::new(4, 1),
|
||||
Point::new(7, 0)..Point::new(8, 4),
|
||||
],
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
view.change_selections(None, cx, |s| {
|
||||
@@ -2596,14 +2604,14 @@ fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) {
|
||||
DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4),
|
||||
])
|
||||
});
|
||||
assert_eq!(view.display_text(cx), "aa…bbb\nccc…eeee\nfffff\nggggg\n…i");
|
||||
assert_eq!(view.display_text(cx), "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i");
|
||||
});
|
||||
|
||||
view.update(cx, |view, cx| {
|
||||
view.split_selection_into_lines(&SplitSelectionIntoLines, cx);
|
||||
assert_eq!(
|
||||
view.display_text(cx),
|
||||
"aaaaa\nbbbbb\nccc…eeee\nfffff\nggggg\n…i"
|
||||
"aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
|
||||
);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
@@ -2983,6 +2991,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||
Point::new(0, 21)..Point::new(0, 24),
|
||||
Point::new(3, 20)..Point::new(3, 22),
|
||||
],
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||
@@ -4193,7 +4202,9 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
||||
let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
|
||||
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
||||
|
||||
let format = editor.update(cx, |editor, cx| editor.perform_format(project.clone(), cx));
|
||||
let format = editor.update(cx, |editor, cx| {
|
||||
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
|
||||
});
|
||||
fake_server
|
||||
.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
|
||||
assert_eq!(
|
||||
@@ -4225,7 +4236,9 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
||||
futures::future::pending::<()>().await;
|
||||
unreachable!()
|
||||
});
|
||||
let format = editor.update(cx, |editor, cx| editor.perform_format(project, cx));
|
||||
let format = editor.update(cx, |editor, cx| {
|
||||
editor.perform_format(project, FormatTrigger::Manual, cx)
|
||||
});
|
||||
cx.foreground().advance_clock(super::FORMAT_TIMEOUT);
|
||||
cx.foreground().start_waiting();
|
||||
format.await.unwrap();
|
||||
@@ -4292,6 +4305,121 @@ async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
document_formatting_provider: Some(lsp::OneOf::Left(true)),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Set up a buffer white some trailing whitespace and no trailing newline.
|
||||
cx.set_state(
|
||||
&[
|
||||
"one ", //
|
||||
"twoˇ", //
|
||||
"three ", //
|
||||
"four", //
|
||||
]
|
||||
.join("\n"),
|
||||
);
|
||||
|
||||
// Submit a format request.
|
||||
let format = cx
|
||||
.update_editor(|editor, cx| editor.format(&Format, cx))
|
||||
.unwrap();
|
||||
|
||||
// Record which buffer changes have been sent to the language server
|
||||
let buffer_changes = Arc::new(Mutex::new(Vec::new()));
|
||||
cx.lsp
|
||||
.handle_notification::<lsp::notification::DidChangeTextDocument, _>({
|
||||
let buffer_changes = buffer_changes.clone();
|
||||
move |params, _| {
|
||||
buffer_changes.lock().extend(
|
||||
params
|
||||
.content_changes
|
||||
.into_iter()
|
||||
.map(|e| (e.range.unwrap(), e.text)),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle formatting requests to the language server.
|
||||
cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
|
||||
let buffer_changes = buffer_changes.clone();
|
||||
move |_, _| {
|
||||
// When formatting is requested, trailing whitespace has already been stripped,
|
||||
// and the trailing newline has already been added.
|
||||
assert_eq!(
|
||||
&buffer_changes.lock()[1..],
|
||||
&[
|
||||
(
|
||||
lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
|
||||
"".into()
|
||||
),
|
||||
(
|
||||
lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
|
||||
"".into()
|
||||
),
|
||||
(
|
||||
lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
|
||||
"\n".into()
|
||||
),
|
||||
]
|
||||
);
|
||||
|
||||
// Insert blank lines between each line of the buffer.
|
||||
async move {
|
||||
Ok(Some(vec![
|
||||
lsp::TextEdit {
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
|
||||
new_text: "\n".into(),
|
||||
},
|
||||
lsp::TextEdit {
|
||||
range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
|
||||
new_text: "\n".into(),
|
||||
},
|
||||
]))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// After formatting the buffer, the trailing whitespace is stripped,
|
||||
// a newline is appended, and the edits provided by the language server
|
||||
// have been applied.
|
||||
format.await.unwrap();
|
||||
cx.assert_editor_state(
|
||||
&[
|
||||
"one", //
|
||||
"", //
|
||||
"twoˇ", //
|
||||
"", //
|
||||
"three", //
|
||||
"four", //
|
||||
"", //
|
||||
]
|
||||
.join("\n"),
|
||||
);
|
||||
|
||||
// Undoing the formatting undoes the trailing whitespace removal, the
|
||||
// trailing newline, and the LSP edits.
|
||||
cx.update_buffer(|buffer, cx| buffer.undo(cx));
|
||||
cx.assert_editor_state(
|
||||
&[
|
||||
"one ", //
|
||||
"twoˇ", //
|
||||
"three ", //
|
||||
"four", //
|
||||
]
|
||||
.join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
@@ -5438,7 +5566,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
|
||||
Settings::test_async(cx);
|
||||
let fs = FakeFs::new(cx.background());
|
||||
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
|
||||
let (_, pane) = cx.add_window(|cx| Pane::new(None, cx));
|
||||
let (_, pane) = cx.add_window(|cx| Pane::new(0, None, || &[], cx));
|
||||
|
||||
let leader = pane.update(cx, |_, cx| {
|
||||
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
|
||||
@@ -5707,11 +5835,11 @@ async fn go_to_hunk(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppCon
|
||||
cx.assert_editor_state(
|
||||
&r#"
|
||||
ˇuse some::modified;
|
||||
|
||||
|
||||
|
||||
|
||||
fn main() {
|
||||
println!("hello there");
|
||||
|
||||
|
||||
println!("around the");
|
||||
println!("world");
|
||||
}
|
||||
|
||||
@@ -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("…", text_style).boxed())
|
||||
.with_child(Label::new("⋯", text_style).boxed())
|
||||
.with_children(jump_icon)
|
||||
.contained()
|
||||
.with_padding_left(gutter_padding)
|
||||
@@ -1606,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();
|
||||
@@ -1616,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
|
||||
@@ -1684,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;
|
||||
@@ -1755,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)
|
||||
@@ -1769,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() {
|
||||
@@ -1802,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(
|
||||
@@ -1845,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,
|
||||
},
|
||||
)
|
||||
@@ -1958,6 +2099,8 @@ impl Element for EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
type BufferRow = u32;
|
||||
|
||||
pub struct LayoutState {
|
||||
position_map: Arc<PositionMap>,
|
||||
gutter_size: Vector2F,
|
||||
@@ -1972,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,
|
||||
@@ -1979,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 {
|
||||
@@ -2277,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
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{Flex, MouseEventHandler, Padding, Text},
|
||||
@@ -327,12 +328,10 @@ impl InfoPopover {
|
||||
MouseEventHandler::<InfoPopover>::new(0, cx, |_, cx| {
|
||||
let mut flex = Flex::new(Axis::Vertical).scrollable::<HoverBlock, _>(1, None, cx);
|
||||
flex.extend(self.contents.iter().map(|content| {
|
||||
let project = self.project.read(cx);
|
||||
if let Some(language) = content
|
||||
.language
|
||||
.clone()
|
||||
.and_then(|language| project.languages().language_for_name(&language))
|
||||
{
|
||||
let languages = self.project.read(cx).languages();
|
||||
if let Some(language) = content.language.clone().and_then(|language| {
|
||||
languages.language_for_name(&language).now_or_never()?.ok()
|
||||
}) {
|
||||
let runs = language
|
||||
.highlight_text(&content.text.as_str().into(), 0..content.text.len());
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ use language::{
|
||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
|
||||
SelectionGoal,
|
||||
};
|
||||
use project::{Item as _, Project, ProjectPath};
|
||||
use project::{FormatTrigger, Item as _, Project, ProjectPath};
|
||||
use rpc::proto::{self, update_view};
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
@@ -608,13 +608,38 @@ impl Item for Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.report_event("save editor", cx);
|
||||
let format = self.perform_format(project.clone(), 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?;
|
||||
|
||||
if buffers.len() == 1 {
|
||||
project
|
||||
.update(&mut cx, |project, cx| project.save_buffers(buffers, cx))
|
||||
.await?;
|
||||
} else {
|
||||
// For multi-buffers, only save those ones that contain changes. For clean buffers
|
||||
// we simulate saving by calling `Buffer::did_save`, so that language servers or
|
||||
// other downstream listeners of save events get notified.
|
||||
let (dirty_buffers, clean_buffers) = buffers.into_iter().partition(|buffer| {
|
||||
buffer.read_with(&cx, |buffer, _| buffer.is_dirty() || buffer.has_conflict())
|
||||
});
|
||||
|
||||
project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.save_buffers(dirty_buffers, cx)
|
||||
})
|
||||
.await?;
|
||||
for buffer in clean_buffers {
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
let version = buffer.saved_version().clone();
|
||||
let fingerprint = buffer.saved_version_fingerprint();
|
||||
let mtime = buffer.saved_mtime();
|
||||
buffer.did_save(version, fingerprint, mtime, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -886,7 +911,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()])
|
||||
});
|
||||
|
||||
@@ -1082,18 +1082,21 @@ impl MultiBuffer {
|
||||
|
||||
let mut cursor = snapshot.excerpts.cursor::<usize>();
|
||||
cursor.seek(&position, Bias::Right, &());
|
||||
cursor.item().map(|excerpt| {
|
||||
(
|
||||
excerpt.id.clone(),
|
||||
self.buffers
|
||||
.borrow()
|
||||
.get(&excerpt.buffer_id)
|
||||
.unwrap()
|
||||
.buffer
|
||||
.clone(),
|
||||
excerpt.range.context.clone(),
|
||||
)
|
||||
})
|
||||
cursor
|
||||
.item()
|
||||
.or_else(|| snapshot.excerpts.last())
|
||||
.map(|excerpt| {
|
||||
(
|
||||
excerpt.id.clone(),
|
||||
self.buffers
|
||||
.borrow()
|
||||
.get(&excerpt.buffer_id)
|
||||
.unwrap()
|
||||
.buffer
|
||||
.clone(),
|
||||
excerpt.range.context.clone(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// If point is at the end of the buffer, the last excerpt is returned
|
||||
|
||||
@@ -39,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.{}",
|
||||
@@ -56,24 +56,16 @@ 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.clone(): "" }}))
|
||||
.await;
|
||||
|
||||
let (window_id, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project.clone(),
|
||||
|_, _| unimplemented!(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("/root", true, cx)
|
||||
@@ -134,7 +126,7 @@ impl<'a> EditorLspTestContext<'a> {
|
||||
(let_chain)
|
||||
(await_expression)
|
||||
] @indent
|
||||
|
||||
|
||||
(_ "[" "]" @end) @indent
|
||||
(_ "<" ">" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
|
||||
@@ -185,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();
|
||||
|
||||
@@ -31,4 +31,4 @@ theme = { path = "../theme" }
|
||||
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
|
||||
urlencoding = "2.1.2"
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
workspace = { path = "../workspace" }
|
||||
|
||||
@@ -1,34 +1,66 @@
|
||||
use gpui::{
|
||||
elements::{MouseEventHandler, ParentElement, Stack, Text},
|
||||
CursorStyle, Element, ElementBox, Entity, MouseButton, RenderContext, View, ViewContext,
|
||||
};
|
||||
use gpui::{elements::*, CursorStyle, Entity, MouseButton, RenderContext, View, ViewContext};
|
||||
use settings::Settings;
|
||||
use workspace::{item::ItemHandle, StatusItemView};
|
||||
|
||||
use crate::feedback_editor::GiveFeedback;
|
||||
use crate::feedback_editor::{FeedbackEditor, GiveFeedback};
|
||||
|
||||
pub struct DeployFeedbackButton;
|
||||
pub struct DeployFeedbackButton {
|
||||
active: bool,
|
||||
}
|
||||
|
||||
impl Entity for DeployFeedbackButton {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl DeployFeedbackButton {
|
||||
pub fn new() -> Self {
|
||||
DeployFeedbackButton { active: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl View for DeployFeedbackButton {
|
||||
fn ui_name() -> &'static str {
|
||||
"DeployFeedbackButton"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
|
||||
let active = self.active;
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<Self>::new(0, cx, |state, cx| {
|
||||
let theme = &cx.global::<Settings>().theme;
|
||||
let theme = &theme.workspace.status_bar.feedback;
|
||||
MouseEventHandler::<Self>::new(0, cx, |state, _| {
|
||||
let style = &theme
|
||||
.workspace
|
||||
.status_bar
|
||||
.sidebar_buttons
|
||||
.item
|
||||
.style_for(state, active);
|
||||
|
||||
Text::new("Give Feedback", theme.style_for(state, true).clone()).boxed()
|
||||
Svg::new("icons/speech_bubble_12.svg")
|
||||
.with_color(style.icon_color)
|
||||
.constrained()
|
||||
.with_width(style.icon_size)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(style.icon_size)
|
||||
.with_height(style.icon_size)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(GiveFeedback))
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
if !active {
|
||||
cx.dispatch_action(GiveFeedback)
|
||||
}
|
||||
})
|
||||
.with_tooltip::<Self, _>(
|
||||
0,
|
||||
"Give Feedback".into(),
|
||||
Some(Box::new(GiveFeedback)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed()
|
||||
@@ -36,5 +68,15 @@ impl View for DeployFeedbackButton {
|
||||
}
|
||||
|
||||
impl StatusItemView for DeployFeedbackButton {
|
||||
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
|
||||
fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
|
||||
if let Some(item) = item {
|
||||
if let Some(_) = item.downcast::<FeedbackEditor>() {
|
||||
self.active = true;
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.active = false;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,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) {
|
||||
@@ -66,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() });
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ use postage::prelude::Stream;
|
||||
|
||||
use project::Project;
|
||||
use serde::Serialize;
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
item::{Item, ItemHandle},
|
||||
searchable::{SearchableItem, SearchableItemHandle},
|
||||
@@ -200,24 +201,28 @@ impl FeedbackEditor {
|
||||
impl FeedbackEditor {
|
||||
pub fn deploy(
|
||||
system_specs: SystemSpecs,
|
||||
workspace: &mut Workspace,
|
||||
_: &mut Workspace,
|
||||
app_state: Arc<AppState>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
workspace
|
||||
.with_local_workspace(&app_state, cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let markdown_language = project.read(cx).languages().language_for_name("Markdown");
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_buffer("", markdown_language, cx)
|
||||
let markdown = app_state.languages.language_for_name("Markdown");
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let markdown = markdown.await.log_err();
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.with_local_workspace(&app_state, cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.create_buffer("", markdown, cx))
|
||||
.expect("creating buffers on a local workspace always succeeds");
|
||||
let feedback_editor = cx
|
||||
.add_view(|cx| FeedbackEditor::new(system_specs, project, buffer, cx));
|
||||
workspace.add_item(Box::new(feedback_editor), cx);
|
||||
})
|
||||
.expect("creating buffers on a local workspace always succeeds");
|
||||
let feedback_editor =
|
||||
cx.add_view(|cx| FeedbackEditor::new(system_specs, project, buffer, cx));
|
||||
workspace.add_item(Box::new(feedback_editor), cx);
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.await;
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use gpui::{
|
||||
elements::Label, Element, ElementBox, Entity, RenderContext, View, ViewContext, ViewHandle,
|
||||
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;
|
||||
use crate::{feedback_editor::FeedbackEditor, OpenZedCommunityRepo};
|
||||
|
||||
pub struct FeedbackInfoText {
|
||||
active_item: Option<ViewHandle<FeedbackEditor>>,
|
||||
@@ -29,9 +31,44 @@ impl View for FeedbackInfoText {
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let text = "We read whatever you submit here. For issues and discussions, visit the community repo on GitHub.";
|
||||
Label::new(text, theme.feedback.info_text.text.clone())
|
||||
.contained()
|
||||
|
||||
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()
|
||||
|
||||
@@ -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,
|
||||
@@ -317,9 +329,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
cx.dispatch_action(window_id, Toggle);
|
||||
|
||||
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
|
||||
@@ -373,11 +383,9 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, 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
|
||||
@@ -449,11 +457,9 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, 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;
|
||||
@@ -475,11 +481,9 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, 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.
|
||||
@@ -529,11 +533,10 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, 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 +554,46 @@ 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::test_new(project, 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);
|
||||
@@ -569,11 +612,9 @@ mod tests {
|
||||
.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)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, 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(""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ pub trait View: Entity + Sized {
|
||||
}
|
||||
fn default_keymap_context() -> keymap_matcher::KeymapContext {
|
||||
let mut cx = keymap_matcher::KeymapContext::default();
|
||||
cx.set.insert(Self::ui_name().into());
|
||||
cx.add_identifier(Self::ui_name());
|
||||
cx
|
||||
}
|
||||
fn debug_json(&self, _: &AppContext) -> serde_json::Value {
|
||||
@@ -485,7 +485,9 @@ pub struct MutableAppContext {
|
||||
cx: AppContext,
|
||||
action_deserializers: HashMap<&'static str, (TypeId, DeserializeActionCallback)>,
|
||||
capture_actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
|
||||
// Entity Types -> { Action Types -> Action Handlers }
|
||||
actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
|
||||
// Action Types -> Action Handlers
|
||||
global_actions: HashMap<TypeId, Box<GlobalActionCallback>>,
|
||||
keystroke_matcher: KeymapMatcher,
|
||||
next_entity_id: usize,
|
||||
@@ -586,17 +588,20 @@ impl MutableAppContext {
|
||||
|
||||
pub fn quit(&mut self) {
|
||||
let mut futures = Vec::new();
|
||||
for model_id in self.cx.models.keys().copied().collect::<Vec<_>>() {
|
||||
let mut model = self.cx.models.remove(&model_id).unwrap();
|
||||
futures.extend(model.app_will_quit(self));
|
||||
self.cx.models.insert(model_id, model);
|
||||
}
|
||||
|
||||
for view_id in self.cx.views.keys().copied().collect::<Vec<_>>() {
|
||||
let mut view = self.cx.views.remove(&view_id).unwrap();
|
||||
futures.extend(view.app_will_quit(self));
|
||||
self.cx.views.insert(view_id, view);
|
||||
}
|
||||
self.update(|cx| {
|
||||
for model_id in cx.models.keys().copied().collect::<Vec<_>>() {
|
||||
let mut model = cx.cx.models.remove(&model_id).unwrap();
|
||||
futures.extend(model.app_will_quit(cx));
|
||||
cx.cx.models.insert(model_id, model);
|
||||
}
|
||||
|
||||
for view_id in cx.views.keys().copied().collect::<Vec<_>>() {
|
||||
let mut view = cx.cx.views.remove(&view_id).unwrap();
|
||||
futures.extend(view.app_will_quit(cx));
|
||||
cx.cx.views.insert(view_id, view);
|
||||
}
|
||||
});
|
||||
|
||||
self.remove_all_windows();
|
||||
|
||||
@@ -1239,20 +1244,34 @@ impl MutableAppContext {
|
||||
action: &dyn Action,
|
||||
) -> Option<SmallVec<[Keystroke; 2]>> {
|
||||
let mut contexts = Vec::new();
|
||||
for view_id in self.ancestors(window_id, view_id) {
|
||||
let mut handler_depth = None;
|
||||
for (i, view_id) in self.ancestors(window_id, view_id).enumerate() {
|
||||
if let Some(view) = self.views.get(&(window_id, view_id)) {
|
||||
if let Some(actions) = self.actions.get(&view.as_any().type_id()) {
|
||||
if actions.contains_key(&action.as_any().type_id()) {
|
||||
handler_depth = Some(i);
|
||||
}
|
||||
}
|
||||
contexts.push(view.keymap_context(self));
|
||||
}
|
||||
}
|
||||
|
||||
if self.global_actions.contains_key(&action.as_any().type_id()) {
|
||||
handler_depth = Some(contexts.len())
|
||||
}
|
||||
|
||||
self.keystroke_matcher
|
||||
.bindings_for_action_type(action.as_any().type_id())
|
||||
.find_map(|b| {
|
||||
if b.match_context(&contexts) {
|
||||
Some(b.keystrokes().into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
handler_depth
|
||||
.map(|highest_handler| {
|
||||
if (0..=highest_handler).any(|depth| b.match_context(&contexts[depth..])) {
|
||||
Some(b.keystrokes().into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1261,29 +1280,42 @@ impl MutableAppContext {
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
) -> impl Iterator<Item = (&'static str, Box<dyn Action>, SmallVec<[&Binding; 1]>)> {
|
||||
let mut action_types: HashSet<_> = self.global_actions.keys().copied().collect();
|
||||
|
||||
let mut contexts = Vec::new();
|
||||
for view_id in self.ancestors(window_id, view_id) {
|
||||
let mut handler_depths_by_action_type = HashMap::<TypeId, usize>::default();
|
||||
for (depth, view_id) in self.ancestors(window_id, view_id).enumerate() {
|
||||
if let Some(view) = self.views.get(&(window_id, view_id)) {
|
||||
contexts.push(view.keymap_context(self));
|
||||
let view_type = view.as_any().type_id();
|
||||
if let Some(actions) = self.actions.get(&view_type) {
|
||||
action_types.extend(actions.keys().copied());
|
||||
handler_depths_by_action_type.extend(
|
||||
actions
|
||||
.keys()
|
||||
.copied()
|
||||
.map(|action_type| (action_type, depth)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handler_depths_by_action_type.extend(
|
||||
self.global_actions
|
||||
.keys()
|
||||
.copied()
|
||||
.map(|action_type| (action_type, contexts.len())),
|
||||
);
|
||||
|
||||
self.action_deserializers
|
||||
.iter()
|
||||
.filter_map(move |(name, (type_id, deserialize))| {
|
||||
if action_types.contains(type_id) {
|
||||
if let Some(action_depth) = handler_depths_by_action_type.get(type_id).copied() {
|
||||
Some((
|
||||
*name,
|
||||
deserialize("{}").ok()?,
|
||||
self.keystroke_matcher
|
||||
.bindings_for_action_type(*type_id)
|
||||
.filter(|b| b.match_context(&contexts))
|
||||
.filter(|b| {
|
||||
(0..=action_depth).any(|depth| b.match_context(&contexts[depth..]))
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
} else {
|
||||
@@ -1294,7 +1326,7 @@ impl MutableAppContext {
|
||||
|
||||
pub fn is_action_available(&self, action: &dyn Action) -> bool {
|
||||
let action_type = action.as_any().type_id();
|
||||
if let Some(window_id) = self.cx.platform.key_window_id() {
|
||||
if let Some(window_id) = self.cx.platform.main_window_id() {
|
||||
if let Some(focused_view_id) = self.focused_view_id(window_id) {
|
||||
for view_id in self.ancestors(window_id, focused_view_id) {
|
||||
if let Some(view) = self.views.get(&(window_id, view_id)) {
|
||||
@@ -2728,6 +2760,12 @@ impl AppContext {
|
||||
Some(self.views.get(&(window_id, view_id))?.ui_name())
|
||||
}
|
||||
|
||||
pub fn view_type_id(&self, window_id: usize, view_id: usize) -> Option<TypeId> {
|
||||
self.views
|
||||
.get(&(window_id, view_id))
|
||||
.map(|view| view.as_any().type_id())
|
||||
}
|
||||
|
||||
pub fn background(&self) -> &Arc<executor::Background> {
|
||||
&self.background
|
||||
}
|
||||
@@ -4159,10 +4197,10 @@ pub struct RenderContext<'a, T: View> {
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MouseState {
|
||||
hovered: bool,
|
||||
clicked: Option<MouseButton>,
|
||||
accessed_hovered: bool,
|
||||
accessed_clicked: bool,
|
||||
pub(crate) hovered: bool,
|
||||
pub(crate) clicked: Option<MouseButton>,
|
||||
pub(crate) accessed_hovered: bool,
|
||||
pub(crate) accessed_clicked: bool,
|
||||
}
|
||||
|
||||
impl MouseState {
|
||||
@@ -5086,7 +5124,7 @@ impl<T: Entity> From<WeakModelHandle<T>> for AnyWeakModelHandle {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Copy)]
|
||||
pub struct WeakViewHandle<T> {
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
@@ -5287,6 +5325,7 @@ impl Subscription {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{actions, elements::*, impl_actions, MouseButton, MouseButtonEvent};
|
||||
use itertools::Itertools;
|
||||
use postage::{sink::Sink, stream::Stream};
|
||||
use serde::Deserialize;
|
||||
use smol::future::poll_once;
|
||||
@@ -6639,12 +6678,12 @@ mod tests {
|
||||
let mut view_1 = View::new(1);
|
||||
let mut view_2 = View::new(2);
|
||||
let mut view_3 = View::new(3);
|
||||
view_1.keymap_context.set.insert("a".into());
|
||||
view_2.keymap_context.set.insert("a".into());
|
||||
view_2.keymap_context.set.insert("b".into());
|
||||
view_3.keymap_context.set.insert("a".into());
|
||||
view_3.keymap_context.set.insert("b".into());
|
||||
view_3.keymap_context.set.insert("c".into());
|
||||
view_1.keymap_context.add_identifier("a");
|
||||
view_2.keymap_context.add_identifier("a");
|
||||
view_2.keymap_context.add_identifier("b");
|
||||
view_3.keymap_context.add_identifier("a");
|
||||
view_3.keymap_context.add_identifier("b");
|
||||
view_3.keymap_context.add_identifier("c");
|
||||
|
||||
let (window_id, view_1) = cx.add_window(Default::default(), |_| view_1);
|
||||
let view_2 = cx.add_view(&view_1, |_| view_2);
|
||||
@@ -6717,6 +6756,128 @@ mod tests {
|
||||
actions.borrow_mut().clear();
|
||||
}
|
||||
|
||||
#[crate::test(self)]
|
||||
fn test_keystrokes_for_action(cx: &mut MutableAppContext) {
|
||||
actions!(test, [Action1, Action2, GlobalAction]);
|
||||
|
||||
struct View1 {}
|
||||
struct View2 {}
|
||||
|
||||
impl Entity for View1 {
|
||||
type Event = ();
|
||||
}
|
||||
impl Entity for View2 {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl super::View for View1 {
|
||||
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
fn ui_name() -> &'static str {
|
||||
"View1"
|
||||
}
|
||||
}
|
||||
impl super::View for View2 {
|
||||
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
fn ui_name() -> &'static str {
|
||||
"View2"
|
||||
}
|
||||
}
|
||||
|
||||
let (window_id, view_1) = cx.add_window(Default::default(), |_| View1 {});
|
||||
let view_2 = cx.add_view(&view_1, |cx| {
|
||||
cx.focus_self();
|
||||
View2 {}
|
||||
});
|
||||
|
||||
cx.add_action(|_: &mut View1, _: &Action1, _cx| {});
|
||||
cx.add_action(|_: &mut View2, _: &Action2, _cx| {});
|
||||
cx.add_global_action(|_: &GlobalAction, _| {});
|
||||
|
||||
cx.add_bindings(vec![
|
||||
Binding::new("a", Action1, Some("View1")),
|
||||
Binding::new("b", Action2, Some("View1 > View2")),
|
||||
Binding::new("c", GlobalAction, Some("View3")), // View 3 does not exist
|
||||
]);
|
||||
|
||||
// Sanity check
|
||||
assert_eq!(
|
||||
cx.keystrokes_for_action(window_id, view_1.id(), &Action1)
|
||||
.unwrap()
|
||||
.as_slice(),
|
||||
&[Keystroke::parse("a").unwrap()]
|
||||
);
|
||||
assert_eq!(
|
||||
cx.keystrokes_for_action(window_id, view_2.id(), &Action2)
|
||||
.unwrap()
|
||||
.as_slice(),
|
||||
&[Keystroke::parse("b").unwrap()]
|
||||
);
|
||||
|
||||
// The 'a' keystroke propagates up the view tree from view_2
|
||||
// to view_1. The action, Action1, is handled by view_1.
|
||||
assert_eq!(
|
||||
cx.keystrokes_for_action(window_id, view_2.id(), &Action1)
|
||||
.unwrap()
|
||||
.as_slice(),
|
||||
&[Keystroke::parse("a").unwrap()]
|
||||
);
|
||||
|
||||
// Actions that are handled below the current view don't have bindings
|
||||
assert_eq!(
|
||||
cx.keystrokes_for_action(window_id, view_1.id(), &Action2),
|
||||
None
|
||||
);
|
||||
|
||||
// Actions that are handled in other branches of the tree should not have a binding
|
||||
assert_eq!(
|
||||
cx.keystrokes_for_action(window_id, view_2.id(), &GlobalAction),
|
||||
None
|
||||
);
|
||||
|
||||
// Produces a list of actions and keybindings
|
||||
fn available_actions(
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Vec<(&'static str, Vec<Keystroke>)> {
|
||||
cx.available_actions(window_id, view_id)
|
||||
.map(|(action_name, _, bindings)| {
|
||||
(
|
||||
action_name,
|
||||
bindings
|
||||
.iter()
|
||||
.map(|binding| binding.keystrokes()[0].clone())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
.sorted_by(|(name1, _), (name2, _)| name1.cmp(name2))
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Check that global actions do not have a binding, even if a binding does exist in another view
|
||||
assert_eq!(
|
||||
&available_actions(window_id, view_1.id(), cx),
|
||||
&[
|
||||
("test::Action1", vec![Keystroke::parse("a").unwrap()]),
|
||||
("test::GlobalAction", vec![])
|
||||
],
|
||||
);
|
||||
|
||||
// Check that view 1 actions and bindings are available even when called from view 2
|
||||
assert_eq!(
|
||||
&available_actions(window_id, view_2.id(), cx),
|
||||
&[
|
||||
("test::Action1", vec![Keystroke::parse("a").unwrap()]),
|
||||
("test::Action2", vec![Keystroke::parse("b").unwrap()]),
|
||||
("test::GlobalAction", vec![]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[crate::test(self)]
|
||||
async fn test_model_condition(cx: &mut TestAppContext) {
|
||||
struct Counter(usize);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -77,9 +77,9 @@ pub(crate) fn setup_menu_handlers(foreground_platform: &dyn ForegroundPlatform,
|
||||
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);
|
||||
if let Some(main_window_id) = cx.cx.platform.main_window_id() {
|
||||
if let Some(view_id) = cx.focused_view_id(main_window_id) {
|
||||
cx.handle_dispatch_action_from_effect(main_window_id, Some(view_id), action);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,10 @@ use smol::stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
executor, geometry::vector::Vector2F, keymap_matcher::Keystroke, platform, Action,
|
||||
AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent,
|
||||
ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith, ReadViewWith,
|
||||
RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle, WeakHandle,
|
||||
AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, Handle, InputHandler,
|
||||
KeyDownEvent, ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith,
|
||||
ReadViewWith, RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle,
|
||||
WeakHandle,
|
||||
};
|
||||
use collections::BTreeMap;
|
||||
|
||||
@@ -329,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
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::{borrow::Cow, cell::RefCell, collections::HashMap};
|
||||
use image::ImageFormat;
|
||||
use std::{borrow::Cow, cell::RefCell, collections::HashMap, sync::Arc};
|
||||
|
||||
use crate::ImageData;
|
||||
|
||||
pub trait AssetSource: 'static + Send + Sync {
|
||||
fn load(&self, path: &str) -> Result<Cow<[u8]>>;
|
||||
@@ -22,6 +25,7 @@ impl AssetSource for () {
|
||||
pub struct AssetCache {
|
||||
source: Box<dyn AssetSource>,
|
||||
svgs: RefCell<HashMap<String, usvg::Tree>>,
|
||||
pngs: RefCell<HashMap<String, Arc<ImageData>>>,
|
||||
}
|
||||
|
||||
impl AssetCache {
|
||||
@@ -29,6 +33,7 @@ impl AssetCache {
|
||||
Self {
|
||||
source: Box::new(source),
|
||||
svgs: RefCell::new(HashMap::new()),
|
||||
pngs: RefCell::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,4 +48,18 @@ impl AssetCache {
|
||||
Ok(svg)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn png(&self, path: &str) -> Result<Arc<ImageData>> {
|
||||
let mut pngs = self.pngs.borrow_mut();
|
||||
if let Some(png) = pngs.get(path) {
|
||||
Ok(png.clone())
|
||||
} else {
|
||||
let bytes = self.source.load(path)?;
|
||||
let image = ImageData::new(
|
||||
image::load_from_memory_with_format(&bytes, ImageFormat::Png)?.into_bgra8(),
|
||||
);
|
||||
pngs.insert(path.to_string(), image.clone());
|
||||
Ok(image)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -153,7 +153,9 @@ impl Element for ConstrainedBox {
|
||||
_: &mut Self::LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) -> Self::PaintState {
|
||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||
cx.paint_layer(Some(visible_bounds), |cx| {
|
||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
|
||||
@@ -22,6 +22,7 @@ pub struct Flex {
|
||||
axis: Axis,
|
||||
children: Vec<ElementBox>,
|
||||
scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
|
||||
child_alignment: f32,
|
||||
}
|
||||
|
||||
impl Flex {
|
||||
@@ -30,6 +31,7 @@ impl Flex {
|
||||
axis,
|
||||
children: Default::default(),
|
||||
scroll_state: None,
|
||||
child_alignment: -1.,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +43,15 @@ impl Flex {
|
||||
Self::new(Axis::Vertical)
|
||||
}
|
||||
|
||||
/// Render children centered relative to the cross-axis of the parent flex.
|
||||
///
|
||||
/// If this is a flex row, children will be centered vertically. If this is a
|
||||
/// flex column, children will be centered horizontally.
|
||||
pub fn align_children_center(mut self) -> Self {
|
||||
self.child_alignment = 0.;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn scrollable<Tag, V>(
|
||||
mut self,
|
||||
element_id: usize,
|
||||
@@ -309,7 +320,30 @@ impl Element for Flex {
|
||||
}
|
||||
}
|
||||
|
||||
child.paint(child_origin, visible_bounds, cx);
|
||||
// We use the child_alignment f32 to determine a point along the cross axis of the
|
||||
// overall flex element and each child. We then align these points. So 0 would center
|
||||
// each child relative to the overall height/width of the flex. -1 puts children at
|
||||
// the start. 1 puts children at the end.
|
||||
let aligned_child_origin = {
|
||||
let cross_axis = self.axis.invert();
|
||||
let my_center = bounds.size().along(cross_axis) / 2.;
|
||||
let my_target = my_center + my_center * self.child_alignment;
|
||||
|
||||
let child_center = child.size().along(cross_axis) / 2.;
|
||||
let child_target = child_center + child_center * self.child_alignment;
|
||||
|
||||
let mut aligned_child_origin = child_origin;
|
||||
match self.axis {
|
||||
Axis::Horizontal => aligned_child_origin
|
||||
.set_y(aligned_child_origin.y() - (child_target - my_target)),
|
||||
Axis::Vertical => aligned_child_origin
|
||||
.set_x(aligned_child_origin.x() - (child_target - my_target)),
|
||||
}
|
||||
|
||||
aligned_child_origin
|
||||
};
|
||||
|
||||
child.paint(aligned_child_origin, visible_bounds, cx);
|
||||
|
||||
match self.axis {
|
||||
Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
|
||||
|
||||
@@ -11,8 +11,13 @@ use crate::{
|
||||
use serde::Deserialize;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
enum ImageSource {
|
||||
Path(&'static str),
|
||||
Data(Arc<ImageData>),
|
||||
}
|
||||
|
||||
pub struct Image {
|
||||
data: Arc<ImageData>,
|
||||
source: ImageSource,
|
||||
style: ImageStyle,
|
||||
}
|
||||
|
||||
@@ -31,9 +36,16 @@ pub struct ImageStyle {
|
||||
}
|
||||
|
||||
impl Image {
|
||||
pub fn new(data: Arc<ImageData>) -> Self {
|
||||
pub fn new(asset_path: &'static str) -> Self {
|
||||
Self {
|
||||
data,
|
||||
source: ImageSource::Path(asset_path),
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_data(data: Arc<ImageData>) -> Self {
|
||||
Self {
|
||||
source: ImageSource::Data(data),
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
@@ -45,39 +57,53 @@ impl Image {
|
||||
}
|
||||
|
||||
impl Element for Image {
|
||||
type LayoutState = ();
|
||||
type LayoutState = Option<Arc<ImageData>>;
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
_: &mut LayoutContext,
|
||||
cx: &mut LayoutContext,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let data = match &self.source {
|
||||
ImageSource::Path(path) => match cx.asset_cache.png(path) {
|
||||
Ok(data) => data,
|
||||
Err(error) => {
|
||||
log::error!("could not load image: {}", error);
|
||||
return (Vector2F::zero(), None);
|
||||
}
|
||||
},
|
||||
ImageSource::Data(data) => data.clone(),
|
||||
};
|
||||
|
||||
let desired_size = vec2f(
|
||||
self.style.width.unwrap_or_else(|| constraint.max.x()),
|
||||
self.style.height.unwrap_or_else(|| constraint.max.y()),
|
||||
);
|
||||
let size = constrain_size_preserving_aspect_ratio(
|
||||
constraint.constrain(desired_size),
|
||||
self.data.size().to_f32(),
|
||||
data.size().to_f32(),
|
||||
);
|
||||
(size, ())
|
||||
|
||||
(size, Some(data))
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: RectF,
|
||||
_: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
layout: &mut Self::LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) -> Self::PaintState {
|
||||
cx.scene.push_image(scene::Image {
|
||||
bounds,
|
||||
border: self.style.border,
|
||||
corner_radius: self.style.corner_radius,
|
||||
grayscale: self.style.grayscale,
|
||||
data: self.data.clone(),
|
||||
});
|
||||
if let Some(data) = layout {
|
||||
cx.scene.push_image(scene::Image {
|
||||
bounds,
|
||||
border: self.style.border,
|
||||
corner_radius: self.style.corner_radius,
|
||||
grayscale: self.style.grayscale,
|
||||
data: data.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
|
||||
@@ -5,7 +5,7 @@ mod keystroke;
|
||||
|
||||
use std::{any::TypeId, fmt::Debug};
|
||||
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use collections::HashMap;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::Action;
|
||||
@@ -68,8 +68,8 @@ impl KeymapMatcher {
|
||||
/// 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 order in the keymap file first and
|
||||
/// position of the matching view second.
|
||||
/// 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,
|
||||
@@ -80,8 +80,7 @@ impl KeymapMatcher {
|
||||
// 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: BTreeMap<usize, Vec<(usize, Box<dyn Action>)>> =
|
||||
Default::default();
|
||||
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());
|
||||
@@ -105,14 +104,11 @@ impl KeymapMatcher {
|
||||
}
|
||||
}
|
||||
|
||||
for (order, binding) in self.keymap.bindings().iter().rev().enumerate() {
|
||||
for binding in self.keymap.bindings().iter().rev() {
|
||||
match binding.match_keys_and_context(&self.pending_keystrokes, &self.contexts[i..])
|
||||
{
|
||||
BindingMatchResult::Complete(action) => {
|
||||
matched_bindings
|
||||
.entry(order)
|
||||
.or_default()
|
||||
.push((*view_id, action));
|
||||
matched_bindings.push((*view_id, action));
|
||||
}
|
||||
BindingMatchResult::Partial => {
|
||||
self.pending_views
|
||||
@@ -131,7 +127,7 @@ 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.into_values().flatten().collect())
|
||||
MatchResult::Matches(matched_bindings)
|
||||
} else if any_pending {
|
||||
MatchResult::Pending
|
||||
} else {
|
||||
@@ -225,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, 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)];
|
||||
|
||||
@@ -367,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();
|
||||
@@ -422,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,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);
|
||||
|
||||
@@ -496,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!(
|
||||
|
||||
@@ -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)]
|
||||
@@ -46,12 +67,12 @@ impl KeymapContextPredicate {
|
||||
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),
|
||||
|
||||
@@ -58,7 +58,7 @@ pub trait Platform: Send + Sync {
|
||||
options: WindowOptions,
|
||||
executor: Rc<executor::Foreground>,
|
||||
) -> Box<dyn Window>;
|
||||
fn key_window_id(&self) -> Option<usize>;
|
||||
fn main_window_id(&self) -> Option<usize>;
|
||||
|
||||
fn add_status_item(&self) -> Box<dyn Window>;
|
||||
|
||||
|
||||
@@ -587,8 +587,8 @@ impl platform::Platform for MacPlatform {
|
||||
Box::new(Window::open(id, options, executor, self.fonts()))
|
||||
}
|
||||
|
||||
fn key_window_id(&self) -> Option<usize> {
|
||||
Window::key_window_id()
|
||||
fn main_window_id(&self) -> Option<usize> {
|
||||
Window::main_window_id()
|
||||
}
|
||||
|
||||
fn add_status_item(&self) -> Box<dyn platform::Window> {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -604,12 +604,12 @@ impl Window {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn key_window_id() -> Option<usize> {
|
||||
pub fn main_window_id() -> Option<usize> {
|
||||
unsafe {
|
||||
let app = NSApplication::sharedApplication(nil);
|
||||
let key_window: id = msg_send![app, keyWindow];
|
||||
if msg_send![key_window, isKindOfClass: WINDOW_CLASS] {
|
||||
let id = get_window_state(&*key_window).borrow().id;
|
||||
let main_window: id = msg_send![app, mainWindow];
|
||||
if msg_send![main_window, isKindOfClass: WINDOW_CLASS] {
|
||||
let id = get_window_state(&*main_window).borrow().id;
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
@@ -737,6 +737,7 @@ impl platform::Window for Window {
|
||||
let title = ns_string(title);
|
||||
let _: () = msg_send![app, changeWindowsItem:window title:title filename:false];
|
||||
let _: () = msg_send![window, setTitle: title];
|
||||
self.0.borrow().move_traffic_light();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ impl super::Platform for Platform {
|
||||
}))
|
||||
}
|
||||
|
||||
fn key_window_id(&self) -> Option<usize> {
|
||||
fn main_window_id(&self) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
@@ -12,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};
|
||||
@@ -507,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;
|
||||
}
|
||||
}
|
||||
@@ -523,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;
|
||||
}
|
||||
}
|
||||
@@ -603,6 +605,24 @@ pub struct LayoutContext<'a> {
|
||||
}
|
||||
|
||||
impl<'a> LayoutContext<'a> {
|
||||
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 {
|
||||
let print_error = |view_id| {
|
||||
format!(
|
||||
|
||||
18
crates/install_cli/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "install_cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/install_cli.rs"
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
smol = "1.2.5"
|
||||
anyhow = "1.0.38"
|
||||
log = "0.4"
|
||||
gpui = { path = "../gpui" }
|
||||
util = { path = "../util" }
|
||||
55
crates/install_cli/src/install_cli.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{actions, AsyncAppContext};
|
||||
use util::ResultExt;
|
||||
|
||||
actions!(cli, [Install]);
|
||||
|
||||
pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> {
|
||||
let cli_path = cx.platform().path_for_auxiliary_executable("cli")?;
|
||||
let link_path = Path::new("/usr/local/bin/zed");
|
||||
let bin_dir_path = link_path.parent().unwrap();
|
||||
|
||||
// Don't re-create symlink if it points to the same CLI binary.
|
||||
if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// If the symlink is not there or is outdated, first try replacing it
|
||||
// without escalating.
|
||||
smol::fs::remove_file(link_path).await.log_err();
|
||||
if smol::fs::unix::symlink(&cli_path, link_path)
|
||||
.await
|
||||
.log_err()
|
||||
.is_some()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// The symlink could not be created, so use osascript with admin privileges
|
||||
// to create it.
|
||||
let status = smol::process::Command::new("osascript")
|
||||
.args([
|
||||
"-e",
|
||||
&format!(
|
||||
"do shell script \" \
|
||||
mkdir -p \'{}\' && \
|
||||
ln -sf \'{}\' \'{}\' \
|
||||
\" with administrator privileges",
|
||||
bin_dir_path.to_string_lossy(),
|
||||
cli_path.to_string_lossy(),
|
||||
link_path.to_string_lossy(),
|
||||
),
|
||||
])
|
||||
.stdout(smol::process::Stdio::inherit())
|
||||
.stderr(smol::process::Stdio::inherit())
|
||||
.output()
|
||||
.await?
|
||||
.status;
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("error running osascript"))
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ editor = { path = "../editor" }
|
||||
gpui = { path = "../gpui" }
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
anyhow = "1.0"
|
||||
chrono = "0.4"
|
||||
dirs = "4.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
|
||||
@@ -48,7 +48,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
async move {
|
||||
let (journal_dir, entry_path) = create_entry.await?;
|
||||
let (workspace, _) = cx
|
||||
.update(|cx| workspace::open_paths(&[journal_dir], &app_state, cx))
|
||||
.update(|cx| workspace::open_paths(&[journal_dir], &app_state, None, cx))
|
||||
.await;
|
||||
|
||||
let opened = workspace
|
||||
@@ -73,7 +73,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
})
|
||||
|
||||
@@ -305,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>)>,
|
||||
}
|
||||
@@ -569,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)
|
||||
@@ -1154,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 {
|
||||
@@ -1286,7 +1353,13 @@ impl Buffer {
|
||||
}
|
||||
|
||||
pub fn remove_active_selections(&mut self, cx: &mut ModelContext<Self>) {
|
||||
self.set_active_selections(Arc::from([]), false, Default::default(), cx);
|
||||
if self
|
||||
.remote_selections
|
||||
.get(&self.text.replica_id())
|
||||
.map_or(true, |set| !set.selections.is_empty())
|
||||
{
|
||||
self.set_active_selections(Arc::from([]), false, Default::default(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_text<T>(&mut self, text: T, cx: &mut ModelContext<Self>) -> Option<clock::Local>
|
||||
@@ -2840,3 +2913,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
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use gpui::{ModelHandle, MutableAppContext};
|
||||
use indoc::indoc;
|
||||
use proto::deserialize_operation;
|
||||
use rand::prelude::*;
|
||||
use regex::RegexBuilder;
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
@@ -18,6 +19,13 @@ use text::network::Network;
|
||||
use unindent::Unindent as _;
|
||||
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]
|
||||
fn init_logger() {
|
||||
@@ -72,31 +80,49 @@ fn test_select_language() {
|
||||
|
||||
// matching file extension
|
||||
assert_eq!(
|
||||
registry.language_for_path("zed/lib.rs").map(|l| l.name()),
|
||||
registry
|
||||
.language_for_path("zed/lib.rs")
|
||||
.now_or_never()
|
||||
.and_then(|l| Some(l.ok()?.name())),
|
||||
Some("Rust".into())
|
||||
);
|
||||
assert_eq!(
|
||||
registry.language_for_path("zed/lib.mk").map(|l| l.name()),
|
||||
registry
|
||||
.language_for_path("zed/lib.mk")
|
||||
.now_or_never()
|
||||
.and_then(|l| Some(l.ok()?.name())),
|
||||
Some("Make".into())
|
||||
);
|
||||
|
||||
// matching filename
|
||||
assert_eq!(
|
||||
registry.language_for_path("zed/Makefile").map(|l| l.name()),
|
||||
registry
|
||||
.language_for_path("zed/Makefile")
|
||||
.now_or_never()
|
||||
.and_then(|l| Some(l.ok()?.name())),
|
||||
Some("Make".into())
|
||||
);
|
||||
|
||||
// matching suffix that is not the full file extension or filename
|
||||
assert_eq!(
|
||||
registry.language_for_path("zed/cars").map(|l| l.name()),
|
||||
registry
|
||||
.language_for_path("zed/cars")
|
||||
.now_or_never()
|
||||
.and_then(|l| Some(l.ok()?.name())),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
registry.language_for_path("zed/a.cars").map(|l| l.name()),
|
||||
registry
|
||||
.language_for_path("zed/a.cars")
|
||||
.now_or_never()
|
||||
.and_then(|l| Some(l.ok()?.name())),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
registry.language_for_path("zed/sumk").map(|l| l.name()),
|
||||
registry
|
||||
.language_for_path("zed/sumk")
|
||||
.now_or_never()
|
||||
.and_then(|l| Some(l.ok()?.name())),
|
||||
None
|
||||
);
|
||||
}
|
||||
@@ -211,6 +237,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() {}";
|
||||
@@ -585,14 +684,14 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||
indoc! {"
|
||||
mod x {
|
||||
moˇd y {
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
let foo = 1;"},
|
||||
vec![indoc! {"
|
||||
mod x «{»
|
||||
mod y {
|
||||
|
||||
|
||||
}
|
||||
«}»
|
||||
let foo = 1;"}],
|
||||
@@ -602,7 +701,7 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||
indoc! {"
|
||||
mod x {
|
||||
mod y ˇ{
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
let foo = 1;"},
|
||||
@@ -610,14 +709,14 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||
indoc! {"
|
||||
mod x «{»
|
||||
mod y {
|
||||
|
||||
|
||||
}
|
||||
«}»
|
||||
let foo = 1;"},
|
||||
indoc! {"
|
||||
mod x {
|
||||
mod y «{»
|
||||
|
||||
|
||||
«}»
|
||||
}
|
||||
let foo = 1;"},
|
||||
@@ -628,7 +727,7 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||
indoc! {"
|
||||
mod x {
|
||||
mod y {
|
||||
|
||||
|
||||
}ˇ
|
||||
}
|
||||
let foo = 1;"},
|
||||
@@ -636,14 +735,14 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||
indoc! {"
|
||||
mod x «{»
|
||||
mod y {
|
||||
|
||||
|
||||
}
|
||||
«}»
|
||||
let foo = 1;"},
|
||||
indoc! {"
|
||||
mod x {
|
||||
mod y «{»
|
||||
|
||||
|
||||
«}»
|
||||
}
|
||||
let foo = 1;"},
|
||||
@@ -654,14 +753,14 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||
indoc! {"
|
||||
mod x {
|
||||
mod y {
|
||||
|
||||
|
||||
}
|
||||
ˇ}
|
||||
let foo = 1;"},
|
||||
vec![indoc! {"
|
||||
mod x «{»
|
||||
mod y {
|
||||
|
||||
|
||||
}
|
||||
«}»
|
||||
let foo = 1;"}],
|
||||
@@ -671,7 +770,7 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||
indoc! {"
|
||||
mod x {
|
||||
mod y {
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
let fˇoo = 1;"},
|
||||
@@ -683,7 +782,7 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||
indoc! {"
|
||||
mod x {
|
||||
mod y {
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
let foo = 1;ˇ"},
|
||||
@@ -1723,25 +1822,31 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
|
||||
}
|
||||
30..=39 if mutation_count != 0 => {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let mut selections = Vec::new();
|
||||
for id in 0..rng.gen_range(1..=5) {
|
||||
let range = buffer.random_byte_range(0, &mut rng);
|
||||
selections.push(Selection {
|
||||
id,
|
||||
start: buffer.anchor_before(range.start),
|
||||
end: buffer.anchor_before(range.end),
|
||||
reversed: false,
|
||||
goal: SelectionGoal::None,
|
||||
});
|
||||
if rng.gen_bool(0.2) {
|
||||
log::info!("peer {} clearing active selections", replica_id);
|
||||
active_selections.remove(&replica_id);
|
||||
buffer.remove_active_selections(cx);
|
||||
} else {
|
||||
let mut selections = Vec::new();
|
||||
for id in 0..rng.gen_range(1..=5) {
|
||||
let range = buffer.random_byte_range(0, &mut rng);
|
||||
selections.push(Selection {
|
||||
id,
|
||||
start: buffer.anchor_before(range.start),
|
||||
end: buffer.anchor_before(range.end),
|
||||
reversed: false,
|
||||
goal: SelectionGoal::None,
|
||||
});
|
||||
}
|
||||
let selections: Arc<[Selection<Anchor>]> = selections.into();
|
||||
log::info!(
|
||||
"peer {} setting active selections: {:?}",
|
||||
replica_id,
|
||||
selections
|
||||
);
|
||||
active_selections.insert(replica_id, selections.clone());
|
||||
buffer.set_active_selections(selections, false, Default::default(), cx);
|
||||
}
|
||||
let selections: Arc<[Selection<Anchor>]> = selections.into();
|
||||
log::info!(
|
||||
"peer {} setting active selections: {:?}",
|
||||
replica_id,
|
||||
selections
|
||||
);
|
||||
active_selections.insert(replica_id, selections.clone());
|
||||
buffer.set_active_selections(selections, false, Default::default(), cx);
|
||||
});
|
||||
mutation_count -= 1;
|
||||
}
|
||||
@@ -1943,6 +2048,45 @@ fn test_contiguous_ranges() {
|
||||
);
|
||||
}
|
||||
|
||||
#[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 {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
|
||||
@@ -13,8 +13,9 @@ use async_trait::async_trait;
|
||||
use client::http::HttpClient;
|
||||
use collections::HashMap;
|
||||
use futures::{
|
||||
channel::oneshot,
|
||||
future::{BoxFuture, Shared},
|
||||
FutureExt, TryFutureExt,
|
||||
FutureExt, TryFutureExt as _,
|
||||
};
|
||||
use gpui::{executor::Background, MutableAppContext, Task};
|
||||
use highlight_map::HighlightMap;
|
||||
@@ -43,7 +44,7 @@ use syntax_map::SyntaxSnapshot;
|
||||
use theme::{SyntaxTheme, Theme};
|
||||
use tree_sitter::{self, Query};
|
||||
use unicase::UniCase;
|
||||
use util::ResultExt;
|
||||
use util::{ResultExt, TryFutureExt as _, UnwrapFuture};
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use futures::channel::mpsc;
|
||||
@@ -484,7 +485,7 @@ impl LanguageRegistry {
|
||||
let (lsp_binary_statuses_tx, lsp_binary_statuses_rx) = async_broadcast::broadcast(16);
|
||||
Self {
|
||||
language_server_download_dir: None,
|
||||
languages: Default::default(),
|
||||
languages: RwLock::new(vec![PLAIN_TEXT.clone()]),
|
||||
available_languages: Default::default(),
|
||||
lsp_binary_statuses_tx,
|
||||
lsp_binary_statuses_rx,
|
||||
@@ -568,12 +569,18 @@ impl LanguageRegistry {
|
||||
self.language_server_download_dir = Some(path.into());
|
||||
}
|
||||
|
||||
pub fn language_for_name(self: &Arc<Self>, name: &str) -> Option<Arc<Language>> {
|
||||
pub fn language_for_name(
|
||||
self: &Arc<Self>,
|
||||
name: &str,
|
||||
) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
|
||||
let name = UniCase::new(name);
|
||||
self.get_or_load_language(|config| UniCase::new(config.name.as_ref()) == name)
|
||||
}
|
||||
|
||||
pub fn language_for_name_or_extension(self: &Arc<Self>, string: &str) -> Option<Arc<Language>> {
|
||||
pub fn language_for_name_or_extension(
|
||||
self: &Arc<Self>,
|
||||
string: &str,
|
||||
) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
|
||||
let string = UniCase::new(string);
|
||||
self.get_or_load_language(|config| {
|
||||
UniCase::new(config.name.as_ref()) == string
|
||||
@@ -584,7 +591,10 @@ impl LanguageRegistry {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn language_for_path(self: &Arc<Self>, path: impl AsRef<Path>) -> Option<Arc<Language>> {
|
||||
pub fn language_for_path(
|
||||
self: &Arc<Self>,
|
||||
path: impl AsRef<Path>,
|
||||
) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
|
||||
let path = path.as_ref();
|
||||
let filename = path.file_name().and_then(|name| name.to_str());
|
||||
let extension = path.extension().and_then(|name| name.to_str());
|
||||
@@ -600,17 +610,17 @@ impl LanguageRegistry {
|
||||
fn get_or_load_language(
|
||||
self: &Arc<Self>,
|
||||
callback: impl Fn(&LanguageConfig) -> bool,
|
||||
) -> Option<Arc<Language>> {
|
||||
) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
if let Some(language) = self
|
||||
.languages
|
||||
.read()
|
||||
.iter()
|
||||
.find(|language| callback(&language.config))
|
||||
{
|
||||
return Some(language.clone());
|
||||
}
|
||||
|
||||
if let Some(executor) = self.executor.clone() {
|
||||
let _ = tx.send(Ok(language.clone()));
|
||||
} else if let Some(executor) = self.executor.clone() {
|
||||
let mut available_languages = self.available_languages.write();
|
||||
|
||||
if let Some(ix) = available_languages.iter().position(|l| callback(&l.config)) {
|
||||
@@ -625,18 +635,29 @@ impl LanguageRegistry {
|
||||
.with_lsp_adapter(language.lsp_adapter)
|
||||
.await;
|
||||
match language.with_queries(queries) {
|
||||
Ok(language) => this.add(Arc::new(language)),
|
||||
Ok(language) => {
|
||||
let language = Arc::new(language);
|
||||
this.add(language.clone());
|
||||
let _ = tx.send(Ok(language));
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("failed to load language {}: {}", name, err);
|
||||
return;
|
||||
let _ = tx.send(Err(anyhow!(
|
||||
"failed to load language {}: {}",
|
||||
name,
|
||||
err
|
||||
)));
|
||||
}
|
||||
};
|
||||
})
|
||||
.detach();
|
||||
} else {
|
||||
let _ = tx.send(Err(anyhow!("language not found")));
|
||||
}
|
||||
} else {
|
||||
let _ = tx.send(Err(anyhow!("executor does not exist")));
|
||||
}
|
||||
|
||||
None
|
||||
rx.unwrap()
|
||||
}
|
||||
|
||||
pub fn to_vec(&self) -> Vec<Arc<Language>> {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{Grammar, InjectionConfig, Language, LanguageRegistry};
|
||||
use collections::HashMap;
|
||||
use futures::FutureExt;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
@@ -165,6 +166,7 @@ struct ParseStep {
|
||||
mode: ParseMode,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ParseStepLanguage {
|
||||
Loaded { language: Arc<Language> },
|
||||
Pending { name: Arc<str> },
|
||||
@@ -381,11 +383,11 @@ impl SyntaxSnapshot {
|
||||
cursor.next(text);
|
||||
while let Some(layer) = cursor.item() {
|
||||
let SyntaxLayerContent::Pending { language_name } = &layer.content else { unreachable!() };
|
||||
if {
|
||||
let language_registry = ®istry;
|
||||
language_registry.language_for_name_or_extension(language_name)
|
||||
}
|
||||
.is_some()
|
||||
if registry
|
||||
.language_for_name_or_extension(language_name)
|
||||
.now_or_never()
|
||||
.and_then(|language| language.ok())
|
||||
.is_some()
|
||||
{
|
||||
resolved_injection_ranges.push(layer.range.to_offset(text));
|
||||
}
|
||||
@@ -514,15 +516,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 +553,6 @@ impl SyntaxSnapshot {
|
||||
grammar,
|
||||
text.as_rope(),
|
||||
step_start_byte,
|
||||
step_start_point,
|
||||
included_ranges,
|
||||
Some(old_tree.clone()),
|
||||
);
|
||||
@@ -551,7 +569,6 @@ impl SyntaxSnapshot {
|
||||
grammar,
|
||||
text.as_rope(),
|
||||
step_start_byte,
|
||||
step_start_point,
|
||||
included_ranges,
|
||||
None,
|
||||
);
|
||||
@@ -1060,17 +1077,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());
|
||||
@@ -1108,7 +1117,10 @@ fn get_injections(
|
||||
combined_injection_ranges.clear();
|
||||
for pattern in &config.patterns {
|
||||
if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined) {
|
||||
if let Some(language) = language_registry.language_for_name_or_extension(language_name)
|
||||
if let Some(language) = language_registry
|
||||
.language_for_name_or_extension(language_name)
|
||||
.now_or_never()
|
||||
.and_then(|language| language.ok())
|
||||
{
|
||||
combined_injection_ranges.insert(language, Vec::new());
|
||||
}
|
||||
@@ -1154,10 +1166,10 @@ fn get_injections(
|
||||
};
|
||||
|
||||
if let Some(language_name) = language_name {
|
||||
let language = {
|
||||
let language_name: &str = &language_name;
|
||||
language_registry.language_for_name_or_extension(language_name)
|
||||
};
|
||||
let language = language_registry
|
||||
.language_for_name_or_extension(&language_name)
|
||||
.now_or_never()
|
||||
.and_then(|language| language.ok());
|
||||
let range = text.anchor_before(step_range.start)..text.anchor_after(step_range.end);
|
||||
if let Some(language) = language {
|
||||
if combined {
|
||||
@@ -2208,6 +2220,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")
|
||||
@@ -2483,7 +2526,11 @@ mod tests {
|
||||
registry.add(Arc::new(html_lang()));
|
||||
registry.add(Arc::new(erb_lang()));
|
||||
registry.add(Arc::new(markdown_lang()));
|
||||
let language = registry.language_for_name(language_name).unwrap();
|
||||
let language = registry
|
||||
.language_for_name(language_name)
|
||||
.now_or_never()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let mut buffer = Buffer::new(0, 0, Default::default());
|
||||
|
||||
let mut mutated_syntax_map = SyntaxMap::new();
|
||||
|
||||
21
crates/language_selector/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "language_selector"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/language_selector.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
editor = { path = "../editor" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
language = { path = "../language" }
|
||||
gpui = { path = "../gpui" }
|
||||
picker = { path = "../picker" }
|
||||
project = { path = "../project" }
|
||||
theme = { path = "../theme" }
|
||||
settings = { path = "../settings" }
|
||||
workspace = { path = "../workspace" }
|
||||
anyhow = "1.0"
|
||||
87
crates/language_selector/src/active_buffer_language.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
elements::*, CursorStyle, Entity, MouseButton, RenderContext, Subscription, View, ViewContext,
|
||||
ViewHandle,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use workspace::{item::ItemHandle, StatusItemView};
|
||||
|
||||
pub struct ActiveBufferLanguage {
|
||||
active_language: Option<Arc<str>>,
|
||||
_observe_active_editor: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl Default for ActiveBufferLanguage {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveBufferLanguage {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
active_language: None,
|
||||
_observe_active_editor: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_language(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
|
||||
self.active_language.take();
|
||||
|
||||
let editor = editor.read(cx);
|
||||
if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
|
||||
if let Some(language) = buffer.read(cx).language() {
|
||||
self.active_language = Some(language.name());
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ActiveBufferLanguage {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for ActiveBufferLanguage {
|
||||
fn ui_name() -> &'static str {
|
||||
"ActiveBufferLanguage"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
if let Some(active_language) = self.active_language.as_ref() {
|
||||
MouseEventHandler::<Self>::new(0, cx, |state, cx| {
|
||||
let theme = &cx.global::<Settings>().theme.workspace.status_bar;
|
||||
let style = theme.active_language.style_for(state, false);
|
||||
Label::new(active_language.to_string(), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(crate::Toggle))
|
||||
.boxed()
|
||||
} else {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for ActiveBufferLanguage {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
|
||||
self._observe_active_editor = Some(cx.observe(&editor, Self::update_language));
|
||||
self.update_language(editor, cx);
|
||||
} else {
|
||||
self.active_language = None;
|
||||
self._observe_active_editor = None;
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
230
crates/language_selector/src/language_selector.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
mod active_buffer_language;
|
||||
|
||||
pub use active_buffer_language::ActiveBufferLanguage;
|
||||
use editor::Editor;
|
||||
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MouseState,
|
||||
MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use language::{Buffer, LanguageRegistry};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use workspace::{AppState, Workspace};
|
||||
|
||||
actions!(language_selector, [Toggle]);
|
||||
|
||||
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
Picker::<LanguageSelector>::init(cx);
|
||||
cx.add_action({
|
||||
let language_registry = app_state.languages.clone();
|
||||
move |workspace, _: &Toggle, cx| {
|
||||
LanguageSelector::toggle(workspace, language_registry.clone(), cx)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
Dismissed,
|
||||
}
|
||||
|
||||
pub struct LanguageSelector {
|
||||
buffer: ModelHandle<Buffer>,
|
||||
project: ModelHandle<Project>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
candidates: Vec<StringMatchCandidate>,
|
||||
matches: Vec<StringMatch>,
|
||||
picker: ViewHandle<Picker<Self>>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl LanguageSelector {
|
||||
fn new(
|
||||
buffer: ModelHandle<Buffer>,
|
||||
project: ModelHandle<Project>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let handle = cx.weak_handle();
|
||||
let picker = cx.add_view(|cx| Picker::new("Select Language...", handle, cx));
|
||||
|
||||
let candidates = language_registry
|
||||
.language_names()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
|
||||
.collect::<Vec<_>>();
|
||||
let mut matches = candidates
|
||||
.iter()
|
||||
.map(|candidate| StringMatch {
|
||||
candidate_id: candidate.id,
|
||||
score: 0.,
|
||||
positions: Default::default(),
|
||||
string: candidate.string.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
matches.sort_unstable_by(|mat1, mat2| mat1.string.cmp(&mat2.string));
|
||||
|
||||
Self {
|
||||
buffer,
|
||||
project,
|
||||
language_registry,
|
||||
candidates,
|
||||
matches,
|
||||
picker,
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle(
|
||||
workspace: &mut Workspace,
|
||||
registry: Arc<LanguageRegistry>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
if let Some((_, buffer, _)) = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|active_item| active_item.act_as::<Editor>(cx))
|
||||
.and_then(|editor| editor.read(cx).active_excerpt(cx))
|
||||
{
|
||||
workspace.toggle_modal(cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let this = cx.add_view(|cx| Self::new(buffer, project, registry, cx));
|
||||
cx.subscribe(&this, Self::on_event).detach();
|
||||
this
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
workspace: &mut Workspace,
|
||||
_: ViewHandle<LanguageSelector>,
|
||||
event: &Event,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
match event {
|
||||
Event::Dismissed => {
|
||||
workspace.dismiss_modal(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for LanguageSelector {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for LanguageSelector {
|
||||
fn ui_name() -> &'static str {
|
||||
"LanguageSelector"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
ChildView::new(self.picker.clone(), cx).boxed()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.picker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for LanguageSelector {
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn confirm(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(mat) = self.matches.get(self.selected_index) {
|
||||
let language_name = &self.candidates[mat.candidate_id].string;
|
||||
let language = self.language_registry.language_for_name(language_name);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let language = language.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.project.update(cx, |project, cx| {
|
||||
project.set_language_for_buffer(&this.buffer, language, cx);
|
||||
});
|
||||
});
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
cx.emit(Event::Dismissed);
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(Event::Dismissed);
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Self>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
|
||||
let background = cx.background().clone();
|
||||
let candidates = self.candidates.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let matches = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, candidate)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: candidate.string,
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Default::default(),
|
||||
background,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.matches = matches;
|
||||
this.selected_index = this
|
||||
.selected_index
|
||||
.min(this.matches.len().saturating_sub(1));
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
mouse_state: &mut MouseState,
|
||||
selected: bool,
|
||||
cx: &AppContext,
|
||||
) -> ElementBox {
|
||||
let settings = cx.global::<Settings>();
|
||||
let theme = &settings.theme;
|
||||
let mat = &self.matches[ix];
|
||||
let style = theme.picker.item.style_for(mouse_state, selected);
|
||||
let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
|
||||
let mut label = mat.string.clone();
|
||||
if buffer_language_name.as_deref() == Some(mat.string.as_str()) {
|
||||
label.push_str(" (current)");
|
||||
}
|
||||
|
||||
Label::new(label, style.label.clone())
|
||||
.with_highlights(mat.positions.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
@@ -104,6 +104,15 @@ impl TestServer {
|
||||
room_name
|
||||
))
|
||||
} else {
|
||||
for track in &room.tracks {
|
||||
client_room
|
||||
.0
|
||||
.lock()
|
||||
.video_track_updates
|
||||
.0
|
||||
.try_broadcast(RemoteVideoTrackUpdate::Subscribed(track.clone()))
|
||||
.unwrap();
|
||||
}
|
||||
room.client_rooms.insert(identity, client_room);
|
||||
Ok(())
|
||||
}
|
||||
@@ -167,11 +176,13 @@ impl TestServer {
|
||||
.get_mut(&*room_name)
|
||||
.ok_or_else(|| anyhow!("room {} does not exist", room_name))?;
|
||||
|
||||
let update = RemoteVideoTrackUpdate::Subscribed(Arc::new(RemoteVideoTrack {
|
||||
let track = Arc::new(RemoteVideoTrack {
|
||||
sid: nanoid::nanoid!(17),
|
||||
publisher_id: identity.clone(),
|
||||
frames_rx: local_track.frames_rx.clone(),
|
||||
}));
|
||||
});
|
||||
|
||||
room.tracks.push(track.clone());
|
||||
|
||||
for (id, client_room) in &room.client_rooms {
|
||||
if *id != identity {
|
||||
@@ -180,18 +191,30 @@ impl TestServer {
|
||||
.lock()
|
||||
.video_track_updates
|
||||
.0
|
||||
.try_broadcast(update.clone())
|
||||
.try_broadcast(RemoteVideoTrackUpdate::Subscribed(track.clone()))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn video_tracks(&self, token: String) -> Result<Vec<Arc<RemoteVideoTrack>>> {
|
||||
let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
|
||||
let room_name = claims.video.room.unwrap();
|
||||
|
||||
let mut server_rooms = self.rooms.lock();
|
||||
let room = server_rooms
|
||||
.get_mut(&*room_name)
|
||||
.ok_or_else(|| anyhow!("room {} does not exist", room_name))?;
|
||||
Ok(room.tracks.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TestServerRoom {
|
||||
client_rooms: HashMap<Sid, Arc<Room>>,
|
||||
tracks: Vec<Arc<RemoteVideoTrack>>,
|
||||
}
|
||||
|
||||
impl TestServerRoom {}
|
||||
@@ -307,8 +330,17 @@ impl Room {
|
||||
|
||||
pub fn unpublish_track(&self, _: LocalTrackPublication) {}
|
||||
|
||||
pub fn remote_video_tracks(&self, _: &str) -> Vec<Arc<RemoteVideoTrack>> {
|
||||
Default::default()
|
||||
pub fn remote_video_tracks(&self, publisher_id: &str) -> Vec<Arc<RemoteVideoTrack>> {
|
||||
if !self.is_connected() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
self.test_server()
|
||||
.video_tracks(self.token())
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter(|track| track.publisher_id() == publisher_id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn remote_video_track_updates(&self) -> impl Stream<Item = RemoteVideoTrackUpdate> {
|
||||
@@ -332,6 +364,13 @@ impl Room {
|
||||
ConnectionState::Connected { token, .. } => token,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_connected(&self) -> bool {
|
||||
match *self.0.lock().connection.1.borrow() {
|
||||
ConnectionState::Disconnected => false,
|
||||
ConnectionState::Connected { .. } => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Room {
|
||||
|
||||
@@ -160,15 +160,13 @@ impl LanguageServer {
|
||||
server: Option<Child>,
|
||||
root_path: &Path,
|
||||
cx: AsyncAppContext,
|
||||
mut on_unhandled_notification: F,
|
||||
on_unhandled_notification: F,
|
||||
) -> Self
|
||||
where
|
||||
Stdin: AsyncWrite + Unpin + Send + 'static,
|
||||
Stdout: AsyncRead + Unpin + Send + 'static,
|
||||
F: FnMut(AnyNotification) + 'static + Send,
|
||||
{
|
||||
let mut stdin = BufWriter::new(stdin);
|
||||
let mut stdout = BufReader::new(stdout);
|
||||
let (outbound_tx, outbound_rx) = channel::unbounded::<Vec<u8>>();
|
||||
let notification_handlers =
|
||||
Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default()));
|
||||
@@ -177,89 +175,19 @@ impl LanguageServer {
|
||||
let input_task = cx.spawn(|cx| {
|
||||
let notification_handlers = notification_handlers.clone();
|
||||
let response_handlers = response_handlers.clone();
|
||||
async move {
|
||||
let _clear_response_handlers = util::defer({
|
||||
let response_handlers = response_handlers.clone();
|
||||
move || {
|
||||
response_handlers.lock().take();
|
||||
}
|
||||
});
|
||||
let mut buffer = Vec::new();
|
||||
loop {
|
||||
buffer.clear();
|
||||
stdout.read_until(b'\n', &mut buffer).await?;
|
||||
stdout.read_until(b'\n', &mut buffer).await?;
|
||||
let message_len: usize = std::str::from_utf8(&buffer)?
|
||||
.strip_prefix(CONTENT_LEN_HEADER)
|
||||
.ok_or_else(|| anyhow!("invalid header"))?
|
||||
.trim_end()
|
||||
.parse()?;
|
||||
|
||||
buffer.resize(message_len, 0);
|
||||
stdout.read_exact(&mut buffer).await?;
|
||||
log::trace!("incoming message:{}", String::from_utf8_lossy(&buffer));
|
||||
|
||||
if let Ok(msg) = serde_json::from_slice::<AnyNotification>(&buffer) {
|
||||
if let Some(handler) = notification_handlers.lock().get_mut(msg.method) {
|
||||
handler(msg.id, msg.params.get(), cx.clone());
|
||||
} else {
|
||||
on_unhandled_notification(msg);
|
||||
}
|
||||
} else if let Ok(AnyResponse {
|
||||
id, error, result, ..
|
||||
}) = serde_json::from_slice(&buffer)
|
||||
{
|
||||
if let Some(handler) = response_handlers
|
||||
.lock()
|
||||
.as_mut()
|
||||
.and_then(|handlers| handlers.remove(&id))
|
||||
{
|
||||
if let Some(error) = error {
|
||||
handler(Err(error));
|
||||
} else if let Some(result) = result {
|
||||
handler(Ok(result.get()));
|
||||
} else {
|
||||
handler(Ok("null"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
"Failed to deserialize message:\n{}",
|
||||
std::str::from_utf8(&buffer)?
|
||||
);
|
||||
}
|
||||
|
||||
// Don't starve the main thread when receiving lots of messages at once.
|
||||
smol::future::yield_now().await;
|
||||
}
|
||||
}
|
||||
Self::handle_input(
|
||||
stdout,
|
||||
on_unhandled_notification,
|
||||
notification_handlers,
|
||||
response_handlers,
|
||||
cx,
|
||||
)
|
||||
.log_err()
|
||||
});
|
||||
let (output_done_tx, output_done_rx) = barrier::channel();
|
||||
let output_task = cx.background().spawn({
|
||||
let response_handlers = response_handlers.clone();
|
||||
async move {
|
||||
let _clear_response_handlers = util::defer({
|
||||
let response_handlers = response_handlers.clone();
|
||||
move || {
|
||||
response_handlers.lock().take();
|
||||
}
|
||||
});
|
||||
let mut content_len_buffer = Vec::new();
|
||||
while let Ok(message) = outbound_rx.recv().await {
|
||||
log::trace!("outgoing message:{}", String::from_utf8_lossy(&message));
|
||||
content_len_buffer.clear();
|
||||
write!(content_len_buffer, "{}", message.len()).unwrap();
|
||||
stdin.write_all(CONTENT_LEN_HEADER.as_bytes()).await?;
|
||||
stdin.write_all(&content_len_buffer).await?;
|
||||
stdin.write_all("\r\n\r\n".as_bytes()).await?;
|
||||
stdin.write_all(&message).await?;
|
||||
stdin.flush().await?;
|
||||
}
|
||||
drop(output_done_tx);
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
Self::handle_output(stdin, outbound_rx, output_done_tx, response_handlers).log_err()
|
||||
});
|
||||
|
||||
Self {
|
||||
@@ -278,6 +206,105 @@ impl LanguageServer {
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_input<Stdout, F>(
|
||||
stdout: Stdout,
|
||||
mut on_unhandled_notification: F,
|
||||
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
|
||||
response_handlers: Arc<Mutex<Option<HashMap<usize, ResponseHandler>>>>,
|
||||
cx: AsyncAppContext,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
Stdout: AsyncRead + Unpin + Send + 'static,
|
||||
F: FnMut(AnyNotification) + 'static + Send,
|
||||
{
|
||||
let mut stdout = BufReader::new(stdout);
|
||||
let _clear_response_handlers = util::defer({
|
||||
let response_handlers = response_handlers.clone();
|
||||
move || {
|
||||
response_handlers.lock().take();
|
||||
}
|
||||
});
|
||||
let mut buffer = Vec::new();
|
||||
loop {
|
||||
buffer.clear();
|
||||
stdout.read_until(b'\n', &mut buffer).await?;
|
||||
stdout.read_until(b'\n', &mut buffer).await?;
|
||||
let message_len: usize = std::str::from_utf8(&buffer)?
|
||||
.strip_prefix(CONTENT_LEN_HEADER)
|
||||
.ok_or_else(|| anyhow!("invalid header"))?
|
||||
.trim_end()
|
||||
.parse()?;
|
||||
|
||||
buffer.resize(message_len, 0);
|
||||
stdout.read_exact(&mut buffer).await?;
|
||||
log::trace!("incoming message:{}", String::from_utf8_lossy(&buffer));
|
||||
|
||||
if let Ok(msg) = serde_json::from_slice::<AnyNotification>(&buffer) {
|
||||
if let Some(handler) = notification_handlers.lock().get_mut(msg.method) {
|
||||
handler(msg.id, msg.params.get(), cx.clone());
|
||||
} else {
|
||||
on_unhandled_notification(msg);
|
||||
}
|
||||
} else if let Ok(AnyResponse {
|
||||
id, error, result, ..
|
||||
}) = serde_json::from_slice(&buffer)
|
||||
{
|
||||
if let Some(handler) = response_handlers
|
||||
.lock()
|
||||
.as_mut()
|
||||
.and_then(|handlers| handlers.remove(&id))
|
||||
{
|
||||
if let Some(error) = error {
|
||||
handler(Err(error));
|
||||
} else if let Some(result) = result {
|
||||
handler(Ok(result.get()));
|
||||
} else {
|
||||
handler(Ok("null"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
"Failed to deserialize message:\n{}",
|
||||
std::str::from_utf8(&buffer)?
|
||||
);
|
||||
}
|
||||
|
||||
// Don't starve the main thread when receiving lots of messages at once.
|
||||
smol::future::yield_now().await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_output<Stdin>(
|
||||
stdin: Stdin,
|
||||
outbound_rx: channel::Receiver<Vec<u8>>,
|
||||
output_done_tx: barrier::Sender,
|
||||
response_handlers: Arc<Mutex<Option<HashMap<usize, ResponseHandler>>>>,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
Stdin: AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
let mut stdin = BufWriter::new(stdin);
|
||||
let _clear_response_handlers = util::defer({
|
||||
let response_handlers = response_handlers.clone();
|
||||
move || {
|
||||
response_handlers.lock().take();
|
||||
}
|
||||
});
|
||||
let mut content_len_buffer = Vec::new();
|
||||
while let Ok(message) = outbound_rx.recv().await {
|
||||
log::trace!("outgoing message:{}", String::from_utf8_lossy(&message));
|
||||
content_len_buffer.clear();
|
||||
write!(content_len_buffer, "{}", message.len()).unwrap();
|
||||
stdin.write_all(CONTENT_LEN_HEADER.as_bytes()).await?;
|
||||
stdin.write_all(&content_len_buffer).await?;
|
||||
stdin.write_all("\r\n\r\n".as_bytes()).await?;
|
||||
stdin.write_all(&message).await?;
|
||||
stdin.flush().await?;
|
||||
}
|
||||
drop(output_done_tx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initializes a language server.
|
||||
/// Note that `options` is used directly to construct [`InitializeParams`],
|
||||
/// which is why it is owned.
|
||||
@@ -389,7 +416,7 @@ impl LanguageServer {
|
||||
output_done.recv().await;
|
||||
log::debug!("language server shutdown finished");
|
||||
drop(tasks);
|
||||
Ok(())
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
)
|
||||
@@ -422,6 +449,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
|
||||
@@ -780,6 +811,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,
|
||||
|
||||
@@ -102,7 +102,10 @@ impl<D: PickerDelegate> View for Picker<D> {
|
||||
.read(cx)
|
||||
.render_match(ix, state, ix == selected_ix, cx)
|
||||
})
|
||||
.on_down(MouseButton::Left, move |_, cx| {
|
||||
// Capture mouse events
|
||||
.on_down(MouseButton::Left, |_, _| {})
|
||||
.on_up(MouseButton::Left, |_, _| {})
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(SelectIndex(ix))
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
@@ -126,7 +129,7 @@ impl<D: PickerDelegate> View for Picker<D> {
|
||||
|
||||
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||
let mut cx = Self::default_keymap_context();
|
||||
cx.set.insert("menu".into());
|
||||
cx.add_identifier("menu");
|
||||
cx
|
||||
}
|
||||
|
||||
@@ -205,6 +208,11 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
self.query_editor.read(cx).text(cx)
|
||||
}
|
||||
|
||||
pub fn set_query(&self, query: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
|
||||
self.query_editor
|
||||
.update(cx, |editor, cx| editor.set_text(query, cx));
|
||||
}
|
||||
|
||||
fn on_query_editor_event(
|
||||
&mut self,
|
||||
_: ViewHandle<Editor>,
|
||||
|
||||
@@ -57,6 +57,7 @@ thiserror = "1.0.29"
|
||||
toml = "0.5"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.3.0"
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
collections = { path = "../collections", features = ["test-support"] }
|
||||
db = { path = "../db", features = ["test-support"] }
|
||||
|
||||