Compare commits
418 Commits
collab-v0.
...
collab-v0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59c9a57570 | ||
|
|
dde6cf596e | ||
|
|
2596fefa04 | ||
|
|
34b69896e4 | ||
|
|
7824ace58b | ||
|
|
b150efbd96 | ||
|
|
c20204d269 | ||
|
|
45bfcfc3b8 | ||
|
|
5218a2f966 | ||
|
|
95748123b5 | ||
|
|
6ad326ac58 | ||
|
|
b0652c55c6 | ||
|
|
790ef19a48 | ||
|
|
99c5f8c713 | ||
|
|
461c2400ad | ||
|
|
073a2988e6 | ||
|
|
70aac75dd5 | ||
|
|
4dc838fbb7 | ||
|
|
d4c8fa3090 | ||
|
|
a594ba8f8a | ||
|
|
f1884d608b | ||
|
|
417db95693 | ||
|
|
0220d7ba5d | ||
|
|
e2b132ef23 | ||
|
|
7e8d9d52d3 | ||
|
|
6a6a032f1f | ||
|
|
fcea254e8e | ||
|
|
9bf0a02eae | ||
|
|
2affbcc495 | ||
|
|
8012e9fcbd | ||
|
|
cd2d593a6c | ||
|
|
9ef00ea44c | ||
|
|
91d6b66fc4 | ||
|
|
5a29a74956 | ||
|
|
db3119b553 | ||
|
|
beea9b68ff | ||
|
|
82397f34d1 | ||
|
|
3cd77bfcc4 | ||
|
|
456396ca6e | ||
|
|
26b5653427 | ||
|
|
895c365485 | ||
|
|
8fa26bfe18 | ||
|
|
aca3f02590 | ||
|
|
d74fb97158 | ||
|
|
5879dcc4e9 | ||
|
|
34388a1d31 | ||
|
|
3a4f8d267a | ||
|
|
0366d725ea | ||
|
|
8bd7b28056 | ||
|
|
2697112a8a | ||
|
|
9bd4bc8813 | ||
|
|
925c9e13bb | ||
|
|
da100a09fb | ||
|
|
c42da5c9b9 | ||
|
|
2733f91d8c | ||
|
|
83aefffa38 | ||
|
|
1b8763d0cf | ||
|
|
7dde54b052 | ||
|
|
b1e37378dc | ||
|
|
e61a38b3a9 | ||
|
|
2cf48c03f9 | ||
|
|
ab978ff1a3 | ||
|
|
dcd4b8f7db | ||
|
|
2eb335158b | ||
|
|
10aecc310e | ||
|
|
750e7eb833 | ||
|
|
36bc90b2b8 | ||
|
|
f6f41510d2 | ||
|
|
cffb064c16 | ||
|
|
3313387b28 | ||
|
|
d71d543337 | ||
|
|
665219fb00 | ||
|
|
1b8f23eeed | ||
|
|
5f31907127 | ||
|
|
97989b04a0 | ||
|
|
694840cdd6 | ||
|
|
1920de81d9 | ||
|
|
3b5b48c043 | ||
|
|
2080d3efff | ||
|
|
fc7b01b74e | ||
|
|
f1b35981c2 | ||
|
|
744714b478 | ||
|
|
35549ffabe | ||
|
|
855f17c378 | ||
|
|
f23f294b86 | ||
|
|
0921178b42 | ||
|
|
30872d3992 | ||
|
|
cd08d289aa | ||
|
|
9a62150dce | ||
|
|
7bbd97cfb9 | ||
|
|
5443d9cffe | ||
|
|
be3fb1e985 | ||
|
|
b97c35a468 | ||
|
|
eec3df09be | ||
|
|
d3c411677a | ||
|
|
d97a8364ad | ||
|
|
0ed731780a | ||
|
|
11c1254e71 | ||
|
|
6ba225f3a5 | ||
|
|
55eb0a3742 | ||
|
|
1ce0863158 | ||
|
|
d609237c32 | ||
|
|
4288f10873 | ||
|
|
80e035cc2c | ||
|
|
a1f273278b | ||
|
|
ffcad4e4e2 | ||
|
|
5262e8c77e | ||
|
|
5e240f98f0 | ||
|
|
189a820113 | ||
|
|
b8d423555b | ||
|
|
8a48567857 | ||
|
|
f68e8d4664 | ||
|
|
1b225fa37c | ||
|
|
a29ccb4ff8 | ||
|
|
9cd6894dc5 | ||
|
|
dd9d20be25 | ||
|
|
260164a711 | ||
|
|
359b8aaf47 | ||
|
|
1cc3e4820a | ||
|
|
b01243109e | ||
|
|
3e0f9d27a7 | ||
|
|
2dc1130902 | ||
|
|
37174f45f0 | ||
|
|
76c42af62a | ||
|
|
cf4c103660 | ||
|
|
e1eff3f4cd | ||
|
|
a47f2ca445 | ||
|
|
e659823e6c | ||
|
|
a8ed95e1dc | ||
|
|
cb1d2cd1f2 | ||
|
|
9077b058a2 | ||
|
|
7ceb5e815e | ||
|
|
992b94eef3 | ||
|
|
a0cb6542ba | ||
|
|
6530658c3e | ||
|
|
75d3d46b1b | ||
|
|
d20d21c6a2 | ||
|
|
c1f7902309 | ||
|
|
4798161118 | ||
|
|
2a5565ca93 | ||
|
|
a5edac312e | ||
|
|
e578f2530e | ||
|
|
c84201fc9f | ||
|
|
4a00f0b062 | ||
|
|
64ac84fdf4 | ||
|
|
f27a9d77d1 | ||
|
|
0186289420 | ||
|
|
6b214acbc4 | ||
|
|
d419f27d75 | ||
|
|
eb0598dac2 | ||
|
|
aa7b909b7b | ||
|
|
b552f1788c | ||
|
|
d492cbced9 | ||
|
|
19aac6a57f | ||
|
|
685bc9fed3 | ||
|
|
406663c75e | ||
|
|
c8face33fa | ||
|
|
3c1b747f64 | ||
|
|
777f05eb76 | ||
|
|
395070cb92 | ||
|
|
a4a1859dfc | ||
|
|
e3fdfe02e5 | ||
|
|
7744c9ba45 | ||
|
|
e6ca0adbcb | ||
|
|
c105f41487 | ||
|
|
ddecba143f | ||
|
|
3451a3c7fe | ||
|
|
b9cbd4084e | ||
|
|
5505a776e6 | ||
|
|
46ff0885f0 | ||
|
|
a9dc46c950 | ||
|
|
7d33520b2c | ||
|
|
e9ea751f3d | ||
|
|
d7bbfb82a3 | ||
|
|
500ecbf915 | ||
|
|
e5c6393f85 | ||
|
|
73f0459a0f | ||
|
|
0c466f806c | ||
|
|
b48e28b555 | ||
|
|
60ebe33518 | ||
|
|
72c1ee904b | ||
|
|
57e10b7dd5 | ||
|
|
4bc1d77535 | ||
|
|
d96f524fb6 | ||
|
|
1c30767592 | ||
|
|
969c314315 | ||
|
|
568de814aa | ||
|
|
27f6ae945d | ||
|
|
1b46b7a7d6 | ||
|
|
7502558631 | ||
|
|
48b6ee313f | ||
|
|
dec5f37e4e | ||
|
|
239a04ea5b | ||
|
|
ea03b48243 | ||
|
|
585ac3e1be | ||
|
|
29a4baf346 | ||
|
|
cfdf0a57b8 | ||
|
|
944d6554de | ||
|
|
e3ac67784a | ||
|
|
62624b81d8 | ||
|
|
256e3e8e0f | ||
|
|
aebc6326a9 | ||
|
|
db1d93576f | ||
|
|
d2385bd6a0 | ||
|
|
19d14737bf | ||
|
|
4f864a20a7 | ||
|
|
2375741bdf | ||
|
|
46f1d5f5c2 | ||
|
|
d70996bb99 | ||
|
|
5a0c39cbed | ||
|
|
41b2fde10d | ||
|
|
023ecd595b | ||
|
|
2b979d3b88 | ||
|
|
5965113fc8 | ||
|
|
4c04d512db | ||
|
|
d1a44b889e | ||
|
|
04d553d4d3 | ||
|
|
2e24d128db | ||
|
|
9e59056e7f | ||
|
|
d9a892a423 | ||
|
|
3a1cd6ed3a | ||
|
|
9f9398476d | ||
|
|
b7294887c7 | ||
|
|
049c0f8ba4 | ||
|
|
11a39226e8 | ||
|
|
ac24600a40 | ||
|
|
d525cfd697 | ||
|
|
4436ec48eb | ||
|
|
5a9a0f9fa5 | ||
|
|
d2cd9c94f7 | ||
|
|
3adc0b947f | ||
|
|
718f802157 | ||
|
|
f71145bb32 | ||
|
|
cd2a8579b9 | ||
|
|
d0709e7bfa | ||
|
|
fa3f100eff | ||
|
|
f0a721032d | ||
|
|
0a565c6bae | ||
|
|
af2a2d2494 | ||
|
|
cd0b663f62 | ||
|
|
2a0ddd99d2 | ||
|
|
5581674f8f | ||
|
|
b0e1d6bc7f | ||
|
|
ae11e4f798 | ||
|
|
0b0fe91545 | ||
|
|
aeea47323a | ||
|
|
e4185f38cf | ||
|
|
09e6d44873 | ||
|
|
525d84e5bf | ||
|
|
55ca085d7d | ||
|
|
03cfd23ac5 | ||
|
|
a666ca3e40 | ||
|
|
b58ae8bdd7 | ||
|
|
5e7652698d | ||
|
|
e51cbf67ab | ||
|
|
8c75df30cb | ||
|
|
1c84e77c37 | ||
|
|
b3a92979a3 | ||
|
|
55d3c09b6b | ||
|
|
436c89650a | ||
|
|
4ead1ecbbf | ||
|
|
074e3cfbd6 | ||
|
|
bb32599ded | ||
|
|
f9cbed5a1f | ||
|
|
0078bea877 | ||
|
|
bb80cee19e | ||
|
|
0c50c0959d | ||
|
|
75b8a12ab3 | ||
|
|
4c1b4953c1 | ||
|
|
c3d556d9bd | ||
|
|
d090d230e2 | ||
|
|
bca635e5d3 | ||
|
|
3938adf60a | ||
|
|
6537def97e | ||
|
|
5020c70a04 | ||
|
|
0a63d2e3e1 | ||
|
|
ce0dfde8ee | ||
|
|
44bb2ce024 | ||
|
|
6c83be3f89 | ||
|
|
0a4517f97e | ||
|
|
c34a5f3177 | ||
|
|
4f39181c4c | ||
|
|
e7e45be6e1 | ||
|
|
8621c88a3c | ||
|
|
7dae21cb36 | ||
|
|
0f4598a243 | ||
|
|
6415809b61 | ||
|
|
fe93263ad4 | ||
|
|
3b34d858b5 | ||
|
|
71eeeedc05 | ||
|
|
532a599239 | ||
|
|
9eee22ff0a | ||
|
|
94fe93c6ee | ||
|
|
93824dd239 | ||
|
|
e5f05c9f3b | ||
|
|
bdb521cb6b | ||
|
|
c613c98e37 | ||
|
|
4e4299d500 | ||
|
|
c1291a093b | ||
|
|
ccc8c247a1 | ||
|
|
8e6c5dbc3b | ||
|
|
3c53fcdb43 | ||
|
|
adf43c87dd | ||
|
|
faf265328e | ||
|
|
9bc57c0c61 | ||
|
|
95369f92eb | ||
|
|
117458f4f6 | ||
|
|
eeb32fa888 | ||
|
|
f9567ae116 | ||
|
|
c151c87e12 | ||
|
|
3190236396 | ||
|
|
17dfbb91ba | ||
|
|
c3cf056fc5 | ||
|
|
275f0ae492 | ||
|
|
f4e9759f26 | ||
|
|
fdf758e050 | ||
|
|
0dfacd7ffa | ||
|
|
36c07f940c | ||
|
|
01929037f1 | ||
|
|
0817f905a2 | ||
|
|
ad67f5e4de | ||
|
|
e9eadcaa6a | ||
|
|
4b1dcf2d55 | ||
|
|
974ef967a3 | ||
|
|
be523617c9 | ||
|
|
6cbf197226 | ||
|
|
3e8fcb04f7 | ||
|
|
42bb5f0e9f | ||
|
|
e401caff7c | ||
|
|
b222e8eb5a | ||
|
|
fb35631337 | ||
|
|
6659dac2e5 | ||
|
|
b9af2ae66e | ||
|
|
0dcdd6ea39 | ||
|
|
a66aa9c09c | ||
|
|
e6c5079a49 | ||
|
|
d7369ace6a | ||
|
|
40073f6100 | ||
|
|
65c5adff05 | ||
|
|
59e8600e4c | ||
|
|
0310e27347 | ||
|
|
9902211af1 | ||
|
|
1da5be6e8f | ||
|
|
ee66adbb49 | ||
|
|
3612c46d6d | ||
|
|
bf9c9b0103 | ||
|
|
ea8778921b | ||
|
|
2ef2b5a053 | ||
|
|
5bb7701de7 | ||
|
|
2145965749 | ||
|
|
11caba4a4c | ||
|
|
9f39dcf7cf | ||
|
|
1135aeecb8 | ||
|
|
b6f78cd5dc | ||
|
|
a6198c9a1a | ||
|
|
ad698fd110 | ||
|
|
0d1d267213 | ||
|
|
c213c98ea4 | ||
|
|
cc58607c3b | ||
|
|
58947c5c72 | ||
|
|
6871bbbc71 | ||
|
|
28aa1567ce | ||
|
|
f639c4c3d1 | ||
|
|
d61c0fb24c | ||
|
|
3d5a3634cf | ||
|
|
9ad8731897 | ||
|
|
44c3cedc48 | ||
|
|
eeeaf6d9a2 | ||
|
|
2d4deaafcd | ||
|
|
c839ab2028 | ||
|
|
5d17347a45 | ||
|
|
9ce3524eb8 | ||
|
|
03115c8d71 | ||
|
|
dafdc4b4a5 | ||
|
|
05a6bd914d | ||
|
|
fb03eb7a3c | ||
|
|
8e70e1934a | ||
|
|
1bb41b6f54 | ||
|
|
90d1d9ac82 | ||
|
|
bed06346d1 | ||
|
|
7e02ac772a | ||
|
|
c0d67d9522 | ||
|
|
d14dd27cdc | ||
|
|
6b4dd2a5de | ||
|
|
9355d501bc | ||
|
|
335db5d03d | ||
|
|
98461ea0cd | ||
|
|
5707bae9b9 | ||
|
|
bbeb685769 | ||
|
|
cea103e47c | ||
|
|
ad31c284c7 | ||
|
|
738893c527 | ||
|
|
6da04d0eee | ||
|
|
7482660456 | ||
|
|
00123ffe2b | ||
|
|
53f8744794 | ||
|
|
537d4762f6 | ||
|
|
2f5004c238 | ||
|
|
7dcd6c920f | ||
|
|
ea42bc3c9b | ||
|
|
d3ba769291 | ||
|
|
3f1b95927f | ||
|
|
c183e854d7 | ||
|
|
86f51ade60 | ||
|
|
c838a7d973 | ||
|
|
9abfa037fd | ||
|
|
5efe2ed6d3 | ||
|
|
847376a4f5 | ||
|
|
1d6af4cf20 | ||
|
|
b6c5c7871e | ||
|
|
5acae094bd | ||
|
|
4d7425f4bf | ||
|
|
2497e7c008 | ||
|
|
474a5dd4f2 | ||
|
|
be6ee3cbff | ||
|
|
4977acf6a5 | ||
|
|
0cd2d9a9c8 | ||
|
|
e2ba8d6df7 |
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -45,8 +45,11 @@ jobs:
|
||||
- name: Run tests
|
||||
run: cargo test --workspace --no-fail-fast
|
||||
|
||||
- name: Build collab binaries
|
||||
run: cargo build --bins --all-features
|
||||
- name: Build collab
|
||||
run: cargo build -p collab
|
||||
|
||||
- name: Build other binaries
|
||||
run: cargo build --workspace --bins --all-features
|
||||
|
||||
bundle:
|
||||
name: Bundle app
|
||||
|
||||
4
.github/workflows/release_actions.yml
vendored
4
.github/workflows/release_actions.yml
vendored
@@ -14,10 +14,10 @@ jobs:
|
||||
content: |
|
||||
📣 Zed ${{ github.event.release.tag_name }} was just released!
|
||||
|
||||
Restart your Zed or head to https://zed.dev/releases to grab it.
|
||||
Restart your Zed or head to https://zed.dev/releases/latest to grab it.
|
||||
|
||||
```md
|
||||
### Changelog
|
||||
# Changelog
|
||||
|
||||
${{ github.event.release.body }}
|
||||
```
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -18,3 +18,4 @@ DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
**/*.db
|
||||
|
||||
1227
Cargo.lock
generated
1227
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,8 @@ members = [
|
||||
"crates/search",
|
||||
"crates/settings",
|
||||
"crates/snippet",
|
||||
"crates/sqlez",
|
||||
"crates/sqlez_macros",
|
||||
"crates/sum_tree",
|
||||
"crates/terminal",
|
||||
"crates/text",
|
||||
@@ -65,7 +67,7 @@ serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
|
||||
rand = { version = "0.8" }
|
||||
|
||||
[patch.crates-io]
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "366210ae925d7ea0891bc7a0c738f60c77c04d7b" }
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da" }
|
||||
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
|
||||
|
||||
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457
|
||||
@@ -81,3 +83,4 @@ split-debuginfo = "unpacked"
|
||||
[profile.release]
|
||||
debug = true
|
||||
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
"ctrl-n": "editor::MoveDown",
|
||||
"ctrl-b": "editor::MoveLeft",
|
||||
"ctrl-f": "editor::MoveRight",
|
||||
"ctrl-l": "editor::CenterScreen",
|
||||
"ctrl-l": "editor::NextScreen",
|
||||
"alt-left": "editor::MoveToPreviousWordStart",
|
||||
"alt-b": "editor::MoveToPreviousWordStart",
|
||||
"alt-right": "editor::MoveToNextWordEnd",
|
||||
@@ -472,6 +472,15 @@
|
||||
"terminal::SendText",
|
||||
"\u0001"
|
||||
],
|
||||
// Terminal.app compatability
|
||||
"alt-left": [
|
||||
"terminal::SendText",
|
||||
"\u001bb"
|
||||
],
|
||||
"alt-right": [
|
||||
"terminal::SendText",
|
||||
"\u001bf"
|
||||
],
|
||||
// There are conflicting bindings for these keys in the global context.
|
||||
// these bindings override them, remove at your own risk:
|
||||
"up": [
|
||||
|
||||
@@ -8,6 +8,22 @@
|
||||
"Namespace": "G"
|
||||
}
|
||||
],
|
||||
"i": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Object": {
|
||||
"around": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"a": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Object": {
|
||||
"around": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"h": "vim::Left",
|
||||
"backspace": "vim::Backspace",
|
||||
"j": "vim::Down",
|
||||
@@ -38,22 +54,6 @@
|
||||
],
|
||||
"%": "vim::Matching",
|
||||
"escape": "editor::Cancel",
|
||||
"i": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Object": {
|
||||
"around": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"a": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Object": {
|
||||
"around": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
|
||||
"1": [
|
||||
"vim::Number",
|
||||
@@ -110,6 +110,12 @@
|
||||
"vim::PushOperator",
|
||||
"Yank"
|
||||
],
|
||||
"z": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Namespace": "Z"
|
||||
}
|
||||
],
|
||||
"i": [
|
||||
"vim::SwitchMode",
|
||||
"Insert"
|
||||
@@ -147,6 +153,30 @@
|
||||
{
|
||||
"focus": true
|
||||
}
|
||||
],
|
||||
"ctrl-f": [
|
||||
"vim::Scroll",
|
||||
"PageDown"
|
||||
],
|
||||
"ctrl-b": [
|
||||
"vim::Scroll",
|
||||
"PageUp"
|
||||
],
|
||||
"ctrl-d": [
|
||||
"vim::Scroll",
|
||||
"HalfPageDown"
|
||||
],
|
||||
"ctrl-u": [
|
||||
"vim::Scroll",
|
||||
"HalfPageUp"
|
||||
],
|
||||
"ctrl-e": [
|
||||
"vim::Scroll",
|
||||
"LineDown"
|
||||
],
|
||||
"ctrl-y": [
|
||||
"vim::Scroll",
|
||||
"LineUp"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -188,6 +218,18 @@
|
||||
"y": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == z",
|
||||
"bindings": {
|
||||
"t": "editor::ScrollCursorTop",
|
||||
"z": "editor::ScrollCursorCenter",
|
||||
"b": "editor::ScrollCursorBottom",
|
||||
"escape": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && VimObject",
|
||||
"bindings": {
|
||||
|
||||
@@ -1,230 +1,233 @@
|
||||
{
|
||||
// The name of the Zed theme to use for the UI
|
||||
"theme": "One Dark",
|
||||
// The name of a font to use for rendering text in the editor
|
||||
"buffer_font_family": "Zed Mono",
|
||||
// The default font size for text in the editor
|
||||
"buffer_font_size": 15,
|
||||
// Whether to enable vim modes and key bindings
|
||||
"vim_mode": false,
|
||||
// Whether to show the informational hover box when moving the mouse
|
||||
// over symbols in the editor.
|
||||
"hover_popover_enabled": true,
|
||||
// Whether the cursor blinks in the editor.
|
||||
"cursor_blink": true,
|
||||
// Whether to pop the completions menu while typing in an editor without
|
||||
// explicitly requesting it.
|
||||
"show_completions_on_input": 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
|
||||
// take four values.
|
||||
//
|
||||
// 1. Never automatically save:
|
||||
// "autosave": "off",
|
||||
// 2. Save when changing focus away from the Zed window:
|
||||
// "autosave": "on_window_change",
|
||||
// 3. Save when changing focus away from a specific buffer:
|
||||
// "autosave": "on_focus_change",
|
||||
// 4. Save when idle for a certain amount of time:
|
||||
// "autosave": { "after_delay": {"milliseconds": 500} },
|
||||
"autosave": "off",
|
||||
// Where to place the dock by default. This setting can take three
|
||||
// values:
|
||||
//
|
||||
// 1. Position the dock attached to the bottom of the workspace
|
||||
// "default_dock_anchor": "bottom"
|
||||
// 2. Position the dock to the right of the workspace like a side panel
|
||||
// "default_dock_anchor": "right"
|
||||
// 3. Position the dock full screen over the entire workspace"
|
||||
// "default_dock_anchor": "expanded"
|
||||
"default_dock_anchor": "right",
|
||||
// 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:
|
||||
//
|
||||
// 1. Format code using the current language server:
|
||||
// "format_on_save": "language_server"
|
||||
// 2. Format code using an external command:
|
||||
// "format_on_save": {
|
||||
// "external": {
|
||||
// "command": "prettier",
|
||||
// "arguments": ["--stdin-filepath", "{buffer_path}"]
|
||||
// }
|
||||
// }
|
||||
"formatter": "language_server",
|
||||
// How to soft-wrap long lines of text. This setting can take
|
||||
// three values:
|
||||
//
|
||||
// 1. Do not soft wrap.
|
||||
// "soft_wrap": "none",
|
||||
// 2. Soft wrap lines that overflow the editor:
|
||||
// "soft_wrap": "editor_width",
|
||||
// 3. Soft wrap lines at the preferred line length
|
||||
// "soft_wrap": "preferred_line_length",
|
||||
"soft_wrap": "none",
|
||||
// The column at which to soft-wrap lines, for buffers where soft-wrap
|
||||
// is enabled.
|
||||
"preferred_line_length": 80,
|
||||
// Whether to indent lines using tab characters, as opposed to multiple
|
||||
// spaces.
|
||||
"hard_tabs": false,
|
||||
// How many columns a tab should occupy.
|
||||
"tab_size": 4,
|
||||
// Git gutter behavior configuration.
|
||||
"git": {
|
||||
// Control whether the git gutter is shown. May take 2 values:
|
||||
// 1. Show the gutter
|
||||
// "git_gutter": "tracked_files"
|
||||
// 2. Hide the gutter
|
||||
// "git_gutter": "hide"
|
||||
"git_gutter": "tracked_files"
|
||||
},
|
||||
// Settings specific to journaling
|
||||
"journal": {
|
||||
// The path of the directory where journal entries are stored
|
||||
"path": "~",
|
||||
// What format to display the hours in
|
||||
// May take 2 values:
|
||||
// 1. hour12
|
||||
// 2. hour24
|
||||
"hour_format": "hour12"
|
||||
},
|
||||
// Settings specific to the terminal
|
||||
"terminal": {
|
||||
// What shell to use when opening a terminal. May take 3 values:
|
||||
// 1. Use the system's default terminal configuration (e.g. $TERM).
|
||||
// "shell": "system"
|
||||
// 2. A program:
|
||||
// "shell": {
|
||||
// "program": "sh"
|
||||
// }
|
||||
// 3. A program with arguments:
|
||||
// "shell": {
|
||||
// "with_arguments": {
|
||||
// "program": "/bin/bash",
|
||||
// "arguments": ["--login"]
|
||||
// }
|
||||
// }
|
||||
"shell": "system",
|
||||
// What working directory to use when launching the terminal.
|
||||
// May take 4 values:
|
||||
// 1. Use the current file's project directory. Will Fallback to the
|
||||
// first project directory strategy if unsuccessful
|
||||
// "working_directory": "current_project_directory"
|
||||
// 2. Use the first project in this workspace's directory
|
||||
// "working_directory": "first_project_directory"
|
||||
// 3. Always use this platform's home directory (if we can find it)
|
||||
// "working_directory": "always_home"
|
||||
// 4. Always use a specific directory. This value will be shell expanded.
|
||||
// If this path is not a valid directory the terminal will default to
|
||||
// this platform's home directory (if we can find it)
|
||||
// "working_directory": {
|
||||
// "always": {
|
||||
// "directory": "~/zed/projects/"
|
||||
// }
|
||||
// }
|
||||
// The name of the Zed theme to use for the UI
|
||||
"theme": "One Dark",
|
||||
// The name of a font to use for rendering text in the editor
|
||||
"buffer_font_family": "Zed Mono",
|
||||
// The default font size for text in the editor
|
||||
"buffer_font_size": 15,
|
||||
// The factor to grow the active pane by. Defaults to 1.0
|
||||
// which gives the same size as all other panes.
|
||||
"active_pane_magnification": 1.0,
|
||||
// Whether to enable vim modes and key bindings
|
||||
"vim_mode": false,
|
||||
// Whether to show the informational hover box when moving the mouse
|
||||
// over symbols in the editor.
|
||||
"hover_popover_enabled": true,
|
||||
// Whether the cursor blinks in the editor.
|
||||
"cursor_blink": true,
|
||||
// Whether to pop the completions menu while typing in an editor without
|
||||
// explicitly requesting it.
|
||||
"show_completions_on_input": 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
|
||||
// take four values.
|
||||
//
|
||||
// 1. Never automatically save:
|
||||
// "autosave": "off",
|
||||
// 2. Save when changing focus away from the Zed window:
|
||||
// "autosave": "on_window_change",
|
||||
// 3. Save when changing focus away from a specific buffer:
|
||||
// "autosave": "on_focus_change",
|
||||
// 4. Save when idle for a certain amount of time:
|
||||
// "autosave": { "after_delay": {"milliseconds": 500} },
|
||||
"autosave": "off",
|
||||
// Where to place the dock by default. This setting can take three
|
||||
// values:
|
||||
//
|
||||
"working_directory": "current_project_directory",
|
||||
// Set the cursor blinking behavior in the terminal.
|
||||
// May take 4 values:
|
||||
// 1. Never blink the cursor, ignoring the terminal mode
|
||||
// "blinking": "off",
|
||||
// 2. Default the cursor blink to off, but allow the terminal to
|
||||
// set blinking
|
||||
// "blinking": "terminal_controlled",
|
||||
// 3. Always blink the cursor, ignoring the terminal mode
|
||||
// "blinking": "on",
|
||||
"blinking": "terminal_controlled",
|
||||
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
|
||||
// Alternate Scroll mode converts mouse scroll events into up / down key
|
||||
// presses when in the alternate screen (e.g. when running applications
|
||||
// like vim or less). The terminal can still set and unset this mode.
|
||||
// May take 2 values:
|
||||
// 1. Default alternate scroll mode to on
|
||||
// "alternate_scroll": "on",
|
||||
// 2. Default alternate scroll mode to off
|
||||
// "alternate_scroll": "off",
|
||||
"alternate_scroll": "off",
|
||||
// Set whether the option key behaves as the meta key.
|
||||
// May take 2 values:
|
||||
// 1. Rely on default platform handling of option key, on macOS
|
||||
// this means generating certain unicode characters
|
||||
// "option_to_meta": false,
|
||||
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
|
||||
// "option_to_meta": true,
|
||||
"option_as_meta": false,
|
||||
// Whether or not selecting text in the terminal will automatically
|
||||
// copy to the system clipboard.
|
||||
"copy_on_select": false,
|
||||
// Any key-value pairs added to this list will be added to the terminal's
|
||||
// enviroment. Use `:` to seperate multiple values.
|
||||
"env": {
|
||||
// "KEY": "value1:value2"
|
||||
}
|
||||
// Set the terminal's font size. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font size.
|
||||
// "font_size": "15"
|
||||
// Set the terminal's font family. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font family.
|
||||
// "font_family": "Zed Mono"
|
||||
},
|
||||
// Different settings for specific languages.
|
||||
"languages": {
|
||||
"Plain Text": {
|
||||
"soft_wrap": "preferred_line_length"
|
||||
},
|
||||
"C": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"C++": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Elixir": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Go": {
|
||||
"tab_size": 4,
|
||||
"hard_tabs": true
|
||||
},
|
||||
"Markdown": {
|
||||
"soft_wrap": "preferred_line_length"
|
||||
},
|
||||
"Rust": {
|
||||
"tab_size": 4
|
||||
},
|
||||
"JavaScript": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"TypeScript": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"TSX": {
|
||||
"tab_size": 2
|
||||
}
|
||||
},
|
||||
// LSP Specific settings.
|
||||
"lsp": {
|
||||
// Specify the LSP name as a key here.
|
||||
// As of 8/10/22, supported LSPs are:
|
||||
// pyright
|
||||
// gopls
|
||||
// rust-analyzer
|
||||
// typescript-language-server
|
||||
// vscode-json-languageserver
|
||||
// "rust_analyzer": {
|
||||
// //These initialization options are merged into Zed's defaults
|
||||
// "initialization_options": {
|
||||
// "checkOnSave": {
|
||||
// "command": "clippy"
|
||||
// }
|
||||
// 1. Position the dock attached to the bottom of the workspace
|
||||
// "default_dock_anchor": "bottom"
|
||||
// 2. Position the dock to the right of the workspace like a side panel
|
||||
// "default_dock_anchor": "right"
|
||||
// 3. Position the dock full screen over the entire workspace"
|
||||
// "default_dock_anchor": "expanded"
|
||||
"default_dock_anchor": "right",
|
||||
// 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:
|
||||
//
|
||||
// 1. Format code using the current language server:
|
||||
// "format_on_save": "language_server"
|
||||
// 2. Format code using an external command:
|
||||
// "format_on_save": {
|
||||
// "external": {
|
||||
// "command": "prettier",
|
||||
// "arguments": ["--stdin-filepath", "{buffer_path}"]
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
"formatter": "language_server",
|
||||
// How to soft-wrap long lines of text. This setting can take
|
||||
// three values:
|
||||
//
|
||||
// 1. Do not soft wrap.
|
||||
// "soft_wrap": "none",
|
||||
// 2. Soft wrap lines that overflow the editor:
|
||||
// "soft_wrap": "editor_width",
|
||||
// 3. Soft wrap lines at the preferred line length
|
||||
// "soft_wrap": "preferred_line_length",
|
||||
"soft_wrap": "none",
|
||||
// The column at which to soft-wrap lines, for buffers where soft-wrap
|
||||
// is enabled.
|
||||
"preferred_line_length": 80,
|
||||
// Whether to indent lines using tab characters, as opposed to multiple
|
||||
// spaces.
|
||||
"hard_tabs": false,
|
||||
// How many columns a tab should occupy.
|
||||
"tab_size": 4,
|
||||
// Git gutter behavior configuration.
|
||||
"git": {
|
||||
// Control whether the git gutter is shown. May take 2 values:
|
||||
// 1. Show the gutter
|
||||
// "git_gutter": "tracked_files"
|
||||
// 2. Hide the gutter
|
||||
// "git_gutter": "hide"
|
||||
"git_gutter": "tracked_files"
|
||||
},
|
||||
// Settings specific to journaling
|
||||
"journal": {
|
||||
// The path of the directory where journal entries are stored
|
||||
"path": "~",
|
||||
// What format to display the hours in
|
||||
// May take 2 values:
|
||||
// 1. hour12
|
||||
// 2. hour24
|
||||
"hour_format": "hour12"
|
||||
},
|
||||
// Settings specific to the terminal
|
||||
"terminal": {
|
||||
// What shell to use when opening a terminal. May take 3 values:
|
||||
// 1. Use the system's default terminal configuration (e.g. $TERM).
|
||||
// "shell": "system"
|
||||
// 2. A program:
|
||||
// "shell": {
|
||||
// "program": "sh"
|
||||
// }
|
||||
// 3. A program with arguments:
|
||||
// "shell": {
|
||||
// "with_arguments": {
|
||||
// "program": "/bin/bash",
|
||||
// "arguments": ["--login"]
|
||||
// }
|
||||
// }
|
||||
"shell": "system",
|
||||
// What working directory to use when launching the terminal.
|
||||
// May take 4 values:
|
||||
// 1. Use the current file's project directory. Will Fallback to the
|
||||
// first project directory strategy if unsuccessful
|
||||
// "working_directory": "current_project_directory"
|
||||
// 2. Use the first project in this workspace's directory
|
||||
// "working_directory": "first_project_directory"
|
||||
// 3. Always use this platform's home directory (if we can find it)
|
||||
// "working_directory": "always_home"
|
||||
// 4. Always use a specific directory. This value will be shell expanded.
|
||||
// If this path is not a valid directory the terminal will default to
|
||||
// this platform's home directory (if we can find it)
|
||||
// "working_directory": {
|
||||
// "always": {
|
||||
// "directory": "~/zed/projects/"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
"working_directory": "current_project_directory",
|
||||
// Set the cursor blinking behavior in the terminal.
|
||||
// May take 4 values:
|
||||
// 1. Never blink the cursor, ignoring the terminal mode
|
||||
// "blinking": "off",
|
||||
// 2. Default the cursor blink to off, but allow the terminal to
|
||||
// set blinking
|
||||
// "blinking": "terminal_controlled",
|
||||
// 3. Always blink the cursor, ignoring the terminal mode
|
||||
// "blinking": "on",
|
||||
"blinking": "terminal_controlled",
|
||||
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
|
||||
// Alternate Scroll mode converts mouse scroll events into up / down key
|
||||
// presses when in the alternate screen (e.g. when running applications
|
||||
// like vim or less). The terminal can still set and unset this mode.
|
||||
// May take 2 values:
|
||||
// 1. Default alternate scroll mode to on
|
||||
// "alternate_scroll": "on",
|
||||
// 2. Default alternate scroll mode to off
|
||||
// "alternate_scroll": "off",
|
||||
"alternate_scroll": "off",
|
||||
// Set whether the option key behaves as the meta key.
|
||||
// May take 2 values:
|
||||
// 1. Rely on default platform handling of option key, on macOS
|
||||
// this means generating certain unicode characters
|
||||
// "option_to_meta": false,
|
||||
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
|
||||
// "option_to_meta": true,
|
||||
"option_as_meta": false,
|
||||
// Whether or not selecting text in the terminal will automatically
|
||||
// copy to the system clipboard.
|
||||
"copy_on_select": false,
|
||||
// Any key-value pairs added to this list will be added to the terminal's
|
||||
// enviroment. Use `:` to seperate multiple values.
|
||||
"env": {
|
||||
// "KEY": "value1:value2"
|
||||
}
|
||||
// Set the terminal's font size. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font size.
|
||||
// "font_size": "15"
|
||||
// Set the terminal's font family. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font family.
|
||||
// "font_family": "Zed Mono"
|
||||
},
|
||||
// Different settings for specific languages.
|
||||
"languages": {
|
||||
"Plain Text": {
|
||||
"soft_wrap": "preferred_line_length"
|
||||
},
|
||||
"C": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"C++": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Elixir": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Go": {
|
||||
"tab_size": 4,
|
||||
"hard_tabs": true
|
||||
},
|
||||
"Markdown": {
|
||||
"soft_wrap": "preferred_line_length"
|
||||
},
|
||||
"Rust": {
|
||||
"tab_size": 4
|
||||
},
|
||||
"JavaScript": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"TypeScript": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"TSX": {
|
||||
"tab_size": 2
|
||||
}
|
||||
},
|
||||
// LSP Specific settings.
|
||||
"lsp": {
|
||||
// Specify the LSP name as a key here.
|
||||
// As of 8/10/22, supported LSPs are:
|
||||
// pyright
|
||||
// gopls
|
||||
// rust-analyzer
|
||||
// typescript-language-server
|
||||
// vscode-json-languageserver
|
||||
// "rust_analyzer": {
|
||||
// //These initialization options are merged into Zed's defaults
|
||||
// "initialization_options": {
|
||||
// "checkOnSave": {
|
||||
// "command": "clippy"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,12 @@ use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
use std::{cmp::Reverse, fmt::Write, sync::Arc};
|
||||
use util::ResultExt;
|
||||
use workspace::{ItemHandle, StatusItemView, Workspace};
|
||||
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
||||
|
||||
actions!(lsp_status, [ShowErrorMessage]);
|
||||
|
||||
const DOWNLOAD_ICON: &str = "icons/download_12.svg";
|
||||
const WARNING_ICON: &str = "icons/triangle_exclamation_12.svg";
|
||||
const DONE_ICON: &str = "icons/circle_check_12.svg";
|
||||
|
||||
pub enum Event {
|
||||
ShowError { lsp_name: Arc<str>, error: String },
|
||||
@@ -237,7 +236,6 @@ impl ActivityIndicator {
|
||||
|
||||
// Show any application auto-update info.
|
||||
if let Some(updater) = &self.auto_updater {
|
||||
// let theme = &cx.global::<Settings>().theme.workspace.status_bar;
|
||||
match &updater.read(cx).status() {
|
||||
AutoUpdateStatus::Checking => (
|
||||
Some(DOWNLOAD_ICON),
|
||||
@@ -254,9 +252,7 @@ impl ActivityIndicator {
|
||||
"Installing Zed update…".to_string(),
|
||||
None,
|
||||
),
|
||||
AutoUpdateStatus::Updated => {
|
||||
(Some(DONE_ICON), "Restart to update Zed".to_string(), None)
|
||||
}
|
||||
AutoUpdateStatus::Updated => (None, "Restart to update Zed".to_string(), None),
|
||||
AutoUpdateStatus::Errored => (
|
||||
Some(WARNING_ICON),
|
||||
"Auto update failed".to_string(),
|
||||
|
||||
@@ -8,6 +8,7 @@ path = "src/auto_update.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
db = { path = "../db" }
|
||||
client = { path = "../client" }
|
||||
gpui = { path = "../gpui" }
|
||||
menu = { path = "../menu" }
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
mod update_notification;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
|
||||
use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{
|
||||
actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||
MutableAppContext, Task, WeakViewHandle,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use serde::Deserialize;
|
||||
use settings::ReleaseChannel;
|
||||
use smol::{fs::File, io::AsyncReadExt, process::Command};
|
||||
use std::{env, ffi::OsString, path::PathBuf, sync::Arc, time::Duration};
|
||||
use update_notification::UpdateNotification;
|
||||
use util::channel::ReleaseChannel;
|
||||
use workspace::Workspace;
|
||||
|
||||
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
|
||||
@@ -41,7 +42,6 @@ pub struct AutoUpdater {
|
||||
current_version: AppVersion,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
pending_poll: Option<Task<()>>,
|
||||
db: project::Db,
|
||||
server_url: String,
|
||||
}
|
||||
|
||||
@@ -55,11 +55,11 @@ impl Entity for AutoUpdater {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
pub fn init(db: project::Db, http_client: Arc<dyn HttpClient>, cx: &mut MutableAppContext) {
|
||||
pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut MutableAppContext) {
|
||||
if let Some(version) = (*ZED_APP_VERSION).or_else(|| cx.platform().app_version().ok()) {
|
||||
let server_url = ZED_SERVER_URL.to_string();
|
||||
let server_url = server_url;
|
||||
let auto_updater = cx.add_model(|cx| {
|
||||
let updater = AutoUpdater::new(version, db.clone(), http_client, server_url.clone());
|
||||
let updater = AutoUpdater::new(version, http_client, server_url.clone());
|
||||
updater.start_polling(cx).detach();
|
||||
updater
|
||||
});
|
||||
@@ -70,7 +70,14 @@ pub fn init(db: project::Db, http_client: Arc<dyn HttpClient>, cx: &mut MutableA
|
||||
}
|
||||
});
|
||||
cx.add_global_action(move |_: &ViewReleaseNotes, cx| {
|
||||
cx.platform().open_url(&format!("{server_url}/releases"));
|
||||
let latest_release_url = if cx.has_global::<ReleaseChannel>()
|
||||
&& *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
|
||||
{
|
||||
format!("{server_url}/releases/preview/latest")
|
||||
} else {
|
||||
format!("{server_url}/releases/latest")
|
||||
};
|
||||
cx.platform().open_url(&latest_release_url);
|
||||
});
|
||||
cx.add_action(UpdateNotification::dismiss);
|
||||
}
|
||||
@@ -113,14 +120,12 @@ impl AutoUpdater {
|
||||
|
||||
fn new(
|
||||
current_version: AppVersion,
|
||||
db: project::Db,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
server_url: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
status: AutoUpdateStatus::Idle,
|
||||
current_version,
|
||||
db,
|
||||
http_client,
|
||||
server_url,
|
||||
pending_poll: None,
|
||||
@@ -290,20 +295,28 @@ impl AutoUpdater {
|
||||
should_show: bool,
|
||||
cx: &AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
let db = self.db.clone();
|
||||
cx.background().spawn(async move {
|
||||
if should_show {
|
||||
db.write_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY, "")?;
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp(
|
||||
SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string(),
|
||||
"".to_string(),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
db.delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?;
|
||||
KEY_VALUE_STORE
|
||||
.delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string())
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
|
||||
let db = self.db.clone();
|
||||
cx.background()
|
||||
.spawn(async move { Ok(db.read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?.is_some()) })
|
||||
cx.background().spawn(async move {
|
||||
Ok(KEY_VALUE_STORE
|
||||
.read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?
|
||||
.is_some())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@ use gpui::{
|
||||
Element, Entity, MouseButton, View, ViewContext,
|
||||
};
|
||||
use menu::Cancel;
|
||||
use settings::{ReleaseChannel, Settings};
|
||||
use workspace::Notification;
|
||||
use settings::Settings;
|
||||
use util::channel::ReleaseChannel;
|
||||
use workspace::notifications::Notification;
|
||||
|
||||
pub struct UpdateNotification {
|
||||
version: AppVersion,
|
||||
@@ -29,7 +30,7 @@ impl View for UpdateNotification {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let theme = &theme.update_notification;
|
||||
|
||||
let app_name = cx.global::<ReleaseChannel>().name();
|
||||
let app_name = cx.global::<ReleaseChannel>().display_name();
|
||||
|
||||
MouseEventHandler::<ViewReleaseNotes>::new(0, cx, |state, cx| {
|
||||
Flex::column()
|
||||
|
||||
@@ -4,7 +4,10 @@ use gpui::{
|
||||
use itertools::Itertools;
|
||||
use search::ProjectSearchView;
|
||||
use settings::Settings;
|
||||
use workspace::{ItemEvent, ItemHandle, ToolbarItemLocation, ToolbarItemView};
|
||||
use workspace::{
|
||||
item::{ItemEvent, ItemHandle},
|
||||
ToolbarItemLocation, ToolbarItemView,
|
||||
};
|
||||
|
||||
pub enum Event {
|
||||
UpdateLocation,
|
||||
|
||||
@@ -22,7 +22,7 @@ pub fn init(client: Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut Mu
|
||||
#[derive(Clone)]
|
||||
pub struct IncomingCall {
|
||||
pub room_id: u64,
|
||||
pub caller: Arc<User>,
|
||||
pub calling_user: Arc<User>,
|
||||
pub participants: Vec<Arc<User>>,
|
||||
pub initial_project: Option<proto::ParticipantProject>,
|
||||
}
|
||||
@@ -78,9 +78,9 @@ impl ActiveCall {
|
||||
user_store.get_users(envelope.payload.participant_user_ids, cx)
|
||||
})
|
||||
.await?,
|
||||
caller: user_store
|
||||
calling_user: user_store
|
||||
.update(&mut cx, |user_store, cx| {
|
||||
user_store.get_user(envelope.payload.caller_user_id, cx)
|
||||
user_store.get_user(envelope.payload.calling_user_id, cx)
|
||||
})
|
||||
.await?,
|
||||
initial_project: envelope.payload.initial_project,
|
||||
@@ -94,12 +94,18 @@ impl ActiveCall {
|
||||
|
||||
async fn handle_call_canceled(
|
||||
this: ModelHandle<Self>,
|
||||
_: TypedEnvelope<proto::CallCanceled>,
|
||||
envelope: TypedEnvelope<proto::CallCanceled>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, _| {
|
||||
*this.incoming_call.0.borrow_mut() = None;
|
||||
let mut incoming_call = this.incoming_call.0.borrow_mut();
|
||||
if incoming_call
|
||||
.as_ref()
|
||||
.map_or(false, |call| call.room_id == envelope.payload.room_id)
|
||||
{
|
||||
incoming_call.take();
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@@ -110,13 +116,13 @@ impl ActiveCall {
|
||||
|
||||
pub fn invite(
|
||||
&mut self,
|
||||
recipient_user_id: u64,
|
||||
called_user_id: u64,
|
||||
initial_project: Option<ModelHandle<Project>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
if !self.pending_invites.insert(recipient_user_id) {
|
||||
if !self.pending_invites.insert(called_user_id) {
|
||||
return Task::ready(Err(anyhow!("user was already invited")));
|
||||
}
|
||||
|
||||
@@ -136,13 +142,13 @@ impl ActiveCall {
|
||||
};
|
||||
|
||||
room.update(&mut cx, |room, cx| {
|
||||
room.call(recipient_user_id, initial_project_id, cx)
|
||||
room.call(called_user_id, initial_project_id, cx)
|
||||
})
|
||||
.await?;
|
||||
} else {
|
||||
let room = cx
|
||||
.update(|cx| {
|
||||
Room::create(recipient_user_id, initial_project, client, user_store, cx)
|
||||
Room::create(called_user_id, initial_project, client, user_store, cx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -155,7 +161,7 @@ impl ActiveCall {
|
||||
|
||||
let result = invite.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.pending_invites.remove(&recipient_user_id);
|
||||
this.pending_invites.remove(&called_user_id);
|
||||
cx.notify();
|
||||
});
|
||||
result
|
||||
@@ -164,7 +170,7 @@ impl ActiveCall {
|
||||
|
||||
pub fn cancel_invite(
|
||||
&mut self,
|
||||
recipient_user_id: u64,
|
||||
called_user_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let room_id = if let Some(room) = self.room() {
|
||||
@@ -178,7 +184,7 @@ impl ActiveCall {
|
||||
client
|
||||
.request(proto::CancelCall {
|
||||
room_id,
|
||||
recipient_user_id,
|
||||
called_user_id,
|
||||
})
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
|
||||
@@ -4,7 +4,7 @@ use collections::HashMap;
|
||||
use gpui::WeakModelHandle;
|
||||
pub use live_kit_client::Frame;
|
||||
use project::Project;
|
||||
use std::sync::Arc;
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ParticipantLocation {
|
||||
@@ -36,7 +36,7 @@ pub struct LocalParticipant {
|
||||
pub active_project: Option<WeakModelHandle<Project>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RemoteParticipant {
|
||||
pub user: Arc<User>,
|
||||
pub projects: Vec<proto::ParticipantProject>,
|
||||
@@ -49,6 +49,12 @@ pub struct RemoteVideoTrack {
|
||||
pub(crate) live_kit_track: Arc<live_kit_client::RemoteVideoTrack>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for RemoteVideoTrack {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("RemoteVideoTrack").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteVideoTrack {
|
||||
pub fn frames(&self) -> async_broadcast::Receiver<Frame> {
|
||||
self.live_kit_track.frames()
|
||||
|
||||
@@ -5,14 +5,18 @@ use crate::{
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
|
||||
use collections::{BTreeMap, HashSet};
|
||||
use futures::StreamExt;
|
||||
use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, WeakModelHandle,
|
||||
};
|
||||
use live_kit_client::{LocalTrackPublication, LocalVideoTrack, RemoteVideoTrackUpdate};
|
||||
use postage::stream::Stream;
|
||||
use project::Project;
|
||||
use std::{mem, os::unix::prelude::OsStrExt, sync::Arc};
|
||||
use std::{mem, sync::Arc, time::Duration};
|
||||
use util::{post_inc, ResultExt};
|
||||
|
||||
pub const RECONNECT_TIMEOUT: Duration = client::RECEIVE_TIMEOUT;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Event {
|
||||
ParticipantLocationChanged {
|
||||
@@ -46,6 +50,7 @@ pub struct Room {
|
||||
user_store: ModelHandle<UserStore>,
|
||||
subscriptions: Vec<client::Subscription>,
|
||||
pending_room_update: Option<Task<()>>,
|
||||
maintain_connection: Option<Task<Result<()>>>,
|
||||
}
|
||||
|
||||
impl Entity for Room {
|
||||
@@ -53,7 +58,7 @@ impl Entity for Room {
|
||||
|
||||
fn release(&mut self, _: &mut MutableAppContext) {
|
||||
if self.status.is_online() {
|
||||
self.client.send(proto::LeaveRoom { id: self.id }).log_err();
|
||||
self.client.send(proto::LeaveRoom {}).log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,21 +71,6 @@ impl Room {
|
||||
user_store: ModelHandle<UserStore>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let mut client_status = client.status();
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
let is_connected = client_status
|
||||
.next()
|
||||
.await
|
||||
.map_or(false, |s| s.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() {
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
let _ = this.update(&mut cx, |this, cx| this.leave(cx));
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let live_kit_room = if let Some(connection_info) = live_kit_connection_info {
|
||||
let room = live_kit_client::Room::new();
|
||||
let mut status = room.status();
|
||||
@@ -131,6 +121,9 @@ impl Room {
|
||||
None
|
||||
};
|
||||
|
||||
let maintain_connection =
|
||||
cx.spawn_weak(|this, cx| Self::maintain_connection(this, client.clone(), cx));
|
||||
|
||||
Self {
|
||||
id,
|
||||
live_kit: live_kit_room,
|
||||
@@ -145,11 +138,12 @@ impl Room {
|
||||
pending_room_update: None,
|
||||
client,
|
||||
user_store,
|
||||
maintain_connection: Some(maintain_connection),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create(
|
||||
recipient_user_id: u64,
|
||||
called_user_id: u64,
|
||||
initial_project: Option<ModelHandle<Project>>,
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
@@ -182,7 +176,7 @@ impl Room {
|
||||
match room
|
||||
.update(&mut cx, |room, cx| {
|
||||
room.leave_when_empty = true;
|
||||
room.call(recipient_user_id, initial_project_id, cx)
|
||||
room.call(called_user_id, initial_project_id, cx)
|
||||
})
|
||||
.await
|
||||
{
|
||||
@@ -241,10 +235,96 @@ impl Room {
|
||||
self.participant_user_ids.clear();
|
||||
self.subscriptions.clear();
|
||||
self.live_kit.take();
|
||||
self.client.send(proto::LeaveRoom { id: self.id })?;
|
||||
self.pending_room_update.take();
|
||||
self.maintain_connection.take();
|
||||
self.client.send(proto::LeaveRoom {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn maintain_connection(
|
||||
this: WeakModelHandle<Self>,
|
||||
client: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let mut client_status = client.status();
|
||||
loop {
|
||||
let is_connected = client_status
|
||||
.next()
|
||||
.await
|
||||
.map_or(false, |s| s.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() {
|
||||
let room_id = this
|
||||
.upgrade(&cx)
|
||||
.ok_or_else(|| anyhow!("room was dropped"))?
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.status = RoomStatus::Rejoining;
|
||||
cx.notify();
|
||||
this.id
|
||||
});
|
||||
|
||||
// Wait for client to re-establish a connection to the server.
|
||||
{
|
||||
let mut reconnection_timeout = cx.background().timer(RECONNECT_TIMEOUT).fuse();
|
||||
let client_reconnection = async {
|
||||
let mut remaining_attempts = 3;
|
||||
while remaining_attempts > 0 {
|
||||
if let Some(status) = client_status.next().await {
|
||||
if status.is_connected() {
|
||||
let rejoin_room = async {
|
||||
let response =
|
||||
client.request(proto::JoinRoom { id: room_id }).await?;
|
||||
let room_proto =
|
||||
response.room.ok_or_else(|| anyhow!("invalid room"))?;
|
||||
this.upgrade(&cx)
|
||||
.ok_or_else(|| anyhow!("room was dropped"))?
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.status = RoomStatus::Online;
|
||||
this.apply_room_update(room_proto, cx)
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
if rejoin_room.await.is_ok() {
|
||||
return true;
|
||||
} else {
|
||||
remaining_attempts -= 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
.fuse();
|
||||
futures::pin_mut!(client_reconnection);
|
||||
|
||||
futures::select_biased! {
|
||||
reconnected = client_reconnection => {
|
||||
if reconnected {
|
||||
// If we successfully joined the room, go back around the loop
|
||||
// waiting for future connection status changes.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_ = reconnection_timeout => {}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
let _ = this.update(&mut cx, |this, cx| this.leave(cx));
|
||||
}
|
||||
return Err(anyhow!(
|
||||
"can't reconnect to room: client failed to re-establish connection"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> u64 {
|
||||
self.id
|
||||
}
|
||||
@@ -294,6 +374,11 @@ impl Room {
|
||||
.position(|participant| Some(participant.user_id) == self.client.user_id());
|
||||
let local_participant = local_participant_ix.map(|ix| room.participants.swap_remove(ix));
|
||||
|
||||
let pending_participant_user_ids = room
|
||||
.pending_participants
|
||||
.iter()
|
||||
.map(|p| p.user_id)
|
||||
.collect::<Vec<_>>();
|
||||
let remote_participant_user_ids = room
|
||||
.participants
|
||||
.iter()
|
||||
@@ -303,7 +388,7 @@ impl Room {
|
||||
self.user_store.update(cx, move |user_store, cx| {
|
||||
(
|
||||
user_store.get_users(remote_participant_user_ids, cx),
|
||||
user_store.get_users(room.pending_participant_user_ids, cx),
|
||||
user_store.get_users(pending_participant_user_ids, cx),
|
||||
)
|
||||
});
|
||||
self.pending_room_update = Some(cx.spawn(|this, mut cx| async move {
|
||||
@@ -320,9 +405,11 @@ impl Room {
|
||||
}
|
||||
|
||||
if let Some(participants) = remote_participants.log_err() {
|
||||
let mut participant_peer_ids = HashSet::default();
|
||||
for (participant, user) in room.participants.into_iter().zip(participants) {
|
||||
let peer_id = PeerId(participant.peer_id);
|
||||
this.participant_user_ids.insert(participant.user_id);
|
||||
participant_peer_ids.insert(peer_id);
|
||||
|
||||
let old_projects = this
|
||||
.remote_participants
|
||||
@@ -389,8 +476,8 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
this.remote_participants.retain(|_, participant| {
|
||||
if this.participant_user_ids.contains(&participant.user.id) {
|
||||
this.remote_participants.retain(|peer_id, participant| {
|
||||
if participant_peer_ids.contains(peer_id) {
|
||||
true
|
||||
} else {
|
||||
for project in &participant.projects {
|
||||
@@ -472,10 +559,12 @@ impl Room {
|
||||
{
|
||||
for participant in self.remote_participants.values() {
|
||||
assert!(self.participant_user_ids.contains(&participant.user.id));
|
||||
assert_ne!(participant.user.id, self.client.user_id().unwrap());
|
||||
}
|
||||
|
||||
for participant in &self.pending_participants {
|
||||
assert!(self.participant_user_ids.contains(&participant.id));
|
||||
assert_ne!(participant.id, self.client.user_id().unwrap());
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
@@ -487,7 +576,7 @@ impl Room {
|
||||
|
||||
pub(crate) fn call(
|
||||
&mut self,
|
||||
recipient_user_id: u64,
|
||||
called_user_id: u64,
|
||||
initial_project_id: Option<u64>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
@@ -503,7 +592,7 @@ impl Room {
|
||||
let result = client
|
||||
.request(proto::Call {
|
||||
room_id,
|
||||
recipient_user_id,
|
||||
called_user_id,
|
||||
initial_project_id,
|
||||
})
|
||||
.await;
|
||||
@@ -538,7 +627,7 @@ impl Room {
|
||||
id: worktree.id().to_proto(),
|
||||
root_name: worktree.root_name().into(),
|
||||
visible: worktree.is_visible(),
|
||||
abs_path: worktree.abs_path().as_os_str().as_bytes().to_vec(),
|
||||
abs_path: worktree.abs_path().to_string_lossy().into(),
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
@@ -746,6 +835,7 @@ impl Default for ScreenTrack {
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum RoomStatus {
|
||||
Online,
|
||||
Rejoining,
|
||||
Offline,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,820 +0,0 @@
|
||||
use super::{
|
||||
proto,
|
||||
user::{User, UserStore},
|
||||
Client, Status, Subscription, TypedEnvelope,
|
||||
};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use futures::lock::Mutex;
|
||||
use gpui::{
|
||||
AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, WeakModelHandle,
|
||||
};
|
||||
use postage::prelude::Stream;
|
||||
use rand::prelude::*;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
mem,
|
||||
ops::Range,
|
||||
sync::Arc,
|
||||
};
|
||||
use sum_tree::{Bias, SumTree};
|
||||
use time::OffsetDateTime;
|
||||
use util::{post_inc, ResultExt as _, TryFutureExt};
|
||||
|
||||
pub struct ChannelList {
|
||||
available_channels: Option<Vec<ChannelDetails>>,
|
||||
channels: HashMap<u64, WeakModelHandle<Channel>>,
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
_task: Task<Option<()>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ChannelDetails {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
pub struct Channel {
|
||||
details: ChannelDetails,
|
||||
messages: SumTree<ChannelMessage>,
|
||||
loaded_all_messages: bool,
|
||||
next_pending_message_id: usize,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
rpc: Arc<Client>,
|
||||
outgoing_messages_lock: Arc<Mutex<()>>,
|
||||
rng: StdRng,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ChannelMessage {
|
||||
pub id: ChannelMessageId,
|
||||
pub body: String,
|
||||
pub timestamp: OffsetDateTime,
|
||||
pub sender: Arc<User>,
|
||||
pub nonce: u128,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum ChannelMessageId {
|
||||
Saved(u64),
|
||||
Pending(usize),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ChannelMessageSummary {
|
||||
max_id: ChannelMessageId,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct Count(usize);
|
||||
|
||||
pub enum ChannelListEvent {}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum ChannelEvent {
|
||||
MessagesUpdated {
|
||||
old_range: Range<usize>,
|
||||
new_count: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl Entity for ChannelList {
|
||||
type Event = ChannelListEvent;
|
||||
}
|
||||
|
||||
impl ChannelList {
|
||||
pub fn new(
|
||||
user_store: ModelHandle<UserStore>,
|
||||
rpc: Arc<Client>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let _task = cx.spawn_weak(|this, mut cx| {
|
||||
let rpc = rpc.clone();
|
||||
async move {
|
||||
let mut status = rpc.status();
|
||||
while let Some((status, this)) = status.recv().await.zip(this.upgrade(&cx)) {
|
||||
match status {
|
||||
Status::Connected { .. } => {
|
||||
let response = rpc
|
||||
.request(proto::GetChannels {})
|
||||
.await
|
||||
.context("failed to fetch available channels")?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.available_channels =
|
||||
Some(response.channels.into_iter().map(Into::into).collect());
|
||||
|
||||
let mut to_remove = Vec::new();
|
||||
for (channel_id, channel) in &this.channels {
|
||||
if let Some(channel) = channel.upgrade(cx) {
|
||||
channel.update(cx, |channel, cx| channel.rejoin(cx))
|
||||
} else {
|
||||
to_remove.push(*channel_id);
|
||||
}
|
||||
}
|
||||
|
||||
for channel_id in to_remove {
|
||||
this.channels.remove(&channel_id);
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
Status::SignedOut { .. } => {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.available_channels = None;
|
||||
this.channels.clear();
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
});
|
||||
|
||||
Self {
|
||||
available_channels: None,
|
||||
channels: Default::default(),
|
||||
user_store,
|
||||
client: rpc,
|
||||
_task,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn available_channels(&self) -> Option<&[ChannelDetails]> {
|
||||
self.available_channels.as_deref()
|
||||
}
|
||||
|
||||
pub fn get_channel(
|
||||
&mut self,
|
||||
id: u64,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Option<ModelHandle<Channel>> {
|
||||
if let Some(channel) = self.channels.get(&id).and_then(|c| c.upgrade(cx)) {
|
||||
return Some(channel);
|
||||
}
|
||||
|
||||
let channels = self.available_channels.as_ref()?;
|
||||
let details = channels.iter().find(|details| details.id == id)?.clone();
|
||||
let channel = cx.add_model(|cx| {
|
||||
Channel::new(details, self.user_store.clone(), self.client.clone(), cx)
|
||||
});
|
||||
self.channels.insert(id, channel.downgrade());
|
||||
Some(channel)
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for Channel {
|
||||
type Event = ChannelEvent;
|
||||
|
||||
fn release(&mut self, _: &mut MutableAppContext) {
|
||||
self.rpc
|
||||
.send(proto::LeaveChannel {
|
||||
channel_id: self.details.id,
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
pub fn init(rpc: &Arc<Client>) {
|
||||
rpc.add_model_message_handler(Self::handle_message_sent);
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
details: ChannelDetails,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
rpc: Arc<Client>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let _subscription = rpc.add_model_for_remote_entity(details.id, cx);
|
||||
|
||||
{
|
||||
let user_store = user_store.clone();
|
||||
let rpc = rpc.clone();
|
||||
let channel_id = details.id;
|
||||
cx.spawn(|channel, mut cx| {
|
||||
async move {
|
||||
let response = rpc.request(proto::JoinChannel { channel_id }).await?;
|
||||
let messages =
|
||||
messages_from_proto(response.messages, &user_store, &mut cx).await?;
|
||||
let loaded_all_messages = response.done;
|
||||
|
||||
channel.update(&mut cx, |channel, cx| {
|
||||
channel.insert_messages(messages, cx);
|
||||
channel.loaded_all_messages = loaded_all_messages;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
Self {
|
||||
details,
|
||||
user_store,
|
||||
rpc,
|
||||
outgoing_messages_lock: Default::default(),
|
||||
messages: Default::default(),
|
||||
loaded_all_messages: false,
|
||||
next_pending_message_id: 0,
|
||||
rng: StdRng::from_entropy(),
|
||||
_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.details.name
|
||||
}
|
||||
|
||||
pub fn send_message(
|
||||
&mut self,
|
||||
body: String,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<Task<Result<()>>> {
|
||||
if body.is_empty() {
|
||||
Err(anyhow!("message body can't be empty"))?;
|
||||
}
|
||||
|
||||
let current_user = self
|
||||
.user_store
|
||||
.read(cx)
|
||||
.current_user()
|
||||
.ok_or_else(|| anyhow!("current_user is not present"))?;
|
||||
|
||||
let channel_id = self.details.id;
|
||||
let pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id));
|
||||
let nonce = self.rng.gen();
|
||||
self.insert_messages(
|
||||
SumTree::from_item(
|
||||
ChannelMessage {
|
||||
id: pending_id,
|
||||
body: body.clone(),
|
||||
sender: current_user,
|
||||
timestamp: OffsetDateTime::now_utc(),
|
||||
nonce,
|
||||
},
|
||||
&(),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
let user_store = self.user_store.clone();
|
||||
let rpc = self.rpc.clone();
|
||||
let outgoing_messages_lock = self.outgoing_messages_lock.clone();
|
||||
Ok(cx.spawn(|this, mut cx| async move {
|
||||
let outgoing_message_guard = outgoing_messages_lock.lock().await;
|
||||
let request = rpc.request(proto::SendChannelMessage {
|
||||
channel_id,
|
||||
body,
|
||||
nonce: Some(nonce.into()),
|
||||
});
|
||||
let response = request.await?;
|
||||
drop(outgoing_message_guard);
|
||||
let message = ChannelMessage::from_proto(
|
||||
response.message.ok_or_else(|| anyhow!("invalid message"))?,
|
||||
&user_store,
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx);
|
||||
Ok(())
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn load_more_messages(&mut self, cx: &mut ModelContext<Self>) -> bool {
|
||||
if !self.loaded_all_messages {
|
||||
let rpc = self.rpc.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let channel_id = self.details.id;
|
||||
if let Some(before_message_id) =
|
||||
self.messages.first().and_then(|message| match message.id {
|
||||
ChannelMessageId::Saved(id) => Some(id),
|
||||
ChannelMessageId::Pending(_) => None,
|
||||
})
|
||||
{
|
||||
cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let response = rpc
|
||||
.request(proto::GetChannelMessages {
|
||||
channel_id,
|
||||
before_message_id,
|
||||
})
|
||||
.await?;
|
||||
let loaded_all_messages = response.done;
|
||||
let messages =
|
||||
messages_from_proto(response.messages, &user_store, &mut cx).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.loaded_all_messages = loaded_all_messages;
|
||||
this.insert_messages(messages, cx);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn rejoin(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let user_store = self.user_store.clone();
|
||||
let rpc = self.rpc.clone();
|
||||
let channel_id = self.details.id;
|
||||
cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let response = rpc.request(proto::JoinChannel { channel_id }).await?;
|
||||
let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?;
|
||||
let loaded_all_messages = response.done;
|
||||
|
||||
let pending_messages = this.update(&mut cx, |this, cx| {
|
||||
if let Some((first_new_message, last_old_message)) =
|
||||
messages.first().zip(this.messages.last())
|
||||
{
|
||||
if first_new_message.id > last_old_message.id {
|
||||
let old_messages = mem::take(&mut this.messages);
|
||||
cx.emit(ChannelEvent::MessagesUpdated {
|
||||
old_range: 0..old_messages.summary().count,
|
||||
new_count: 0,
|
||||
});
|
||||
this.loaded_all_messages = loaded_all_messages;
|
||||
}
|
||||
}
|
||||
|
||||
this.insert_messages(messages, cx);
|
||||
if loaded_all_messages {
|
||||
this.loaded_all_messages = loaded_all_messages;
|
||||
}
|
||||
|
||||
this.pending_messages().cloned().collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
for pending_message in pending_messages {
|
||||
let request = rpc.request(proto::SendChannelMessage {
|
||||
channel_id,
|
||||
body: pending_message.body,
|
||||
nonce: Some(pending_message.nonce.into()),
|
||||
});
|
||||
let response = request.await?;
|
||||
let message = ChannelMessage::from_proto(
|
||||
response.message.ok_or_else(|| anyhow!("invalid message"))?,
|
||||
&user_store,
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx);
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn message_count(&self) -> usize {
|
||||
self.messages.summary().count
|
||||
}
|
||||
|
||||
pub fn messages(&self) -> &SumTree<ChannelMessage> {
|
||||
&self.messages
|
||||
}
|
||||
|
||||
pub fn message(&self, ix: usize) -> &ChannelMessage {
|
||||
let mut cursor = self.messages.cursor::<Count>();
|
||||
cursor.seek(&Count(ix), Bias::Right, &());
|
||||
cursor.item().unwrap()
|
||||
}
|
||||
|
||||
pub fn messages_in_range(&self, range: Range<usize>) -> impl Iterator<Item = &ChannelMessage> {
|
||||
let mut cursor = self.messages.cursor::<Count>();
|
||||
cursor.seek(&Count(range.start), Bias::Right, &());
|
||||
cursor.take(range.len())
|
||||
}
|
||||
|
||||
pub fn pending_messages(&self) -> impl Iterator<Item = &ChannelMessage> {
|
||||
let mut cursor = self.messages.cursor::<ChannelMessageId>();
|
||||
cursor.seek(&ChannelMessageId::Pending(0), Bias::Left, &());
|
||||
cursor
|
||||
}
|
||||
|
||||
async fn handle_message_sent(
|
||||
this: ModelHandle<Self>,
|
||||
message: TypedEnvelope<proto::ChannelMessageSent>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
|
||||
let message = message
|
||||
.payload
|
||||
.message
|
||||
.ok_or_else(|| anyhow!("empty message"))?;
|
||||
|
||||
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx)
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_messages(&mut self, messages: SumTree<ChannelMessage>, cx: &mut ModelContext<Self>) {
|
||||
if let Some((first_message, last_message)) = messages.first().zip(messages.last()) {
|
||||
let nonces = messages
|
||||
.cursor::<()>()
|
||||
.map(|m| m.nonce)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let mut old_cursor = self.messages.cursor::<(ChannelMessageId, Count)>();
|
||||
let mut new_messages = old_cursor.slice(&first_message.id, Bias::Left, &());
|
||||
let start_ix = old_cursor.start().1 .0;
|
||||
let removed_messages = old_cursor.slice(&last_message.id, Bias::Right, &());
|
||||
let removed_count = removed_messages.summary().count;
|
||||
let new_count = messages.summary().count;
|
||||
let end_ix = start_ix + removed_count;
|
||||
|
||||
new_messages.push_tree(messages, &());
|
||||
|
||||
let mut ranges = Vec::<Range<usize>>::new();
|
||||
if new_messages.last().unwrap().is_pending() {
|
||||
new_messages.push_tree(old_cursor.suffix(&()), &());
|
||||
} else {
|
||||
new_messages.push_tree(
|
||||
old_cursor.slice(&ChannelMessageId::Pending(0), Bias::Left, &()),
|
||||
&(),
|
||||
);
|
||||
|
||||
while let Some(message) = old_cursor.item() {
|
||||
let message_ix = old_cursor.start().1 .0;
|
||||
if nonces.contains(&message.nonce) {
|
||||
if ranges.last().map_or(false, |r| r.end == message_ix) {
|
||||
ranges.last_mut().unwrap().end += 1;
|
||||
} else {
|
||||
ranges.push(message_ix..message_ix + 1);
|
||||
}
|
||||
} else {
|
||||
new_messages.push(message.clone(), &());
|
||||
}
|
||||
old_cursor.next(&());
|
||||
}
|
||||
}
|
||||
|
||||
drop(old_cursor);
|
||||
self.messages = new_messages;
|
||||
|
||||
for range in ranges.into_iter().rev() {
|
||||
cx.emit(ChannelEvent::MessagesUpdated {
|
||||
old_range: range,
|
||||
new_count: 0,
|
||||
});
|
||||
}
|
||||
cx.emit(ChannelEvent::MessagesUpdated {
|
||||
old_range: start_ix..end_ix,
|
||||
new_count,
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn messages_from_proto(
|
||||
proto_messages: Vec<proto::ChannelMessage>,
|
||||
user_store: &ModelHandle<UserStore>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<SumTree<ChannelMessage>> {
|
||||
let unique_user_ids = proto_messages
|
||||
.iter()
|
||||
.map(|m| m.sender_id)
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect();
|
||||
user_store
|
||||
.update(cx, |user_store, cx| {
|
||||
user_store.get_users(unique_user_ids, cx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut messages = Vec::with_capacity(proto_messages.len());
|
||||
for message in proto_messages {
|
||||
messages.push(ChannelMessage::from_proto(message, user_store, cx).await?);
|
||||
}
|
||||
let mut result = SumTree::new();
|
||||
result.extend(messages, &());
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
impl From<proto::Channel> for ChannelDetails {
|
||||
fn from(message: proto::Channel) -> Self {
|
||||
Self {
|
||||
id: message.id,
|
||||
name: message.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChannelMessage {
|
||||
pub async fn from_proto(
|
||||
message: proto::ChannelMessage,
|
||||
user_store: &ModelHandle<UserStore>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Self> {
|
||||
let sender = user_store
|
||||
.update(cx, |user_store, cx| {
|
||||
user_store.get_user(message.sender_id, cx)
|
||||
})
|
||||
.await?;
|
||||
Ok(ChannelMessage {
|
||||
id: ChannelMessageId::Saved(message.id),
|
||||
body: message.body,
|
||||
timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)?,
|
||||
sender,
|
||||
nonce: message
|
||||
.nonce
|
||||
.ok_or_else(|| anyhow!("nonce is required"))?
|
||||
.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_pending(&self) -> bool {
|
||||
matches!(self.id, ChannelMessageId::Pending(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Item for ChannelMessage {
|
||||
type Summary = ChannelMessageSummary;
|
||||
|
||||
fn summary(&self) -> Self::Summary {
|
||||
ChannelMessageSummary {
|
||||
max_id: self.id,
|
||||
count: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ChannelMessageId {
|
||||
fn default() -> Self {
|
||||
Self::Saved(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Summary for ChannelMessageSummary {
|
||||
type Context = ();
|
||||
|
||||
fn add_summary(&mut self, summary: &Self, _: &()) {
|
||||
self.max_id = summary.max_id;
|
||||
self.count += summary.count;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for ChannelMessageId {
|
||||
fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
|
||||
debug_assert!(summary.max_id > *self);
|
||||
*self = summary.max_id;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for Count {
|
||||
fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
|
||||
self.0 += summary.count;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test::{FakeHttpClient, FakeServer};
|
||||
use gpui::TestAppContext;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
let user_id = 5;
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = cx.update(|cx| Client::new(http_client.clone(), cx));
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
|
||||
Channel::init(&client);
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
||||
|
||||
let channel_list = cx.add_model(|cx| ChannelList::new(user_store, client.clone(), cx));
|
||||
channel_list.read_with(cx, |list, _| assert_eq!(list.available_channels(), None));
|
||||
|
||||
// Get the available channels.
|
||||
let get_channels = server.receive::<proto::GetChannels>().await.unwrap();
|
||||
server
|
||||
.respond(
|
||||
get_channels.receipt(),
|
||||
proto::GetChannelsResponse {
|
||||
channels: vec![proto::Channel {
|
||||
id: 5,
|
||||
name: "the-channel".to_string(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
channel_list.next_notification(cx).await;
|
||||
channel_list.read_with(cx, |list, _| {
|
||||
assert_eq!(
|
||||
list.available_channels().unwrap(),
|
||||
&[ChannelDetails {
|
||||
id: 5,
|
||||
name: "the-channel".into(),
|
||||
}]
|
||||
)
|
||||
});
|
||||
|
||||
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
||||
assert_eq!(get_users.payload.user_ids, vec![5]);
|
||||
server
|
||||
.respond(
|
||||
get_users.receipt(),
|
||||
proto::UsersResponse {
|
||||
users: vec![proto::User {
|
||||
id: 5,
|
||||
github_login: "nathansobo".into(),
|
||||
avatar_url: "http://avatar.com/nathansobo".into(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// Join a channel and populate its existing messages.
|
||||
let channel = channel_list
|
||||
.update(cx, |list, cx| {
|
||||
let channel_id = list.available_channels().unwrap()[0].id;
|
||||
list.get_channel(channel_id, cx)
|
||||
})
|
||||
.unwrap();
|
||||
channel.read_with(cx, |channel, _| assert!(channel.messages().is_empty()));
|
||||
let join_channel = server.receive::<proto::JoinChannel>().await.unwrap();
|
||||
server
|
||||
.respond(
|
||||
join_channel.receipt(),
|
||||
proto::JoinChannelResponse {
|
||||
messages: vec![
|
||||
proto::ChannelMessage {
|
||||
id: 10,
|
||||
body: "a".into(),
|
||||
timestamp: 1000,
|
||||
sender_id: 5,
|
||||
nonce: Some(1.into()),
|
||||
},
|
||||
proto::ChannelMessage {
|
||||
id: 11,
|
||||
body: "b".into(),
|
||||
timestamp: 1001,
|
||||
sender_id: 6,
|
||||
nonce: Some(2.into()),
|
||||
},
|
||||
],
|
||||
done: false,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// Client requests all users for the received messages
|
||||
let mut get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
||||
get_users.payload.user_ids.sort();
|
||||
assert_eq!(get_users.payload.user_ids, vec![6]);
|
||||
server
|
||||
.respond(
|
||||
get_users.receipt(),
|
||||
proto::UsersResponse {
|
||||
users: vec![proto::User {
|
||||
id: 6,
|
||||
github_login: "maxbrunsfeld".into(),
|
||||
avatar_url: "http://avatar.com/maxbrunsfeld".into(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
channel.next_event(cx).await,
|
||||
ChannelEvent::MessagesUpdated {
|
||||
old_range: 0..0,
|
||||
new_count: 2,
|
||||
}
|
||||
);
|
||||
channel.read_with(cx, |channel, _| {
|
||||
assert_eq!(
|
||||
channel
|
||||
.messages_in_range(0..2)
|
||||
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
("nathansobo".into(), "a".into()),
|
||||
("maxbrunsfeld".into(), "b".into())
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// Receive a new message.
|
||||
server.send(proto::ChannelMessageSent {
|
||||
channel_id: channel.read_with(cx, |channel, _| channel.details.id),
|
||||
message: Some(proto::ChannelMessage {
|
||||
id: 12,
|
||||
body: "c".into(),
|
||||
timestamp: 1002,
|
||||
sender_id: 7,
|
||||
nonce: Some(3.into()),
|
||||
}),
|
||||
});
|
||||
|
||||
// Client requests user for message since they haven't seen them yet
|
||||
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
||||
assert_eq!(get_users.payload.user_ids, vec![7]);
|
||||
server
|
||||
.respond(
|
||||
get_users.receipt(),
|
||||
proto::UsersResponse {
|
||||
users: vec![proto::User {
|
||||
id: 7,
|
||||
github_login: "as-cii".into(),
|
||||
avatar_url: "http://avatar.com/as-cii".into(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
channel.next_event(cx).await,
|
||||
ChannelEvent::MessagesUpdated {
|
||||
old_range: 2..2,
|
||||
new_count: 1,
|
||||
}
|
||||
);
|
||||
channel.read_with(cx, |channel, _| {
|
||||
assert_eq!(
|
||||
channel
|
||||
.messages_in_range(2..3)
|
||||
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
&[("as-cii".into(), "c".into())]
|
||||
)
|
||||
});
|
||||
|
||||
// Scroll up to view older messages.
|
||||
channel.update(cx, |channel, cx| {
|
||||
assert!(channel.load_more_messages(cx));
|
||||
});
|
||||
let get_messages = server.receive::<proto::GetChannelMessages>().await.unwrap();
|
||||
assert_eq!(get_messages.payload.channel_id, 5);
|
||||
assert_eq!(get_messages.payload.before_message_id, 10);
|
||||
server
|
||||
.respond(
|
||||
get_messages.receipt(),
|
||||
proto::GetChannelMessagesResponse {
|
||||
done: true,
|
||||
messages: vec![
|
||||
proto::ChannelMessage {
|
||||
id: 8,
|
||||
body: "y".into(),
|
||||
timestamp: 998,
|
||||
sender_id: 5,
|
||||
nonce: Some(4.into()),
|
||||
},
|
||||
proto::ChannelMessage {
|
||||
id: 9,
|
||||
body: "z".into(),
|
||||
timestamp: 999,
|
||||
sender_id: 6,
|
||||
nonce: Some(5.into()),
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
channel.next_event(cx).await,
|
||||
ChannelEvent::MessagesUpdated {
|
||||
old_range: 0..0,
|
||||
new_count: 2,
|
||||
}
|
||||
);
|
||||
channel.read_with(cx, |channel, _| {
|
||||
assert_eq!(
|
||||
channel
|
||||
.messages_in_range(0..2)
|
||||
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
("nathansobo".into(), "y".into()),
|
||||
("maxbrunsfeld".into(), "z".into())
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
|
||||
pub mod channel;
|
||||
pub mod http;
|
||||
pub mod telemetry;
|
||||
pub mod user;
|
||||
@@ -12,14 +11,12 @@ use async_tungstenite::tungstenite::{
|
||||
error::Error as WebsocketError,
|
||||
http::{Request, StatusCode},
|
||||
};
|
||||
use db::Db;
|
||||
use futures::{future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryStreamExt};
|
||||
use gpui::{
|
||||
actions,
|
||||
serde_json::{self, Value},
|
||||
AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext,
|
||||
AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext,
|
||||
ViewHandle,
|
||||
AsyncAppContext, Entity, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use http::HttpClient;
|
||||
use lazy_static::lazy_static;
|
||||
@@ -28,13 +25,13 @@ use postage::watch;
|
||||
use rand::prelude::*;
|
||||
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage};
|
||||
use serde::Deserialize;
|
||||
use settings::ReleaseChannel;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
collections::HashMap,
|
||||
convert::TryFrom,
|
||||
fmt::Write as _,
|
||||
future::Future,
|
||||
marker::PhantomData,
|
||||
path::PathBuf,
|
||||
sync::{Arc, Weak},
|
||||
time::{Duration, Instant},
|
||||
@@ -42,9 +39,9 @@ use std::{
|
||||
use telemetry::Telemetry;
|
||||
use thiserror::Error;
|
||||
use url::Url;
|
||||
use util::channel::ReleaseChannel;
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
pub use channel::*;
|
||||
pub use rpc::*;
|
||||
pub use user::*;
|
||||
|
||||
@@ -174,7 +171,7 @@ struct ClientState {
|
||||
entity_id_extractors: HashMap<TypeId, fn(&dyn AnyTypedEnvelope) -> u64>,
|
||||
_reconnect_task: Option<Task<()>>,
|
||||
reconnect_interval: Duration,
|
||||
entities_by_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakEntityHandle>,
|
||||
entities_by_type_and_remote_id: HashMap<(TypeId, u64), WeakSubscriber>,
|
||||
models_by_message_type: HashMap<TypeId, AnyWeakModelHandle>,
|
||||
entity_types_by_message_type: HashMap<TypeId, TypeId>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
@@ -184,7 +181,7 @@ struct ClientState {
|
||||
dyn Send
|
||||
+ Sync
|
||||
+ Fn(
|
||||
AnyEntityHandle,
|
||||
Subscriber,
|
||||
Box<dyn AnyTypedEnvelope>,
|
||||
&Arc<Client>,
|
||||
AsyncAppContext,
|
||||
@@ -193,12 +190,13 @@ struct ClientState {
|
||||
>,
|
||||
}
|
||||
|
||||
enum AnyWeakEntityHandle {
|
||||
enum WeakSubscriber {
|
||||
Model(AnyWeakModelHandle),
|
||||
View(AnyWeakViewHandle),
|
||||
Pending(Vec<Box<dyn AnyTypedEnvelope>>),
|
||||
}
|
||||
|
||||
enum AnyEntityHandle {
|
||||
enum Subscriber {
|
||||
Model(AnyModelHandle),
|
||||
View(AnyViewHandle),
|
||||
}
|
||||
@@ -256,6 +254,54 @@ impl Drop for Subscription {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PendingEntitySubscription<T: Entity> {
|
||||
client: Arc<Client>,
|
||||
remote_id: u64,
|
||||
_entity_type: PhantomData<T>,
|
||||
consumed: bool,
|
||||
}
|
||||
|
||||
impl<T: Entity> PendingEntitySubscription<T> {
|
||||
pub fn set_model(mut self, model: &ModelHandle<T>, cx: &mut AsyncAppContext) -> Subscription {
|
||||
self.consumed = true;
|
||||
let mut state = self.client.state.write();
|
||||
let id = (TypeId::of::<T>(), self.remote_id);
|
||||
let Some(WeakSubscriber::Pending(messages)) =
|
||||
state.entities_by_type_and_remote_id.remove(&id)
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
state
|
||||
.entities_by_type_and_remote_id
|
||||
.insert(id, WeakSubscriber::Model(model.downgrade().into()));
|
||||
drop(state);
|
||||
for message in messages {
|
||||
self.client.handle_message(message, cx);
|
||||
}
|
||||
Subscription::Entity {
|
||||
client: Arc::downgrade(&self.client),
|
||||
id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Entity> Drop for PendingEntitySubscription<T> {
|
||||
fn drop(&mut self) {
|
||||
if !self.consumed {
|
||||
let mut state = self.client.state.write();
|
||||
if let Some(WeakSubscriber::Pending(messages)) = state
|
||||
.entities_by_type_and_remote_id
|
||||
.remove(&(TypeId::of::<T>(), self.remote_id))
|
||||
{
|
||||
for message in messages {
|
||||
log::info!("unhandled message {}", message.payload_type_name());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(http: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
@@ -351,7 +397,11 @@ impl Client {
|
||||
let this = self.clone();
|
||||
let reconnect_interval = state.reconnect_interval;
|
||||
state._reconnect_task = Some(cx.spawn(|cx| async move {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
let mut rng = StdRng::seed_from_u64(0);
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
let mut rng = StdRng::from_entropy();
|
||||
|
||||
let mut delay = INITIAL_RECONNECTION_DELAY;
|
||||
while let Err(error) = this.authenticate_and_connect(true, &cx).await {
|
||||
log::error!("failed to connect {}", error);
|
||||
@@ -389,26 +439,28 @@ impl Client {
|
||||
self.state
|
||||
.write()
|
||||
.entities_by_type_and_remote_id
|
||||
.insert(id, AnyWeakEntityHandle::View(cx.weak_handle().into()));
|
||||
.insert(id, WeakSubscriber::View(cx.weak_handle().into()));
|
||||
Subscription::Entity {
|
||||
client: Arc::downgrade(self),
|
||||
id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_model_for_remote_entity<T: Entity>(
|
||||
pub fn subscribe_to_entity<T: Entity>(
|
||||
self: &Arc<Self>,
|
||||
remote_id: u64,
|
||||
cx: &mut ModelContext<T>,
|
||||
) -> Subscription {
|
||||
) -> PendingEntitySubscription<T> {
|
||||
let id = (TypeId::of::<T>(), remote_id);
|
||||
self.state
|
||||
.write()
|
||||
.entities_by_type_and_remote_id
|
||||
.insert(id, AnyWeakEntityHandle::Model(cx.weak_handle().into()));
|
||||
Subscription::Entity {
|
||||
client: Arc::downgrade(self),
|
||||
id,
|
||||
.insert(id, WeakSubscriber::Pending(Default::default()));
|
||||
|
||||
PendingEntitySubscription {
|
||||
client: self.clone(),
|
||||
remote_id,
|
||||
consumed: false,
|
||||
_entity_type: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,7 +488,7 @@ impl Client {
|
||||
let prev_handler = state.message_handlers.insert(
|
||||
message_type_id,
|
||||
Arc::new(move |handle, envelope, client, cx| {
|
||||
let handle = if let AnyEntityHandle::Model(handle) = handle {
|
||||
let handle = if let Subscriber::Model(handle) = handle {
|
||||
handle
|
||||
} else {
|
||||
unreachable!();
|
||||
@@ -490,7 +542,7 @@ impl Client {
|
||||
F: 'static + Future<Output = Result<()>>,
|
||||
{
|
||||
self.add_entity_message_handler::<M, E, _, _>(move |handle, message, client, cx| {
|
||||
if let AnyEntityHandle::View(handle) = handle {
|
||||
if let Subscriber::View(handle) = handle {
|
||||
handler(handle.downcast::<E>().unwrap(), message, client, cx)
|
||||
} else {
|
||||
unreachable!();
|
||||
@@ -509,7 +561,7 @@ impl Client {
|
||||
F: 'static + Future<Output = Result<()>>,
|
||||
{
|
||||
self.add_entity_message_handler::<M, E, _, _>(move |handle, message, client, cx| {
|
||||
if let AnyEntityHandle::Model(handle) = handle {
|
||||
if let Subscriber::Model(handle) = handle {
|
||||
handler(handle.downcast::<E>().unwrap(), message, client, cx)
|
||||
} else {
|
||||
unreachable!();
|
||||
@@ -524,7 +576,7 @@ impl Client {
|
||||
H: 'static
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Fn(AnyEntityHandle, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
|
||||
+ Fn(Subscriber, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
|
||||
F: 'static + Future<Output = Result<()>>,
|
||||
{
|
||||
let model_type_id = TypeId::of::<E>();
|
||||
@@ -786,94 +838,8 @@ impl Client {
|
||||
let cx = cx.clone();
|
||||
let this = self.clone();
|
||||
async move {
|
||||
let mut message_id = 0_usize;
|
||||
while let Some(message) = incoming.next().await {
|
||||
let mut state = this.state.write();
|
||||
message_id += 1;
|
||||
let type_name = message.payload_type_name();
|
||||
let payload_type_id = message.payload_type_id();
|
||||
let sender_id = message.original_sender_id().map(|id| id.0);
|
||||
|
||||
let model = state
|
||||
.models_by_message_type
|
||||
.get(&payload_type_id)
|
||||
.and_then(|model| model.upgrade(&cx))
|
||||
.map(AnyEntityHandle::Model)
|
||||
.or_else(|| {
|
||||
let entity_type_id =
|
||||
*state.entity_types_by_message_type.get(&payload_type_id)?;
|
||||
let entity_id = state
|
||||
.entity_id_extractors
|
||||
.get(&message.payload_type_id())
|
||||
.map(|extract_entity_id| {
|
||||
(extract_entity_id)(message.as_ref())
|
||||
})?;
|
||||
|
||||
let entity = state
|
||||
.entities_by_type_and_remote_id
|
||||
.get(&(entity_type_id, entity_id))?;
|
||||
if let Some(entity) = entity.upgrade(&cx) {
|
||||
Some(entity)
|
||||
} else {
|
||||
state
|
||||
.entities_by_type_and_remote_id
|
||||
.remove(&(entity_type_id, entity_id));
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let model = if let Some(model) = model {
|
||||
model
|
||||
} else {
|
||||
log::info!("unhandled message {}", type_name);
|
||||
continue;
|
||||
};
|
||||
|
||||
let handler = state.message_handlers.get(&payload_type_id).cloned();
|
||||
// Dropping the state prevents deadlocks if the handler interacts with rpc::Client.
|
||||
// It also ensures we don't hold the lock while yielding back to the executor, as
|
||||
// that might cause the executor thread driving this future to block indefinitely.
|
||||
drop(state);
|
||||
|
||||
if let Some(handler) = handler {
|
||||
let future = handler(model, message, &this, cx.clone());
|
||||
let client_id = this.id;
|
||||
log::debug!(
|
||||
"rpc message received. client_id:{}, message_id:{}, sender_id:{:?}, type:{}",
|
||||
client_id,
|
||||
message_id,
|
||||
sender_id,
|
||||
type_name
|
||||
);
|
||||
cx.foreground()
|
||||
.spawn(async move {
|
||||
match future.await {
|
||||
Ok(()) => {
|
||||
log::debug!(
|
||||
"rpc message handled. client_id:{}, message_id:{}, sender_id:{:?}, type:{}",
|
||||
client_id,
|
||||
message_id,
|
||||
sender_id,
|
||||
type_name
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!(
|
||||
"error handling message. client_id:{}, message_id:{}, sender_id:{:?}, type:{}, error:{:?}",
|
||||
client_id,
|
||||
message_id,
|
||||
sender_id,
|
||||
type_name,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
} else {
|
||||
log::info!("unhandled message {}", type_name);
|
||||
}
|
||||
|
||||
this.handle_message(message, &cx);
|
||||
// Don't starve the main thread when receiving lots of messages at once.
|
||||
smol::future::yield_now().await;
|
||||
}
|
||||
@@ -1220,8 +1186,99 @@ impl Client {
|
||||
self.peer.respond_with_error(receipt, error)
|
||||
}
|
||||
|
||||
pub fn start_telemetry(&self, db: Db) {
|
||||
self.telemetry.start(db.clone());
|
||||
fn handle_message(
|
||||
self: &Arc<Client>,
|
||||
message: Box<dyn AnyTypedEnvelope>,
|
||||
cx: &AsyncAppContext,
|
||||
) {
|
||||
let mut state = self.state.write();
|
||||
let type_name = message.payload_type_name();
|
||||
let payload_type_id = message.payload_type_id();
|
||||
let sender_id = message.original_sender_id().map(|id| id.0);
|
||||
|
||||
let mut subscriber = None;
|
||||
|
||||
if let Some(message_model) = state
|
||||
.models_by_message_type
|
||||
.get(&payload_type_id)
|
||||
.and_then(|model| model.upgrade(cx))
|
||||
{
|
||||
subscriber = Some(Subscriber::Model(message_model));
|
||||
} else if let Some((extract_entity_id, entity_type_id)) =
|
||||
state.entity_id_extractors.get(&payload_type_id).zip(
|
||||
state
|
||||
.entity_types_by_message_type
|
||||
.get(&payload_type_id)
|
||||
.copied(),
|
||||
)
|
||||
{
|
||||
let entity_id = (extract_entity_id)(message.as_ref());
|
||||
|
||||
match state
|
||||
.entities_by_type_and_remote_id
|
||||
.get_mut(&(entity_type_id, entity_id))
|
||||
{
|
||||
Some(WeakSubscriber::Pending(pending)) => {
|
||||
pending.push(message);
|
||||
return;
|
||||
}
|
||||
Some(weak_subscriber @ _) => subscriber = weak_subscriber.upgrade(cx),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let subscriber = if let Some(subscriber) = subscriber {
|
||||
subscriber
|
||||
} else {
|
||||
log::info!("unhandled message {}", type_name);
|
||||
return;
|
||||
};
|
||||
|
||||
let handler = state.message_handlers.get(&payload_type_id).cloned();
|
||||
// Dropping the state prevents deadlocks if the handler interacts with rpc::Client.
|
||||
// It also ensures we don't hold the lock while yielding back to the executor, as
|
||||
// that might cause the executor thread driving this future to block indefinitely.
|
||||
drop(state);
|
||||
|
||||
if let Some(handler) = handler {
|
||||
let future = handler(subscriber, message, &self, cx.clone());
|
||||
let client_id = self.id;
|
||||
log::debug!(
|
||||
"rpc message received. client_id:{}, sender_id:{:?}, type:{}",
|
||||
client_id,
|
||||
sender_id,
|
||||
type_name
|
||||
);
|
||||
cx.foreground()
|
||||
.spawn(async move {
|
||||
match future.await {
|
||||
Ok(()) => {
|
||||
log::debug!(
|
||||
"rpc message handled. client_id:{}, sender_id:{:?}, type:{}",
|
||||
client_id,
|
||||
sender_id,
|
||||
type_name
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!(
|
||||
"error handling message. client_id:{}, sender_id:{:?}, type:{}, error:{:?}",
|
||||
client_id,
|
||||
sender_id,
|
||||
type_name,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
} else {
|
||||
log::info!("unhandled message {}", type_name);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_telemetry(&self) {
|
||||
self.telemetry.start();
|
||||
}
|
||||
|
||||
pub fn report_event(&self, kind: &str, properties: Value) {
|
||||
@@ -1233,11 +1290,12 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
impl AnyWeakEntityHandle {
|
||||
fn upgrade(&self, cx: &AsyncAppContext) -> Option<AnyEntityHandle> {
|
||||
impl WeakSubscriber {
|
||||
fn upgrade(&self, cx: &AsyncAppContext) -> Option<Subscriber> {
|
||||
match self {
|
||||
AnyWeakEntityHandle::Model(handle) => handle.upgrade(cx).map(AnyEntityHandle::Model),
|
||||
AnyWeakEntityHandle::View(handle) => handle.upgrade(cx).map(AnyEntityHandle::View),
|
||||
WeakSubscriber::Model(handle) => handle.upgrade(cx).map(Subscriber::Model),
|
||||
WeakSubscriber::View(handle) => handle.upgrade(cx).map(Subscriber::View),
|
||||
WeakSubscriber::Pending(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1482,11 +1540,17 @@ mod tests {
|
||||
subscription: None,
|
||||
});
|
||||
|
||||
let _subscription1 = model1.update(cx, |_, cx| client.add_model_for_remote_entity(1, cx));
|
||||
let _subscription2 = model2.update(cx, |_, cx| client.add_model_for_remote_entity(2, cx));
|
||||
let _subscription1 = client
|
||||
.subscribe_to_entity(1)
|
||||
.set_model(&model1, &mut cx.to_async());
|
||||
let _subscription2 = client
|
||||
.subscribe_to_entity(2)
|
||||
.set_model(&model2, &mut cx.to_async());
|
||||
// Ensure dropping a subscription for the same entity type still allows receiving of
|
||||
// messages for other entity IDs of the same type.
|
||||
let subscription3 = model3.update(cx, |_, cx| client.add_model_for_remote_entity(3, cx));
|
||||
let subscription3 = client
|
||||
.subscribe_to_entity(3)
|
||||
.set_model(&model3, &mut cx.to_async());
|
||||
drop(subscription3);
|
||||
|
||||
server.send(proto::JoinProject { project_id: 1 });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::http::HttpClient;
|
||||
use db::Db;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{
|
||||
executor::Background,
|
||||
serde_json::{self, value::Map, Value},
|
||||
@@ -10,7 +10,6 @@ use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use settings::ReleaseChannel;
|
||||
use std::{
|
||||
io::Write,
|
||||
mem,
|
||||
@@ -19,7 +18,7 @@ use std::{
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tempfile::NamedTempFile;
|
||||
use util::{post_inc, ResultExt, TryFutureExt};
|
||||
use util::{channel::ReleaseChannel, post_inc, ResultExt, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct Telemetry {
|
||||
@@ -32,7 +31,6 @@ pub struct Telemetry {
|
||||
struct TelemetryState {
|
||||
metrics_id: Option<Arc<str>>,
|
||||
device_id: Option<Arc<str>>,
|
||||
app: &'static str,
|
||||
app_version: Option<Arc<str>>,
|
||||
release_channel: Option<&'static str>,
|
||||
os_version: Option<Arc<str>>,
|
||||
@@ -80,8 +78,6 @@ struct MixpanelEventProperties {
|
||||
app_version: Option<Arc<str>>,
|
||||
#[serde(rename = "Signed In")]
|
||||
signed_in: bool,
|
||||
#[serde(rename = "App")]
|
||||
app: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -110,7 +106,7 @@ impl Telemetry {
|
||||
pub fn new(client: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
|
||||
let platform = cx.platform();
|
||||
let release_channel = if cx.has_global::<ReleaseChannel>() {
|
||||
Some(cx.global::<ReleaseChannel>().name())
|
||||
Some(cx.global::<ReleaseChannel>().display_name())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -120,7 +116,6 @@ impl Telemetry {
|
||||
state: Mutex::new(TelemetryState {
|
||||
os_version: platform.os_version().ok().map(|v| v.to_string().into()),
|
||||
os_name: platform.os_name().into(),
|
||||
app: "Zed",
|
||||
app_version: platform.app_version().ok().map(|v| v.to_string().into()),
|
||||
release_channel,
|
||||
device_id: None,
|
||||
@@ -152,18 +147,21 @@ impl Telemetry {
|
||||
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
|
||||
}
|
||||
|
||||
pub fn start(self: &Arc<Self>, db: Db) {
|
||||
pub fn start(self: &Arc<Self>) {
|
||||
let this = self.clone();
|
||||
self.executor
|
||||
.spawn(
|
||||
async move {
|
||||
let device_id = if let Ok(Some(device_id)) = db.read_kvp("device_id") {
|
||||
device_id
|
||||
} else {
|
||||
let device_id = Uuid::new_v4().to_string();
|
||||
db.write_kvp("device_id", &device_id)?;
|
||||
device_id
|
||||
};
|
||||
let device_id =
|
||||
if let Ok(Some(device_id)) = KEY_VALUE_STORE.read_kvp("device_id") {
|
||||
device_id
|
||||
} else {
|
||||
let device_id = Uuid::new_v4().to_string();
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp("device_id".to_string(), device_id.clone())
|
||||
.await?;
|
||||
device_id
|
||||
};
|
||||
|
||||
let device_id: Arc<str> = device_id.into();
|
||||
let mut state = this.state.lock();
|
||||
@@ -205,7 +203,11 @@ impl Telemetry {
|
||||
let json_bytes = serde_json::to_vec(&[MixpanelEngageRequest {
|
||||
token,
|
||||
distinct_id: device_id,
|
||||
set: json!({ "Staff": is_staff, "ID": metrics_id }),
|
||||
set: json!({
|
||||
"Staff": is_staff,
|
||||
"ID": metrics_id,
|
||||
"App": true
|
||||
}),
|
||||
}])?;
|
||||
let request = Request::post(MIXPANEL_ENGAGE_URL)
|
||||
.header("Content-Type", "application/json")
|
||||
@@ -241,7 +243,6 @@ impl Telemetry {
|
||||
release_channel: state.release_channel,
|
||||
app_version: state.app_version.clone(),
|
||||
signed_in: state.metrics_id.is_some(),
|
||||
app: state.app,
|
||||
},
|
||||
};
|
||||
state.queue.push(event);
|
||||
|
||||
@@ -150,7 +150,6 @@ impl UserStore {
|
||||
client.telemetry.set_authenticated_user_info(None, false);
|
||||
}
|
||||
|
||||
client.telemetry.report_event("sign in", Default::default());
|
||||
current_user_tx.send(user).await.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.2.2"
|
||||
version = "0.3.7"
|
||||
|
||||
[[bin]]
|
||||
name = "collab"
|
||||
@@ -19,12 +19,12 @@ rpc = { path = "../rpc" }
|
||||
util = { path = "../util" }
|
||||
|
||||
anyhow = "1.0.40"
|
||||
async-trait = "0.1.50"
|
||||
async-tungstenite = "0.16"
|
||||
axum = { version = "0.5", features = ["json", "headers", "ws"] }
|
||||
axum-extra = { version = "0.3", features = ["erased-json"] }
|
||||
base64 = "0.13"
|
||||
clap = { version = "3.1", features = ["derive"], optional = true }
|
||||
dashmap = "5.4"
|
||||
envy = "0.4.2"
|
||||
futures = "0.3"
|
||||
hyper = "0.14"
|
||||
@@ -36,9 +36,13 @@ prometheus = "0.13"
|
||||
rand = "0.8"
|
||||
reqwest = { version = "0.11", features = ["json"], optional = true }
|
||||
scrypt = "0.7"
|
||||
# Remove fork dependency when a version with https://github.com/SeaQL/sea-orm/pull/1283 is released.
|
||||
sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls"] }
|
||||
sea-query = "0.27"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_json = "1.0"
|
||||
sha-1 = "0.9"
|
||||
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }
|
||||
time = { version = "0.3", features = ["serde", "serde-well-known"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-tungstenite = "0.17"
|
||||
@@ -49,10 +53,6 @@ tracing = "0.1.34"
|
||||
tracing-log = "0.1.3"
|
||||
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
|
||||
|
||||
[dependencies.sqlx]
|
||||
version = "0.6"
|
||||
features = ["runtime-tokio-rustls", "postgres", "time", "uuid"]
|
||||
|
||||
[dev-dependencies]
|
||||
collections = { path = "../collections", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
@@ -64,6 +64,7 @@ fs = { path = "../fs", features = ["test-support"] }
|
||||
git = { path = "../git", features = ["test-support"] }
|
||||
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
@@ -75,7 +76,9 @@ env_logger = "0.9"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
util = { path = "../util" }
|
||||
lazy_static = "1.4"
|
||||
sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-sqlite"] }
|
||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||
sqlx = { version = "0.6", features = ["sqlite"] }
|
||||
unindent = "0.1"
|
||||
|
||||
[features]
|
||||
|
||||
136
crates/collab/migrations.sqlite/20221109000000_test_schema.sql
Normal file
136
crates/collab/migrations.sqlite/20221109000000_test_schema.sql
Normal file
@@ -0,0 +1,136 @@
|
||||
CREATE TABLE "users" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"github_login" VARCHAR,
|
||||
"admin" BOOLEAN,
|
||||
"email_address" VARCHAR(255) DEFAULT NULL,
|
||||
"invite_code" VARCHAR(64),
|
||||
"invite_count" INTEGER NOT NULL DEFAULT 0,
|
||||
"inviter_id" INTEGER REFERENCES users (id),
|
||||
"connected_once" BOOLEAN NOT NULL DEFAULT false,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT now,
|
||||
"metrics_id" TEXT,
|
||||
"github_user_id" INTEGER
|
||||
);
|
||||
CREATE UNIQUE INDEX "index_users_github_login" ON "users" ("github_login");
|
||||
CREATE UNIQUE INDEX "index_invite_code_users" ON "users" ("invite_code");
|
||||
CREATE INDEX "index_users_on_email_address" ON "users" ("email_address");
|
||||
CREATE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id");
|
||||
|
||||
CREATE TABLE "access_tokens" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"user_id" INTEGER REFERENCES users (id),
|
||||
"hash" VARCHAR(128)
|
||||
);
|
||||
CREATE INDEX "index_access_tokens_user_id" ON "access_tokens" ("user_id");
|
||||
|
||||
CREATE TABLE "contacts" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"user_id_a" INTEGER REFERENCES users (id) NOT NULL,
|
||||
"user_id_b" INTEGER REFERENCES users (id) NOT NULL,
|
||||
"a_to_b" BOOLEAN NOT NULL,
|
||||
"should_notify" BOOLEAN NOT NULL,
|
||||
"accepted" BOOLEAN NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX "index_contacts_user_ids" ON "contacts" ("user_id_a", "user_id_b");
|
||||
CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b");
|
||||
|
||||
CREATE TABLE "rooms" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"live_kit_room" VARCHAR NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "projects" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"room_id" INTEGER REFERENCES rooms (id) NOT NULL,
|
||||
"host_user_id" INTEGER REFERENCES users (id) NOT NULL,
|
||||
"host_connection_id" INTEGER NOT NULL,
|
||||
"host_connection_epoch" TEXT NOT NULL,
|
||||
"unregistered" BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
CREATE INDEX "index_projects_on_host_connection_epoch" ON "projects" ("host_connection_epoch");
|
||||
|
||||
CREATE TABLE "worktrees" (
|
||||
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
|
||||
"id" INTEGER NOT NULL,
|
||||
"root_name" VARCHAR NOT NULL,
|
||||
"abs_path" VARCHAR NOT NULL,
|
||||
"visible" BOOL NOT NULL,
|
||||
"scan_id" INTEGER NOT NULL,
|
||||
"is_complete" BOOL NOT NULL,
|
||||
PRIMARY KEY(project_id, id)
|
||||
);
|
||||
CREATE INDEX "index_worktrees_on_project_id" ON "worktrees" ("project_id");
|
||||
|
||||
CREATE TABLE "worktree_entries" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INTEGER NOT NULL,
|
||||
"id" INTEGER NOT NULL,
|
||||
"is_dir" BOOL NOT NULL,
|
||||
"path" VARCHAR NOT NULL,
|
||||
"inode" INTEGER NOT NULL,
|
||||
"mtime_seconds" INTEGER NOT NULL,
|
||||
"mtime_nanos" INTEGER NOT NULL,
|
||||
"is_symlink" BOOL NOT NULL,
|
||||
"is_ignored" BOOL NOT NULL,
|
||||
PRIMARY KEY(project_id, worktree_id, id),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "index_worktree_entries_on_project_id" ON "worktree_entries" ("project_id");
|
||||
CREATE INDEX "index_worktree_entries_on_project_id_and_worktree_id" ON "worktree_entries" ("project_id", "worktree_id");
|
||||
|
||||
CREATE TABLE "worktree_diagnostic_summaries" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INTEGER NOT NULL,
|
||||
"path" VARCHAR NOT NULL,
|
||||
"language_server_id" INTEGER NOT NULL,
|
||||
"error_count" INTEGER NOT NULL,
|
||||
"warning_count" INTEGER NOT NULL,
|
||||
PRIMARY KEY(project_id, worktree_id, path),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "index_worktree_diagnostic_summaries_on_project_id" ON "worktree_diagnostic_summaries" ("project_id");
|
||||
CREATE INDEX "index_worktree_diagnostic_summaries_on_project_id_and_worktree_id" ON "worktree_diagnostic_summaries" ("project_id", "worktree_id");
|
||||
|
||||
CREATE TABLE "language_servers" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
|
||||
"name" VARCHAR NOT NULL,
|
||||
PRIMARY KEY(project_id, id)
|
||||
);
|
||||
CREATE INDEX "index_language_servers_on_project_id" ON "language_servers" ("project_id");
|
||||
|
||||
CREATE TABLE "project_collaborators" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
|
||||
"connection_id" INTEGER NOT NULL,
|
||||
"connection_epoch" TEXT NOT NULL,
|
||||
"user_id" INTEGER NOT NULL,
|
||||
"replica_id" INTEGER NOT NULL,
|
||||
"is_host" BOOLEAN NOT NULL
|
||||
);
|
||||
CREATE INDEX "index_project_collaborators_on_project_id" ON "project_collaborators" ("project_id");
|
||||
CREATE UNIQUE INDEX "index_project_collaborators_on_project_id_and_replica_id" ON "project_collaborators" ("project_id", "replica_id");
|
||||
CREATE INDEX "index_project_collaborators_on_connection_epoch" ON "project_collaborators" ("connection_epoch");
|
||||
CREATE INDEX "index_project_collaborators_on_connection_id" ON "project_collaborators" ("connection_id");
|
||||
CREATE UNIQUE INDEX "index_project_collaborators_on_project_id_connection_id_and_epoch" ON "project_collaborators" ("project_id", "connection_id", "connection_epoch");
|
||||
|
||||
CREATE TABLE "room_participants" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"room_id" INTEGER NOT NULL REFERENCES rooms (id),
|
||||
"user_id" INTEGER NOT NULL REFERENCES users (id),
|
||||
"answering_connection_id" INTEGER,
|
||||
"answering_connection_epoch" TEXT,
|
||||
"answering_connection_lost" BOOLEAN NOT NULL,
|
||||
"location_kind" INTEGER,
|
||||
"location_project_id" INTEGER,
|
||||
"initial_project_id" INTEGER,
|
||||
"calling_user_id" INTEGER NOT NULL REFERENCES users (id),
|
||||
"calling_connection_id" INTEGER NOT NULL,
|
||||
"calling_connection_epoch" TEXT NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_id");
|
||||
CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id");
|
||||
CREATE INDEX "index_room_participants_on_answering_connection_epoch" ON "room_participants" ("answering_connection_epoch");
|
||||
CREATE INDEX "index_room_participants_on_calling_connection_epoch" ON "room_participants" ("calling_connection_epoch");
|
||||
CREATE INDEX "index_room_participants_on_answering_connection_id" ON "room_participants" ("answering_connection_id");
|
||||
CREATE UNIQUE INDEX "index_room_participants_on_answering_connection_id_and_answering_connection_epoch" ON "room_participants" ("answering_connection_id", "answering_connection_epoch");
|
||||
@@ -0,0 +1,90 @@
|
||||
CREATE TABLE IF NOT EXISTS "rooms" (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"live_kit_room" VARCHAR NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE "projects"
|
||||
ADD "room_id" INTEGER REFERENCES rooms (id),
|
||||
ADD "host_connection_id" INTEGER,
|
||||
ADD "host_connection_epoch" UUID;
|
||||
CREATE INDEX "index_projects_on_host_connection_epoch" ON "projects" ("host_connection_epoch");
|
||||
|
||||
CREATE TABLE "worktrees" (
|
||||
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
|
||||
"id" INT8 NOT NULL,
|
||||
"root_name" VARCHAR NOT NULL,
|
||||
"abs_path" VARCHAR NOT NULL,
|
||||
"visible" BOOL NOT NULL,
|
||||
"scan_id" INT8 NOT NULL,
|
||||
"is_complete" BOOL NOT NULL,
|
||||
PRIMARY KEY(project_id, id)
|
||||
);
|
||||
CREATE INDEX "index_worktrees_on_project_id" ON "worktrees" ("project_id");
|
||||
|
||||
CREATE TABLE "worktree_entries" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INT8 NOT NULL,
|
||||
"id" INT8 NOT NULL,
|
||||
"is_dir" BOOL NOT NULL,
|
||||
"path" VARCHAR NOT NULL,
|
||||
"inode" INT8 NOT NULL,
|
||||
"mtime_seconds" INT8 NOT NULL,
|
||||
"mtime_nanos" INTEGER NOT NULL,
|
||||
"is_symlink" BOOL NOT NULL,
|
||||
"is_ignored" BOOL NOT NULL,
|
||||
PRIMARY KEY(project_id, worktree_id, id),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "index_worktree_entries_on_project_id" ON "worktree_entries" ("project_id");
|
||||
CREATE INDEX "index_worktree_entries_on_project_id_and_worktree_id" ON "worktree_entries" ("project_id", "worktree_id");
|
||||
|
||||
CREATE TABLE "worktree_diagnostic_summaries" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INT8 NOT NULL,
|
||||
"path" VARCHAR NOT NULL,
|
||||
"language_server_id" INT8 NOT NULL,
|
||||
"error_count" INTEGER NOT NULL,
|
||||
"warning_count" INTEGER NOT NULL,
|
||||
PRIMARY KEY(project_id, worktree_id, path),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "index_worktree_diagnostic_summaries_on_project_id" ON "worktree_diagnostic_summaries" ("project_id");
|
||||
CREATE INDEX "index_worktree_diagnostic_summaries_on_project_id_and_worktree_id" ON "worktree_diagnostic_summaries" ("project_id", "worktree_id");
|
||||
|
||||
CREATE TABLE "language_servers" (
|
||||
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
|
||||
"id" INT8 NOT NULL,
|
||||
"name" VARCHAR NOT NULL,
|
||||
PRIMARY KEY(project_id, id)
|
||||
);
|
||||
CREATE INDEX "index_language_servers_on_project_id" ON "language_servers" ("project_id");
|
||||
|
||||
CREATE TABLE "project_collaborators" (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
|
||||
"connection_id" INTEGER NOT NULL,
|
||||
"connection_epoch" UUID NOT NULL,
|
||||
"user_id" INTEGER NOT NULL,
|
||||
"replica_id" INTEGER NOT NULL,
|
||||
"is_host" BOOLEAN NOT NULL
|
||||
);
|
||||
CREATE INDEX "index_project_collaborators_on_project_id" ON "project_collaborators" ("project_id");
|
||||
CREATE UNIQUE INDEX "index_project_collaborators_on_project_id_and_replica_id" ON "project_collaborators" ("project_id", "replica_id");
|
||||
CREATE INDEX "index_project_collaborators_on_connection_epoch" ON "project_collaborators" ("connection_epoch");
|
||||
|
||||
CREATE TABLE "room_participants" (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"room_id" INTEGER NOT NULL REFERENCES rooms (id),
|
||||
"user_id" INTEGER NOT NULL REFERENCES users (id),
|
||||
"answering_connection_id" INTEGER,
|
||||
"answering_connection_epoch" UUID,
|
||||
"location_kind" INTEGER,
|
||||
"location_project_id" INTEGER,
|
||||
"initial_project_id" INTEGER,
|
||||
"calling_user_id" INTEGER NOT NULL REFERENCES users (id),
|
||||
"calling_connection_id" INTEGER NOT NULL,
|
||||
"calling_connection_epoch" UUID NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_id");
|
||||
CREATE INDEX "index_room_participants_on_answering_connection_epoch" ON "room_participants" ("answering_connection_epoch");
|
||||
CREATE INDEX "index_room_participants_on_calling_connection_epoch" ON "room_participants" ("calling_connection_epoch");
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "signups"
|
||||
ADD "added_to_mailing_list" BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
@@ -0,0 +1,7 @@
|
||||
ALTER TABLE "room_participants"
|
||||
ADD "answering_connection_lost" BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
CREATE INDEX "index_project_collaborators_on_connection_id" ON "project_collaborators" ("connection_id");
|
||||
CREATE UNIQUE INDEX "index_project_collaborators_on_project_id_connection_id_and_epoch" ON "project_collaborators" ("project_id", "connection_id", "connection_epoch");
|
||||
CREATE INDEX "index_room_participants_on_answering_connection_id" ON "room_participants" ("answering_connection_id");
|
||||
CREATE UNIQUE INDEX "index_room_participants_on_answering_connection_id_and_answering_connection_epoch" ON "room_participants" ("answering_connection_id", "answering_connection_epoch");
|
||||
@@ -0,0 +1 @@
|
||||
CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id");
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
auth,
|
||||
db::{Invite, NewUserParams, ProjectId, Signup, User, UserId, WaitlistSummary},
|
||||
db::{Invite, NewSignup, NewUserParams, User, UserId, WaitlistSummary},
|
||||
rpc::{self, ResultExt},
|
||||
AppState, Error, Result,
|
||||
};
|
||||
@@ -16,9 +16,7 @@ use axum::{
|
||||
};
|
||||
use axum_extra::response::ErasedJson;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use time::OffsetDateTime;
|
||||
use std::sync::Arc;
|
||||
use tower::ServiceBuilder;
|
||||
use tracing::instrument;
|
||||
|
||||
@@ -32,16 +30,6 @@ pub fn routes(rpc_server: Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body
|
||||
.route("/invite_codes/:code", get(get_user_for_invite_code))
|
||||
.route("/panic", post(trace_panic))
|
||||
.route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
|
||||
.route(
|
||||
"/user_activity/summary",
|
||||
get(get_top_users_activity_summary),
|
||||
)
|
||||
.route(
|
||||
"/user_activity/timeline/:user_id",
|
||||
get(get_user_activity_timeline),
|
||||
)
|
||||
.route("/user_activity/counts", get(get_active_user_counts))
|
||||
.route("/project_metadata", get(get_project_metadata))
|
||||
.route("/signups", post(create_signup))
|
||||
.route("/signups_summary", get(get_waitlist_summary))
|
||||
.route("/user_invites", post(create_invite_from_code))
|
||||
@@ -216,7 +204,7 @@ async fn create_user(
|
||||
#[derive(Deserialize)]
|
||||
struct UpdateUserParams {
|
||||
admin: Option<bool>,
|
||||
invite_count: Option<u32>,
|
||||
invite_count: Option<i32>,
|
||||
}
|
||||
|
||||
async fn update_user(
|
||||
@@ -283,93 +271,6 @@ async fn get_rpc_server_snapshot(
|
||||
Ok(ErasedJson::pretty(rpc_server.snapshot().await))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TimePeriodParams {
|
||||
#[serde(with = "time::serde::iso8601")]
|
||||
start: OffsetDateTime,
|
||||
#[serde(with = "time::serde::iso8601")]
|
||||
end: OffsetDateTime,
|
||||
}
|
||||
|
||||
async fn get_top_users_activity_summary(
|
||||
Query(params): Query<TimePeriodParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<ErasedJson> {
|
||||
let summary = app
|
||||
.db
|
||||
.get_top_users_activity_summary(params.start..params.end, 100)
|
||||
.await?;
|
||||
Ok(ErasedJson::pretty(summary))
|
||||
}
|
||||
|
||||
async fn get_user_activity_timeline(
|
||||
Path(user_id): Path<i32>,
|
||||
Query(params): Query<TimePeriodParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<ErasedJson> {
|
||||
let summary = app
|
||||
.db
|
||||
.get_user_activity_timeline(params.start..params.end, UserId(user_id))
|
||||
.await?;
|
||||
Ok(ErasedJson::pretty(summary))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ActiveUserCountParams {
|
||||
#[serde(flatten)]
|
||||
period: TimePeriodParams,
|
||||
durations_in_minutes: String,
|
||||
#[serde(default)]
|
||||
only_collaborative: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ActiveUserSet {
|
||||
active_time_in_minutes: u64,
|
||||
user_count: usize,
|
||||
}
|
||||
|
||||
async fn get_active_user_counts(
|
||||
Query(params): Query<ActiveUserCountParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<ErasedJson> {
|
||||
let durations_in_minutes = params.durations_in_minutes.split(',');
|
||||
let mut user_sets = Vec::new();
|
||||
for duration in durations_in_minutes {
|
||||
let duration = duration
|
||||
.parse()
|
||||
.map_err(|_| anyhow!("invalid duration: {duration}"))?;
|
||||
user_sets.push(ActiveUserSet {
|
||||
active_time_in_minutes: duration,
|
||||
user_count: app
|
||||
.db
|
||||
.get_active_user_count(
|
||||
params.period.start..params.period.end,
|
||||
Duration::from_secs(duration * 60),
|
||||
params.only_collaborative,
|
||||
)
|
||||
.await?,
|
||||
})
|
||||
}
|
||||
Ok(ErasedJson::pretty(user_sets))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GetProjectMetadataParams {
|
||||
project_id: u64,
|
||||
}
|
||||
|
||||
async fn get_project_metadata(
|
||||
Query(params): Query<GetProjectMetadataParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<ErasedJson> {
|
||||
let extensions = app
|
||||
.db
|
||||
.get_project_extensions(ProjectId::from_proto(params.project_id))
|
||||
.await?;
|
||||
Ok(ErasedJson::pretty(json!({ "extensions": extensions })))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CreateAccessTokenQueryParams {
|
||||
public_key: String,
|
||||
@@ -434,10 +335,10 @@ async fn get_user_for_invite_code(
|
||||
}
|
||||
|
||||
async fn create_signup(
|
||||
Json(params): Json<Signup>,
|
||||
Json(params): Json<NewSignup>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<()> {
|
||||
app.db.create_signup(params).await?;
|
||||
app.db.create_signup(¶ms).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
|
||||
|
||||
const MAX_ACCESS_TOKENS_TO_STORE: usize = 8;
|
||||
|
||||
pub async fn create_access_token(db: &dyn db::Db, user_id: UserId) -> Result<String> {
|
||||
pub async fn create_access_token(db: &db::Database, user_id: UserId) -> Result<String> {
|
||||
let access_token = rpc::auth::random_token();
|
||||
let access_token_hash =
|
||||
hash_access_token(&access_token).context("failed to hash access token")?;
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
use collab::{Error, Result};
|
||||
use db::{Db, PostgresDb, UserId};
|
||||
use rand::prelude::*;
|
||||
use collab::db;
|
||||
use db::{ConnectOptions, Database};
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
use std::fmt::Write;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
|
||||
#[allow(unused)]
|
||||
#[path = "../db.rs"]
|
||||
mod db;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GitHubUser {
|
||||
@@ -18,9 +12,8 @@ struct GitHubUser {
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let mut rng = StdRng::from_entropy();
|
||||
let database_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL env var");
|
||||
let db = PostgresDb::new(&database_url, 5)
|
||||
let db = Database::new(ConnectOptions::new(database_url))
|
||||
.await
|
||||
.expect("failed to connect to postgres database");
|
||||
let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var");
|
||||
@@ -64,16 +57,14 @@ async fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
let mut zed_user_ids = Vec::<UserId>::new();
|
||||
for (github_user, admin) in zed_users {
|
||||
if let Some(user) = db
|
||||
if db
|
||||
.get_user_by_github_account(&github_user.login, Some(github_user.id))
|
||||
.await
|
||||
.expect("failed to fetch user")
|
||||
.is_none()
|
||||
{
|
||||
zed_user_ids.push(user.id);
|
||||
} else if let Some(email) = &github_user.email {
|
||||
zed_user_ids.push(
|
||||
if let Some(email) = &github_user.email {
|
||||
db.create_user(
|
||||
email,
|
||||
admin,
|
||||
@@ -84,11 +75,8 @@ async fn main() {
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("failed to insert user")
|
||||
.user_id,
|
||||
);
|
||||
} else if admin {
|
||||
zed_user_ids.push(
|
||||
.expect("failed to insert user");
|
||||
} else if admin {
|
||||
db.create_user(
|
||||
&format!("{}@zed.dev", github_user.login),
|
||||
admin,
|
||||
@@ -99,62 +87,10 @@ async fn main() {
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("failed to insert user")
|
||||
.user_id,
|
||||
);
|
||||
.expect("failed to insert user");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let zed_org_id = if let Some(org) = db
|
||||
.find_org_by_slug("zed")
|
||||
.await
|
||||
.expect("failed to fetch org")
|
||||
{
|
||||
org.id
|
||||
} else {
|
||||
db.create_org("Zed", "zed")
|
||||
.await
|
||||
.expect("failed to insert org")
|
||||
};
|
||||
|
||||
let general_channel_id = if let Some(channel) = db
|
||||
.get_org_channels(zed_org_id)
|
||||
.await
|
||||
.expect("failed to fetch channels")
|
||||
.iter()
|
||||
.find(|c| c.name == "General")
|
||||
{
|
||||
channel.id
|
||||
} else {
|
||||
let channel_id = db
|
||||
.create_org_channel(zed_org_id, "General")
|
||||
.await
|
||||
.expect("failed to insert channel");
|
||||
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let max_seconds = Duration::days(100).as_seconds_f64();
|
||||
let mut timestamps = (0..1000)
|
||||
.map(|_| now - Duration::seconds_f64(rng.gen_range(0_f64..=max_seconds)))
|
||||
.collect::<Vec<_>>();
|
||||
timestamps.sort();
|
||||
for timestamp in timestamps {
|
||||
let sender_id = *zed_user_ids.choose(&mut rng).unwrap();
|
||||
let body = lipsum::lipsum_words(rng.gen_range(1..=50));
|
||||
db.create_channel_message(channel_id, sender_id, &body, timestamp, rng.gen())
|
||||
.await
|
||||
.expect("failed to insert message");
|
||||
}
|
||||
channel_id
|
||||
};
|
||||
|
||||
for user_id in zed_user_ids {
|
||||
db.add_org_member(zed_org_id, user_id, true)
|
||||
.await
|
||||
.expect("failed to insert org membership");
|
||||
db.add_channel_member(general_channel_id, user_id, true)
|
||||
.await
|
||||
.expect("failed to insert channel membership");
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_github<T: DeserializeOwned>(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
29
crates/collab/src/db/access_token.rs
Normal file
29
crates/collab/src/db/access_token.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use super::{AccessTokenId, UserId};
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "access_tokens")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: AccessTokenId,
|
||||
pub user_id: UserId,
|
||||
pub hash: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "super::user::Column::Id"
|
||||
)]
|
||||
User,
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
58
crates/collab/src/db/contact.rs
Normal file
58
crates/collab/src/db/contact.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use super::{ContactId, UserId};
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "contacts")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: ContactId,
|
||||
pub user_id_a: UserId,
|
||||
pub user_id_b: UserId,
|
||||
pub a_to_b: bool,
|
||||
pub should_notify: bool,
|
||||
pub accepted: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::room_participant::Entity",
|
||||
from = "Column::UserIdA",
|
||||
to = "super::room_participant::Column::UserId"
|
||||
)]
|
||||
UserARoomParticipant,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::room_participant::Entity",
|
||||
from = "Column::UserIdB",
|
||||
to = "super::room_participant::Column::UserId"
|
||||
)]
|
||||
UserBRoomParticipant,
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Contact {
|
||||
Accepted {
|
||||
user_id: UserId,
|
||||
should_notify: bool,
|
||||
busy: bool,
|
||||
},
|
||||
Outgoing {
|
||||
user_id: UserId,
|
||||
},
|
||||
Incoming {
|
||||
user_id: UserId,
|
||||
should_notify: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl Contact {
|
||||
pub fn user_id(&self) -> UserId {
|
||||
match self {
|
||||
Contact::Accepted { user_id, .. } => *user_id,
|
||||
Contact::Outgoing { user_id } => *user_id,
|
||||
Contact::Incoming { user_id, .. } => *user_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
30
crates/collab/src/db/language_server.rs
Normal file
30
crates/collab/src/db/language_server.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use super::ProjectId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "language_servers")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub project_id: ProjectId,
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::project::Entity",
|
||||
from = "Column::ProjectId",
|
||||
to = "super::project::Column::Id"
|
||||
)]
|
||||
Project,
|
||||
}
|
||||
|
||||
impl Related<super::project::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Project.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
67
crates/collab/src/db/project.rs
Normal file
67
crates/collab/src/db/project.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use super::{ProjectId, RoomId, UserId};
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "projects")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: ProjectId,
|
||||
pub room_id: RoomId,
|
||||
pub host_user_id: UserId,
|
||||
pub host_connection_id: i32,
|
||||
pub host_connection_epoch: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::HostUserId",
|
||||
to = "super::user::Column::Id"
|
||||
)]
|
||||
HostUser,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::room::Entity",
|
||||
from = "Column::RoomId",
|
||||
to = "super::room::Column::Id"
|
||||
)]
|
||||
Room,
|
||||
#[sea_orm(has_many = "super::worktree::Entity")]
|
||||
Worktrees,
|
||||
#[sea_orm(has_many = "super::project_collaborator::Entity")]
|
||||
Collaborators,
|
||||
#[sea_orm(has_many = "super::language_server::Entity")]
|
||||
LanguageServers,
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::HostUser.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::room::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Room.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::worktree::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Worktrees.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::project_collaborator::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Collaborators.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::language_server::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::LanguageServers.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
33
crates/collab/src/db/project_collaborator.rs
Normal file
33
crates/collab/src/db/project_collaborator.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use super::{ProjectCollaboratorId, ProjectId, ReplicaId, UserId};
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "project_collaborators")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: ProjectCollaboratorId,
|
||||
pub project_id: ProjectId,
|
||||
pub connection_id: i32,
|
||||
pub connection_epoch: Uuid,
|
||||
pub user_id: UserId,
|
||||
pub replica_id: ReplicaId,
|
||||
pub is_host: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::project::Entity",
|
||||
from = "Column::ProjectId",
|
||||
to = "super::project::Column::Id"
|
||||
)]
|
||||
Project,
|
||||
}
|
||||
|
||||
impl Related<super::project::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Project.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
32
crates/collab/src/db/room.rs
Normal file
32
crates/collab/src/db/room.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use super::RoomId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "rooms")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: RoomId,
|
||||
pub live_kit_room: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::room_participant::Entity")]
|
||||
RoomParticipant,
|
||||
#[sea_orm(has_many = "super::project::Entity")]
|
||||
Project,
|
||||
}
|
||||
|
||||
impl Related<super::room_participant::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::RoomParticipant.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::project::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Project.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
50
crates/collab/src/db/room_participant.rs
Normal file
50
crates/collab/src/db/room_participant.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use super::{ProjectId, RoomId, RoomParticipantId, UserId};
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "room_participants")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: RoomParticipantId,
|
||||
pub room_id: RoomId,
|
||||
pub user_id: UserId,
|
||||
pub answering_connection_id: Option<i32>,
|
||||
pub answering_connection_epoch: Option<Uuid>,
|
||||
pub answering_connection_lost: bool,
|
||||
pub location_kind: Option<i32>,
|
||||
pub location_project_id: Option<ProjectId>,
|
||||
pub initial_project_id: Option<ProjectId>,
|
||||
pub calling_user_id: UserId,
|
||||
pub calling_connection_id: i32,
|
||||
pub calling_connection_epoch: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "super::user::Column::Id"
|
||||
)]
|
||||
User,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::room::Entity",
|
||||
from = "Column::RoomId",
|
||||
to = "super::room::Column::Id"
|
||||
)]
|
||||
Room,
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::room::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Room.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
57
crates/collab/src/db/signup.rs
Normal file
57
crates/collab/src/db/signup.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use super::{SignupId, UserId};
|
||||
use sea_orm::{entity::prelude::*, FromQueryResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "signups")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: SignupId,
|
||||
pub email_address: String,
|
||||
pub email_confirmation_code: String,
|
||||
pub email_confirmation_sent: bool,
|
||||
pub created_at: DateTime,
|
||||
pub device_id: Option<String>,
|
||||
pub user_id: Option<UserId>,
|
||||
pub inviting_user_id: Option<UserId>,
|
||||
pub platform_mac: bool,
|
||||
pub platform_linux: bool,
|
||||
pub platform_windows: bool,
|
||||
pub platform_unknown: bool,
|
||||
pub editor_features: Option<Vec<String>>,
|
||||
pub programming_languages: Option<Vec<String>>,
|
||||
pub added_to_mailing_list: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, FromQueryResult, Serialize, Deserialize)]
|
||||
pub struct Invite {
|
||||
pub email_address: String,
|
||||
pub email_confirmation_code: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct NewSignup {
|
||||
pub email_address: String,
|
||||
pub platform_mac: bool,
|
||||
pub platform_windows: bool,
|
||||
pub platform_linux: bool,
|
||||
pub editor_features: Vec<String>,
|
||||
pub programming_languages: Vec<String>,
|
||||
pub device_id: Option<String>,
|
||||
pub added_to_mailing_list: bool,
|
||||
pub created_at: Option<DateTime>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, FromQueryResult)]
|
||||
pub struct WaitlistSummary {
|
||||
pub count: i64,
|
||||
pub linux_count: i64,
|
||||
pub mac_count: i64,
|
||||
pub windows_count: i64,
|
||||
pub unknown_count: i64,
|
||||
}
|
||||
1051
crates/collab/src/db/tests.rs
Normal file
1051
crates/collab/src/db/tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
49
crates/collab/src/db/user.rs
Normal file
49
crates/collab/src/db/user.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use super::UserId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)]
|
||||
#[sea_orm(table_name = "users")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: UserId,
|
||||
pub github_login: String,
|
||||
pub github_user_id: Option<i32>,
|
||||
pub email_address: Option<String>,
|
||||
pub admin: bool,
|
||||
pub invite_code: Option<String>,
|
||||
pub invite_count: i32,
|
||||
pub inviter_id: Option<UserId>,
|
||||
pub connected_once: bool,
|
||||
pub metrics_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::access_token::Entity")]
|
||||
AccessToken,
|
||||
#[sea_orm(has_one = "super::room_participant::Entity")]
|
||||
RoomParticipant,
|
||||
#[sea_orm(has_many = "super::project::Entity")]
|
||||
HostedProjects,
|
||||
}
|
||||
|
||||
impl Related<super::access_token::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::AccessToken.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::room_participant::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::RoomParticipant.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::project::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::HostedProjects.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
34
crates/collab/src/db/worktree.rs
Normal file
34
crates/collab/src/db/worktree.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use super::ProjectId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "worktrees")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
#[sea_orm(primary_key)]
|
||||
pub project_id: ProjectId,
|
||||
pub abs_path: String,
|
||||
pub root_name: String,
|
||||
pub visible: bool,
|
||||
pub scan_id: i64,
|
||||
pub is_complete: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::project::Entity",
|
||||
from = "Column::ProjectId",
|
||||
to = "super::project::Column::Id"
|
||||
)]
|
||||
Project,
|
||||
}
|
||||
|
||||
impl Related<super::project::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Project.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
21
crates/collab/src/db/worktree_diagnostic_summary.rs
Normal file
21
crates/collab/src/db/worktree_diagnostic_summary.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use super::ProjectId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "worktree_diagnostic_summaries")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub project_id: ProjectId,
|
||||
#[sea_orm(primary_key)]
|
||||
pub worktree_id: i64,
|
||||
#[sea_orm(primary_key)]
|
||||
pub path: String,
|
||||
pub language_server_id: i64,
|
||||
pub error_count: i32,
|
||||
pub warning_count: i32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
25
crates/collab/src/db/worktree_entry.rs
Normal file
25
crates/collab/src/db/worktree_entry.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use super::ProjectId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "worktree_entries")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub project_id: ProjectId,
|
||||
#[sea_orm(primary_key)]
|
||||
pub worktree_id: i64,
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
pub is_dir: bool,
|
||||
pub path: String,
|
||||
pub inode: i64,
|
||||
pub mtime_seconds: i64,
|
||||
pub mtime_nanos: i32,
|
||||
pub is_symlink: bool,
|
||||
pub is_ignored: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
File diff suppressed because it is too large
Load Diff
36
crates/collab/src/executor.rs
Normal file
36
crates/collab/src/executor.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use std::{future::Future, time::Duration};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Executor {
|
||||
Production,
|
||||
#[cfg(test)]
|
||||
Deterministic(std::sync::Arc<gpui::executor::Background>),
|
||||
}
|
||||
|
||||
impl Executor {
|
||||
pub fn spawn_detached<F>(&self, future: F)
|
||||
where
|
||||
F: 'static + Send + Future<Output = ()>,
|
||||
{
|
||||
match self {
|
||||
Executor::Production => {
|
||||
tokio::spawn(future);
|
||||
}
|
||||
#[cfg(test)]
|
||||
Executor::Deterministic(background) => {
|
||||
background.spawn(future).detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sleep(&self, duration: Duration) -> impl Future<Output = ()> {
|
||||
let this = self.clone();
|
||||
async move {
|
||||
match this {
|
||||
Executor::Production => tokio::time::sleep(duration).await,
|
||||
#[cfg(test)]
|
||||
Executor::Deterministic(background) => background.timer(duration).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,22 @@
|
||||
pub mod api;
|
||||
pub mod auth;
|
||||
pub mod db;
|
||||
pub mod env;
|
||||
pub mod executor;
|
||||
#[cfg(test)]
|
||||
mod integration_tests;
|
||||
pub mod rpc;
|
||||
|
||||
use axum::{http::StatusCode, response::IntoResponse};
|
||||
use db::Database;
|
||||
use serde::Deserialize;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
pub enum Error {
|
||||
Http(StatusCode, String),
|
||||
Database(sea_orm::error::DbErr),
|
||||
Internal(anyhow::Error),
|
||||
}
|
||||
|
||||
@@ -13,9 +26,9 @@ impl From<anyhow::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sqlx::Error> for Error {
|
||||
fn from(error: sqlx::Error) -> Self {
|
||||
Self::Internal(error.into())
|
||||
impl From<sea_orm::error::DbErr> for Error {
|
||||
fn from(error: sea_orm::error::DbErr) -> Self {
|
||||
Self::Database(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +54,9 @@ impl IntoResponse for Error {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
match self {
|
||||
Error::Http(code, message) => (code, message).into_response(),
|
||||
Error::Database(error) => {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response()
|
||||
}
|
||||
Error::Internal(error) => {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response()
|
||||
}
|
||||
@@ -52,6 +68,7 @@ impl std::fmt::Debug for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::Http(code, message) => (code, message).fmt(f),
|
||||
Error::Database(error) => error.fmt(f),
|
||||
Error::Internal(error) => error.fmt(f),
|
||||
}
|
||||
}
|
||||
@@ -61,9 +78,64 @@ impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::Http(code, message) => write!(f, "{code}: {message}"),
|
||||
Error::Database(error) => error.fmt(f),
|
||||
Error::Internal(error) => error.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
pub struct Config {
|
||||
pub http_port: u16,
|
||||
pub database_url: String,
|
||||
pub api_token: String,
|
||||
pub invite_link_prefix: String,
|
||||
pub live_kit_server: Option<String>,
|
||||
pub live_kit_key: Option<String>,
|
||||
pub live_kit_secret: Option<String>,
|
||||
pub rust_log: Option<String>,
|
||||
pub log_json: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
pub struct MigrateConfig {
|
||||
pub database_url: String,
|
||||
pub migrations_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
pub db: Arc<Database>,
|
||||
pub live_kit_client: Option<Arc<dyn live_kit_server::api::Client>>,
|
||||
pub config: Config,
|
||||
}
|
||||
|
||||
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);
|
||||
let db = Database::new(db_options).await?;
|
||||
let live_kit_client = if let Some(((server, key), secret)) = config
|
||||
.live_kit_server
|
||||
.as_ref()
|
||||
.zip(config.live_kit_key.as_ref())
|
||||
.zip(config.live_kit_secret.as_ref())
|
||||
{
|
||||
Some(Arc::new(live_kit_server::api::LiveKitClient::new(
|
||||
server.clone(),
|
||||
key.clone(),
|
||||
secret.clone(),
|
||||
)) as Arc<dyn live_kit_server::api::Client>)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let this = Self {
|
||||
db: Arc::new(db),
|
||||
live_kit_client,
|
||||
config,
|
||||
};
|
||||
Ok(Arc::new(this))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,86 +1,19 @@
|
||||
mod api;
|
||||
mod auth;
|
||||
mod db;
|
||||
mod env;
|
||||
mod rpc;
|
||||
|
||||
#[cfg(test)]
|
||||
mod db_tests;
|
||||
#[cfg(test)]
|
||||
mod integration_tests;
|
||||
|
||||
use crate::rpc::ResultExt as _;
|
||||
use anyhow::anyhow;
|
||||
use axum::{routing::get, Router};
|
||||
use collab::{Error, Result};
|
||||
use db::{Db, PostgresDb};
|
||||
use serde::Deserialize;
|
||||
use collab::{db, env, executor::Executor, AppState, Config, MigrateConfig, Result};
|
||||
use db::Database;
|
||||
use std::{
|
||||
env::args,
|
||||
net::{SocketAddr, TcpListener},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
path::Path,
|
||||
};
|
||||
use tokio::signal;
|
||||
use tokio::signal::unix::SignalKind;
|
||||
use tracing_log::LogTracer;
|
||||
use tracing_subscriber::{filter::EnvFilter, fmt::format::JsonFields, Layer};
|
||||
use util::ResultExt;
|
||||
|
||||
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
pub struct Config {
|
||||
pub http_port: u16,
|
||||
pub database_url: String,
|
||||
pub api_token: String,
|
||||
pub invite_link_prefix: String,
|
||||
pub live_kit_server: Option<String>,
|
||||
pub live_kit_key: Option<String>,
|
||||
pub live_kit_secret: Option<String>,
|
||||
pub rust_log: Option<String>,
|
||||
pub log_json: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
pub struct MigrateConfig {
|
||||
pub database_url: String,
|
||||
pub migrations_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
db: Arc<dyn Db>,
|
||||
live_kit_client: Option<Arc<dyn live_kit_server::api::Client>>,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
async fn new(config: Config) -> Result<Arc<Self>> {
|
||||
let db = PostgresDb::new(&config.database_url, 5).await?;
|
||||
let live_kit_client = if let Some(((server, key), secret)) = config
|
||||
.live_kit_server
|
||||
.as_ref()
|
||||
.zip(config.live_kit_key.as_ref())
|
||||
.zip(config.live_kit_secret.as_ref())
|
||||
{
|
||||
Some(Arc::new(live_kit_server::api::LiveKitClient::new(
|
||||
server.clone(),
|
||||
key.clone(),
|
||||
secret.clone(),
|
||||
)) as Arc<dyn live_kit_server::api::Client>)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let this = Self {
|
||||
db: Arc::new(db),
|
||||
live_kit_client,
|
||||
config,
|
||||
};
|
||||
Ok(Arc::new(this))
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
if let Err(error) = env::load_dotenv() {
|
||||
@@ -96,13 +29,14 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
Some("migrate") => {
|
||||
let config = envy::from_env::<MigrateConfig>().expect("error loading config");
|
||||
let db = PostgresDb::new(&config.database_url, 5).await?;
|
||||
let mut db_options = db::ConnectOptions::new(config.database_url.clone());
|
||||
db_options.max_connections(5);
|
||||
let db = Database::new(db_options).await?;
|
||||
|
||||
let migrations_path = config
|
||||
.migrations_path
|
||||
.as_deref()
|
||||
.or(db::DEFAULT_MIGRATIONS_PATH.map(|s| s.as_ref()))
|
||||
.ok_or_else(|| anyhow!("missing MIGRATIONS_PATH environment variable"))?;
|
||||
.unwrap_or_else(|| Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/migrations")));
|
||||
|
||||
let migrations = db.migrate(&migrations_path, false).await?;
|
||||
for (migration, duration) in migrations {
|
||||
@@ -119,20 +53,32 @@ async fn main() -> Result<()> {
|
||||
init_tracing(&config);
|
||||
|
||||
let state = AppState::new(config).await?;
|
||||
|
||||
let listener = TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port))
|
||||
.expect("failed to bind TCP listener");
|
||||
|
||||
let rpc_server = rpc::Server::new(state.clone(), None);
|
||||
rpc_server
|
||||
.start_recording_project_activity(Duration::from_secs(5 * 60), rpc::RealExecutor);
|
||||
let rpc_server = collab::rpc::Server::new(state.clone(), Executor::Production);
|
||||
rpc_server.start();
|
||||
|
||||
let app = api::routes(rpc_server.clone(), state.clone())
|
||||
.merge(rpc::routes(rpc_server.clone()))
|
||||
let app = collab::api::routes(rpc_server.clone(), state.clone())
|
||||
.merge(collab::rpc::routes(rpc_server.clone()))
|
||||
.merge(Router::new().route("/", get(handle_root)));
|
||||
|
||||
axum::Server::from_tcp(listener)?
|
||||
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
|
||||
.with_graceful_shutdown(graceful_shutdown(rpc_server, state))
|
||||
.with_graceful_shutdown(async move {
|
||||
let mut sigterm = tokio::signal::unix::signal(SignalKind::terminate())
|
||||
.expect("failed to listen for interrupt signal");
|
||||
let mut sigint = tokio::signal::unix::signal(SignalKind::interrupt())
|
||||
.expect("failed to listen for interrupt signal");
|
||||
let sigterm = sigterm.recv();
|
||||
let sigint = sigint.recv();
|
||||
futures::pin_mut!(sigterm);
|
||||
futures::pin_mut!(sigint);
|
||||
futures::future::select(sigterm, sigint).await;
|
||||
tracing::info!("Received interrupt signal");
|
||||
rpc_server.teardown();
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
_ => {
|
||||
@@ -177,52 +123,3 @@ pub fn init_tracing(config: &Config) -> Option<()> {
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
async fn graceful_shutdown(rpc_server: Arc<rpc::Server>, state: Arc<AppState>) {
|
||||
let ctrl_c = async {
|
||||
signal::ctrl_c()
|
||||
.await
|
||||
.expect("failed to install Ctrl+C handler");
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
let terminate = async {
|
||||
signal::unix::signal(signal::unix::SignalKind::terminate())
|
||||
.expect("failed to install signal handler")
|
||||
.recv()
|
||||
.await;
|
||||
};
|
||||
|
||||
#[cfg(not(unix))]
|
||||
let terminate = std::future::pending::<()>();
|
||||
|
||||
tokio::select! {
|
||||
_ = ctrl_c => {},
|
||||
_ = terminate => {},
|
||||
}
|
||||
|
||||
if let Some(live_kit) = state.live_kit_client.as_ref() {
|
||||
let deletions = rpc_server
|
||||
.store()
|
||||
.await
|
||||
.rooms()
|
||||
.values()
|
||||
.map(|room| {
|
||||
let name = room.live_kit_room.clone();
|
||||
async {
|
||||
live_kit.delete_room(name).await.trace_err();
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
tracing::info!("deleting all live-kit rooms");
|
||||
if let Err(_) = tokio::time::timeout(
|
||||
Duration::from_secs(10),
|
||||
futures::future::join_all(deletions),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!("timed out waiting for live-kit room deletion");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
98
crates/collab/src/rpc/connection_pool.rs
Normal file
98
crates/collab/src/rpc/connection_pool.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
use crate::db::UserId;
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::{BTreeMap, HashSet};
|
||||
use rpc::ConnectionId;
|
||||
use serde::Serialize;
|
||||
use tracing::instrument;
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
pub struct ConnectionPool {
|
||||
connections: BTreeMap<ConnectionId, Connection>,
|
||||
connected_users: BTreeMap<UserId, ConnectedUser>,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
struct ConnectedUser {
|
||||
connection_ids: HashSet<ConnectionId>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Connection {
|
||||
pub user_id: UserId,
|
||||
pub admin: bool,
|
||||
}
|
||||
|
||||
impl ConnectionPool {
|
||||
pub fn reset(&mut self) {
|
||||
self.connections.clear();
|
||||
self.connected_users.clear();
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub fn add_connection(&mut self, connection_id: ConnectionId, user_id: UserId, admin: bool) {
|
||||
self.connections
|
||||
.insert(connection_id, Connection { user_id, admin });
|
||||
let connected_user = self.connected_users.entry(user_id).or_default();
|
||||
connected_user.connection_ids.insert(connection_id);
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub fn remove_connection(&mut self, connection_id: ConnectionId) -> Result<()> {
|
||||
let connection = self
|
||||
.connections
|
||||
.get_mut(&connection_id)
|
||||
.ok_or_else(|| anyhow!("no such connection"))?;
|
||||
|
||||
let user_id = connection.user_id;
|
||||
let connected_user = self.connected_users.get_mut(&user_id).unwrap();
|
||||
connected_user.connection_ids.remove(&connection_id);
|
||||
if connected_user.connection_ids.is_empty() {
|
||||
self.connected_users.remove(&user_id);
|
||||
}
|
||||
self.connections.remove(&connection_id).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn connections(&self) -> impl Iterator<Item = &Connection> {
|
||||
self.connections.values()
|
||||
}
|
||||
|
||||
pub fn user_connection_ids(&self, user_id: UserId) -> impl Iterator<Item = ConnectionId> + '_ {
|
||||
self.connected_users
|
||||
.get(&user_id)
|
||||
.into_iter()
|
||||
.map(|state| &state.connection_ids)
|
||||
.flatten()
|
||||
.copied()
|
||||
}
|
||||
|
||||
pub fn is_user_online(&self, user_id: UserId) -> bool {
|
||||
!self
|
||||
.connected_users
|
||||
.get(&user_id)
|
||||
.unwrap_or(&Default::default())
|
||||
.connection_ids
|
||||
.is_empty()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn check_invariants(&self) {
|
||||
for (connection_id, connection) in &self.connections {
|
||||
assert!(self
|
||||
.connected_users
|
||||
.get(&connection.user_id)
|
||||
.unwrap()
|
||||
.connection_ids
|
||||
.contains(connection_id));
|
||||
}
|
||||
|
||||
for (user_id, state) in &self.connected_users {
|
||||
for connection_id in &state.connection_ids {
|
||||
assert_eq!(
|
||||
self.connections.get(connection_id).unwrap().user_id,
|
||||
*user_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -43,7 +43,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
project_id,
|
||||
app_state.client.clone(),
|
||||
app_state.user_store.clone(),
|
||||
app_state.project_store.clone(),
|
||||
app_state.languages.clone(),
|
||||
app_state.fs.clone(),
|
||||
cx.clone(),
|
||||
@@ -51,7 +50,13 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
.await?;
|
||||
|
||||
let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
|
||||
let mut workspace = Workspace::new(project, app_state.default_item_factory, cx);
|
||||
let mut workspace = Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project,
|
||||
app_state.dock_default_item_factory,
|
||||
cx,
|
||||
);
|
||||
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
|
||||
workspace
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ use gpui::{
|
||||
elements::*, impl_internal_actions, Entity, ModelHandle, MutableAppContext, RenderContext,
|
||||
View, ViewContext,
|
||||
};
|
||||
use workspace::Notification;
|
||||
use workspace::notifications::Notification;
|
||||
|
||||
impl_internal_actions!(contact_notifications, [Dismiss, RespondToContactRequest]);
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ impl IncomingCallNotification {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
if action.accept {
|
||||
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
|
||||
let caller_user_id = self.call.caller.id;
|
||||
let caller_user_id = self.call.calling_user.id;
|
||||
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
|
||||
cx.spawn_weak(|_, mut cx| async move {
|
||||
join.await?;
|
||||
@@ -105,7 +105,7 @@ impl IncomingCallNotification {
|
||||
.as_ref()
|
||||
.unwrap_or(&default_project);
|
||||
Flex::row()
|
||||
.with_children(self.call.caller.avatar.clone().map(|avatar| {
|
||||
.with_children(self.call.calling_user.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
.with_style(theme.caller_avatar)
|
||||
.aligned()
|
||||
@@ -115,7 +115,7 @@ impl IncomingCallNotification {
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Label::new(
|
||||
self.call.caller.github_login.clone(),
|
||||
self.call.calling_user.github_login.clone(),
|
||||
theme.caller_username.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
|
||||
@@ -350,8 +350,9 @@ mod tests {
|
||||
});
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let editor = cx.add_view(&workspace, |cx| {
|
||||
let mut editor = Editor::single_line(None, cx);
|
||||
editor.set_text("abc", cx);
|
||||
|
||||
@@ -12,16 +12,20 @@ test-support = []
|
||||
|
||||
[dependencies]
|
||||
collections = { path = "../collections" }
|
||||
gpui = { path = "../gpui" }
|
||||
sqlez = { path = "../sqlez" }
|
||||
sqlez_macros = { path = "../sqlez_macros" }
|
||||
util = { path = "../util" }
|
||||
anyhow = "1.0.57"
|
||||
indoc = "1.0.4"
|
||||
async-trait = "0.1"
|
||||
lazy_static = "1.4.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
parking_lot = "0.11.1"
|
||||
rusqlite = { version = "0.28.0", features = ["bundled", "serde_json"] }
|
||||
rusqlite_migration = { git = "https://github.com/cljoly/rusqlite_migration", rev = "c433555d7c1b41b103426e35756eb3144d0ebbc6" }
|
||||
serde = { workspace = true }
|
||||
serde_rusqlite = "0.31.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
smol = "1.2"
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
env_logger = "0.9.1"
|
||||
tempdir = { version = "0.3.7" }
|
||||
|
||||
5
crates/db/README.md
Normal file
5
crates/db/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Building Queries
|
||||
|
||||
First, craft your test data. The examples folder shows a template for building a test-db, and can be ran with `cargo run --example [your-example]`.
|
||||
|
||||
To actually use and test your queries, import the generated DB file into https://sqliteonline.com/
|
||||
@@ -1,119 +1,365 @@
|
||||
mod kvp;
|
||||
mod migrations;
|
||||
pub mod kvp;
|
||||
pub mod query;
|
||||
|
||||
use std::fs;
|
||||
// Re-export
|
||||
pub use anyhow;
|
||||
use anyhow::Context;
|
||||
pub use indoc::indoc;
|
||||
pub use lazy_static;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
pub use smol;
|
||||
pub use sqlez;
|
||||
pub use sqlez_macros;
|
||||
pub use util::channel::{RELEASE_CHANNEL, RELEASE_CHANNEL_NAME};
|
||||
pub use util::paths::DB_DIR;
|
||||
|
||||
use sqlez::domain::Migrator;
|
||||
use sqlez::thread_safe_connection::ThreadSafeConnection;
|
||||
use sqlez_macros::sql;
|
||||
use std::fs::create_dir_all;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use util::{async_iife, ResultExt};
|
||||
use util::channel::ReleaseChannel;
|
||||
|
||||
use anyhow::Result;
|
||||
use log::error;
|
||||
use parking_lot::Mutex;
|
||||
use rusqlite::Connection;
|
||||
const CONNECTION_INITIALIZE_QUERY: &'static str = sql!(
|
||||
PRAGMA foreign_keys=TRUE;
|
||||
);
|
||||
|
||||
use migrations::MIGRATIONS;
|
||||
const DB_INITIALIZE_QUERY: &'static str = sql!(
|
||||
PRAGMA journal_mode=WAL;
|
||||
PRAGMA busy_timeout=1;
|
||||
PRAGMA case_sensitive_like=TRUE;
|
||||
PRAGMA synchronous=NORMAL;
|
||||
);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Db {
|
||||
Real(Arc<RealDb>),
|
||||
Null,
|
||||
const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB";
|
||||
|
||||
const DB_FILE_NAME: &'static str = "db.sqlite";
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
|
||||
pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
|
||||
pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false);
|
||||
}
|
||||
|
||||
pub struct RealDb {
|
||||
connection: Mutex<Connection>,
|
||||
path: Option<PathBuf>,
|
||||
}
|
||||
/// Open or create a database at the given directory path.
|
||||
/// This will retry a couple times if there are failures. If opening fails once, the db directory
|
||||
/// is moved to a backup folder and a new one is created. If that fails, a shared in memory db is created.
|
||||
/// In either case, static variables are set so that the user can be notified.
|
||||
pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, release_channel: &ReleaseChannel) -> ThreadSafeConnection<M> {
|
||||
let release_channel_name = release_channel.dev_name();
|
||||
let main_db_dir = db_dir.join(Path::new(&format!("0-{}", release_channel_name)));
|
||||
|
||||
impl Db {
|
||||
/// Open or create a database at the given directory path.
|
||||
pub fn open(db_dir: &Path, channel: &'static str) -> Self {
|
||||
// Use 0 for now. Will implement incrementing and clearing of old db files soon TM
|
||||
let current_db_dir = db_dir.join(Path::new(&format!("0-{}", channel)));
|
||||
fs::create_dir_all(¤t_db_dir)
|
||||
.expect("Should be able to create the database directory");
|
||||
let db_path = current_db_dir.join(Path::new("db.sqlite"));
|
||||
|
||||
Connection::open(db_path)
|
||||
.map_err(Into::into)
|
||||
.and_then(|connection| Self::initialize(connection))
|
||||
.map(|connection| {
|
||||
Db::Real(Arc::new(RealDb {
|
||||
connection,
|
||||
path: Some(db_dir.to_path_buf()),
|
||||
}))
|
||||
})
|
||||
.unwrap_or_else(|e| {
|
||||
error!(
|
||||
"Connecting to file backed db failed. Reverting to null db. {}",
|
||||
e
|
||||
);
|
||||
Self::Null
|
||||
})
|
||||
}
|
||||
|
||||
/// Open a in memory database for testing and as a fallback.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn open_in_memory() -> Self {
|
||||
Connection::open_in_memory()
|
||||
.map_err(Into::into)
|
||||
.and_then(|connection| Self::initialize(connection))
|
||||
.map(|connection| {
|
||||
Db::Real(Arc::new(RealDb {
|
||||
connection,
|
||||
path: None,
|
||||
}))
|
||||
})
|
||||
.unwrap_or_else(|e| {
|
||||
error!(
|
||||
"Connecting to in memory db failed. Reverting to null db. {}",
|
||||
e
|
||||
);
|
||||
Self::Null
|
||||
})
|
||||
}
|
||||
|
||||
fn initialize(mut conn: Connection) -> Result<Mutex<Connection>> {
|
||||
MIGRATIONS.to_latest(&mut conn)?;
|
||||
|
||||
conn.pragma_update(None, "journal_mode", "WAL")?;
|
||||
conn.pragma_update(None, "synchronous", "NORMAL")?;
|
||||
conn.pragma_update(None, "foreign_keys", true)?;
|
||||
conn.pragma_update(None, "case_sensitive_like", true)?;
|
||||
|
||||
Ok(Mutex::new(conn))
|
||||
}
|
||||
|
||||
pub fn persisting(&self) -> bool {
|
||||
self.real().and_then(|db| db.path.as_ref()).is_some()
|
||||
}
|
||||
|
||||
pub fn real(&self) -> Option<&RealDb> {
|
||||
match self {
|
||||
Db::Real(db) => Some(&db),
|
||||
_ => None,
|
||||
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
|
||||
// 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
|
||||
// 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
|
||||
// a bad time.
|
||||
|
||||
// If no db folder, create one at 0-{channel}
|
||||
create_dir_all(&main_db_dir).context("Could not create db directory")?;
|
||||
let db_path = main_db_dir.join(Path::new(DB_FILE_NAME));
|
||||
|
||||
// Optimistically open databases in parallel
|
||||
if !DB_FILE_OPERATIONS.is_locked() {
|
||||
// Try building a connection
|
||||
if let Some(connection) = open_main_db(&db_path).await {
|
||||
return Ok(connection)
|
||||
};
|
||||
}
|
||||
|
||||
// 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();
|
||||
if let Some(connection) = open_main_db(&db_path).await {
|
||||
return Ok(connection)
|
||||
};
|
||||
|
||||
let backup_timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("System clock is set before the unix timestamp, Zed does not support this region of spacetime")
|
||||
.as_millis();
|
||||
|
||||
// If failed, move 0-{channel} to {current unix timestamp}-{channel}
|
||||
let backup_db_dir = db_dir.join(Path::new(&format!(
|
||||
"{}-{}",
|
||||
backup_timestamp,
|
||||
release_channel_name,
|
||||
)));
|
||||
|
||||
std::fs::rename(&main_db_dir, &backup_db_dir)
|
||||
.context("Failed clean up corrupted database, panicking.")?;
|
||||
|
||||
// Set a static ref with the failed timestamp and error so we can notify the user
|
||||
{
|
||||
let mut guard = BACKUP_DB_PATH.write();
|
||||
*guard = Some(backup_db_dir);
|
||||
}
|
||||
|
||||
// Create a new 0-{channel}
|
||||
create_dir_all(&main_db_dir).context("Should be able to create the database directory")?;
|
||||
let db_path = main_db_dir.join(Path::new(DB_FILE_NAME));
|
||||
|
||||
// Try again
|
||||
open_main_db(&db_path).await.context("Could not newly created db")
|
||||
}).await.log_err();
|
||||
|
||||
if let Some(connection) = connection {
|
||||
return connection;
|
||||
}
|
||||
|
||||
// Set another static ref so that we can escalate the notification
|
||||
ALL_FILE_DB_FAILED.store(true, Ordering::Release);
|
||||
|
||||
// If still failed, create an in memory db with a known name
|
||||
open_fallback_db().await
|
||||
}
|
||||
|
||||
impl Drop for Db {
|
||||
fn drop(&mut self) {
|
||||
match self {
|
||||
Db::Real(real_db) => {
|
||||
let lock = real_db.connection.lock();
|
||||
async fn open_main_db<M: Migrator>(db_path: &PathBuf) -> Option<ThreadSafeConnection<M>> {
|
||||
log::info!("Opening main db");
|
||||
ThreadSafeConnection::<M>::builder(db_path.to_string_lossy().as_ref(), true)
|
||||
.with_db_initialization_query(DB_INITIALIZE_QUERY)
|
||||
.with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
|
||||
.build()
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
|
||||
let _ = lock.pragma_update(None, "analysis_limit", "500");
|
||||
let _ = lock.pragma_update(None, "optimize", "");
|
||||
async fn open_fallback_db<M: Migrator>() -> ThreadSafeConnection<M> {
|
||||
log::info!("Opening fallback db");
|
||||
ThreadSafeConnection::<M>::builder(FALLBACK_DB_NAME, false)
|
||||
.with_db_initialization_query(DB_INITIALIZE_QUERY)
|
||||
.with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
|
||||
.build()
|
||||
.await
|
||||
.expect(
|
||||
"Fallback in memory database failed. Likely initialization queries or migrations have fundamental errors",
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn open_test_db<M: Migrator>(db_name: &str) -> ThreadSafeConnection<M> {
|
||||
use sqlez::thread_safe_connection::locking_queue;
|
||||
|
||||
ThreadSafeConnection::<M>::builder(db_name, false)
|
||||
.with_db_initialization_query(DB_INITIALIZE_QUERY)
|
||||
.with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
|
||||
// Serialize queued writes via a mutex and run them synchronously
|
||||
.with_write_queue_constructor(locking_queue())
|
||||
.build()
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Implements a basic DB wrapper for a given domain
|
||||
#[macro_export]
|
||||
macro_rules! define_connection {
|
||||
(pub static ref $id:ident: $t:ident<()> = $migrations:expr;) => {
|
||||
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection<$t>);
|
||||
|
||||
impl ::std::ops::Deref for $t {
|
||||
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection<$t>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
Db::Null => {}
|
||||
}
|
||||
}
|
||||
|
||||
impl $crate::sqlez::domain::Domain for $t {
|
||||
fn name() -> &'static str {
|
||||
stringify!($t)
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
$migrations
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
$crate::lazy_static::lazy_static! {
|
||||
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_test_db(stringify!($id))));
|
||||
}
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
$crate::lazy_static::lazy_static! {
|
||||
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_db(&$crate::DB_DIR, &$crate::RELEASE_CHANNEL)));
|
||||
}
|
||||
};
|
||||
(pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr;) => {
|
||||
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection<( $($d),+, $t )>);
|
||||
|
||||
impl ::std::ops::Deref for $t {
|
||||
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection<($($d),+, $t)>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl $crate::sqlez::domain::Domain for $t {
|
||||
fn name() -> &'static str {
|
||||
stringify!($t)
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
$migrations
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
$crate::lazy_static::lazy_static! {
|
||||
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_test_db(stringify!($id))));
|
||||
}
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
$crate::lazy_static::lazy_static! {
|
||||
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_db(&$crate::DB_DIR, &$crate::RELEASE_CHANNEL)));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::migrations::MIGRATIONS;
|
||||
use std::{fs, thread};
|
||||
|
||||
#[test]
|
||||
fn test_migrations() {
|
||||
assert!(MIGRATIONS.validate().is_ok());
|
||||
use sqlez::{domain::Domain, connection::Connection};
|
||||
use sqlez_macros::sql;
|
||||
use tempdir::TempDir;
|
||||
|
||||
use crate::{open_db, DB_FILE_NAME};
|
||||
|
||||
// Test bad migration panics
|
||||
#[gpui::test]
|
||||
#[should_panic]
|
||||
async fn test_bad_migration_panics() {
|
||||
enum BadDB {}
|
||||
|
||||
impl Domain for BadDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests"
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test(value);),
|
||||
// failure because test already exists
|
||||
sql!(CREATE TABLE test(value);)]
|
||||
}
|
||||
}
|
||||
|
||||
let tempdir = TempDir::new("DbTests").unwrap();
|
||||
let _bad_db = open_db::<BadDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
|
||||
}
|
||||
|
||||
/// Test that DB exists but corrupted (causing recreate)
|
||||
#[gpui::test]
|
||||
async fn test_db_corruption() {
|
||||
enum CorruptedDB {}
|
||||
|
||||
impl Domain for CorruptedDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests"
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test(value);)]
|
||||
}
|
||||
}
|
||||
|
||||
enum GoodDB {}
|
||||
|
||||
impl Domain for GoodDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests" //Notice same name
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test2(value);)] //But different migration
|
||||
}
|
||||
}
|
||||
|
||||
let tempdir = TempDir::new("DbTests").unwrap();
|
||||
{
|
||||
let corrupt_db = open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
|
||||
assert!(corrupt_db.persistent());
|
||||
}
|
||||
|
||||
|
||||
let good_db = open_db::<GoodDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
|
||||
assert!(good_db.select_row::<usize>("SELECT * FROM test2").unwrap()().unwrap().is_none());
|
||||
|
||||
let mut corrupted_backup_dir = fs::read_dir(
|
||||
tempdir.path()
|
||||
).unwrap().find(|entry| {
|
||||
!entry.as_ref().unwrap().file_name().to_str().unwrap().starts_with("0")
|
||||
}
|
||||
).unwrap().unwrap().path();
|
||||
corrupted_backup_dir.push(DB_FILE_NAME);
|
||||
|
||||
dbg!(&corrupted_backup_dir);
|
||||
|
||||
let backup = Connection::open_file(&corrupted_backup_dir.to_string_lossy());
|
||||
assert!(backup.select_row::<usize>("SELECT * FROM test").unwrap()().unwrap().is_none());
|
||||
}
|
||||
|
||||
/// Test that DB exists but corrupted (causing recreate)
|
||||
#[gpui::test]
|
||||
async fn test_simultaneous_db_corruption() {
|
||||
enum CorruptedDB {}
|
||||
|
||||
impl Domain for CorruptedDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests"
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test(value);)]
|
||||
}
|
||||
}
|
||||
|
||||
enum GoodDB {}
|
||||
|
||||
impl Domain for GoodDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests" //Notice same name
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test2(value);)] //But different migration
|
||||
}
|
||||
}
|
||||
|
||||
let tempdir = TempDir::new("DbTests").unwrap();
|
||||
{
|
||||
// Setup the bad database
|
||||
let corrupt_db = open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
|
||||
assert!(corrupt_db.persistent());
|
||||
}
|
||||
|
||||
// Try to connect to it a bunch of times at once
|
||||
let mut guards = vec![];
|
||||
for _ in 0..10 {
|
||||
let tmp_path = tempdir.path().to_path_buf();
|
||||
let guard = thread::spawn(move || {
|
||||
let good_db = smol::block_on(open_db::<GoodDB>(tmp_path.as_path(), &util::channel::ReleaseChannel::Dev));
|
||||
assert!(good_db.select_row::<usize>("SELECT * FROM test2").unwrap()().unwrap().is_none());
|
||||
});
|
||||
|
||||
guards.push(guard);
|
||||
|
||||
}
|
||||
|
||||
for guard in guards.into_iter() {
|
||||
assert!(guard.join().is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,311 +0,0 @@
|
||||
use std::{ffi::OsStr, fmt::Display, hash::Hash, os::unix::prelude::OsStrExt, path::PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::HashSet;
|
||||
use rusqlite::{named_params, params};
|
||||
|
||||
use super::Db;
|
||||
|
||||
pub(crate) const ITEMS_M_1: &str = "
|
||||
CREATE TABLE items(
|
||||
id INTEGER PRIMARY KEY,
|
||||
kind TEXT
|
||||
) STRICT;
|
||||
CREATE TABLE item_path(
|
||||
item_id INTEGER PRIMARY KEY,
|
||||
path BLOB
|
||||
) STRICT;
|
||||
CREATE TABLE item_query(
|
||||
item_id INTEGER PRIMARY KEY,
|
||||
query TEXT
|
||||
) STRICT;
|
||||
";
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Debug)]
|
||||
pub enum SerializedItemKind {
|
||||
Editor,
|
||||
Terminal,
|
||||
ProjectSearch,
|
||||
Diagnostics,
|
||||
}
|
||||
|
||||
impl Display for SerializedItemKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&format!("{:?}", self))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum SerializedItem {
|
||||
Editor(usize, PathBuf),
|
||||
Terminal(usize),
|
||||
ProjectSearch(usize, String),
|
||||
Diagnostics(usize),
|
||||
}
|
||||
|
||||
impl SerializedItem {
|
||||
fn kind(&self) -> SerializedItemKind {
|
||||
match self {
|
||||
SerializedItem::Editor(_, _) => SerializedItemKind::Editor,
|
||||
SerializedItem::Terminal(_) => SerializedItemKind::Terminal,
|
||||
SerializedItem::ProjectSearch(_, _) => SerializedItemKind::ProjectSearch,
|
||||
SerializedItem::Diagnostics(_) => SerializedItemKind::Diagnostics,
|
||||
}
|
||||
}
|
||||
|
||||
fn id(&self) -> usize {
|
||||
match self {
|
||||
SerializedItem::Editor(id, _)
|
||||
| SerializedItem::Terminal(id)
|
||||
| SerializedItem::ProjectSearch(id, _)
|
||||
| SerializedItem::Diagnostics(id) => *id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Db {
|
||||
fn write_item(&self, serialized_item: SerializedItem) -> Result<()> {
|
||||
self.real()
|
||||
.map(|db| {
|
||||
let mut lock = db.connection.lock();
|
||||
let tx = lock.transaction()?;
|
||||
|
||||
// Serialize the item
|
||||
let id = serialized_item.id();
|
||||
{
|
||||
let mut stmt = tx.prepare_cached(
|
||||
"INSERT OR REPLACE INTO items(id, kind) VALUES ((?), (?))",
|
||||
)?;
|
||||
|
||||
dbg!("inserting item");
|
||||
stmt.execute(params![id, serialized_item.kind().to_string()])?;
|
||||
}
|
||||
|
||||
// Serialize item data
|
||||
match &serialized_item {
|
||||
SerializedItem::Editor(_, path) => {
|
||||
dbg!("inserting path");
|
||||
let mut stmt = tx.prepare_cached(
|
||||
"INSERT OR REPLACE INTO item_path(item_id, path) VALUES ((?), (?))",
|
||||
)?;
|
||||
|
||||
let path_bytes = path.as_os_str().as_bytes();
|
||||
stmt.execute(params![id, path_bytes])?;
|
||||
}
|
||||
SerializedItem::ProjectSearch(_, query) => {
|
||||
dbg!("inserting query");
|
||||
let mut stmt = tx.prepare_cached(
|
||||
"INSERT OR REPLACE INTO item_query(item_id, query) VALUES ((?), (?))",
|
||||
)?;
|
||||
|
||||
stmt.execute(params![id, query])?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
tx.commit()?;
|
||||
|
||||
let mut stmt = lock.prepare_cached("SELECT id, kind FROM items")?;
|
||||
let _ = stmt
|
||||
.query_map([], |row| {
|
||||
let zero: usize = row.get(0)?;
|
||||
let one: String = row.get(1)?;
|
||||
|
||||
dbg!(zero, one);
|
||||
Ok(())
|
||||
})?
|
||||
.collect::<Vec<Result<(), _>>>();
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.unwrap_or(Ok(()))
|
||||
}
|
||||
|
||||
fn delete_item(&self, item_id: usize) -> Result<()> {
|
||||
self.real()
|
||||
.map(|db| {
|
||||
let lock = db.connection.lock();
|
||||
|
||||
let mut stmt = lock.prepare_cached(
|
||||
r#"
|
||||
DELETE FROM items WHERE id = (:id);
|
||||
DELETE FROM item_path WHERE id = (:id);
|
||||
DELETE FROM item_query WHERE id = (:id);
|
||||
"#,
|
||||
)?;
|
||||
|
||||
stmt.execute(named_params! {":id": item_id})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.unwrap_or(Ok(()))
|
||||
}
|
||||
|
||||
fn take_items(&self) -> Result<HashSet<SerializedItem>> {
|
||||
self.real()
|
||||
.map(|db| {
|
||||
let mut lock = db.connection.lock();
|
||||
|
||||
let tx = lock.transaction()?;
|
||||
|
||||
// When working with transactions in rusqlite, need to make this kind of scope
|
||||
// To make the borrow stuff work correctly. Don't know why, rust is wild.
|
||||
let result = {
|
||||
let mut editors_stmt = tx.prepare_cached(
|
||||
r#"
|
||||
SELECT items.id, item_path.path
|
||||
FROM items
|
||||
LEFT JOIN item_path
|
||||
ON items.id = item_path.item_id
|
||||
WHERE items.kind = ?;
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let editors_iter = editors_stmt.query_map(
|
||||
[SerializedItemKind::Editor.to_string()],
|
||||
|row| {
|
||||
let id: usize = row.get(0)?;
|
||||
|
||||
let buf: Vec<u8> = row.get(1)?;
|
||||
let path: PathBuf = OsStr::from_bytes(&buf).into();
|
||||
|
||||
Ok(SerializedItem::Editor(id, path))
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut terminals_stmt = tx.prepare_cached(
|
||||
r#"
|
||||
SELECT items.id
|
||||
FROM items
|
||||
WHERE items.kind = ?;
|
||||
"#,
|
||||
)?;
|
||||
let terminals_iter = terminals_stmt.query_map(
|
||||
[SerializedItemKind::Terminal.to_string()],
|
||||
|row| {
|
||||
let id: usize = row.get(0)?;
|
||||
|
||||
Ok(SerializedItem::Terminal(id))
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut search_stmt = tx.prepare_cached(
|
||||
r#"
|
||||
SELECT items.id, item_query.query
|
||||
FROM items
|
||||
LEFT JOIN item_query
|
||||
ON items.id = item_query.item_id
|
||||
WHERE items.kind = ?;
|
||||
"#,
|
||||
)?;
|
||||
let searches_iter = search_stmt.query_map(
|
||||
[SerializedItemKind::ProjectSearch.to_string()],
|
||||
|row| {
|
||||
let id: usize = row.get(0)?;
|
||||
let query = row.get(1)?;
|
||||
|
||||
Ok(SerializedItem::ProjectSearch(id, query))
|
||||
},
|
||||
)?;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let tmp =
|
||||
searches_iter.collect::<Vec<Result<SerializedItem, rusqlite::Error>>>();
|
||||
#[cfg(debug_assertions)]
|
||||
debug_assert!(tmp.len() == 0 || tmp.len() == 1);
|
||||
#[cfg(debug_assertions)]
|
||||
let searches_iter = tmp.into_iter();
|
||||
|
||||
let mut diagnostic_stmt = tx.prepare_cached(
|
||||
r#"
|
||||
SELECT items.id
|
||||
FROM items
|
||||
WHERE items.kind = ?;
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let diagnostics_iter = diagnostic_stmt.query_map(
|
||||
[SerializedItemKind::Diagnostics.to_string()],
|
||||
|row| {
|
||||
let id: usize = row.get(0)?;
|
||||
|
||||
Ok(SerializedItem::Diagnostics(id))
|
||||
},
|
||||
)?;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let tmp =
|
||||
diagnostics_iter.collect::<Vec<Result<SerializedItem, rusqlite::Error>>>();
|
||||
#[cfg(debug_assertions)]
|
||||
debug_assert!(tmp.len() == 0 || tmp.len() == 1);
|
||||
#[cfg(debug_assertions)]
|
||||
let diagnostics_iter = tmp.into_iter();
|
||||
|
||||
let res = editors_iter
|
||||
.chain(terminals_iter)
|
||||
.chain(diagnostics_iter)
|
||||
.chain(searches_iter)
|
||||
.collect::<Result<HashSet<SerializedItem>, rusqlite::Error>>()?;
|
||||
|
||||
let mut delete_stmt = tx.prepare_cached(
|
||||
r#"
|
||||
DELETE FROM items;
|
||||
DELETE FROM item_path;
|
||||
DELETE FROM item_query;
|
||||
"#,
|
||||
)?;
|
||||
|
||||
delete_stmt.execute([])?;
|
||||
|
||||
res
|
||||
};
|
||||
|
||||
tx.commit()?;
|
||||
|
||||
Ok(result)
|
||||
})
|
||||
.unwrap_or(Ok(HashSet::default()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use anyhow::Result;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_items_round_trip() -> Result<()> {
|
||||
let db = Db::open_in_memory();
|
||||
|
||||
let mut items = vec![
|
||||
SerializedItem::Editor(0, PathBuf::from("/tmp/test.txt")),
|
||||
SerializedItem::Terminal(1),
|
||||
SerializedItem::ProjectSearch(2, "Test query!".to_string()),
|
||||
SerializedItem::Diagnostics(3),
|
||||
]
|
||||
.into_iter()
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
for item in items.iter() {
|
||||
dbg!("Inserting... ");
|
||||
db.write_item(item.clone())?;
|
||||
}
|
||||
|
||||
assert_eq!(items, db.take_items()?);
|
||||
|
||||
// Check that it's empty, as expected
|
||||
assert_eq!(HashSet::default(), db.take_items()?);
|
||||
|
||||
for item in items.iter() {
|
||||
db.write_item(item.clone())?;
|
||||
}
|
||||
|
||||
items.remove(&SerializedItem::ProjectSearch(2, "Test query!".to_string()));
|
||||
db.delete_item(2)?;
|
||||
|
||||
assert_eq!(items, db.take_items()?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,82 +1,62 @@
|
||||
use anyhow::Result;
|
||||
use rusqlite::OptionalExtension;
|
||||
use sqlez_macros::sql;
|
||||
|
||||
use super::Db;
|
||||
use crate::{define_connection, query};
|
||||
|
||||
pub(crate) const KVP_M_1_UP: &str = "
|
||||
CREATE TABLE kv_store(
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
) STRICT;
|
||||
";
|
||||
define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
|
||||
&[sql!(
|
||||
CREATE TABLE IF NOT EXISTS kv_store(
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
) STRICT;
|
||||
)];
|
||||
);
|
||||
|
||||
impl Db {
|
||||
pub fn read_kvp(&self, key: &str) -> Result<Option<String>> {
|
||||
self.real()
|
||||
.map(|db| {
|
||||
let lock = db.connection.lock();
|
||||
let mut stmt = lock.prepare_cached("SELECT value FROM kv_store WHERE key = (?)")?;
|
||||
|
||||
Ok(stmt.query_row([key], |row| row.get(0)).optional()?)
|
||||
})
|
||||
.unwrap_or(Ok(None))
|
||||
impl KeyValueStore {
|
||||
query! {
|
||||
pub fn read_kvp(key: &str) -> Result<Option<String>> {
|
||||
SELECT value FROM kv_store WHERE key = (?)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_kvp(&self, key: &str, value: &str) -> Result<()> {
|
||||
self.real()
|
||||
.map(|db| {
|
||||
let lock = db.connection.lock();
|
||||
|
||||
let mut stmt = lock.prepare_cached(
|
||||
"INSERT OR REPLACE INTO kv_store(key, value) VALUES ((?), (?))",
|
||||
)?;
|
||||
|
||||
stmt.execute([key, value])?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.unwrap_or(Ok(()))
|
||||
query! {
|
||||
pub async fn write_kvp(key: String, value: String) -> Result<()> {
|
||||
INSERT OR REPLACE INTO kv_store(key, value) VALUES ((?), (?))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_kvp(&self, key: &str) -> Result<()> {
|
||||
self.real()
|
||||
.map(|db| {
|
||||
let lock = db.connection.lock();
|
||||
|
||||
let mut stmt = lock.prepare_cached("DELETE FROM kv_store WHERE key = (?)")?;
|
||||
|
||||
stmt.execute([key])?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.unwrap_or(Ok(()))
|
||||
query! {
|
||||
pub async fn delete_kvp(key: String) -> Result<()> {
|
||||
DELETE FROM kv_store WHERE key = (?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
use crate::kvp::KeyValueStore;
|
||||
|
||||
use super::*;
|
||||
#[gpui::test]
|
||||
async fn test_kvp() {
|
||||
let db = KeyValueStore(crate::open_test_db("test_kvp").await);
|
||||
|
||||
#[test]
|
||||
fn test_kvp() -> Result<()> {
|
||||
let db = Db::open_in_memory();
|
||||
assert_eq!(db.read_kvp("key-1").unwrap(), None);
|
||||
|
||||
assert_eq!(db.read_kvp("key-1")?, None);
|
||||
db.write_kvp("key-1".to_string(), "one".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(db.read_kvp("key-1").unwrap(), Some("one".to_string()));
|
||||
|
||||
db.write_kvp("key-1", "one")?;
|
||||
assert_eq!(db.read_kvp("key-1")?, Some("one".to_string()));
|
||||
db.write_kvp("key-1".to_string(), "one-2".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(db.read_kvp("key-1").unwrap(), Some("one-2".to_string()));
|
||||
|
||||
db.write_kvp("key-1", "one-2")?;
|
||||
assert_eq!(db.read_kvp("key-1")?, Some("one-2".to_string()));
|
||||
db.write_kvp("key-2".to_string(), "two".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(db.read_kvp("key-2").unwrap(), Some("two".to_string()));
|
||||
|
||||
db.write_kvp("key-2", "two")?;
|
||||
assert_eq!(db.read_kvp("key-2")?, Some("two".to_string()));
|
||||
|
||||
db.delete_kvp("key-1")?;
|
||||
assert_eq!(db.read_kvp("key-1")?, None);
|
||||
|
||||
Ok(())
|
||||
db.delete_kvp("key-1".to_string()).await.unwrap();
|
||||
assert_eq!(db.read_kvp("key-1").unwrap(), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
use rusqlite_migration::{Migrations, M};
|
||||
|
||||
// use crate::items::ITEMS_M_1;
|
||||
use crate::kvp::KVP_M_1_UP;
|
||||
|
||||
// This must be ordered by development time! Only ever add new migrations to the end!!
|
||||
// Bad things will probably happen if you don't monotonically edit this vec!!!!
|
||||
// And no re-ordering ever!!!!!!!!!! The results of these migrations are on the user's
|
||||
// file system and so everything we do here is locked in _f_o_r_e_v_e_r_.
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref MIGRATIONS: Migrations<'static> = Migrations::new(vec![
|
||||
M::up(KVP_M_1_UP),
|
||||
// M::up(ITEMS_M_1),
|
||||
]);
|
||||
}
|
||||
314
crates/db/src/query.rs
Normal file
314
crates/db/src/query.rs
Normal file
@@ -0,0 +1,314 @@
|
||||
#[macro_export]
|
||||
macro_rules! query {
|
||||
($vis:vis fn $id:ident() -> Result<()> { $($sql:tt)+ }) => {
|
||||
$vis fn $id(&self) -> $crate::anyhow::Result<()> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
self.exec(sql_stmt)?().context(::std::format!(
|
||||
"Error in {}, exec failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt,
|
||||
))
|
||||
}
|
||||
};
|
||||
($vis:vis async fn $id:ident() -> Result<()> { $($sql:tt)+ }) => {
|
||||
$vis async fn $id(&self) -> $crate::anyhow::Result<()> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
self.write(|connection| {
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
connection.exec(sql_stmt)?().context(::std::format!(
|
||||
"Error in {}, exec failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}).await
|
||||
}
|
||||
};
|
||||
($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<()> { $($sql:tt)+ }) => {
|
||||
$vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<()> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
self.exec_bound::<($($arg_type),+)>(sql_stmt)?(($($arg),+))
|
||||
.context(::std::format!(
|
||||
"Error in {}, exec_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}
|
||||
};
|
||||
($vis:vis async fn $id:ident($arg:ident: $arg_type:ty) -> Result<()> { $($sql:tt)+ }) => {
|
||||
$vis async fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result<()> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
self.write(move |connection| {
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
connection.exec_bound::<$arg_type>(sql_stmt)?($arg)
|
||||
.context(::std::format!(
|
||||
"Error in {}, exec_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}).await
|
||||
}
|
||||
};
|
||||
($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<()> { $($sql:tt)+ }) => {
|
||||
$vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<()> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
self.write(move |connection| {
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
connection.exec_bound::<($($arg_type),+)>(sql_stmt)?(($($arg),+))
|
||||
.context(::std::format!(
|
||||
"Error in {}, exec_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}).await
|
||||
}
|
||||
};
|
||||
($vis:vis fn $id:ident() -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
|
||||
$vis fn $id(&self) -> $crate::anyhow::Result<Vec<$return_type>> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
self.select::<$return_type>(sql_stmt)?(())
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}
|
||||
};
|
||||
($vis:vis async fn $id:ident() -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
|
||||
pub async fn $id(&self) -> $crate::anyhow::Result<Vec<$return_type>> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
self.write(|connection| {
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
connection.select::<$return_type>(sql_stmt)?(())
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}).await
|
||||
}
|
||||
};
|
||||
($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
|
||||
$vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Vec<$return_type>> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
self.select_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
|
||||
.context(::std::format!(
|
||||
"Error in {}, exec_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}
|
||||
};
|
||||
($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
|
||||
$vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Vec<$return_type>> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
self.write(|connection| {
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
connection.select_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
|
||||
.context(::std::format!(
|
||||
"Error in {}, exec_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}).await
|
||||
}
|
||||
};
|
||||
($vis:vis fn $id:ident() -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
|
||||
$vis fn $id(&self) -> $crate::anyhow::Result<Option<$return_type>> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
self.select_row::<$return_type>(sql_stmt)?()
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}
|
||||
};
|
||||
($vis:vis async fn $id:ident() -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
|
||||
$vis async fn $id(&self) -> $crate::anyhow::Result<Option<$return_type>> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
self.write(|connection| {
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
connection.select_row::<$return_type>(sql_stmt)?()
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}).await
|
||||
}
|
||||
};
|
||||
($vis:vis fn $id:ident($arg:ident: $arg_type:ty) -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
|
||||
$vis fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result<Option<$return_type>> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
self.select_row_bound::<$arg_type, $return_type>(sql_stmt)?($arg)
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
|
||||
}
|
||||
};
|
||||
($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
|
||||
$vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Option<$return_type>> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
self.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
|
||||
}
|
||||
};
|
||||
($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
|
||||
$vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Option<$return_type>> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
|
||||
self.write(move |connection| {
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
connection.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}).await
|
||||
}
|
||||
};
|
||||
($vis:vis fn $id:ident() -> Result<$return_type:ty> { $($sql:tt)+ }) => {
|
||||
$vis fn $id(&self) -> $crate::anyhow::Result<$return_type> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
self.select_row::<$return_type>(indoc! { $sql })?()
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))?
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound expected single row result but found none for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}
|
||||
};
|
||||
($vis:vis async fn $id:ident() -> Result<$return_type:ty> { $($sql:tt)+ }) => {
|
||||
$vis async fn $id(&self) -> $crate::anyhow::Result<$return_type> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
self.write(|connection| {
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
connection.select_row::<$return_type>(sql_stmt)?()
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))?
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound expected single row result but found none for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}).await
|
||||
}
|
||||
};
|
||||
($vis:vis fn $id:ident($arg:ident: $arg_type:ty) -> Result<$return_type:ty> { $($sql:tt)+ }) => {
|
||||
pub fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result<$return_type> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
self.select_row_bound::<$arg_type, $return_type>(sql_stmt)?($arg)
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))?
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound expected single row result but found none for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}
|
||||
};
|
||||
($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<$return_type:ty> { $($sql:tt)+ }) => {
|
||||
$vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<$return_type> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
self.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))?
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound expected single row result but found none for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}
|
||||
};
|
||||
($vis:vis fn async $id:ident($($arg:ident: $arg_type:ty),+) -> Result<$return_type:ty> { $($sql:tt)+ }) => {
|
||||
$vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<$return_type> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
|
||||
self.write(|connection| {
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
connection.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))?
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound expected single row result but found none for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}).await
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -5,8 +5,9 @@ use collections::{BTreeMap, HashSet};
|
||||
use editor::{
|
||||
diagnostic_block_renderer,
|
||||
display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock},
|
||||
highlight_diagnostic_message, Autoscroll, Editor, ExcerptId, ExcerptRange, MultiBuffer,
|
||||
ToOffset,
|
||||
highlight_diagnostic_message,
|
||||
scroll::autoscroll::Autoscroll,
|
||||
Editor, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
|
||||
};
|
||||
use gpui::{
|
||||
actions, elements::*, fonts::TextStyle, impl_internal_actions, serde_json, AnyViewHandle,
|
||||
@@ -29,7 +30,10 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use util::TryFutureExt;
|
||||
use workspace::{ItemHandle as _, ItemNavHistory, Workspace};
|
||||
use workspace::{
|
||||
item::{Item, ItemEvent, ItemHandle},
|
||||
ItemNavHistory, Pane, Workspace,
|
||||
};
|
||||
|
||||
actions!(diagnostics, [Deploy]);
|
||||
|
||||
@@ -322,7 +326,7 @@ impl ProjectDiagnosticsEditor {
|
||||
);
|
||||
let excerpt_id = excerpts
|
||||
.insert_excerpts_after(
|
||||
&prev_excerpt_id,
|
||||
prev_excerpt_id,
|
||||
buffer.clone(),
|
||||
[ExcerptRange {
|
||||
context: excerpt_start..excerpt_end,
|
||||
@@ -384,7 +388,7 @@ impl ProjectDiagnosticsEditor {
|
||||
|
||||
groups_to_add.push(group_state);
|
||||
} else if let Some((group_ix, group_state)) = to_remove {
|
||||
excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx);
|
||||
excerpts.remove_excerpts(group_state.excerpts.iter().copied(), excerpts_cx);
|
||||
group_ixs_to_remove.push(group_ix);
|
||||
blocks_to_remove.extend(group_state.blocks.iter().copied());
|
||||
} else if let Some((_, group)) = to_keep {
|
||||
@@ -452,15 +456,20 @@ impl ProjectDiagnosticsEditor {
|
||||
} else {
|
||||
groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
|
||||
new_excerpt_ids_by_selection_id =
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| s.refresh());
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.refresh());
|
||||
selections = editor.selections.all::<usize>(cx);
|
||||
}
|
||||
|
||||
// If any selection has lost its position, move it to start of the next primary diagnostic.
|
||||
let snapshot = editor.snapshot(cx);
|
||||
for selection in &mut selections {
|
||||
if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
|
||||
let group_ix = match groups.binary_search_by(|probe| {
|
||||
probe.excerpts.last().unwrap().cmp(new_excerpt_id)
|
||||
probe
|
||||
.excerpts
|
||||
.last()
|
||||
.unwrap()
|
||||
.cmp(new_excerpt_id, &snapshot.buffer_snapshot)
|
||||
}) {
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
@@ -498,7 +507,7 @@ impl ProjectDiagnosticsEditor {
|
||||
}
|
||||
}
|
||||
|
||||
impl workspace::Item for ProjectDiagnosticsEditor {
|
||||
impl Item for ProjectDiagnosticsEditor {
|
||||
fn tab_content(
|
||||
&self,
|
||||
_detail: Option<usize>,
|
||||
@@ -566,7 +575,7 @@ impl workspace::Item for ProjectDiagnosticsEditor {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
|
||||
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
|
||||
Editor::to_item_events(event)
|
||||
}
|
||||
|
||||
@@ -576,7 +585,11 @@ impl workspace::Item for ProjectDiagnosticsEditor {
|
||||
});
|
||||
}
|
||||
|
||||
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: workspace::WorkspaceId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -605,6 +618,20 @@ impl workspace::Item for ProjectDiagnosticsEditor {
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, |editor, cx| editor.deactivated(cx));
|
||||
}
|
||||
|
||||
fn serialized_item_kind() -> Option<&'static str> {
|
||||
Some("diagnostics")
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
project: ModelHandle<Project>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
_workspace_id: workspace::WorkspaceId,
|
||||
_item_id: workspace::ItemId,
|
||||
cx: &mut ViewContext<Pane>,
|
||||
) -> Task<Result<ViewHandle<Self>>> {
|
||||
Task::ready(Ok(cx.add_view(|cx| Self::new(project, workspace, cx))))
|
||||
}
|
||||
}
|
||||
|
||||
fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
||||
@@ -738,7 +765,7 @@ mod tests {
|
||||
DisplayPoint,
|
||||
};
|
||||
use gpui::TestAppContext;
|
||||
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
|
||||
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
|
||||
use serde_json::json;
|
||||
use unindent::Unindent as _;
|
||||
use workspace::AppState;
|
||||
@@ -776,8 +803,15 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project.clone(),
|
||||
|_, _| unimplemented!(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// Create some diagnostics
|
||||
project.update(cx, |project, cx| {
|
||||
@@ -788,7 +822,7 @@ mod tests {
|
||||
None,
|
||||
vec![
|
||||
DiagnosticEntry {
|
||||
range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
|
||||
range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
|
||||
diagnostic: Diagnostic {
|
||||
message:
|
||||
"move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
|
||||
@@ -801,7 +835,7 @@ mod tests {
|
||||
},
|
||||
},
|
||||
DiagnosticEntry {
|
||||
range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
|
||||
range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
|
||||
diagnostic: Diagnostic {
|
||||
message:
|
||||
"move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
|
||||
@@ -814,7 +848,7 @@ mod tests {
|
||||
},
|
||||
},
|
||||
DiagnosticEntry {
|
||||
range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
|
||||
range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
|
||||
diagnostic: Diagnostic {
|
||||
message: "value moved here".to_string(),
|
||||
severity: DiagnosticSeverity::INFORMATION,
|
||||
@@ -825,7 +859,7 @@ mod tests {
|
||||
},
|
||||
},
|
||||
DiagnosticEntry {
|
||||
range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
|
||||
range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
|
||||
diagnostic: Diagnostic {
|
||||
message: "value moved here".to_string(),
|
||||
severity: DiagnosticSeverity::INFORMATION,
|
||||
@@ -836,7 +870,7 @@ mod tests {
|
||||
},
|
||||
},
|
||||
DiagnosticEntry {
|
||||
range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
|
||||
range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
|
||||
diagnostic: Diagnostic {
|
||||
message: "use of moved value\nvalue used here after move".to_string(),
|
||||
severity: DiagnosticSeverity::ERROR,
|
||||
@@ -847,7 +881,7 @@ mod tests {
|
||||
},
|
||||
},
|
||||
DiagnosticEntry {
|
||||
range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
|
||||
range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
|
||||
diagnostic: Diagnostic {
|
||||
message: "use of moved value\nvalue used here after move".to_string(),
|
||||
severity: DiagnosticSeverity::ERROR,
|
||||
@@ -939,7 +973,7 @@ mod tests {
|
||||
PathBuf::from("/test/consts.rs"),
|
||||
None,
|
||||
vec![DiagnosticEntry {
|
||||
range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
|
||||
range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
|
||||
diagnostic: Diagnostic {
|
||||
message: "mismatched types\nexpected `usize`, found `char`".to_string(),
|
||||
severity: DiagnosticSeverity::ERROR,
|
||||
@@ -1040,7 +1074,8 @@ mod tests {
|
||||
None,
|
||||
vec![
|
||||
DiagnosticEntry {
|
||||
range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
|
||||
range: Unclipped(PointUtf16::new(0, 15))
|
||||
..Unclipped(PointUtf16::new(0, 15)),
|
||||
diagnostic: Diagnostic {
|
||||
message: "mismatched types\nexpected `usize`, found `char`"
|
||||
.to_string(),
|
||||
@@ -1052,7 +1087,8 @@ mod tests {
|
||||
},
|
||||
},
|
||||
DiagnosticEntry {
|
||||
range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
|
||||
range: Unclipped(PointUtf16::new(1, 15))
|
||||
..Unclipped(PointUtf16::new(1, 15)),
|
||||
diagnostic: Diagnostic {
|
||||
message: "unresolved name `c`".to_string(),
|
||||
severity: DiagnosticSeverity::ERROR,
|
||||
|
||||
@@ -7,7 +7,7 @@ use gpui::{
|
||||
use language::Diagnostic;
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use workspace::StatusItemView;
|
||||
use workspace::{item::ItemHandle, StatusItemView};
|
||||
|
||||
pub struct DiagnosticIndicator {
|
||||
summary: project::DiagnosticSummary,
|
||||
@@ -219,7 +219,7 @@ impl View for DiagnosticIndicator {
|
||||
impl StatusItemView for DiagnosticIndicator {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn workspace::ItemHandle>,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
|
||||
|
||||
@@ -12,4 +12,4 @@ collections = { path = "../collections" }
|
||||
gpui = { path = "../gpui" }
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
|
||||
@@ -2,29 +2,68 @@ use std::{any::Any, rc::Rc};
|
||||
|
||||
use collections::HashSet;
|
||||
use gpui::{
|
||||
elements::{MouseEventHandler, Overlay},
|
||||
geometry::vector::Vector2F,
|
||||
scene::MouseDrag,
|
||||
elements::{Empty, MouseEventHandler, Overlay},
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
scene::{MouseDown, MouseDrag},
|
||||
CursorStyle, Element, ElementBox, EventContext, MouseButton, MutableAppContext, RenderContext,
|
||||
View, WeakViewHandle,
|
||||
};
|
||||
|
||||
struct State<V: View> {
|
||||
window_id: usize,
|
||||
position: Vector2F,
|
||||
region_offset: Vector2F,
|
||||
payload: Rc<dyn Any + 'static>,
|
||||
render: Rc<dyn Fn(Rc<dyn Any>, &mut RenderContext<V>) -> ElementBox>,
|
||||
const DEAD_ZONE: f32 = 4.;
|
||||
|
||||
enum State<V: View> {
|
||||
Down {
|
||||
region_offset: Vector2F,
|
||||
region: RectF,
|
||||
},
|
||||
DeadZone {
|
||||
region_offset: Vector2F,
|
||||
region: RectF,
|
||||
},
|
||||
Dragging {
|
||||
window_id: usize,
|
||||
position: Vector2F,
|
||||
region_offset: Vector2F,
|
||||
region: RectF,
|
||||
payload: Rc<dyn Any + 'static>,
|
||||
render: Rc<dyn Fn(Rc<dyn Any>, &mut RenderContext<V>) -> ElementBox>,
|
||||
},
|
||||
Canceled,
|
||||
}
|
||||
|
||||
impl<V: View> Clone for State<V> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
window_id: self.window_id.clone(),
|
||||
position: self.position.clone(),
|
||||
region_offset: self.region_offset.clone(),
|
||||
payload: self.payload.clone(),
|
||||
render: self.render.clone(),
|
||||
match self {
|
||||
&State::Down {
|
||||
region_offset,
|
||||
region,
|
||||
} => State::Down {
|
||||
region_offset,
|
||||
region,
|
||||
},
|
||||
&State::DeadZone {
|
||||
region_offset,
|
||||
region,
|
||||
} => State::DeadZone {
|
||||
region_offset,
|
||||
region,
|
||||
},
|
||||
State::Dragging {
|
||||
window_id,
|
||||
position,
|
||||
region_offset,
|
||||
region,
|
||||
payload,
|
||||
render,
|
||||
} => Self::Dragging {
|
||||
window_id: window_id.clone(),
|
||||
position: position.clone(),
|
||||
region_offset: region_offset.clone(),
|
||||
region: region.clone(),
|
||||
payload: payload.clone(),
|
||||
render: render.clone(),
|
||||
},
|
||||
State::Canceled => State::Canceled,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,24 +88,36 @@ impl<V: View> DragAndDrop<V> {
|
||||
}
|
||||
|
||||
pub fn currently_dragged<T: Any>(&self, window_id: usize) -> Option<(Vector2F, Rc<T>)> {
|
||||
self.currently_dragged.as_ref().and_then(
|
||||
|State {
|
||||
position,
|
||||
payload,
|
||||
window_id: window_dragged_from,
|
||||
..
|
||||
}| {
|
||||
self.currently_dragged.as_ref().and_then(|state| {
|
||||
if let State::Dragging {
|
||||
position,
|
||||
payload,
|
||||
window_id: window_dragged_from,
|
||||
..
|
||||
} = state
|
||||
{
|
||||
if &window_id != window_dragged_from {
|
||||
return None;
|
||||
}
|
||||
|
||||
payload
|
||||
.clone()
|
||||
.downcast::<T>()
|
||||
.ok()
|
||||
.is::<T>()
|
||||
.then(|| payload.clone().downcast::<T>().ok())
|
||||
.flatten()
|
||||
.map(|payload| (position.clone(), payload))
|
||||
},
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn drag_started(event: MouseDown, cx: &mut EventContext) {
|
||||
cx.update_global(|this: &mut Self, _| {
|
||||
this.currently_dragged = Some(State::Down {
|
||||
region_offset: event.position - event.region.origin(),
|
||||
region: event.region,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn dragging<T: Any>(
|
||||
@@ -76,75 +127,155 @@ impl<V: View> DragAndDrop<V> {
|
||||
render: Rc<impl 'static + Fn(&T, &mut RenderContext<V>) -> ElementBox>,
|
||||
) {
|
||||
let window_id = cx.window_id();
|
||||
cx.update_global::<Self, _, _>(|this, cx| {
|
||||
let region_offset = if let Some(previous_state) = this.currently_dragged.as_ref() {
|
||||
previous_state.region_offset
|
||||
} else {
|
||||
event.region.origin() - event.prev_mouse_position
|
||||
};
|
||||
|
||||
this.currently_dragged = Some(State {
|
||||
window_id,
|
||||
region_offset,
|
||||
position: event.position,
|
||||
payload,
|
||||
render: Rc::new(move |payload, cx| {
|
||||
render(payload.downcast_ref::<T>().unwrap(), cx)
|
||||
}),
|
||||
});
|
||||
|
||||
cx.update_global(|this: &mut Self, cx| {
|
||||
this.notify_containers_for_window(window_id, cx);
|
||||
|
||||
match this.currently_dragged.as_ref() {
|
||||
Some(&State::Down {
|
||||
region_offset,
|
||||
region,
|
||||
})
|
||||
| Some(&State::DeadZone {
|
||||
region_offset,
|
||||
region,
|
||||
}) => {
|
||||
if (event.position - (region.origin() + region_offset)).length() > DEAD_ZONE {
|
||||
this.currently_dragged = Some(State::Dragging {
|
||||
window_id,
|
||||
region_offset,
|
||||
region,
|
||||
position: event.position,
|
||||
payload,
|
||||
render: Rc::new(move |payload, cx| {
|
||||
render(payload.downcast_ref::<T>().unwrap(), cx)
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
this.currently_dragged = Some(State::DeadZone {
|
||||
region_offset,
|
||||
region,
|
||||
})
|
||||
}
|
||||
}
|
||||
Some(&State::Dragging {
|
||||
region_offset,
|
||||
region,
|
||||
..
|
||||
}) => {
|
||||
this.currently_dragged = Some(State::Dragging {
|
||||
window_id,
|
||||
region_offset,
|
||||
region,
|
||||
position: event.position,
|
||||
payload,
|
||||
render: Rc::new(move |payload, cx| {
|
||||
render(payload.downcast_ref::<T>().unwrap(), cx)
|
||||
}),
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn render(cx: &mut RenderContext<V>) -> Option<ElementBox> {
|
||||
let currently_dragged = cx.global::<Self>().currently_dragged.clone();
|
||||
enum DraggedElementHandler {}
|
||||
cx.global::<Self>()
|
||||
.currently_dragged
|
||||
.clone()
|
||||
.and_then(|state| {
|
||||
match state {
|
||||
State::Down { .. } => None,
|
||||
State::DeadZone { .. } => None,
|
||||
State::Dragging {
|
||||
window_id,
|
||||
region_offset,
|
||||
position,
|
||||
region,
|
||||
payload,
|
||||
render,
|
||||
} => {
|
||||
if cx.window_id() != window_id {
|
||||
return None;
|
||||
}
|
||||
|
||||
currently_dragged.and_then(
|
||||
|State {
|
||||
window_id,
|
||||
region_offset,
|
||||
position,
|
||||
payload,
|
||||
render,
|
||||
}| {
|
||||
if cx.window_id() != window_id {
|
||||
return None;
|
||||
}
|
||||
let position = position - region_offset;
|
||||
Some(
|
||||
Overlay::new(
|
||||
MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, cx| {
|
||||
render(payload, cx)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::Arrow)
|
||||
.on_up(MouseButton::Left, |_, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.update_global::<Self, _, _>(|this, cx| {
|
||||
this.finish_dragging(cx)
|
||||
});
|
||||
});
|
||||
cx.propagate_event();
|
||||
})
|
||||
.on_up_out(MouseButton::Left, |_, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.update_global::<Self, _, _>(|this, cx| {
|
||||
this.finish_dragging(cx)
|
||||
});
|
||||
});
|
||||
})
|
||||
// Don't block hover events or invalidations
|
||||
.with_hoverable(false)
|
||||
.constrained()
|
||||
.with_width(region.width())
|
||||
.with_height(region.height())
|
||||
.boxed(),
|
||||
)
|
||||
.with_anchor_position(position)
|
||||
.boxed(),
|
||||
)
|
||||
}
|
||||
|
||||
let position = position + region_offset;
|
||||
|
||||
enum DraggedElementHandler {}
|
||||
Some(
|
||||
Overlay::new(
|
||||
MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, cx| {
|
||||
render(payload, cx)
|
||||
State::Canceled => Some(
|
||||
MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, _| {
|
||||
Empty::new()
|
||||
.constrained()
|
||||
.with_width(0.)
|
||||
.with_height(0.)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::Arrow)
|
||||
.on_up(MouseButton::Left, |_, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
|
||||
cx.update_global::<Self, _, _>(|this, _| {
|
||||
this.currently_dragged = None;
|
||||
});
|
||||
});
|
||||
cx.propagate_event();
|
||||
})
|
||||
.on_up_out(MouseButton::Left, |_, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
|
||||
cx.update_global::<Self, _, _>(|this, _| {
|
||||
this.currently_dragged = None;
|
||||
});
|
||||
});
|
||||
})
|
||||
// Don't block hover events or invalidations
|
||||
.with_hoverable(false)
|
||||
.boxed(),
|
||||
)
|
||||
.with_anchor_position(position)
|
||||
.boxed(),
|
||||
)
|
||||
},
|
||||
)
|
||||
),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn stop_dragging(&mut self, cx: &mut MutableAppContext) {
|
||||
if let Some(State { window_id, .. }) = self.currently_dragged.take() {
|
||||
pub fn cancel_dragging<P: Any>(&mut self, cx: &mut MutableAppContext) {
|
||||
if let Some(State::Dragging {
|
||||
payload, window_id, ..
|
||||
}) = &self.currently_dragged
|
||||
{
|
||||
if payload.is::<P>() {
|
||||
let window_id = *window_id;
|
||||
self.currently_dragged = Some(State::Canceled);
|
||||
self.notify_containers_for_window(window_id, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_dragging(&mut self, cx: &mut MutableAppContext) {
|
||||
if let Some(State::Dragging { window_id, .. }) = self.currently_dragged.take() {
|
||||
self.notify_containers_for_window(window_id, cx);
|
||||
}
|
||||
}
|
||||
@@ -184,7 +315,11 @@ impl<Tag> Draggable for MouseEventHandler<Tag> {
|
||||
{
|
||||
let payload = Rc::new(payload);
|
||||
let render = Rc::new(render);
|
||||
self.on_drag(MouseButton::Left, move |e, cx| {
|
||||
self.on_down(MouseButton::Left, move |e, cx| {
|
||||
cx.propagate_event();
|
||||
DragAndDrop::<V>::drag_started(e, cx);
|
||||
})
|
||||
.on_drag(MouseButton::Left, move |e, cx| {
|
||||
let payload = payload.clone();
|
||||
let render = render.clone();
|
||||
DragAndDrop::<V>::dragging(e, payload, cx, render)
|
||||
|
||||
@@ -23,6 +23,7 @@ test-support = [
|
||||
drag_and_drop = { path = "../drag_and_drop" }
|
||||
text = { path = "../text" }
|
||||
clock = { path = "../clock" }
|
||||
db = { path = "../db" }
|
||||
collections = { path = "../collections" }
|
||||
context_menu = { path = "../context_menu" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
@@ -37,6 +38,7 @@ snippet = { path = "../snippet" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
sqlez = { path = "../sqlez" }
|
||||
workspace = { path = "../workspace" }
|
||||
aho-corasick = "0.7"
|
||||
anyhow = "1.0"
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::{
|
||||
wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
|
||||
TextHighlights,
|
||||
};
|
||||
use crate::{Anchor, ExcerptRange, ToPoint as _};
|
||||
use crate::{Anchor, ExcerptId, ExcerptRange, ToPoint as _};
|
||||
use collections::{Bound, HashMap, HashSet};
|
||||
use gpui::{ElementBox, RenderContext};
|
||||
use language::{BufferSnapshot, Chunk, Patch, Point};
|
||||
@@ -107,7 +107,7 @@ struct Transform {
|
||||
pub enum TransformBlock {
|
||||
Custom(Arc<Block>),
|
||||
ExcerptHeader {
|
||||
key: usize,
|
||||
id: ExcerptId,
|
||||
buffer: BufferSnapshot,
|
||||
range: ExcerptRange<text::Anchor>,
|
||||
height: u8,
|
||||
@@ -371,7 +371,7 @@ impl BlockMap {
|
||||
.make_wrap_point(Point::new(excerpt_boundary.row, 0), Bias::Left)
|
||||
.row(),
|
||||
TransformBlock::ExcerptHeader {
|
||||
key: excerpt_boundary.key,
|
||||
id: excerpt_boundary.id,
|
||||
buffer: excerpt_boundary.buffer,
|
||||
range: excerpt_boundary.range,
|
||||
height: if excerpt_boundary.starts_new_buffer {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@ use crate::test::{
|
||||
};
|
||||
use gpui::{
|
||||
executor::Deterministic,
|
||||
geometry::rect::RectF,
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
platform::{WindowBounds, WindowOptions},
|
||||
};
|
||||
use language::{FakeLspAdapter, LanguageConfig, LanguageRegistry, Point};
|
||||
@@ -22,7 +22,10 @@ use util::{
|
||||
assert_set_eq,
|
||||
test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
|
||||
};
|
||||
use workspace::{FollowableItem, ItemHandle, NavigationEntry, Pane};
|
||||
use workspace::{
|
||||
item::{FollowableItem, ItemHandle},
|
||||
NavigationEntry, Pane,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_edit_events(cx: &mut MutableAppContext) {
|
||||
@@ -475,7 +478,7 @@ fn test_clone(cx: &mut gpui::MutableAppContext) {
|
||||
fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
cx.set_global(DragAndDrop::<Workspace>::default());
|
||||
use workspace::Item;
|
||||
use workspace::item::Item;
|
||||
let (_, pane) = cx.add_window(Default::default(), |cx| Pane::new(None, cx));
|
||||
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
|
||||
|
||||
@@ -541,31 +544,30 @@ fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
|
||||
|
||||
// Set scroll position to check later
|
||||
editor.set_scroll_position(Vector2F::new(5.5, 5.5), cx);
|
||||
let original_scroll_position = editor.scroll_position;
|
||||
let original_scroll_top_anchor = editor.scroll_top_anchor.clone();
|
||||
let original_scroll_position = editor.scroll_manager.anchor();
|
||||
|
||||
// Jump to the end of the document and adjust scroll
|
||||
editor.move_to_end(&MoveToEnd, cx);
|
||||
editor.set_scroll_position(Vector2F::new(-2.5, -0.5), cx);
|
||||
assert_ne!(editor.scroll_position, original_scroll_position);
|
||||
assert_ne!(editor.scroll_top_anchor, original_scroll_top_anchor);
|
||||
assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
|
||||
|
||||
let nav_entry = pop_history(&mut editor, cx).unwrap();
|
||||
editor.navigate(nav_entry.data.unwrap(), cx);
|
||||
assert_eq!(editor.scroll_position, original_scroll_position);
|
||||
assert_eq!(editor.scroll_top_anchor, original_scroll_top_anchor);
|
||||
assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
|
||||
|
||||
// Ensure we don't panic when navigation data contains invalid anchors *and* points.
|
||||
let mut invalid_anchor = editor.scroll_top_anchor.clone();
|
||||
let mut invalid_anchor = editor.scroll_manager.anchor().top_anchor;
|
||||
invalid_anchor.text_anchor.buffer_id = Some(999);
|
||||
let invalid_point = Point::new(9999, 0);
|
||||
editor.navigate(
|
||||
Box::new(NavigationData {
|
||||
cursor_anchor: invalid_anchor.clone(),
|
||||
cursor_anchor: invalid_anchor,
|
||||
cursor_position: invalid_point,
|
||||
scroll_top_anchor: invalid_anchor,
|
||||
scroll_anchor: ScrollAnchor {
|
||||
top_anchor: invalid_anchor,
|
||||
offset: Default::default(),
|
||||
},
|
||||
scroll_top_row: invalid_point.row,
|
||||
scroll_position: Default::default(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
@@ -4146,14 +4148,26 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
handle_resolve_completion_request(
|
||||
&mut cx,
|
||||
Some((
|
||||
indoc! {"
|
||||
one.second_completion
|
||||
two
|
||||
threeˇ
|
||||
"},
|
||||
"\nadditional edit",
|
||||
)),
|
||||
Some(vec![
|
||||
(
|
||||
//This overlaps with the primary completion edit which is
|
||||
//misbehavior from the LSP spec, test that we filter it out
|
||||
indoc! {"
|
||||
one.second_ˇcompletion
|
||||
two
|
||||
threeˇ
|
||||
"},
|
||||
"overlapping aditional edit",
|
||||
),
|
||||
(
|
||||
indoc! {"
|
||||
one.second_completion
|
||||
two
|
||||
threeˇ
|
||||
"},
|
||||
"\nadditional edit",
|
||||
),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
apply_additional_edits.await.unwrap();
|
||||
@@ -4303,19 +4317,24 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
async fn handle_resolve_completion_request<'a>(
|
||||
cx: &mut EditorLspTestContext<'a>,
|
||||
edit: Option<(&'static str, &'static str)>,
|
||||
edits: Option<Vec<(&'static str, &'static str)>>,
|
||||
) {
|
||||
let edit = edit.map(|(marked_string, new_text)| {
|
||||
let (_, marked_ranges) = marked_text_ranges(marked_string, false);
|
||||
let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
|
||||
vec![lsp::TextEdit::new(replace_range, new_text.to_string())]
|
||||
let edits = edits.map(|edits| {
|
||||
edits
|
||||
.iter()
|
||||
.map(|(marked_string, new_text)| {
|
||||
let (_, marked_ranges) = marked_text_ranges(marked_string, false);
|
||||
let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
|
||||
lsp::TextEdit::new(replace_range, new_text.to_string())
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
|
||||
let edit = edit.clone();
|
||||
let edits = edits.clone();
|
||||
async move {
|
||||
Ok(lsp::CompletionItem {
|
||||
additional_text_edits: edit,
|
||||
additional_text_edits: edits,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
@@ -4701,9 +4720,7 @@ fn test_refresh_selections(cx: &mut gpui::MutableAppContext) {
|
||||
|
||||
// Refreshing selections is a no-op when excerpts haven't changed.
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.refresh();
|
||||
});
|
||||
editor.change_selections(None, cx, |s| s.refresh());
|
||||
assert_eq!(
|
||||
editor.selections.ranges(cx),
|
||||
[
|
||||
@@ -4714,7 +4731,7 @@ fn test_refresh_selections(cx: &mut gpui::MutableAppContext) {
|
||||
});
|
||||
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.remove_excerpts([&excerpt1_id.unwrap()], cx);
|
||||
multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
|
||||
});
|
||||
editor.update(cx, |editor, cx| {
|
||||
// Removing an excerpt causes the first selection to become degenerate.
|
||||
@@ -4728,9 +4745,7 @@ fn test_refresh_selections(cx: &mut gpui::MutableAppContext) {
|
||||
|
||||
// Refreshing selections will relocate the first selection to the original buffer
|
||||
// location.
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.refresh();
|
||||
});
|
||||
editor.change_selections(None, cx, |s| s.refresh());
|
||||
assert_eq!(
|
||||
editor.selections.ranges(cx),
|
||||
[
|
||||
@@ -4784,7 +4799,7 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut gpui::MutableAppC
|
||||
});
|
||||
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.remove_excerpts([&excerpt1_id.unwrap()], cx);
|
||||
multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
|
||||
});
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
@@ -4793,9 +4808,7 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut gpui::MutableAppC
|
||||
);
|
||||
|
||||
// Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.refresh();
|
||||
});
|
||||
editor.change_selections(None, cx, |s| s.refresh());
|
||||
assert_eq!(
|
||||
editor.selections.ranges(cx),
|
||||
[Point::new(0, 3)..Point::new(0, 3)]
|
||||
@@ -4970,9 +4983,11 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
|
||||
|cx| build_editor(buffer.clone(), cx),
|
||||
);
|
||||
|
||||
let is_still_following = Rc::new(RefCell::new(true));
|
||||
let pending_update = Rc::new(RefCell::new(None));
|
||||
follower.update(cx, {
|
||||
let update = pending_update.clone();
|
||||
let is_still_following = is_still_following.clone();
|
||||
|_, cx| {
|
||||
cx.subscribe(&leader, move |_, leader, event, cx| {
|
||||
leader
|
||||
@@ -4980,6 +4995,13 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
|
||||
.add_event_to_update_proto(event, &mut *update.borrow_mut(), cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.subscribe(&follower, move |_, _, event, cx| {
|
||||
if Editor::should_unfollow_on_event(event, cx) {
|
||||
*is_still_following.borrow_mut() = false;
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4993,6 +5015,7 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
|
||||
.unwrap();
|
||||
});
|
||||
assert_eq!(follower.read(cx).selections.ranges(cx), vec![1..1]);
|
||||
assert_eq!(*is_still_following.borrow(), true);
|
||||
|
||||
// Update the scroll position only
|
||||
leader.update(cx, |leader, cx| {
|
||||
@@ -5007,11 +5030,12 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
|
||||
follower.update(cx, |follower, cx| follower.scroll_position(cx)),
|
||||
vec2f(1.5, 3.5)
|
||||
);
|
||||
assert_eq!(*is_still_following.borrow(), true);
|
||||
|
||||
// Update the selections and scroll position
|
||||
leader.update(cx, |leader, cx| {
|
||||
leader.change_selections(None, cx, |s| s.select_ranges([0..0]));
|
||||
leader.request_autoscroll(Autoscroll::Newest, cx);
|
||||
leader.request_autoscroll(Autoscroll::newest(), cx);
|
||||
leader.set_scroll_position(vec2f(1.5, 3.5), cx);
|
||||
});
|
||||
follower.update(cx, |follower, cx| {
|
||||
@@ -5020,9 +5044,10 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
|
||||
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
|
||||
.unwrap();
|
||||
assert_eq!(follower.scroll_position(cx), initial_scroll_position);
|
||||
assert!(follower.autoscroll_request.is_some());
|
||||
assert!(follower.scroll_manager.has_autoscroll_request());
|
||||
});
|
||||
assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..0]);
|
||||
assert_eq!(*is_still_following.borrow(), true);
|
||||
|
||||
// Creating a pending selection that precedes another selection
|
||||
leader.update(cx, |leader, cx| {
|
||||
@@ -5035,6 +5060,7 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
|
||||
.unwrap();
|
||||
});
|
||||
assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..0, 1..1]);
|
||||
assert_eq!(*is_still_following.borrow(), true);
|
||||
|
||||
// Extend the pending selection so that it surrounds another selection
|
||||
leader.update(cx, |leader, cx| {
|
||||
@@ -5046,6 +5072,19 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
|
||||
.unwrap();
|
||||
});
|
||||
assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..2]);
|
||||
|
||||
// Scrolling locally breaks the follow
|
||||
follower.update(cx, |follower, cx| {
|
||||
let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
|
||||
follower.set_scroll_anchor(
|
||||
ScrollAnchor {
|
||||
top_anchor,
|
||||
offset: vec2f(0.0, 0.5),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
assert_eq!(*is_still_following.borrow(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::{
|
||||
display_map::{BlockContext, ToDisplayPoint},
|
||||
Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Scroll, Select, SelectPhase,
|
||||
SoftWrap, ToPoint, MAX_LINE_LEN,
|
||||
Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Select, SelectPhase, SoftWrap,
|
||||
ToPoint, MAX_LINE_LEN,
|
||||
};
|
||||
use crate::{
|
||||
display_map::{BlockStyle, DisplaySnapshot, TransformBlock},
|
||||
@@ -13,6 +13,7 @@ use crate::{
|
||||
GoToFetchedDefinition, GoToFetchedTypeDefinition, UpdateGoToDefinitionLink,
|
||||
},
|
||||
mouse_context_menu::DeployMouseContextMenu,
|
||||
scroll::actions::Scroll,
|
||||
EditorStyle,
|
||||
};
|
||||
use clock::ReplicaId;
|
||||
@@ -192,8 +193,14 @@ impl EditorElement {
|
||||
.on_scroll({
|
||||
let position_map = position_map.clone();
|
||||
move |e, cx| {
|
||||
if !Self::scroll(e.position, e.delta, e.precise, &position_map, bounds, cx)
|
||||
{
|
||||
if !Self::scroll(
|
||||
e.position,
|
||||
*e.delta.raw(),
|
||||
e.delta.precise(),
|
||||
&position_map,
|
||||
bounds,
|
||||
cx,
|
||||
) {
|
||||
cx.propagate_event()
|
||||
}
|
||||
}
|
||||
@@ -949,7 +956,7 @@ impl EditorElement {
|
||||
move |_, cx| {
|
||||
if let Some(view) = view.upgrade(cx.deref_mut()) {
|
||||
view.update(cx.deref_mut(), |view, cx| {
|
||||
view.make_scrollbar_visible(cx);
|
||||
view.scroll_manager.show_scrollbar(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -971,7 +978,7 @@ impl EditorElement {
|
||||
position.set_y(top_row as f32);
|
||||
view.set_scroll_position(position, cx);
|
||||
} else {
|
||||
view.make_scrollbar_visible(cx);
|
||||
view.scroll_manager.show_scrollbar(cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1292,7 +1299,7 @@ impl EditorElement {
|
||||
};
|
||||
|
||||
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
||||
let scroll_x = snapshot.scroll_position.x();
|
||||
let scroll_x = snapshot.scroll_anchor.offset.x();
|
||||
let (fixed_blocks, non_fixed_blocks) = snapshot
|
||||
.blocks_in_range(rows.clone())
|
||||
.partition::<Vec<_>, _>(|(_, block)| match block {
|
||||
@@ -1328,12 +1335,13 @@ impl EditorElement {
|
||||
})
|
||||
}
|
||||
TransformBlock::ExcerptHeader {
|
||||
key,
|
||||
id,
|
||||
buffer,
|
||||
range,
|
||||
starts_new_buffer,
|
||||
..
|
||||
} => {
|
||||
let id = *id;
|
||||
let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
|
||||
let jump_position = range
|
||||
.primary
|
||||
@@ -1350,7 +1358,7 @@ impl EditorElement {
|
||||
|
||||
enum JumpIcon {}
|
||||
cx.render(&editor, |_, cx| {
|
||||
MouseEventHandler::<JumpIcon>::new(*key, cx, |state, _| {
|
||||
MouseEventHandler::<JumpIcon>::new(id.into(), cx, |state, _| {
|
||||
let style = style.jump_icon.style_for(state, false);
|
||||
Svg::new("icons/arrow_up_right_8.svg")
|
||||
.with_color(style.color)
|
||||
@@ -1369,7 +1377,7 @@ impl EditorElement {
|
||||
cx.dispatch_action(jump_action.clone())
|
||||
})
|
||||
.with_tooltip::<JumpIcon, _>(
|
||||
*key,
|
||||
id.into(),
|
||||
"Jump to Buffer".to_string(),
|
||||
Some(Box::new(crate::OpenExcerpts)),
|
||||
tooltip_style.clone(),
|
||||
@@ -1600,16 +1608,13 @@ impl Element for EditorElement {
|
||||
|
||||
highlighted_rows = view.highlighted_rows();
|
||||
let theme = cx.global::<Settings>().theme.as_ref();
|
||||
highlighted_ranges = view.background_highlights_in_range(
|
||||
start_anchor.clone()..end_anchor.clone(),
|
||||
&display_map,
|
||||
theme,
|
||||
);
|
||||
highlighted_ranges =
|
||||
view.background_highlights_in_range(start_anchor..end_anchor, &display_map, theme);
|
||||
|
||||
let mut remote_selections = HashMap::default();
|
||||
for (replica_id, line_mode, cursor_shape, selection) in display_map
|
||||
.buffer_snapshot
|
||||
.remote_selections_in_range(&(start_anchor.clone()..end_anchor.clone()))
|
||||
.remote_selections_in_range(&(start_anchor..end_anchor))
|
||||
{
|
||||
// The local selections match the leader's selections.
|
||||
if Some(replica_id) == view.leader_replica_id {
|
||||
@@ -1666,7 +1671,7 @@ impl Element for EditorElement {
|
||||
));
|
||||
}
|
||||
|
||||
show_scrollbars = view.show_scrollbars();
|
||||
show_scrollbars = view.scroll_manager.scrollbars_visible();
|
||||
include_root = view
|
||||
.project
|
||||
.as_ref()
|
||||
@@ -1721,7 +1726,7 @@ impl Element for EditorElement {
|
||||
);
|
||||
|
||||
self.update_view(cx.app, |view, cx| {
|
||||
let clamped = view.clamp_scroll_left(scroll_max.x());
|
||||
let clamped = view.scroll_manager.clamp_scroll_left(scroll_max.x());
|
||||
|
||||
let autoscrolled = if autoscroll_horizontally {
|
||||
view.autoscroll_horizontally(
|
||||
|
||||
@@ -221,7 +221,7 @@ fn show_hover(
|
||||
|
||||
start..end
|
||||
} else {
|
||||
anchor.clone()..anchor.clone()
|
||||
anchor..anchor
|
||||
};
|
||||
|
||||
Some(InfoPopover {
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
use crate::{
|
||||
display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
|
||||
movement::surrounding_word, Anchor, Autoscroll, Editor, Event, ExcerptId, MultiBuffer,
|
||||
MultiBufferSnapshot, NavigationData, ToPoint as _, FORMAT_TIMEOUT,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
|
||||
RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
|
||||
RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use language::{Bias, Buffer, File as _, OffsetRangeExt, Point, SelectionGoal};
|
||||
use project::{File, FormatTrigger, Project, ProjectEntryId, ProjectPath};
|
||||
@@ -22,11 +17,18 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use text::Selection;
|
||||
use util::TryFutureExt;
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
|
||||
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
|
||||
FollowableItem, Item, ItemEvent, ItemHandle, ItemNavHistory, ProjectItem, StatusItemView,
|
||||
ToolbarItemLocation,
|
||||
ItemId, ItemNavHistory, Pane, StatusItemView, ToolbarItemLocation, Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
|
||||
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
|
||||
Event, ExcerptId, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
|
||||
FORMAT_TIMEOUT,
|
||||
};
|
||||
|
||||
pub const MAX_TAB_TITLE_LEN: usize = 24;
|
||||
@@ -86,14 +88,16 @@ impl FollowableItem for Editor {
|
||||
}
|
||||
|
||||
if let Some(anchor) = state.scroll_top_anchor {
|
||||
editor.set_scroll_top_anchor(
|
||||
Anchor {
|
||||
buffer_id: Some(state.buffer_id as usize),
|
||||
excerpt_id,
|
||||
text_anchor: language::proto::deserialize_anchor(anchor)
|
||||
.ok_or_else(|| anyhow!("invalid scroll top"))?,
|
||||
editor.set_scroll_anchor_remote(
|
||||
ScrollAnchor {
|
||||
top_anchor: Anchor {
|
||||
buffer_id: Some(state.buffer_id as usize),
|
||||
excerpt_id,
|
||||
text_anchor: language::proto::deserialize_anchor(anchor)
|
||||
.ok_or_else(|| anyhow!("invalid scroll top"))?,
|
||||
},
|
||||
offset: vec2f(state.scroll_x, state.scroll_y),
|
||||
},
|
||||
vec2f(state.scroll_x, state.scroll_y),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -131,13 +135,14 @@ impl FollowableItem for Editor {
|
||||
|
||||
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
|
||||
let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id();
|
||||
let scroll_anchor = self.scroll_manager.anchor();
|
||||
Some(proto::view::Variant::Editor(proto::view::Editor {
|
||||
buffer_id,
|
||||
scroll_top_anchor: Some(language::proto::serialize_anchor(
|
||||
&self.scroll_top_anchor.text_anchor,
|
||||
&scroll_anchor.top_anchor.text_anchor,
|
||||
)),
|
||||
scroll_x: self.scroll_position.x(),
|
||||
scroll_y: self.scroll_position.y(),
|
||||
scroll_x: scroll_anchor.offset.x(),
|
||||
scroll_y: scroll_anchor.offset.y(),
|
||||
selections: self
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
@@ -159,11 +164,12 @@ impl FollowableItem for Editor {
|
||||
match update {
|
||||
proto::update_view::Variant::Editor(update) => match event {
|
||||
Event::ScrollPositionChanged { .. } => {
|
||||
let scroll_anchor = self.scroll_manager.anchor();
|
||||
update.scroll_top_anchor = Some(language::proto::serialize_anchor(
|
||||
&self.scroll_top_anchor.text_anchor,
|
||||
&scroll_anchor.top_anchor.text_anchor,
|
||||
));
|
||||
update.scroll_x = self.scroll_position.x();
|
||||
update.scroll_y = self.scroll_position.y();
|
||||
update.scroll_x = scroll_anchor.offset.x();
|
||||
update.scroll_y = scroll_anchor.offset.y();
|
||||
true
|
||||
}
|
||||
Event::SelectionsChanged { .. } => {
|
||||
@@ -204,16 +210,18 @@ impl FollowableItem for Editor {
|
||||
|
||||
if !selections.is_empty() {
|
||||
self.set_selections_from_remote(selections, cx);
|
||||
self.request_autoscroll_remotely(Autoscroll::Newest, cx);
|
||||
self.request_autoscroll_remotely(Autoscroll::newest(), cx);
|
||||
} else if let Some(anchor) = message.scroll_top_anchor {
|
||||
self.set_scroll_top_anchor(
|
||||
Anchor {
|
||||
buffer_id: Some(buffer_id),
|
||||
excerpt_id,
|
||||
text_anchor: language::proto::deserialize_anchor(anchor)
|
||||
.ok_or_else(|| anyhow!("invalid scroll top"))?,
|
||||
self.set_scroll_anchor_remote(
|
||||
ScrollAnchor {
|
||||
top_anchor: Anchor {
|
||||
buffer_id: Some(buffer_id),
|
||||
excerpt_id,
|
||||
text_anchor: language::proto::deserialize_anchor(anchor)
|
||||
.ok_or_else(|| anyhow!("invalid scroll top"))?,
|
||||
},
|
||||
offset: vec2f(message.scroll_x, message.scroll_y),
|
||||
},
|
||||
vec2f(message.scroll_x, message.scroll_y),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -278,13 +286,12 @@ impl Item for Editor {
|
||||
buffer.clip_point(data.cursor_position, Bias::Left)
|
||||
};
|
||||
|
||||
let scroll_top_anchor = if buffer.can_resolve(&data.scroll_top_anchor) {
|
||||
data.scroll_top_anchor
|
||||
} else {
|
||||
buffer.anchor_before(
|
||||
let mut scroll_anchor = data.scroll_anchor;
|
||||
if !buffer.can_resolve(&scroll_anchor.top_anchor) {
|
||||
scroll_anchor.top_anchor = buffer.anchor_before(
|
||||
buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left),
|
||||
)
|
||||
};
|
||||
);
|
||||
}
|
||||
|
||||
drop(buffer);
|
||||
|
||||
@@ -292,9 +299,8 @@ impl Item for Editor {
|
||||
false
|
||||
} else {
|
||||
let nav_history = self.nav_history.take();
|
||||
self.scroll_position = data.scroll_position;
|
||||
self.scroll_top_anchor = scroll_top_anchor;
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.set_scroll_anchor(scroll_anchor, cx);
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges([offset..offset])
|
||||
});
|
||||
self.nav_history = nav_history;
|
||||
@@ -367,7 +373,7 @@ impl Item for Editor {
|
||||
self.buffer.read(cx).is_singleton()
|
||||
}
|
||||
|
||||
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
|
||||
fn clone_on_split(&self, _workspace_id: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -466,7 +472,7 @@ impl Item for Editor {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let transaction = reload_buffers.log_err().await;
|
||||
this.update(&mut cx, |editor, cx| {
|
||||
editor.request_autoscroll(Autoscroll::Fit, cx)
|
||||
editor.request_autoscroll(Autoscroll::fit(), cx)
|
||||
});
|
||||
buffer.update(&mut cx, |buffer, _| {
|
||||
if let Some(transaction) = transaction {
|
||||
@@ -490,7 +496,7 @@ impl Item for Editor {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
|
||||
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
|
||||
let mut result = Vec::new();
|
||||
match event {
|
||||
Event::Closed => result.push(ItemEvent::CloseItem),
|
||||
@@ -552,6 +558,87 @@ impl Item for Editor {
|
||||
}));
|
||||
Some(breadcrumbs)
|
||||
}
|
||||
|
||||
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
|
||||
let workspace_id = workspace.database_id();
|
||||
let item_id = cx.view_id();
|
||||
|
||||
fn serialize(
|
||||
buffer: ModelHandle<Buffer>,
|
||||
workspace_id: WorkspaceId,
|
||||
item_id: ItemId,
|
||||
cx: &mut MutableAppContext,
|
||||
) {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
|
||||
let path = file.abs_path(cx);
|
||||
|
||||
cx.background()
|
||||
.spawn(async move {
|
||||
DB.save_path(item_id, workspace_id, path.clone())
|
||||
.await
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||
serialize(buffer.clone(), workspace_id, item_id, cx);
|
||||
|
||||
cx.subscribe(&buffer, |this, buffer, event, cx| {
|
||||
if let Some(workspace_id) = this.workspace_id {
|
||||
if let language::Event::FileHandleChanged = event {
|
||||
serialize(buffer, workspace_id, cx.view_id(), cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
fn serialized_item_kind() -> Option<&'static str> {
|
||||
Some("Editor")
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
project: ModelHandle<Project>,
|
||||
_workspace: WeakViewHandle<Workspace>,
|
||||
workspace_id: workspace::WorkspaceId,
|
||||
item_id: ItemId,
|
||||
cx: &mut ViewContext<Pane>,
|
||||
) -> Task<Result<ViewHandle<Self>>> {
|
||||
let project_item: Result<_> = project.update(cx, |project, cx| {
|
||||
// Look up the path with this key associated, create a self with that path
|
||||
let path = DB
|
||||
.get_path(item_id, workspace_id)?
|
||||
.context("No path stored for this editor")?;
|
||||
|
||||
let (worktree, path) = project
|
||||
.find_local_worktree(&path, cx)
|
||||
.with_context(|| format!("No worktree for path: {path:?}"))?;
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
path: path.into(),
|
||||
};
|
||||
|
||||
Ok(project.open_path(project_path, cx))
|
||||
});
|
||||
|
||||
project_item
|
||||
.map(|project_item| {
|
||||
cx.spawn(|pane, mut cx| async move {
|
||||
let (_, project_item) = project_item.await?;
|
||||
let buffer = project_item
|
||||
.downcast::<Buffer>()
|
||||
.context("Project item at stored path was not a buffer")?;
|
||||
|
||||
Ok(cx.update(|cx| {
|
||||
cx.add_view(pane, |cx| Editor::for_buffer(buffer, Some(project), cx))
|
||||
}))
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|error| Task::ready(Err(error)))
|
||||
}
|
||||
}
|
||||
|
||||
impl ProjectItem for Editor {
|
||||
@@ -619,7 +706,7 @@ impl SearchableItem for Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.unfold_ranges([matches[index].clone()], false, cx);
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges([matches[index].clone()])
|
||||
});
|
||||
}
|
||||
@@ -819,11 +906,20 @@ impl StatusItemView for CursorPosition {
|
||||
|
||||
fn path_for_buffer<'a>(
|
||||
buffer: &ModelHandle<MultiBuffer>,
|
||||
mut height: usize,
|
||||
height: usize,
|
||||
include_filename: bool,
|
||||
cx: &'a AppContext,
|
||||
) -> Option<Cow<'a, Path>> {
|
||||
let file = buffer.read(cx).as_singleton()?.read(cx).file()?;
|
||||
path_for_file(file, height, include_filename, cx)
|
||||
}
|
||||
|
||||
fn path_for_file<'a>(
|
||||
file: &'a dyn language::File,
|
||||
mut height: usize,
|
||||
include_filename: bool,
|
||||
cx: &'a AppContext,
|
||||
) -> Option<Cow<'a, Path>> {
|
||||
// Ensure we always render at least the filename.
|
||||
height += 1;
|
||||
|
||||
@@ -845,13 +941,82 @@ fn path_for_buffer<'a>(
|
||||
if include_filename {
|
||||
Some(full_path.into())
|
||||
} else {
|
||||
Some(full_path.parent().unwrap().to_path_buf().into())
|
||||
Some(full_path.parent()?.to_path_buf().into())
|
||||
}
|
||||
} else {
|
||||
let mut path = file.path().strip_prefix(prefix).unwrap();
|
||||
let mut path = file.path().strip_prefix(prefix).ok()?;
|
||||
if !include_filename {
|
||||
path = path.parent().unwrap();
|
||||
path = path.parent()?;
|
||||
}
|
||||
Some(path.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::MutableAppContext;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_path_for_file(cx: &mut MutableAppContext) {
|
||||
let file = TestFile {
|
||||
path: Path::new("").into(),
|
||||
full_path: PathBuf::from(""),
|
||||
};
|
||||
assert_eq!(path_for_file(&file, 0, false, cx), None);
|
||||
}
|
||||
|
||||
struct TestFile {
|
||||
path: Arc<Path>,
|
||||
full_path: PathBuf,
|
||||
}
|
||||
|
||||
impl language::File for TestFile {
|
||||
fn path(&self) -> &Arc<Path> {
|
||||
&self.path
|
||||
}
|
||||
|
||||
fn full_path(&self, _: &gpui::AppContext) -> PathBuf {
|
||||
self.full_path.clone()
|
||||
}
|
||||
|
||||
fn as_local(&self) -> Option<&dyn language::LocalFile> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn mtime(&self) -> std::time::SystemTime {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn file_name<'a>(&'a self, _: &'a gpui::AppContext) -> &'a std::ffi::OsStr {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn is_deleted(&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn save(
|
||||
&self,
|
||||
_: u64,
|
||||
_: language::Rope,
|
||||
_: clock::Global,
|
||||
_: project::LineEnding,
|
||||
_: &mut MutableAppContext,
|
||||
) -> gpui::Task<anyhow::Result<(clock::Global, String, std::time::SystemTime)>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn to_proto(&self) -> rpc::proto::File {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -811,7 +811,7 @@ mod tests {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let anchor_range = snapshot.anchor_before(selection_range.start)
|
||||
..snapshot.anchor_after(selection_range.end);
|
||||
editor.change_selections(Some(crate::Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(crate::Autoscroll::fit()), cx, |s| {
|
||||
s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ use language::{
|
||||
char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape,
|
||||
DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Outline,
|
||||
OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _,
|
||||
ToPoint as _, ToPointUtf16 as _, TransactionId,
|
||||
ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
@@ -36,13 +36,13 @@ use util::post_inc;
|
||||
|
||||
const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
|
||||
|
||||
pub type ExcerptId = Locator;
|
||||
#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct ExcerptId(usize);
|
||||
|
||||
pub struct MultiBuffer {
|
||||
snapshot: RefCell<MultiBufferSnapshot>,
|
||||
buffers: RefCell<HashMap<usize, BufferState>>,
|
||||
used_excerpt_ids: SumTree<ExcerptId>,
|
||||
next_excerpt_key: usize,
|
||||
next_excerpt_id: usize,
|
||||
subscriptions: Topic,
|
||||
singleton: bool,
|
||||
replica_id: ReplicaId,
|
||||
@@ -92,7 +92,7 @@ struct BufferState {
|
||||
last_diagnostics_update_count: usize,
|
||||
last_file_update_count: usize,
|
||||
last_git_diff_update_count: usize,
|
||||
excerpts: Vec<ExcerptId>,
|
||||
excerpts: Vec<Locator>,
|
||||
_subscriptions: [gpui::Subscription; 2],
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ struct BufferState {
|
||||
pub struct MultiBufferSnapshot {
|
||||
singleton: bool,
|
||||
excerpts: SumTree<Excerpt>,
|
||||
excerpt_ids: SumTree<ExcerptIdMapping>,
|
||||
parse_count: usize,
|
||||
diagnostics_update_count: usize,
|
||||
trailing_excerpt_update_count: usize,
|
||||
@@ -111,7 +112,6 @@ pub struct MultiBufferSnapshot {
|
||||
|
||||
pub struct ExcerptBoundary {
|
||||
pub id: ExcerptId,
|
||||
pub key: usize,
|
||||
pub row: u32,
|
||||
pub buffer: BufferSnapshot,
|
||||
pub range: ExcerptRange<text::Anchor>,
|
||||
@@ -121,7 +121,7 @@ pub struct ExcerptBoundary {
|
||||
#[derive(Clone)]
|
||||
struct Excerpt {
|
||||
id: ExcerptId,
|
||||
key: usize,
|
||||
locator: Locator,
|
||||
buffer_id: usize,
|
||||
buffer: BufferSnapshot,
|
||||
range: ExcerptRange<text::Anchor>,
|
||||
@@ -130,6 +130,12 @@ struct Excerpt {
|
||||
has_trailing_newline: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ExcerptIdMapping {
|
||||
id: ExcerptId,
|
||||
locator: Locator,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ExcerptRange<T> {
|
||||
pub context: Range<T>,
|
||||
@@ -139,6 +145,7 @@ pub struct ExcerptRange<T> {
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct ExcerptSummary {
|
||||
excerpt_id: ExcerptId,
|
||||
excerpt_locator: Locator,
|
||||
max_buffer_row: u32,
|
||||
text: TextSummary,
|
||||
}
|
||||
@@ -178,8 +185,7 @@ impl MultiBuffer {
|
||||
Self {
|
||||
snapshot: Default::default(),
|
||||
buffers: Default::default(),
|
||||
used_excerpt_ids: Default::default(),
|
||||
next_excerpt_key: Default::default(),
|
||||
next_excerpt_id: 1,
|
||||
subscriptions: Default::default(),
|
||||
singleton: false,
|
||||
replica_id,
|
||||
@@ -218,8 +224,7 @@ impl MultiBuffer {
|
||||
Self {
|
||||
snapshot: RefCell::new(self.snapshot.borrow().clone()),
|
||||
buffers: RefCell::new(buffers),
|
||||
used_excerpt_ids: self.used_excerpt_ids.clone(),
|
||||
next_excerpt_key: self.next_excerpt_key,
|
||||
next_excerpt_id: 1,
|
||||
subscriptions: Default::default(),
|
||||
singleton: self.singleton,
|
||||
replica_id: self.replica_id,
|
||||
@@ -610,11 +615,14 @@ impl MultiBuffer {
|
||||
let mut selections_by_buffer: HashMap<usize, Vec<Selection<text::Anchor>>> =
|
||||
Default::default();
|
||||
let snapshot = self.read(cx);
|
||||
let mut cursor = snapshot.excerpts.cursor::<Option<&ExcerptId>>();
|
||||
let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
|
||||
for selection in selections {
|
||||
cursor.seek(&Some(&selection.start.excerpt_id), Bias::Left, &());
|
||||
let start_locator = snapshot.excerpt_locator_for_id(selection.start.excerpt_id);
|
||||
let end_locator = snapshot.excerpt_locator_for_id(selection.end.excerpt_id);
|
||||
|
||||
cursor.seek(&Some(start_locator), Bias::Left, &());
|
||||
while let Some(excerpt) = cursor.item() {
|
||||
if excerpt.id > selection.end.excerpt_id {
|
||||
if excerpt.locator > *end_locator {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -745,7 +753,7 @@ impl MultiBuffer {
|
||||
where
|
||||
O: text::ToOffset,
|
||||
{
|
||||
self.insert_excerpts_after(&ExcerptId::max(), buffer, ranges, cx)
|
||||
self.insert_excerpts_after(ExcerptId::max(), buffer, ranges, cx)
|
||||
}
|
||||
|
||||
pub fn push_excerpts_with_context_lines<O>(
|
||||
@@ -818,7 +826,7 @@ impl MultiBuffer {
|
||||
|
||||
pub fn insert_excerpts_after<O>(
|
||||
&mut self,
|
||||
prev_excerpt_id: &ExcerptId,
|
||||
prev_excerpt_id: ExcerptId,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
ranges: impl IntoIterator<Item = ExcerptRange<O>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
@@ -854,8 +862,12 @@ impl MultiBuffer {
|
||||
});
|
||||
|
||||
let mut snapshot = self.snapshot.borrow_mut();
|
||||
let mut cursor = snapshot.excerpts.cursor::<Option<&ExcerptId>>();
|
||||
let mut new_excerpts = cursor.slice(&Some(prev_excerpt_id), Bias::Right, &());
|
||||
|
||||
let mut prev_locator = snapshot.excerpt_locator_for_id(prev_excerpt_id).clone();
|
||||
let mut new_excerpt_ids = mem::take(&mut snapshot.excerpt_ids);
|
||||
let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
|
||||
let mut new_excerpts = cursor.slice(&prev_locator, Bias::Right, &());
|
||||
prev_locator = cursor.start().unwrap_or(Locator::min_ref()).clone();
|
||||
|
||||
let edit_start = new_excerpts.summary().text.len;
|
||||
new_excerpts.update_last(
|
||||
@@ -865,25 +877,17 @@ impl MultiBuffer {
|
||||
&(),
|
||||
);
|
||||
|
||||
let mut used_cursor = self.used_excerpt_ids.cursor::<Locator>();
|
||||
used_cursor.seek(prev_excerpt_id, Bias::Right, &());
|
||||
let mut prev_id = if let Some(excerpt_id) = used_cursor.prev_item() {
|
||||
excerpt_id.clone()
|
||||
let next_locator = if let Some(excerpt) = cursor.item() {
|
||||
excerpt.locator.clone()
|
||||
} else {
|
||||
ExcerptId::min()
|
||||
Locator::max()
|
||||
};
|
||||
let next_id = if let Some(excerpt_id) = used_cursor.item() {
|
||||
excerpt_id.clone()
|
||||
} else {
|
||||
ExcerptId::max()
|
||||
};
|
||||
drop(used_cursor);
|
||||
|
||||
let mut ids = Vec::new();
|
||||
while let Some(range) = ranges.next() {
|
||||
let id = ExcerptId::between(&prev_id, &next_id);
|
||||
if let Err(ix) = buffer_state.excerpts.binary_search(&id) {
|
||||
buffer_state.excerpts.insert(ix, id.clone());
|
||||
let locator = Locator::between(&prev_locator, &next_locator);
|
||||
if let Err(ix) = buffer_state.excerpts.binary_search(&locator) {
|
||||
buffer_state.excerpts.insert(ix, locator.clone());
|
||||
}
|
||||
let range = ExcerptRange {
|
||||
context: buffer_snapshot.anchor_before(&range.context.start)
|
||||
@@ -893,22 +897,20 @@ impl MultiBuffer {
|
||||
..buffer_snapshot.anchor_after(&primary.end)
|
||||
}),
|
||||
};
|
||||
let id = ExcerptId(post_inc(&mut self.next_excerpt_id));
|
||||
let excerpt = Excerpt::new(
|
||||
id.clone(),
|
||||
post_inc(&mut self.next_excerpt_key),
|
||||
id,
|
||||
locator.clone(),
|
||||
buffer_id,
|
||||
buffer_snapshot.clone(),
|
||||
range,
|
||||
ranges.peek().is_some() || cursor.item().is_some(),
|
||||
);
|
||||
new_excerpts.push(excerpt, &());
|
||||
prev_id = id.clone();
|
||||
prev_locator = locator.clone();
|
||||
new_excerpt_ids.push(ExcerptIdMapping { id, locator }, &());
|
||||
ids.push(id);
|
||||
}
|
||||
self.used_excerpt_ids.edit(
|
||||
ids.iter().cloned().map(sum_tree::Edit::Insert).collect(),
|
||||
&(),
|
||||
);
|
||||
|
||||
let edit_end = new_excerpts.summary().text.len;
|
||||
|
||||
@@ -917,6 +919,7 @@ impl MultiBuffer {
|
||||
new_excerpts.push_tree(suffix, &());
|
||||
drop(cursor);
|
||||
snapshot.excerpts = new_excerpts;
|
||||
snapshot.excerpt_ids = new_excerpt_ids;
|
||||
if changed_trailing_excerpt {
|
||||
snapshot.trailing_excerpt_update_count += 1;
|
||||
}
|
||||
@@ -956,16 +959,16 @@ impl MultiBuffer {
|
||||
let mut excerpts = Vec::new();
|
||||
let snapshot = self.read(cx);
|
||||
let buffers = self.buffers.borrow();
|
||||
let mut cursor = snapshot.excerpts.cursor::<Option<&ExcerptId>>();
|
||||
for excerpt_id in buffers
|
||||
let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
|
||||
for locator in buffers
|
||||
.get(&buffer.id())
|
||||
.map(|state| &state.excerpts)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
cursor.seek_forward(&Some(excerpt_id), Bias::Left, &());
|
||||
cursor.seek_forward(&Some(locator), Bias::Left, &());
|
||||
if let Some(excerpt) = cursor.item() {
|
||||
if excerpt.id == *excerpt_id {
|
||||
if excerpt.locator == *locator {
|
||||
excerpts.push((excerpt.id.clone(), excerpt.range.clone()));
|
||||
}
|
||||
}
|
||||
@@ -975,10 +978,11 @@ impl MultiBuffer {
|
||||
}
|
||||
|
||||
pub fn excerpt_ids(&self) -> Vec<ExcerptId> {
|
||||
self.buffers
|
||||
self.snapshot
|
||||
.borrow()
|
||||
.values()
|
||||
.flat_map(|state| state.excerpts.iter().cloned())
|
||||
.excerpts
|
||||
.iter()
|
||||
.map(|entry| entry.id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -1061,32 +1065,34 @@ impl MultiBuffer {
|
||||
result
|
||||
}
|
||||
|
||||
pub fn remove_excerpts<'a>(
|
||||
pub fn remove_excerpts(
|
||||
&mut self,
|
||||
excerpt_ids: impl IntoIterator<Item = &'a ExcerptId>,
|
||||
excerpt_ids: impl IntoIterator<Item = ExcerptId>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.sync(cx);
|
||||
let mut buffers = self.buffers.borrow_mut();
|
||||
let mut snapshot = self.snapshot.borrow_mut();
|
||||
let mut new_excerpts = SumTree::new();
|
||||
let mut cursor = snapshot.excerpts.cursor::<(Option<&ExcerptId>, usize)>();
|
||||
let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>();
|
||||
let mut edits = Vec::new();
|
||||
let mut excerpt_ids = excerpt_ids.into_iter().peekable();
|
||||
|
||||
while let Some(mut excerpt_id) = excerpt_ids.next() {
|
||||
while let Some(excerpt_id) = excerpt_ids.next() {
|
||||
// Seek to the next excerpt to remove, preserving any preceding excerpts.
|
||||
new_excerpts.push_tree(cursor.slice(&Some(excerpt_id), Bias::Left, &()), &());
|
||||
let locator = snapshot.excerpt_locator_for_id(excerpt_id);
|
||||
new_excerpts.push_tree(cursor.slice(&Some(locator), Bias::Left, &()), &());
|
||||
|
||||
if let Some(mut excerpt) = cursor.item() {
|
||||
if excerpt.id != *excerpt_id {
|
||||
if excerpt.id != excerpt_id {
|
||||
continue;
|
||||
}
|
||||
let mut old_start = cursor.start().1;
|
||||
|
||||
// Skip over the removed excerpt.
|
||||
loop {
|
||||
'remove_excerpts: loop {
|
||||
if let Some(buffer_state) = buffers.get_mut(&excerpt.buffer_id) {
|
||||
buffer_state.excerpts.retain(|id| id != excerpt_id);
|
||||
buffer_state.excerpts.retain(|l| l != &excerpt.locator);
|
||||
if buffer_state.excerpts.is_empty() {
|
||||
buffers.remove(&excerpt.buffer_id);
|
||||
}
|
||||
@@ -1094,14 +1100,16 @@ impl MultiBuffer {
|
||||
cursor.next(&());
|
||||
|
||||
// Skip over any subsequent excerpts that are also removed.
|
||||
if let Some(&next_excerpt_id) = excerpt_ids.peek() {
|
||||
while let Some(&next_excerpt_id) = excerpt_ids.peek() {
|
||||
let next_locator = snapshot.excerpt_locator_for_id(next_excerpt_id);
|
||||
if let Some(next_excerpt) = cursor.item() {
|
||||
if next_excerpt.id == *next_excerpt_id {
|
||||
if next_excerpt.locator == *next_locator {
|
||||
excerpt_ids.next();
|
||||
excerpt = next_excerpt;
|
||||
excerpt_id = excerpt_ids.next().unwrap();
|
||||
continue;
|
||||
continue 'remove_excerpts;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -1128,6 +1136,7 @@ impl MultiBuffer {
|
||||
new_excerpts.push_tree(suffix, &());
|
||||
drop(cursor);
|
||||
snapshot.excerpts = new_excerpts;
|
||||
|
||||
if changed_trailing_excerpt {
|
||||
snapshot.trailing_excerpt_update_count += 1;
|
||||
}
|
||||
@@ -1307,7 +1316,7 @@ impl MultiBuffer {
|
||||
buffer_state
|
||||
.excerpts
|
||||
.iter()
|
||||
.map(|excerpt_id| (excerpt_id, buffer_state.buffer.clone(), buffer_edited)),
|
||||
.map(|locator| (locator, buffer_state.buffer.clone(), buffer_edited)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1333,14 +1342,14 @@ impl MultiBuffer {
|
||||
snapshot.is_dirty = is_dirty;
|
||||
snapshot.has_conflict = has_conflict;
|
||||
|
||||
excerpts_to_edit.sort_unstable_by_key(|(excerpt_id, _, _)| *excerpt_id);
|
||||
excerpts_to_edit.sort_unstable_by_key(|(locator, _, _)| *locator);
|
||||
|
||||
let mut edits = Vec::new();
|
||||
let mut new_excerpts = SumTree::new();
|
||||
let mut cursor = snapshot.excerpts.cursor::<(Option<&ExcerptId>, usize)>();
|
||||
let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>();
|
||||
|
||||
for (id, buffer, buffer_edited) in excerpts_to_edit {
|
||||
new_excerpts.push_tree(cursor.slice(&Some(id), Bias::Left, &()), &());
|
||||
for (locator, buffer, buffer_edited) in excerpts_to_edit {
|
||||
new_excerpts.push_tree(cursor.slice(&Some(locator), Bias::Left, &()), &());
|
||||
let old_excerpt = cursor.item().unwrap();
|
||||
let buffer_id = buffer.id();
|
||||
let buffer = buffer.read(cx);
|
||||
@@ -1365,8 +1374,8 @@ impl MultiBuffer {
|
||||
);
|
||||
|
||||
new_excerpt = Excerpt::new(
|
||||
id.clone(),
|
||||
old_excerpt.key,
|
||||
old_excerpt.id,
|
||||
locator.clone(),
|
||||
buffer_id,
|
||||
buffer.snapshot(),
|
||||
old_excerpt.range.clone(),
|
||||
@@ -1467,13 +1476,7 @@ impl MultiBuffer {
|
||||
continue;
|
||||
}
|
||||
|
||||
let excerpt_ids = self
|
||||
.buffers
|
||||
.borrow()
|
||||
.values()
|
||||
.flat_map(|b| &b.excerpts)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let excerpt_ids = self.excerpt_ids();
|
||||
if excerpt_ids.is_empty() || (rng.gen() && excerpt_ids.len() < max_excerpts) {
|
||||
let buffer_handle = if rng.gen() || self.buffers.borrow().is_empty() {
|
||||
let text = RandomCharIter::new(&mut *rng).take(10).collect::<String>();
|
||||
@@ -1511,24 +1514,26 @@ impl MultiBuffer {
|
||||
log::info!(
|
||||
"Inserting excerpts from buffer {} and ranges {:?}: {:?}",
|
||||
buffer_handle.id(),
|
||||
ranges,
|
||||
ranges.iter().map(|r| &r.context).collect::<Vec<_>>(),
|
||||
ranges
|
||||
.iter()
|
||||
.map(|range| &buffer_text[range.context.clone()])
|
||||
.map(|r| &buffer_text[r.context.clone()])
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
let excerpt_id = self.push_excerpts(buffer_handle.clone(), ranges, cx);
|
||||
log::info!("Inserted with id: {:?}", excerpt_id);
|
||||
log::info!("Inserted with ids: {:?}", excerpt_id);
|
||||
} else {
|
||||
let remove_count = rng.gen_range(1..=excerpt_ids.len());
|
||||
let mut excerpts_to_remove = excerpt_ids
|
||||
.choose_multiple(rng, remove_count)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
excerpts_to_remove.sort();
|
||||
let snapshot = self.snapshot.borrow();
|
||||
excerpts_to_remove.sort_unstable_by(|a, b| a.cmp(b, &*snapshot));
|
||||
drop(snapshot);
|
||||
log::info!("Removing excerpts {:?}", excerpts_to_remove);
|
||||
self.remove_excerpts(&excerpts_to_remove, cx);
|
||||
self.remove_excerpts(excerpts_to_remove, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1563,6 +1568,38 @@ impl MultiBuffer {
|
||||
} else {
|
||||
self.randomly_edit_excerpts(rng, mutation_count, cx);
|
||||
}
|
||||
|
||||
self.check_invariants(cx);
|
||||
}
|
||||
|
||||
fn check_invariants(&self, cx: &mut ModelContext<Self>) {
|
||||
let snapshot = self.read(cx);
|
||||
let excerpts = snapshot.excerpts.items(&());
|
||||
let excerpt_ids = snapshot.excerpt_ids.items(&());
|
||||
|
||||
for (ix, excerpt) in excerpts.iter().enumerate() {
|
||||
if ix == 0 {
|
||||
if excerpt.locator <= Locator::min() {
|
||||
panic!("invalid first excerpt locator {:?}", excerpt.locator);
|
||||
}
|
||||
} else {
|
||||
if excerpt.locator <= excerpts[ix - 1].locator {
|
||||
panic!("excerpts are out-of-order: {:?}", excerpts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (ix, entry) in excerpt_ids.iter().enumerate() {
|
||||
if ix == 0 {
|
||||
if entry.id.cmp(&ExcerptId::min(), &*snapshot).is_le() {
|
||||
panic!("invalid first excerpt id {:?}", entry.id);
|
||||
}
|
||||
} else {
|
||||
if entry.id <= excerpt_ids[ix - 1].id {
|
||||
panic!("excerpt ids are out-of-order: {:?}", excerpt_ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1749,20 +1786,20 @@ impl MultiBufferSnapshot {
|
||||
*cursor.start() + overshoot
|
||||
}
|
||||
|
||||
pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 {
|
||||
pub fn clip_point_utf16(&self, point: Unclipped<PointUtf16>, bias: Bias) -> PointUtf16 {
|
||||
if let Some((_, _, buffer)) = self.as_singleton() {
|
||||
return buffer.clip_point_utf16(point, bias);
|
||||
}
|
||||
|
||||
let mut cursor = self.excerpts.cursor::<PointUtf16>();
|
||||
cursor.seek(&point, Bias::Right, &());
|
||||
cursor.seek(&point.0, Bias::Right, &());
|
||||
let overshoot = if let Some(excerpt) = cursor.item() {
|
||||
let excerpt_start = excerpt
|
||||
.buffer
|
||||
.offset_to_point_utf16(excerpt.range.context.start.to_offset(&excerpt.buffer));
|
||||
let buffer_point = excerpt
|
||||
.buffer
|
||||
.clip_point_utf16(excerpt_start + (point - cursor.start()), bias);
|
||||
.clip_point_utf16(Unclipped(excerpt_start + (point.0 - cursor.start())), bias);
|
||||
buffer_point.saturating_sub(excerpt_start)
|
||||
} else {
|
||||
PointUtf16::zero()
|
||||
@@ -2151,7 +2188,9 @@ impl MultiBufferSnapshot {
|
||||
D: TextDimension + Ord + Sub<D, Output = D>,
|
||||
{
|
||||
let mut cursor = self.excerpts.cursor::<ExcerptSummary>();
|
||||
cursor.seek(&Some(&anchor.excerpt_id), Bias::Left, &());
|
||||
let locator = self.excerpt_locator_for_id(anchor.excerpt_id);
|
||||
|
||||
cursor.seek(locator, Bias::Left, &());
|
||||
if cursor.item().is_none() {
|
||||
cursor.next(&());
|
||||
}
|
||||
@@ -2189,24 +2228,25 @@ impl MultiBufferSnapshot {
|
||||
let mut cursor = self.excerpts.cursor::<ExcerptSummary>();
|
||||
let mut summaries = Vec::new();
|
||||
while let Some(anchor) = anchors.peek() {
|
||||
let excerpt_id = &anchor.excerpt_id;
|
||||
let excerpt_id = anchor.excerpt_id;
|
||||
let excerpt_anchors = iter::from_fn(|| {
|
||||
let anchor = anchors.peek()?;
|
||||
if anchor.excerpt_id == *excerpt_id {
|
||||
if anchor.excerpt_id == excerpt_id {
|
||||
Some(&anchors.next().unwrap().text_anchor)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
cursor.seek_forward(&Some(excerpt_id), Bias::Left, &());
|
||||
let locator = self.excerpt_locator_for_id(excerpt_id);
|
||||
cursor.seek_forward(locator, Bias::Left, &());
|
||||
if cursor.item().is_none() {
|
||||
cursor.next(&());
|
||||
}
|
||||
|
||||
let position = D::from_text_summary(&cursor.start().text);
|
||||
if let Some(excerpt) = cursor.item() {
|
||||
if excerpt.id == *excerpt_id {
|
||||
if excerpt.id == excerpt_id {
|
||||
let excerpt_buffer_start =
|
||||
excerpt.range.context.start.summary::<D>(&excerpt.buffer);
|
||||
let excerpt_buffer_end =
|
||||
@@ -2240,13 +2280,18 @@ impl MultiBufferSnapshot {
|
||||
I: 'a + IntoIterator<Item = &'a Anchor>,
|
||||
{
|
||||
let mut anchors = anchors.into_iter().enumerate().peekable();
|
||||
let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
|
||||
let mut cursor = self.excerpts.cursor::<Option<&Locator>>();
|
||||
cursor.next(&());
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
while let Some((_, anchor)) = anchors.peek() {
|
||||
let old_excerpt_id = &anchor.excerpt_id;
|
||||
let old_excerpt_id = anchor.excerpt_id;
|
||||
|
||||
// Find the location where this anchor's excerpt should be.
|
||||
cursor.seek_forward(&Some(old_excerpt_id), Bias::Left, &());
|
||||
let old_locator = self.excerpt_locator_for_id(old_excerpt_id);
|
||||
cursor.seek_forward(&Some(old_locator), Bias::Left, &());
|
||||
|
||||
if cursor.item().is_none() {
|
||||
cursor.next(&());
|
||||
}
|
||||
@@ -2256,27 +2301,22 @@ impl MultiBufferSnapshot {
|
||||
|
||||
// Process all of the anchors for this excerpt.
|
||||
while let Some((_, anchor)) = anchors.peek() {
|
||||
if anchor.excerpt_id != *old_excerpt_id {
|
||||
if anchor.excerpt_id != old_excerpt_id {
|
||||
break;
|
||||
}
|
||||
let mut kept_position = false;
|
||||
let (anchor_ix, anchor) = anchors.next().unwrap();
|
||||
let mut anchor = anchor.clone();
|
||||
|
||||
let id_invalid =
|
||||
*old_excerpt_id == ExcerptId::max() || *old_excerpt_id == ExcerptId::min();
|
||||
let still_exists = next_excerpt.map_or(false, |excerpt| {
|
||||
excerpt.id == *old_excerpt_id && excerpt.contains(&anchor)
|
||||
});
|
||||
let mut anchor = *anchor;
|
||||
|
||||
// Leave min and max anchors unchanged if invalid or
|
||||
// if the old excerpt still exists at this location
|
||||
if id_invalid || still_exists {
|
||||
kept_position = true;
|
||||
}
|
||||
let mut kept_position = next_excerpt
|
||||
.map_or(false, |e| e.id == old_excerpt_id && e.contains(&anchor))
|
||||
|| old_excerpt_id == ExcerptId::max()
|
||||
|| old_excerpt_id == ExcerptId::min();
|
||||
|
||||
// If the old excerpt no longer exists at this location, then attempt to
|
||||
// find an equivalent position for this anchor in an adjacent excerpt.
|
||||
else {
|
||||
if !kept_position {
|
||||
for excerpt in [next_excerpt, prev_excerpt].iter().filter_map(|e| *e) {
|
||||
if excerpt.contains(&anchor) {
|
||||
anchor.excerpt_id = excerpt.id.clone();
|
||||
@@ -2285,6 +2325,7 @@ impl MultiBufferSnapshot {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no adjacent excerpt that contains the anchor's position,
|
||||
// then report that the anchor has lost its position.
|
||||
if !kept_position {
|
||||
@@ -2354,7 +2395,7 @@ impl MultiBufferSnapshot {
|
||||
};
|
||||
}
|
||||
|
||||
let mut cursor = self.excerpts.cursor::<(usize, Option<&ExcerptId>)>();
|
||||
let mut cursor = self.excerpts.cursor::<(usize, Option<ExcerptId>)>();
|
||||
cursor.seek(&offset, Bias::Right, &());
|
||||
if cursor.item().is_none() && offset == cursor.start().0 && bias == Bias::Left {
|
||||
cursor.prev(&());
|
||||
@@ -2382,8 +2423,9 @@ impl MultiBufferSnapshot {
|
||||
}
|
||||
|
||||
pub fn anchor_in_excerpt(&self, excerpt_id: ExcerptId, text_anchor: text::Anchor) -> Anchor {
|
||||
let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
|
||||
cursor.seek(&Some(&excerpt_id), Bias::Left, &());
|
||||
let locator = self.excerpt_locator_for_id(excerpt_id);
|
||||
let mut cursor = self.excerpts.cursor::<Option<&Locator>>();
|
||||
cursor.seek(locator, Bias::Left, &());
|
||||
if let Some(excerpt) = cursor.item() {
|
||||
if excerpt.id == excerpt_id {
|
||||
let text_anchor = excerpt.clip_anchor(text_anchor);
|
||||
@@ -2401,7 +2443,7 @@ impl MultiBufferSnapshot {
|
||||
pub fn can_resolve(&self, anchor: &Anchor) -> bool {
|
||||
if anchor.excerpt_id == ExcerptId::min() || anchor.excerpt_id == ExcerptId::max() {
|
||||
true
|
||||
} else if let Some(excerpt) = self.excerpt(&anchor.excerpt_id) {
|
||||
} else if let Some(excerpt) = self.excerpt(anchor.excerpt_id) {
|
||||
excerpt.buffer.can_resolve(&anchor.text_anchor)
|
||||
} else {
|
||||
false
|
||||
@@ -2456,7 +2498,6 @@ impl MultiBufferSnapshot {
|
||||
let starts_new_buffer = Some(excerpt.buffer_id) != prev_buffer_id;
|
||||
let boundary = ExcerptBoundary {
|
||||
id: excerpt.id.clone(),
|
||||
key: excerpt.key,
|
||||
row: cursor.start().1.row,
|
||||
buffer: excerpt.buffer.clone(),
|
||||
range: excerpt.range.clone(),
|
||||
@@ -2678,8 +2719,8 @@ impl MultiBufferSnapshot {
|
||||
.flatten()
|
||||
.map(|item| OutlineItem {
|
||||
depth: item.depth,
|
||||
range: self.anchor_in_excerpt(excerpt_id.clone(), item.range.start)
|
||||
..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end),
|
||||
range: self.anchor_in_excerpt(excerpt_id, item.range.start)
|
||||
..self.anchor_in_excerpt(excerpt_id, item.range.end),
|
||||
text: item.text,
|
||||
highlight_ranges: item.highlight_ranges,
|
||||
name_ranges: item.name_ranges,
|
||||
@@ -2688,11 +2729,29 @@ impl MultiBufferSnapshot {
|
||||
))
|
||||
}
|
||||
|
||||
fn excerpt<'a>(&'a self, excerpt_id: &'a ExcerptId) -> Option<&'a Excerpt> {
|
||||
let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
|
||||
cursor.seek(&Some(excerpt_id), Bias::Left, &());
|
||||
fn excerpt_locator_for_id<'a>(&'a self, id: ExcerptId) -> &'a Locator {
|
||||
if id == ExcerptId::min() {
|
||||
Locator::min_ref()
|
||||
} else if id == ExcerptId::max() {
|
||||
Locator::max_ref()
|
||||
} else {
|
||||
let mut cursor = self.excerpt_ids.cursor::<ExcerptId>();
|
||||
cursor.seek(&id, Bias::Left, &());
|
||||
if let Some(entry) = cursor.item() {
|
||||
if entry.id == id {
|
||||
return &entry.locator;
|
||||
}
|
||||
}
|
||||
panic!("invalid excerpt id {:?}", id)
|
||||
}
|
||||
}
|
||||
|
||||
fn excerpt<'a>(&'a self, excerpt_id: ExcerptId) -> Option<&'a Excerpt> {
|
||||
let mut cursor = self.excerpts.cursor::<Option<&Locator>>();
|
||||
let locator = self.excerpt_locator_for_id(excerpt_id);
|
||||
cursor.seek(&Some(locator), Bias::Left, &());
|
||||
if let Some(excerpt) = cursor.item() {
|
||||
if excerpt.id == *excerpt_id {
|
||||
if excerpt.id == excerpt_id {
|
||||
return Some(excerpt);
|
||||
}
|
||||
}
|
||||
@@ -2703,10 +2762,12 @@ impl MultiBufferSnapshot {
|
||||
&'a self,
|
||||
range: &'a Range<Anchor>,
|
||||
) -> impl 'a + Iterator<Item = (ReplicaId, bool, CursorShape, Selection<Anchor>)> {
|
||||
let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
|
||||
cursor.seek(&Some(&range.start.excerpt_id), Bias::Left, &());
|
||||
let mut cursor = self.excerpts.cursor::<ExcerptSummary>();
|
||||
let start_locator = self.excerpt_locator_for_id(range.start.excerpt_id);
|
||||
let end_locator = self.excerpt_locator_for_id(range.end.excerpt_id);
|
||||
cursor.seek(start_locator, Bias::Left, &());
|
||||
cursor
|
||||
.take_while(move |excerpt| excerpt.id <= range.end.excerpt_id)
|
||||
.take_while(move |excerpt| excerpt.locator <= *end_locator)
|
||||
.flat_map(move |excerpt| {
|
||||
let mut query_range = excerpt.range.context.start..excerpt.range.context.end;
|
||||
if excerpt.id == range.start.excerpt_id {
|
||||
@@ -2916,7 +2977,7 @@ impl History {
|
||||
impl Excerpt {
|
||||
fn new(
|
||||
id: ExcerptId,
|
||||
key: usize,
|
||||
locator: Locator,
|
||||
buffer_id: usize,
|
||||
buffer: BufferSnapshot,
|
||||
range: ExcerptRange<text::Anchor>,
|
||||
@@ -2924,7 +2985,7 @@ impl Excerpt {
|
||||
) -> Self {
|
||||
Excerpt {
|
||||
id,
|
||||
key,
|
||||
locator,
|
||||
max_buffer_row: range.context.end.to_point(&buffer).row,
|
||||
text_summary: buffer
|
||||
.text_summary_for_range::<TextSummary, _>(range.context.to_offset(&buffer)),
|
||||
@@ -3010,10 +3071,33 @@ impl Excerpt {
|
||||
}
|
||||
}
|
||||
|
||||
impl ExcerptId {
|
||||
pub fn min() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
|
||||
pub fn max() -> Self {
|
||||
Self(usize::MAX)
|
||||
}
|
||||
|
||||
pub fn cmp(&self, other: &Self, snapshot: &MultiBufferSnapshot) -> cmp::Ordering {
|
||||
let a = snapshot.excerpt_locator_for_id(*self);
|
||||
let b = snapshot.excerpt_locator_for_id(*other);
|
||||
a.cmp(&b).then_with(|| self.0.cmp(&other.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<usize> for ExcerptId {
|
||||
fn into(self) -> usize {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Excerpt {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Excerpt")
|
||||
.field("id", &self.id)
|
||||
.field("locator", &self.locator)
|
||||
.field("buffer_id", &self.buffer_id)
|
||||
.field("range", &self.range)
|
||||
.field("text_summary", &self.text_summary)
|
||||
@@ -3031,19 +3115,44 @@ impl sum_tree::Item for Excerpt {
|
||||
text += TextSummary::from("\n");
|
||||
}
|
||||
ExcerptSummary {
|
||||
excerpt_id: self.id.clone(),
|
||||
excerpt_id: self.id,
|
||||
excerpt_locator: self.locator.clone(),
|
||||
max_buffer_row: self.max_buffer_row,
|
||||
text,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Item for ExcerptIdMapping {
|
||||
type Summary = ExcerptId;
|
||||
|
||||
fn summary(&self) -> Self::Summary {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::KeyedItem for ExcerptIdMapping {
|
||||
type Key = ExcerptId;
|
||||
|
||||
fn key(&self) -> Self::Key {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Summary for ExcerptId {
|
||||
type Context = ();
|
||||
|
||||
fn add_summary(&mut self, other: &Self, _: &()) {
|
||||
*self = *other;
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Summary for ExcerptSummary {
|
||||
type Context = ();
|
||||
|
||||
fn add_summary(&mut self, summary: &Self, _: &()) {
|
||||
debug_assert!(summary.excerpt_id > self.excerpt_id);
|
||||
self.excerpt_id = summary.excerpt_id.clone();
|
||||
debug_assert!(summary.excerpt_locator > self.excerpt_locator);
|
||||
self.excerpt_locator = summary.excerpt_locator.clone();
|
||||
self.text.add_summary(&summary.text, &());
|
||||
self.max_buffer_row = cmp::max(self.max_buffer_row, summary.max_buffer_row);
|
||||
}
|
||||
@@ -3067,9 +3176,15 @@ impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for usize {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for Option<&'a ExcerptId> {
|
||||
impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, Option<&'a Locator>> for Locator {
|
||||
fn cmp(&self, cursor_location: &Option<&'a Locator>, _: &()) -> cmp::Ordering {
|
||||
Ord::cmp(&Some(self), cursor_location)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for Locator {
|
||||
fn cmp(&self, cursor_location: &ExcerptSummary, _: &()) -> cmp::Ordering {
|
||||
Ord::cmp(self, &Some(&cursor_location.excerpt_id))
|
||||
Ord::cmp(self, &cursor_location.excerpt_locator)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3091,9 +3206,15 @@ impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for PointUtf16 {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option<&'a ExcerptId> {
|
||||
impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option<&'a Locator> {
|
||||
fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) {
|
||||
*self = Some(&summary.excerpt_id);
|
||||
*self = Some(&summary.excerpt_locator);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option<ExcerptId> {
|
||||
fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) {
|
||||
*self = Some(summary.excerpt_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3274,12 +3395,6 @@ impl ToOffset for Point {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOffset for PointUtf16 {
|
||||
fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize {
|
||||
snapshot.point_utf16_to_offset(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOffset for usize {
|
||||
fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize {
|
||||
assert!(*self <= snapshot.len(), "offset is out of range");
|
||||
@@ -3293,6 +3408,12 @@ impl ToOffset for OffsetUtf16 {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOffset for PointUtf16 {
|
||||
fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize {
|
||||
snapshot.point_utf16_to_offset(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOffsetUtf16 for OffsetUtf16 {
|
||||
fn to_offset_utf16(&self, _snapshot: &MultiBufferSnapshot) -> OffsetUtf16 {
|
||||
*self
|
||||
@@ -3591,7 +3712,7 @@ mod tests {
|
||||
let snapshot = multibuffer.update(cx, |multibuffer, cx| {
|
||||
let (buffer_2_excerpt_id, _) =
|
||||
multibuffer.excerpts_for_buffer(&buffer_2, cx)[0].clone();
|
||||
multibuffer.remove_excerpts(&[buffer_2_excerpt_id], cx);
|
||||
multibuffer.remove_excerpts([buffer_2_excerpt_id], cx);
|
||||
multibuffer.snapshot(cx)
|
||||
});
|
||||
|
||||
@@ -3780,7 +3901,7 @@ mod tests {
|
||||
|
||||
// Replace the buffer 1 excerpt with new excerpts from buffer 2.
|
||||
let (excerpt_id_2, excerpt_id_3) = multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.remove_excerpts([&excerpt_id_1], cx);
|
||||
multibuffer.remove_excerpts([excerpt_id_1], cx);
|
||||
let mut ids = multibuffer
|
||||
.push_excerpts(
|
||||
buffer_2.clone(),
|
||||
@@ -3810,9 +3931,8 @@ mod tests {
|
||||
assert_ne!(excerpt_id_2, excerpt_id_1);
|
||||
|
||||
// Resolve some anchors from the previous snapshot in the new snapshot.
|
||||
// Although there is still an excerpt with the same id, it is for
|
||||
// a different buffer, so we don't attempt to resolve the old text
|
||||
// anchor in the new buffer.
|
||||
// The current excerpts are from a different buffer, so we don't attempt to
|
||||
// resolve the old text anchor in the new buffer.
|
||||
assert_eq!(
|
||||
snapshot_2.summary_for_anchor::<usize>(&snapshot_1.anchor_before(2)),
|
||||
0
|
||||
@@ -3824,6 +3944,9 @@ mod tests {
|
||||
]),
|
||||
vec![0, 0]
|
||||
);
|
||||
|
||||
// Refresh anchors from the old snapshot. The return value indicates that both
|
||||
// anchors lost their original excerpt.
|
||||
let refresh =
|
||||
snapshot_2.refresh_anchors(&[snapshot_1.anchor_before(2), snapshot_1.anchor_after(3)]);
|
||||
assert_eq!(
|
||||
@@ -3837,10 +3960,10 @@ mod tests {
|
||||
// Replace the middle excerpt with a smaller excerpt in buffer 2,
|
||||
// that intersects the old excerpt.
|
||||
let excerpt_id_5 = multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.remove_excerpts([&excerpt_id_3], cx);
|
||||
multibuffer.remove_excerpts([excerpt_id_3], cx);
|
||||
multibuffer
|
||||
.insert_excerpts_after(
|
||||
&excerpt_id_3,
|
||||
excerpt_id_2,
|
||||
buffer_2.clone(),
|
||||
[ExcerptRange {
|
||||
context: 5..8,
|
||||
@@ -3857,8 +3980,8 @@ mod tests {
|
||||
assert_ne!(excerpt_id_5, excerpt_id_3);
|
||||
|
||||
// Resolve some anchors from the previous snapshot in the new snapshot.
|
||||
// The anchor in the middle excerpt snaps to the beginning of the
|
||||
// excerpt, since it is not
|
||||
// The third anchor can't be resolved, since its excerpt has been removed,
|
||||
// so it resolves to the same position as its predecessor.
|
||||
let anchors = [
|
||||
snapshot_2.anchor_before(0),
|
||||
snapshot_2.anchor_after(2),
|
||||
@@ -3867,7 +3990,7 @@ mod tests {
|
||||
];
|
||||
assert_eq!(
|
||||
snapshot_3.summaries_for_anchors::<usize, _>(&anchors),
|
||||
&[0, 2, 5, 13]
|
||||
&[0, 2, 9, 13]
|
||||
);
|
||||
|
||||
let new_anchors = snapshot_3.refresh_anchors(&anchors);
|
||||
@@ -3889,7 +4012,7 @@ mod tests {
|
||||
|
||||
let mut buffers: Vec<ModelHandle<Buffer>> = Vec::new();
|
||||
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
|
||||
let mut excerpt_ids = Vec::new();
|
||||
let mut excerpt_ids = Vec::<ExcerptId>::new();
|
||||
let mut expected_excerpts = Vec::<(ModelHandle<Buffer>, Range<text::Anchor>)>::new();
|
||||
let mut anchors = Vec::new();
|
||||
let mut old_versions = Vec::new();
|
||||
@@ -3919,9 +4042,11 @@ mod tests {
|
||||
.collect::<String>(),
|
||||
);
|
||||
}
|
||||
ids_to_remove.sort_unstable();
|
||||
let snapshot = multibuffer.read(cx).read(cx);
|
||||
ids_to_remove.sort_unstable_by(|a, b| a.cmp(&b, &snapshot));
|
||||
drop(snapshot);
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.remove_excerpts(&ids_to_remove, cx)
|
||||
multibuffer.remove_excerpts(ids_to_remove, cx)
|
||||
});
|
||||
}
|
||||
30..=39 if !expected_excerpts.is_empty() => {
|
||||
@@ -3945,7 +4070,6 @@ mod tests {
|
||||
// Ensure the newly-refreshed anchors point to a valid excerpt and don't
|
||||
// overshoot its boundaries.
|
||||
assert_eq!(anchors.len(), prev_len);
|
||||
let mut cursor = multibuffer.excerpts.cursor::<Option<&ExcerptId>>();
|
||||
for anchor in &anchors {
|
||||
if anchor.excerpt_id == ExcerptId::min()
|
||||
|| anchor.excerpt_id == ExcerptId::max()
|
||||
@@ -3953,8 +4077,7 @@ mod tests {
|
||||
continue;
|
||||
}
|
||||
|
||||
cursor.seek_forward(&Some(&anchor.excerpt_id), Bias::Left, &());
|
||||
let excerpt = cursor.item().unwrap();
|
||||
let excerpt = multibuffer.excerpt(anchor.excerpt_id).unwrap();
|
||||
assert_eq!(excerpt.id, anchor.excerpt_id);
|
||||
assert!(excerpt.contains(anchor));
|
||||
}
|
||||
@@ -3994,7 +4117,7 @@ mod tests {
|
||||
let excerpt_id = multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer
|
||||
.insert_excerpts_after(
|
||||
&prev_excerpt_id,
|
||||
prev_excerpt_id,
|
||||
buffer_handle.clone(),
|
||||
[ExcerptRange {
|
||||
context: start_ix..end_ix,
|
||||
@@ -4158,12 +4281,14 @@ mod tests {
|
||||
}
|
||||
|
||||
for _ in 0..ch.len_utf16() {
|
||||
let left_point_utf16 = snapshot.clip_point_utf16(point_utf16, Bias::Left);
|
||||
let right_point_utf16 = snapshot.clip_point_utf16(point_utf16, Bias::Right);
|
||||
let left_point_utf16 =
|
||||
snapshot.clip_point_utf16(Unclipped(point_utf16), Bias::Left);
|
||||
let right_point_utf16 =
|
||||
snapshot.clip_point_utf16(Unclipped(point_utf16), Bias::Right);
|
||||
let buffer_left_point_utf16 =
|
||||
buffer.clip_point_utf16(buffer_point_utf16, Bias::Left);
|
||||
buffer.clip_point_utf16(Unclipped(buffer_point_utf16), Bias::Left);
|
||||
let buffer_right_point_utf16 =
|
||||
buffer.clip_point_utf16(buffer_point_utf16, Bias::Right);
|
||||
buffer.clip_point_utf16(Unclipped(buffer_point_utf16), Bias::Right);
|
||||
assert_eq!(
|
||||
left_point_utf16,
|
||||
excerpt_start.lines_utf16()
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::{
|
||||
};
|
||||
use sum_tree::Bias;
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)]
|
||||
pub struct Anchor {
|
||||
pub(crate) buffer_id: Option<usize>,
|
||||
pub(crate) excerpt_id: ExcerptId,
|
||||
@@ -30,16 +30,16 @@ impl Anchor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn excerpt_id(&self) -> &ExcerptId {
|
||||
&self.excerpt_id
|
||||
pub fn excerpt_id(&self) -> ExcerptId {
|
||||
self.excerpt_id
|
||||
}
|
||||
|
||||
pub fn cmp(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Ordering {
|
||||
let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id);
|
||||
let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id, snapshot);
|
||||
if excerpt_id_cmp.is_eq() {
|
||||
if self.excerpt_id == ExcerptId::min() || self.excerpt_id == ExcerptId::max() {
|
||||
Ordering::Equal
|
||||
} else if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) {
|
||||
} else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
|
||||
self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer)
|
||||
} else {
|
||||
Ordering::Equal
|
||||
@@ -51,7 +51,7 @@ impl Anchor {
|
||||
|
||||
pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
|
||||
if self.text_anchor.bias != Bias::Left {
|
||||
if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) {
|
||||
if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
|
||||
return Self {
|
||||
buffer_id: self.buffer_id,
|
||||
excerpt_id: self.excerpt_id.clone(),
|
||||
@@ -64,7 +64,7 @@ impl Anchor {
|
||||
|
||||
pub fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
|
||||
if self.text_anchor.bias != Bias::Right {
|
||||
if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) {
|
||||
if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
|
||||
return Self {
|
||||
buffer_id: self.buffer_id,
|
||||
excerpt_id: self.excerpt_id.clone(),
|
||||
|
||||
36
crates/editor/src/persistence.rs
Normal file
36
crates/editor/src/persistence.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use db::sqlez_macros::sql;
|
||||
use db::{define_connection, query};
|
||||
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
||||
|
||||
define_connection!(
|
||||
pub static ref DB: EditorDb<WorkspaceDb> =
|
||||
&[sql! (
|
||||
CREATE TABLE editors(
|
||||
item_id INTEGER NOT NULL,
|
||||
workspace_id INTEGER NOT NULL,
|
||||
path BLOB NOT NULL,
|
||||
PRIMARY KEY(item_id, workspace_id),
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
) STRICT;
|
||||
)];
|
||||
);
|
||||
|
||||
impl EditorDb {
|
||||
query! {
|
||||
pub fn get_path(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
|
||||
SELECT path FROM editors
|
||||
WHERE item_id = ? AND workspace_id = ?
|
||||
}
|
||||
}
|
||||
|
||||
query! {
|
||||
pub async fn save_path(item_id: ItemId, workspace_id: WorkspaceId, path: PathBuf) -> Result<()> {
|
||||
INSERT OR REPLACE INTO editors(item_id, workspace_id, path)
|
||||
VALUES (?, ?, ?)
|
||||
}
|
||||
}
|
||||
}
|
||||
348
crates/editor/src/scroll.rs
Normal file
348
crates/editor/src/scroll.rs
Normal file
@@ -0,0 +1,348 @@
|
||||
pub mod actions;
|
||||
pub mod autoscroll;
|
||||
pub mod scroll_amount;
|
||||
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use gpui::{
|
||||
geometry::vector::{vec2f, Vector2F},
|
||||
Axis, MutableAppContext, Task, ViewContext,
|
||||
};
|
||||
use language::Bias;
|
||||
|
||||
use crate::{
|
||||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||
hover_popover::hide_hover,
|
||||
Anchor, DisplayPoint, Editor, EditorMode, Event, MultiBufferSnapshot, ToPoint,
|
||||
};
|
||||
|
||||
use self::{
|
||||
autoscroll::{Autoscroll, AutoscrollStrategy},
|
||||
scroll_amount::ScrollAmount,
|
||||
};
|
||||
|
||||
pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
|
||||
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ScrollbarAutoHide(pub bool);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct ScrollAnchor {
|
||||
pub offset: Vector2F,
|
||||
pub top_anchor: Anchor,
|
||||
}
|
||||
|
||||
impl ScrollAnchor {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
offset: Vector2F::zero(),
|
||||
top_anchor: Anchor::min(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F {
|
||||
let mut scroll_position = self.offset;
|
||||
if self.top_anchor != Anchor::min() {
|
||||
let scroll_top = self.top_anchor.to_display_point(snapshot).row() as f32;
|
||||
scroll_position.set_y(scroll_top + scroll_position.y());
|
||||
} else {
|
||||
scroll_position.set_y(0.);
|
||||
}
|
||||
scroll_position
|
||||
}
|
||||
|
||||
pub fn top_row(&self, buffer: &MultiBufferSnapshot) -> u32 {
|
||||
self.top_anchor.to_point(buffer).row
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct OngoingScroll {
|
||||
last_event: Instant,
|
||||
axis: Option<Axis>,
|
||||
}
|
||||
|
||||
impl OngoingScroll {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
last_event: Instant::now() - SCROLL_EVENT_SEPARATION,
|
||||
axis: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn filter(&self, delta: &mut Vector2F) -> Option<Axis> {
|
||||
const UNLOCK_PERCENT: f32 = 1.9;
|
||||
const UNLOCK_LOWER_BOUND: f32 = 6.;
|
||||
let mut axis = self.axis;
|
||||
|
||||
let x = delta.x().abs();
|
||||
let y = delta.y().abs();
|
||||
let duration = Instant::now().duration_since(self.last_event);
|
||||
if duration > SCROLL_EVENT_SEPARATION {
|
||||
//New ongoing scroll will start, determine axis
|
||||
axis = if x <= y {
|
||||
Some(Axis::Vertical)
|
||||
} else {
|
||||
Some(Axis::Horizontal)
|
||||
};
|
||||
} else if x.max(y) >= UNLOCK_LOWER_BOUND {
|
||||
//Check if the current ongoing will need to unlock
|
||||
match axis {
|
||||
Some(Axis::Vertical) => {
|
||||
if x > y && x >= y * UNLOCK_PERCENT {
|
||||
axis = None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(Axis::Horizontal) => {
|
||||
if y > x && y >= x * UNLOCK_PERCENT {
|
||||
axis = None;
|
||||
}
|
||||
}
|
||||
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
match axis {
|
||||
Some(Axis::Vertical) => *delta = vec2f(0., delta.y()),
|
||||
Some(Axis::Horizontal) => *delta = vec2f(delta.x(), 0.),
|
||||
None => {}
|
||||
}
|
||||
|
||||
axis
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ScrollManager {
|
||||
vertical_scroll_margin: f32,
|
||||
anchor: ScrollAnchor,
|
||||
ongoing: OngoingScroll,
|
||||
autoscroll_request: Option<(Autoscroll, bool)>,
|
||||
last_autoscroll: Option<(Vector2F, f32, f32, AutoscrollStrategy)>,
|
||||
show_scrollbars: bool,
|
||||
hide_scrollbar_task: Option<Task<()>>,
|
||||
visible_line_count: Option<f32>,
|
||||
}
|
||||
|
||||
impl ScrollManager {
|
||||
pub fn new() -> Self {
|
||||
ScrollManager {
|
||||
vertical_scroll_margin: 3.0,
|
||||
anchor: ScrollAnchor::new(),
|
||||
ongoing: OngoingScroll::new(),
|
||||
autoscroll_request: None,
|
||||
show_scrollbars: true,
|
||||
hide_scrollbar_task: None,
|
||||
last_autoscroll: None,
|
||||
visible_line_count: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clone_state(&mut self, other: &Self) {
|
||||
self.anchor = other.anchor;
|
||||
self.ongoing = other.ongoing;
|
||||
}
|
||||
|
||||
pub fn anchor(&self) -> ScrollAnchor {
|
||||
self.anchor
|
||||
}
|
||||
|
||||
pub fn ongoing_scroll(&self) -> OngoingScroll {
|
||||
self.ongoing
|
||||
}
|
||||
|
||||
pub fn update_ongoing_scroll(&mut self, axis: Option<Axis>) {
|
||||
self.ongoing.last_event = Instant::now();
|
||||
self.ongoing.axis = axis;
|
||||
}
|
||||
|
||||
pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F {
|
||||
self.anchor.scroll_position(snapshot)
|
||||
}
|
||||
|
||||
fn set_scroll_position(
|
||||
&mut self,
|
||||
scroll_position: Vector2F,
|
||||
map: &DisplaySnapshot,
|
||||
local: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
let new_anchor = if scroll_position.y() <= 0. {
|
||||
ScrollAnchor {
|
||||
top_anchor: Anchor::min(),
|
||||
offset: scroll_position.max(vec2f(0., 0.)),
|
||||
}
|
||||
} else {
|
||||
let scroll_top_buffer_offset =
|
||||
DisplayPoint::new(scroll_position.y() as u32, 0).to_offset(&map, Bias::Right);
|
||||
let top_anchor = map
|
||||
.buffer_snapshot
|
||||
.anchor_at(scroll_top_buffer_offset, Bias::Right);
|
||||
|
||||
ScrollAnchor {
|
||||
top_anchor,
|
||||
offset: vec2f(
|
||||
scroll_position.x(),
|
||||
scroll_position.y() - top_anchor.to_display_point(&map).row() as f32,
|
||||
),
|
||||
}
|
||||
};
|
||||
|
||||
self.set_anchor(new_anchor, local, cx);
|
||||
}
|
||||
|
||||
fn set_anchor(&mut self, anchor: ScrollAnchor, local: bool, cx: &mut ViewContext<Editor>) {
|
||||
self.anchor = anchor;
|
||||
cx.emit(Event::ScrollPositionChanged { local });
|
||||
self.show_scrollbar(cx);
|
||||
self.autoscroll_request.take();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn show_scrollbar(&mut self, cx: &mut ViewContext<Editor>) {
|
||||
if !self.show_scrollbars {
|
||||
self.show_scrollbars = true;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
if cx.default_global::<ScrollbarAutoHide>().0 {
|
||||
self.hide_scrollbar_task = Some(cx.spawn_weak(|editor, mut cx| async move {
|
||||
cx.background().timer(SCROLLBAR_SHOW_INTERVAL).await;
|
||||
if let Some(editor) = editor.upgrade(&cx) {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
editor.scroll_manager.show_scrollbars = false;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
self.hide_scrollbar_task = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scrollbars_visible(&self) -> bool {
|
||||
self.show_scrollbars
|
||||
}
|
||||
|
||||
pub fn has_autoscroll_request(&self) -> bool {
|
||||
self.autoscroll_request.is_some()
|
||||
}
|
||||
|
||||
pub fn clamp_scroll_left(&mut self, max: f32) -> bool {
|
||||
if max < self.anchor.offset.x() {
|
||||
self.anchor.offset.set_x(max);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn vertical_scroll_margin(&mut self) -> usize {
|
||||
self.scroll_manager.vertical_scroll_margin as usize
|
||||
}
|
||||
|
||||
pub fn set_vertical_scroll_margin(&mut self, margin_rows: usize, cx: &mut ViewContext<Self>) {
|
||||
self.scroll_manager.vertical_scroll_margin = margin_rows as f32;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn visible_line_count(&self) -> Option<f32> {
|
||||
self.scroll_manager.visible_line_count
|
||||
}
|
||||
|
||||
pub(crate) fn set_visible_line_count(&mut self, lines: f32) {
|
||||
self.scroll_manager.visible_line_count = Some(lines)
|
||||
}
|
||||
|
||||
pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
|
||||
self.set_scroll_position_internal(scroll_position, true, cx);
|
||||
}
|
||||
|
||||
pub(crate) fn set_scroll_position_internal(
|
||||
&mut self,
|
||||
scroll_position: Vector2F,
|
||||
local: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
hide_hover(self, cx);
|
||||
self.scroll_manager
|
||||
.set_scroll_position(scroll_position, &map, local, cx);
|
||||
}
|
||||
|
||||
pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
self.scroll_manager.anchor.scroll_position(&display_map)
|
||||
}
|
||||
|
||||
pub fn set_scroll_anchor(&mut self, scroll_anchor: ScrollAnchor, cx: &mut ViewContext<Self>) {
|
||||
hide_hover(self, cx);
|
||||
self.scroll_manager.set_anchor(scroll_anchor, true, cx);
|
||||
}
|
||||
|
||||
pub(crate) fn set_scroll_anchor_remote(
|
||||
&mut self,
|
||||
scroll_anchor: ScrollAnchor,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
hide_hover(self, cx);
|
||||
self.scroll_manager.set_anchor(scroll_anchor, false, cx);
|
||||
}
|
||||
|
||||
pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate_action();
|
||||
return;
|
||||
}
|
||||
|
||||
if self.take_rename(true, cx).is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if amount.move_context_menu_selection(self, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cur_position = self.scroll_position(cx);
|
||||
let new_pos = cur_position + vec2f(0., amount.lines(self) - 1.);
|
||||
self.set_scroll_position(new_pos, cx);
|
||||
}
|
||||
|
||||
/// Returns an ordering. The newest selection is:
|
||||
/// Ordering::Equal => on screen
|
||||
/// Ordering::Less => above the screen
|
||||
/// Ordering::Greater => below the screen
|
||||
pub fn newest_selection_on_screen(&self, cx: &mut MutableAppContext) -> Ordering {
|
||||
let snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let newest_head = self
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.head()
|
||||
.to_display_point(&snapshot);
|
||||
let screen_top = self
|
||||
.scroll_manager
|
||||
.anchor
|
||||
.top_anchor
|
||||
.to_display_point(&snapshot);
|
||||
|
||||
if screen_top > newest_head {
|
||||
return Ordering::Less;
|
||||
}
|
||||
|
||||
if let Some(visible_lines) = self.visible_line_count() {
|
||||
if newest_head.row() < screen_top.row() + visible_lines as u32 {
|
||||
return Ordering::Equal;
|
||||
}
|
||||
}
|
||||
|
||||
Ordering::Greater
|
||||
}
|
||||
}
|
||||
159
crates/editor/src/scroll/actions.rs
Normal file
159
crates/editor/src/scroll/actions.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
use gpui::{
|
||||
actions, geometry::vector::Vector2F, impl_internal_actions, Axis, MutableAppContext,
|
||||
ViewContext,
|
||||
};
|
||||
use language::Bias;
|
||||
|
||||
use crate::{Editor, EditorMode};
|
||||
|
||||
use super::{autoscroll::Autoscroll, scroll_amount::ScrollAmount, ScrollAnchor};
|
||||
|
||||
actions!(
|
||||
editor,
|
||||
[
|
||||
LineDown,
|
||||
LineUp,
|
||||
HalfPageDown,
|
||||
HalfPageUp,
|
||||
PageDown,
|
||||
PageUp,
|
||||
NextScreen,
|
||||
ScrollCursorTop,
|
||||
ScrollCursorCenter,
|
||||
ScrollCursorBottom,
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct Scroll {
|
||||
pub scroll_position: Vector2F,
|
||||
pub axis: Option<Axis>,
|
||||
}
|
||||
|
||||
impl_internal_actions!(editor, [Scroll]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(Editor::next_screen);
|
||||
cx.add_action(Editor::scroll);
|
||||
cx.add_action(Editor::scroll_cursor_top);
|
||||
cx.add_action(Editor::scroll_cursor_center);
|
||||
cx.add_action(Editor::scroll_cursor_bottom);
|
||||
cx.add_action(|this: &mut Editor, _: &LineDown, cx| {
|
||||
this.scroll_screen(&ScrollAmount::LineDown, cx)
|
||||
});
|
||||
cx.add_action(|this: &mut Editor, _: &LineUp, cx| {
|
||||
this.scroll_screen(&ScrollAmount::LineUp, cx)
|
||||
});
|
||||
cx.add_action(|this: &mut Editor, _: &HalfPageDown, cx| {
|
||||
this.scroll_screen(&ScrollAmount::HalfPageDown, cx)
|
||||
});
|
||||
cx.add_action(|this: &mut Editor, _: &HalfPageUp, cx| {
|
||||
this.scroll_screen(&ScrollAmount::HalfPageUp, cx)
|
||||
});
|
||||
cx.add_action(|this: &mut Editor, _: &PageDown, cx| {
|
||||
this.scroll_screen(&ScrollAmount::PageDown, cx)
|
||||
});
|
||||
cx.add_action(|this: &mut Editor, _: &PageUp, cx| {
|
||||
this.scroll_screen(&ScrollAmount::PageUp, cx)
|
||||
});
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn next_screen(&mut self, _: &NextScreen, cx: &mut ViewContext<Editor>) -> Option<()> {
|
||||
if self.take_rename(true, cx).is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.mouse_context_menu.read(cx).visible() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate_action();
|
||||
return None;
|
||||
}
|
||||
self.request_autoscroll(Autoscroll::Next, cx);
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn scroll(&mut self, action: &Scroll, cx: &mut ViewContext<Self>) {
|
||||
self.scroll_manager.update_ongoing_scroll(action.axis);
|
||||
self.set_scroll_position(action.scroll_position, cx);
|
||||
}
|
||||
|
||||
fn scroll_cursor_top(editor: &mut Editor, _: &ScrollCursorTop, cx: &mut ViewContext<Editor>) {
|
||||
let snapshot = editor.snapshot(cx).display_snapshot;
|
||||
let scroll_margin_rows = editor.vertical_scroll_margin() as u32;
|
||||
|
||||
let mut new_screen_top = editor.selections.newest_display(cx).head();
|
||||
*new_screen_top.row_mut() = new_screen_top.row().saturating_sub(scroll_margin_rows);
|
||||
*new_screen_top.column_mut() = 0;
|
||||
let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
|
||||
let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top);
|
||||
|
||||
editor.set_scroll_anchor(
|
||||
ScrollAnchor {
|
||||
top_anchor: new_anchor,
|
||||
offset: Default::default(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
fn scroll_cursor_center(
|
||||
editor: &mut Editor,
|
||||
_: &ScrollCursorCenter,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
let snapshot = editor.snapshot(cx).display_snapshot;
|
||||
let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
|
||||
visible_rows as u32
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut new_screen_top = editor.selections.newest_display(cx).head();
|
||||
*new_screen_top.row_mut() = new_screen_top.row().saturating_sub(visible_rows / 2);
|
||||
*new_screen_top.column_mut() = 0;
|
||||
let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
|
||||
let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top);
|
||||
|
||||
editor.set_scroll_anchor(
|
||||
ScrollAnchor {
|
||||
top_anchor: new_anchor,
|
||||
offset: Default::default(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
fn scroll_cursor_bottom(
|
||||
editor: &mut Editor,
|
||||
_: &ScrollCursorBottom,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
let snapshot = editor.snapshot(cx).display_snapshot;
|
||||
let scroll_margin_rows = editor.vertical_scroll_margin() as u32;
|
||||
let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
|
||||
visible_rows as u32
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut new_screen_top = editor.selections.newest_display(cx).head();
|
||||
*new_screen_top.row_mut() = new_screen_top
|
||||
.row()
|
||||
.saturating_sub(visible_rows.saturating_sub(scroll_margin_rows));
|
||||
*new_screen_top.column_mut() = 0;
|
||||
let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
|
||||
let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top);
|
||||
|
||||
editor.set_scroll_anchor(
|
||||
ScrollAnchor {
|
||||
top_anchor: new_anchor,
|
||||
offset: Default::default(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}
|
||||
246
crates/editor/src/scroll/autoscroll.rs
Normal file
246
crates/editor/src/scroll/autoscroll.rs
Normal file
@@ -0,0 +1,246 @@
|
||||
use std::cmp;
|
||||
|
||||
use gpui::{text_layout, ViewContext};
|
||||
use language::Point;
|
||||
|
||||
use crate::{display_map::ToDisplayPoint, Editor, EditorMode};
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum Autoscroll {
|
||||
Next,
|
||||
Strategy(AutoscrollStrategy),
|
||||
}
|
||||
|
||||
impl Autoscroll {
|
||||
pub fn fit() -> Self {
|
||||
Self::Strategy(AutoscrollStrategy::Fit)
|
||||
}
|
||||
|
||||
pub fn newest() -> Self {
|
||||
Self::Strategy(AutoscrollStrategy::Newest)
|
||||
}
|
||||
|
||||
pub fn center() -> Self {
|
||||
Self::Strategy(AutoscrollStrategy::Center)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Default)]
|
||||
pub enum AutoscrollStrategy {
|
||||
Fit,
|
||||
Newest,
|
||||
#[default]
|
||||
Center,
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
impl AutoscrollStrategy {
|
||||
fn next(&self) -> Self {
|
||||
match self {
|
||||
AutoscrollStrategy::Center => AutoscrollStrategy::Top,
|
||||
AutoscrollStrategy::Top => AutoscrollStrategy::Bottom,
|
||||
_ => AutoscrollStrategy::Center,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn autoscroll_vertically(
|
||||
&mut self,
|
||||
viewport_height: f32,
|
||||
line_height: f32,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> bool {
|
||||
let visible_lines = viewport_height / line_height;
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
|
||||
let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
|
||||
(display_map.max_point().row() as f32 - visible_lines + 1.).max(0.)
|
||||
} else {
|
||||
display_map.max_point().row() as f32
|
||||
};
|
||||
if scroll_position.y() > max_scroll_top {
|
||||
scroll_position.set_y(max_scroll_top);
|
||||
self.set_scroll_position(scroll_position, cx);
|
||||
}
|
||||
|
||||
let (autoscroll, local) =
|
||||
if let Some(autoscroll) = self.scroll_manager.autoscroll_request.take() {
|
||||
autoscroll
|
||||
} else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let first_cursor_top;
|
||||
let last_cursor_bottom;
|
||||
if let Some(highlighted_rows) = &self.highlighted_rows {
|
||||
first_cursor_top = highlighted_rows.start as f32;
|
||||
last_cursor_bottom = first_cursor_top + 1.;
|
||||
} else if autoscroll == Autoscroll::newest() {
|
||||
let newest_selection = self.selections.newest::<Point>(cx);
|
||||
first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32;
|
||||
last_cursor_bottom = first_cursor_top + 1.;
|
||||
} else {
|
||||
let selections = self.selections.all::<Point>(cx);
|
||||
first_cursor_top = selections
|
||||
.first()
|
||||
.unwrap()
|
||||
.head()
|
||||
.to_display_point(&display_map)
|
||||
.row() as f32;
|
||||
last_cursor_bottom = selections
|
||||
.last()
|
||||
.unwrap()
|
||||
.head()
|
||||
.to_display_point(&display_map)
|
||||
.row() as f32
|
||||
+ 1.0;
|
||||
}
|
||||
|
||||
let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
|
||||
0.
|
||||
} else {
|
||||
((visible_lines - (last_cursor_bottom - first_cursor_top)) / 2.0).floor()
|
||||
};
|
||||
if margin < 0.0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let strategy = match autoscroll {
|
||||
Autoscroll::Strategy(strategy) => strategy,
|
||||
Autoscroll::Next => {
|
||||
let last_autoscroll = &self.scroll_manager.last_autoscroll;
|
||||
if let Some(last_autoscroll) = last_autoscroll {
|
||||
if self.scroll_manager.anchor.offset == last_autoscroll.0
|
||||
&& first_cursor_top == last_autoscroll.1
|
||||
&& last_cursor_bottom == last_autoscroll.2
|
||||
{
|
||||
last_autoscroll.3.next()
|
||||
} else {
|
||||
AutoscrollStrategy::default()
|
||||
}
|
||||
} else {
|
||||
AutoscrollStrategy::default()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match strategy {
|
||||
AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => {
|
||||
let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
|
||||
let target_top = (first_cursor_top - margin).max(0.0);
|
||||
let target_bottom = last_cursor_bottom + margin;
|
||||
let start_row = scroll_position.y();
|
||||
let end_row = start_row + visible_lines;
|
||||
|
||||
if target_top < start_row {
|
||||
scroll_position.set_y(target_top);
|
||||
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||
} else if target_bottom >= end_row {
|
||||
scroll_position.set_y(target_bottom - visible_lines);
|
||||
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||
}
|
||||
}
|
||||
AutoscrollStrategy::Center => {
|
||||
scroll_position.set_y((first_cursor_top - margin).max(0.0));
|
||||
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||
}
|
||||
AutoscrollStrategy::Top => {
|
||||
scroll_position.set_y((first_cursor_top).max(0.0));
|
||||
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||
}
|
||||
AutoscrollStrategy::Bottom => {
|
||||
scroll_position.set_y((last_cursor_bottom - visible_lines).max(0.0));
|
||||
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||
}
|
||||
}
|
||||
|
||||
self.scroll_manager.last_autoscroll = Some((
|
||||
self.scroll_manager.anchor.offset,
|
||||
first_cursor_top,
|
||||
last_cursor_bottom,
|
||||
strategy,
|
||||
));
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn autoscroll_horizontally(
|
||||
&mut self,
|
||||
start_row: u32,
|
||||
viewport_width: f32,
|
||||
scroll_width: f32,
|
||||
max_glyph_width: f32,
|
||||
layouts: &[text_layout::Line],
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> bool {
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let selections = self.selections.all::<Point>(cx);
|
||||
|
||||
let mut target_left;
|
||||
let mut target_right;
|
||||
|
||||
if self.highlighted_rows.is_some() {
|
||||
target_left = 0.0_f32;
|
||||
target_right = 0.0_f32;
|
||||
} else {
|
||||
target_left = std::f32::INFINITY;
|
||||
target_right = 0.0_f32;
|
||||
for selection in selections {
|
||||
let head = selection.head().to_display_point(&display_map);
|
||||
if head.row() >= start_row && head.row() < start_row + layouts.len() as u32 {
|
||||
let start_column = head.column().saturating_sub(3);
|
||||
let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3);
|
||||
target_left = target_left.min(
|
||||
layouts[(head.row() - start_row) as usize]
|
||||
.x_for_index(start_column as usize),
|
||||
);
|
||||
target_right = target_right.max(
|
||||
layouts[(head.row() - start_row) as usize].x_for_index(end_column as usize)
|
||||
+ max_glyph_width,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
target_right = target_right.min(scroll_width);
|
||||
|
||||
if target_right - target_left > viewport_width {
|
||||
return false;
|
||||
}
|
||||
|
||||
let scroll_left = self.scroll_manager.anchor.offset.x() * max_glyph_width;
|
||||
let scroll_right = scroll_left + viewport_width;
|
||||
|
||||
if target_left < scroll_left {
|
||||
self.scroll_manager
|
||||
.anchor
|
||||
.offset
|
||||
.set_x(target_left / max_glyph_width);
|
||||
true
|
||||
} else if target_right > scroll_right {
|
||||
self.scroll_manager
|
||||
.anchor
|
||||
.offset
|
||||
.set_x((target_right - viewport_width) / max_glyph_width);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext<Self>) {
|
||||
self.scroll_manager.autoscroll_request = Some((autoscroll, true));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn request_autoscroll_remotely(
|
||||
&mut self,
|
||||
autoscroll: Autoscroll,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.scroll_manager.autoscroll_request = Some((autoscroll, false));
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
48
crates/editor/src/scroll/scroll_amount.rs
Normal file
48
crates/editor/src/scroll/scroll_amount.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use gpui::ViewContext;
|
||||
use serde::Deserialize;
|
||||
use util::iife;
|
||||
|
||||
use crate::Editor;
|
||||
|
||||
#[derive(Clone, PartialEq, Deserialize)]
|
||||
pub enum ScrollAmount {
|
||||
LineUp,
|
||||
LineDown,
|
||||
HalfPageUp,
|
||||
HalfPageDown,
|
||||
PageUp,
|
||||
PageDown,
|
||||
}
|
||||
|
||||
impl ScrollAmount {
|
||||
pub fn move_context_menu_selection(
|
||||
&self,
|
||||
editor: &mut Editor,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> bool {
|
||||
iife!({
|
||||
let context_menu = editor.context_menu.as_mut()?;
|
||||
|
||||
match self {
|
||||
Self::LineDown | Self::HalfPageDown => context_menu.select_next(cx),
|
||||
Self::LineUp | Self::HalfPageUp => context_menu.select_prev(cx),
|
||||
Self::PageDown => context_menu.select_last(cx),
|
||||
Self::PageUp => context_menu.select_first(cx),
|
||||
}
|
||||
.then_some(())
|
||||
})
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn lines(&self, editor: &mut Editor) -> f32 {
|
||||
match self {
|
||||
Self::LineDown => 1.,
|
||||
Self::LineUp => -1.,
|
||||
Self::HalfPageDown => editor.visible_line_count().map(|l| l / 2.).unwrap_or(1.),
|
||||
Self::HalfPageUp => -editor.visible_line_count().map(|l| l / 2.).unwrap_or(1.),
|
||||
// Minus 1. here so that there is a pivot line that stays on the screen
|
||||
Self::PageDown => editor.visible_line_count().unwrap_or(1.) - 1.,
|
||||
Self::PageUp => -editor.visible_line_count().unwrap_or(1.) - 1.,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ impl SelectionsCollection {
|
||||
self.buffer.read(cx).read(cx)
|
||||
}
|
||||
|
||||
pub fn set_state(&mut self, other: &SelectionsCollection) {
|
||||
pub fn clone_state(&mut self, other: &SelectionsCollection) {
|
||||
self.next_selection_id = other.next_selection_id;
|
||||
self.line_mode = other.line_mode;
|
||||
self.disjoint = other.disjoint.clone();
|
||||
@@ -544,11 +544,21 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
T: ToOffset,
|
||||
{
|
||||
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
|
||||
let ranges = ranges
|
||||
.into_iter()
|
||||
.map(|range| range.start.to_offset(&buffer)..range.end.to_offset(&buffer));
|
||||
self.select_offset_ranges(ranges);
|
||||
}
|
||||
|
||||
fn select_offset_ranges<I>(&mut self, ranges: I)
|
||||
where
|
||||
I: IntoIterator<Item = Range<usize>>,
|
||||
{
|
||||
let selections = ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
let mut start = range.start.to_offset(&buffer);
|
||||
let mut end = range.end.to_offset(&buffer);
|
||||
let mut start = range.start;
|
||||
let mut end = range.end;
|
||||
let reversed = if start > end {
|
||||
mem::swap(&mut start, &mut end);
|
||||
true
|
||||
@@ -677,6 +687,19 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn maybe_move_cursors_with(
|
||||
&mut self,
|
||||
mut update_cursor_position: impl FnMut(
|
||||
&DisplaySnapshot,
|
||||
DisplayPoint,
|
||||
SelectionGoal,
|
||||
) -> Option<(DisplayPoint, SelectionGoal)>,
|
||||
) {
|
||||
self.move_cursors_with(|map, point, goal| {
|
||||
update_cursor_position(map, point, goal).unwrap_or((point, goal))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn replace_cursors_with(
|
||||
&mut self,
|
||||
mut find_replacement_cursors: impl FnMut(&DisplaySnapshot) -> Vec<DisplayPoint>,
|
||||
|
||||
@@ -63,8 +63,15 @@ impl<'a> EditorLspTestContext<'a> {
|
||||
.insert_tree("/root", json!({ "dir": { file_name: "" }}))
|
||||
.await;
|
||||
|
||||
let (window_id, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
|
||||
let (window_id, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project.clone(),
|
||||
|_, _| unimplemented!(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("/root", true, cx)
|
||||
@@ -76,7 +83,9 @@ impl<'a> EditorLspTestContext<'a> {
|
||||
|
||||
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
|
||||
let item = workspace
|
||||
.update(cx, |workspace, cx| workspace.open_path(file, true, cx))
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_path(file, None, true, cx)
|
||||
})
|
||||
.await
|
||||
.expect("Could not open test file");
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ impl<'a> EditorTestContext<'a> {
|
||||
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
|
||||
self.editor.update(self.cx, |editor, cx| {
|
||||
editor.set_text(unmarked_text, cx);
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges(selection_ranges)
|
||||
})
|
||||
});
|
||||
|
||||
@@ -104,7 +104,7 @@ impl FileFinder {
|
||||
match event {
|
||||
Event::Selected(project_path) => {
|
||||
workspace
|
||||
.open_path(project_path.clone(), true, cx)
|
||||
.open_path(project_path.clone(), None, true, cx)
|
||||
.detach_and_log_err(cx);
|
||||
workspace.dismiss_modal(cx);
|
||||
}
|
||||
@@ -316,8 +316,9 @@ 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(project, |_, _| unimplemented!(), cx));
|
||||
let (window_id, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
cx.dispatch_action(window_id, Toggle);
|
||||
|
||||
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
|
||||
@@ -371,8 +372,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(project, |_, _| unimplemented!(), cx));
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||
|
||||
@@ -446,8 +448,9 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||
finder
|
||||
@@ -471,8 +474,9 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||
|
||||
@@ -524,8 +528,9 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||
|
||||
@@ -563,8 +568,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(project, |_, _| unimplemented!(), cx));
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||
finder
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use editor::{display_map::ToDisplayPoint, Autoscroll, DisplayPoint, Editor};
|
||||
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
|
||||
use gpui::{
|
||||
actions, elements::*, geometry::vector::Vector2F, AnyViewHandle, Axis, Entity,
|
||||
MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
|
||||
@@ -83,7 +83,7 @@ impl GoToLine {
|
||||
if let Some(rows) = active_editor.highlighted_rows() {
|
||||
let snapshot = active_editor.snapshot(cx).display_snapshot;
|
||||
let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
|
||||
active_editor.change_selections(Some(Autoscroll::Center), cx, |s| {
|
||||
active_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
s.select_ranges([position..position])
|
||||
});
|
||||
}
|
||||
@@ -127,7 +127,7 @@ impl GoToLine {
|
||||
let display_point = point.to_display_point(&snapshot);
|
||||
let row = display_point.row();
|
||||
active_editor.highlight_rows(Some(row..row + 1));
|
||||
active_editor.request_autoscroll(Autoscroll::Center, cx);
|
||||
active_editor.request_autoscroll(Autoscroll::center(), cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ collections = { path = "../collections" }
|
||||
gpui_macros = { path = "../gpui_macros" }
|
||||
util = { path = "../util" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
sqlez = { path = "../sqlez" }
|
||||
async-task = "4.0.3"
|
||||
backtrace = { version = "0.3", optional = true }
|
||||
ctor = "0.1"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#include "nan.h"
|
||||
#include "tree_sitter/parser.h"
|
||||
#include <node.h>
|
||||
#include "nan.h"
|
||||
|
||||
using namespace v8;
|
||||
|
||||
extern "C" TSLanguage * tree_sitter_context_predicate();
|
||||
extern "C" TSLanguage *tree_sitter_context_predicate();
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -16,13 +16,15 @@ void Init(Local<Object> exports, Local<Object> module) {
|
||||
tpl->InstanceTemplate()->SetInternalFieldCount(1);
|
||||
|
||||
Local<Function> constructor = Nan::GetFunction(tpl).ToLocalChecked();
|
||||
Local<Object> instance = constructor->NewInstance(Nan::GetCurrentContext()).ToLocalChecked();
|
||||
Local<Object> instance =
|
||||
constructor->NewInstance(Nan::GetCurrentContext()).ToLocalChecked();
|
||||
Nan::SetInternalFieldPointer(instance, 0, tree_sitter_context_predicate());
|
||||
|
||||
Nan::Set(instance, Nan::New("name").ToLocalChecked(), Nan::New("context_predicate").ToLocalChecked());
|
||||
Nan::Set(instance, Nan::New("name").ToLocalChecked(),
|
||||
Nan::New("context_predicate").ToLocalChecked());
|
||||
Nan::Set(module, Nan::New("exports").ToLocalChecked(), instance);
|
||||
}
|
||||
|
||||
NODE_MODULE(tree_sitter_context_predicate_binding, Init)
|
||||
|
||||
} // namespace
|
||||
} // namespace
|
||||
|
||||
@@ -594,6 +594,9 @@ type ReleaseObservationCallback = Box<dyn FnOnce(&dyn Any, &mut MutableAppContex
|
||||
type ActionObservationCallback = Box<dyn FnMut(TypeId, &mut MutableAppContext)>;
|
||||
type WindowActivationCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>;
|
||||
type WindowFullscreenCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>;
|
||||
type KeystrokeCallback = Box<
|
||||
dyn FnMut(&Keystroke, &MatchResult, Option<&Box<dyn Action>>, &mut MutableAppContext) -> bool,
|
||||
>;
|
||||
type DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>;
|
||||
type WindowShouldCloseSubscriptionCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
|
||||
|
||||
@@ -619,6 +622,7 @@ pub struct MutableAppContext {
|
||||
observations: CallbackCollection<usize, ObservationCallback>,
|
||||
window_activation_observations: CallbackCollection<usize, WindowActivationCallback>,
|
||||
window_fullscreen_observations: CallbackCollection<usize, WindowFullscreenCallback>,
|
||||
keystroke_observations: CallbackCollection<usize, KeystrokeCallback>,
|
||||
|
||||
release_observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, ReleaseObservationCallback>>>>,
|
||||
action_dispatch_observations: Arc<Mutex<BTreeMap<usize, ActionObservationCallback>>>,
|
||||
@@ -678,6 +682,7 @@ impl MutableAppContext {
|
||||
global_observations: Default::default(),
|
||||
window_activation_observations: Default::default(),
|
||||
window_fullscreen_observations: Default::default(),
|
||||
keystroke_observations: Default::default(),
|
||||
action_dispatch_observations: Default::default(),
|
||||
presenters_and_platform_windows: Default::default(),
|
||||
foreground,
|
||||
@@ -763,11 +768,11 @@ impl MutableAppContext {
|
||||
.with_context(|| format!("invalid data for action {}", name))
|
||||
}
|
||||
|
||||
pub fn add_action<A, V, F>(&mut self, handler: F)
|
||||
pub fn add_action<A, V, F, R>(&mut self, handler: F)
|
||||
where
|
||||
A: Action,
|
||||
V: View,
|
||||
F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>),
|
||||
F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>) -> R,
|
||||
{
|
||||
self.add_action_internal(handler, false)
|
||||
}
|
||||
@@ -781,11 +786,11 @@ impl MutableAppContext {
|
||||
self.add_action_internal(handler, true)
|
||||
}
|
||||
|
||||
fn add_action_internal<A, V, F>(&mut self, mut handler: F, capture: bool)
|
||||
fn add_action_internal<A, V, F, R>(&mut self, mut handler: F, capture: bool)
|
||||
where
|
||||
A: Action,
|
||||
V: View,
|
||||
F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>),
|
||||
F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>) -> R,
|
||||
{
|
||||
let handler = Box::new(
|
||||
move |view: &mut dyn AnyView,
|
||||
@@ -1255,6 +1260,27 @@ impl MutableAppContext {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn observe_keystrokes<F>(&mut self, window_id: usize, callback: F) -> Subscription
|
||||
where
|
||||
F: 'static
|
||||
+ FnMut(
|
||||
&Keystroke,
|
||||
&MatchResult,
|
||||
Option<&Box<dyn Action>>,
|
||||
&mut MutableAppContext,
|
||||
) -> bool,
|
||||
{
|
||||
let subscription_id = post_inc(&mut self.next_subscription_id);
|
||||
self.keystroke_observations
|
||||
.add_callback(window_id, subscription_id, Box::new(callback));
|
||||
|
||||
Subscription::KeystrokeObservation {
|
||||
id: subscription_id,
|
||||
window_id,
|
||||
observations: Some(self.keystroke_observations.downgrade()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut MutableAppContext)) {
|
||||
self.pending_effects.push_back(Effect::Deferred {
|
||||
callback: Box::new(callback),
|
||||
@@ -1405,8 +1431,8 @@ impl MutableAppContext {
|
||||
true
|
||||
}
|
||||
|
||||
// Returns an iterator over all of the view ids from the passed view up to the root of the window
|
||||
// Includes the passed view itself
|
||||
/// Returns an iterator over all of the view ids from the passed view up to the root of the window
|
||||
/// Includes the passed view itself
|
||||
fn ancestors(&self, window_id: usize, mut view_id: usize) -> impl Iterator<Item = usize> + '_ {
|
||||
std::iter::once(view_id)
|
||||
.into_iter()
|
||||
@@ -1538,27 +1564,39 @@ impl MutableAppContext {
|
||||
})
|
||||
.collect();
|
||||
|
||||
match self
|
||||
let match_result = self
|
||||
.keystroke_matcher
|
||||
.push_keystroke(keystroke.clone(), dispatch_path)
|
||||
{
|
||||
.push_keystroke(keystroke.clone(), dispatch_path);
|
||||
let mut handled_by = None;
|
||||
|
||||
let keystroke_handled = match &match_result {
|
||||
MatchResult::None => false,
|
||||
MatchResult::Pending => true,
|
||||
MatchResult::Matches(matches) => {
|
||||
for (view_id, action) in matches {
|
||||
if self.handle_dispatch_action_from_effect(
|
||||
window_id,
|
||||
Some(view_id),
|
||||
Some(*view_id),
|
||||
action.as_ref(),
|
||||
) {
|
||||
self.keystroke_matcher.clear_pending();
|
||||
return true;
|
||||
handled_by = Some(action.boxed_clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
false
|
||||
handled_by.is_some()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.keystroke(
|
||||
window_id,
|
||||
keystroke.clone(),
|
||||
handled_by,
|
||||
match_result.clone(),
|
||||
);
|
||||
keystroke_handled
|
||||
} else {
|
||||
self.keystroke(window_id, keystroke.clone(), None, MatchResult::None);
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -2110,6 +2148,12 @@ impl MutableAppContext {
|
||||
} => {
|
||||
self.handle_window_should_close_subscription_effect(window_id, callback)
|
||||
}
|
||||
Effect::Keystroke {
|
||||
window_id,
|
||||
keystroke,
|
||||
handled_by,
|
||||
result,
|
||||
} => self.handle_keystroke_effect(window_id, keystroke, handled_by, result),
|
||||
}
|
||||
self.pending_notifications.clear();
|
||||
self.remove_dropped_entities();
|
||||
@@ -2188,6 +2232,21 @@ impl MutableAppContext {
|
||||
});
|
||||
}
|
||||
|
||||
fn keystroke(
|
||||
&mut self,
|
||||
window_id: usize,
|
||||
keystroke: Keystroke,
|
||||
handled_by: Option<Box<dyn Action>>,
|
||||
result: MatchResult,
|
||||
) {
|
||||
self.pending_effects.push_back(Effect::Keystroke {
|
||||
window_id,
|
||||
keystroke,
|
||||
handled_by,
|
||||
result,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn refresh_windows(&mut self) {
|
||||
self.pending_effects.push_back(Effect::RefreshWindows);
|
||||
}
|
||||
@@ -2299,6 +2358,21 @@ impl MutableAppContext {
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_keystroke_effect(
|
||||
&mut self,
|
||||
window_id: usize,
|
||||
keystroke: Keystroke,
|
||||
handled_by: Option<Box<dyn Action>>,
|
||||
result: MatchResult,
|
||||
) {
|
||||
self.update(|this| {
|
||||
let mut observations = this.keystroke_observations.clone();
|
||||
observations.emit_and_cleanup(window_id, this, {
|
||||
move |callback, this| callback(&keystroke, &result, handled_by.as_ref(), this)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_window_activation_effect(&mut self, window_id: usize, active: bool) {
|
||||
//Short circuit evaluation if we're already g2g
|
||||
if self
|
||||
@@ -2852,6 +2926,12 @@ pub enum Effect {
|
||||
subscription_id: usize,
|
||||
callback: WindowFullscreenCallback,
|
||||
},
|
||||
Keystroke {
|
||||
window_id: usize,
|
||||
keystroke: Keystroke,
|
||||
handled_by: Option<Box<dyn Action>>,
|
||||
result: MatchResult,
|
||||
},
|
||||
RefreshWindows,
|
||||
DispatchActionFrom {
|
||||
window_id: usize,
|
||||
@@ -2995,6 +3075,21 @@ impl Debug for Effect {
|
||||
.debug_struct("Effect::WindowShouldCloseSubscription")
|
||||
.field("window_id", window_id)
|
||||
.finish(),
|
||||
Effect::Keystroke {
|
||||
window_id,
|
||||
keystroke,
|
||||
handled_by,
|
||||
result,
|
||||
} => f
|
||||
.debug_struct("Effect::Keystroke")
|
||||
.field("window_id", window_id)
|
||||
.field("keystroke", keystroke)
|
||||
.field(
|
||||
"keystroke",
|
||||
&handled_by.as_ref().map(|handled_by| handled_by.name()),
|
||||
)
|
||||
.field("result", result)
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3600,6 +3695,7 @@ impl<'a, T: View> ViewContext<'a, T> {
|
||||
return false;
|
||||
}
|
||||
self.ancestors(view.window_id, view.view_id)
|
||||
.skip(1) // Skip self id
|
||||
.any(|parent| parent == self.view_id)
|
||||
}
|
||||
|
||||
@@ -3826,6 +3922,33 @@ impl<'a, T: View> ViewContext<'a, T> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn observe_keystroke<F>(&mut self, mut callback: F) -> Subscription
|
||||
where
|
||||
F: 'static
|
||||
+ FnMut(
|
||||
&mut T,
|
||||
&Keystroke,
|
||||
Option<&Box<dyn Action>>,
|
||||
&MatchResult,
|
||||
&mut ViewContext<T>,
|
||||
) -> bool,
|
||||
{
|
||||
let observer = self.weak_handle();
|
||||
self.app.observe_keystrokes(
|
||||
self.window_id(),
|
||||
move |keystroke, result, handled_by, cx| {
|
||||
if let Some(observer) = observer.upgrade(cx) {
|
||||
observer.update(cx, |observer, cx| {
|
||||
callback(observer, keystroke, handled_by, result, cx);
|
||||
});
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn emit(&mut self, payload: T::Event) {
|
||||
self.app.pending_effects.push_back(Effect::Event {
|
||||
entity_id: self.view_id,
|
||||
@@ -5018,6 +5141,11 @@ pub enum Subscription {
|
||||
window_id: usize,
|
||||
observations: Option<Weak<Mapping<usize, WindowFullscreenCallback>>>,
|
||||
},
|
||||
KeystrokeObservation {
|
||||
id: usize,
|
||||
window_id: usize,
|
||||
observations: Option<Weak<Mapping<usize, KeystrokeCallback>>>,
|
||||
},
|
||||
|
||||
ReleaseObservation {
|
||||
id: usize,
|
||||
@@ -5056,6 +5184,9 @@ impl Subscription {
|
||||
Subscription::ActionObservation { observations, .. } => {
|
||||
observations.take();
|
||||
}
|
||||
Subscription::KeystrokeObservation { observations, .. } => {
|
||||
observations.take();
|
||||
}
|
||||
Subscription::WindowActivationObservation { observations, .. } => {
|
||||
observations.take();
|
||||
}
|
||||
@@ -5175,6 +5306,27 @@ impl Drop for Subscription {
|
||||
observations.lock().remove(id);
|
||||
}
|
||||
}
|
||||
Subscription::KeystrokeObservation {
|
||||
id,
|
||||
window_id,
|
||||
observations,
|
||||
} => {
|
||||
if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
|
||||
match observations
|
||||
.lock()
|
||||
.entry(*window_id)
|
||||
.or_default()
|
||||
.entry(*id)
|
||||
{
|
||||
btree_map::Entry::Vacant(entry) => {
|
||||
entry.insert(None);
|
||||
}
|
||||
btree_map::Entry::Occupied(entry) => {
|
||||
entry.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Subscription::WindowActivationObservation {
|
||||
id,
|
||||
window_id,
|
||||
|
||||
@@ -257,17 +257,19 @@ impl Element for Flex {
|
||||
let axis = self.axis;
|
||||
move |e, cx| {
|
||||
if remaining_space < 0. {
|
||||
let scroll_delta = e.delta.raw();
|
||||
|
||||
let mut delta = match axis {
|
||||
Axis::Horizontal => {
|
||||
if e.delta.x().abs() >= e.delta.y().abs() {
|
||||
e.delta.x()
|
||||
if scroll_delta.x().abs() >= scroll_delta.y().abs() {
|
||||
scroll_delta.x()
|
||||
} else {
|
||||
e.delta.y()
|
||||
scroll_delta.y()
|
||||
}
|
||||
}
|
||||
Axis::Vertical => e.delta.y(),
|
||||
Axis::Vertical => scroll_delta.y(),
|
||||
};
|
||||
if !e.precise {
|
||||
if !e.delta.precise() {
|
||||
delta *= 20.;
|
||||
}
|
||||
|
||||
|
||||
@@ -258,8 +258,8 @@ impl Element for List {
|
||||
state.0.borrow_mut().scroll(
|
||||
&scroll_top,
|
||||
height,
|
||||
e.platform_event.delta,
|
||||
e.platform_event.precise,
|
||||
*e.platform_event.delta.raw(),
|
||||
e.platform_event.delta.precise(),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ impl Tooltip {
|
||||
} else {
|
||||
state.visible.set(false);
|
||||
state.debounce.take();
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -295,15 +295,19 @@ impl Element for UniformList {
|
||||
move |MouseScrollWheel {
|
||||
platform_event:
|
||||
ScrollWheelEvent {
|
||||
position,
|
||||
delta,
|
||||
precise,
|
||||
..
|
||||
position, delta, ..
|
||||
},
|
||||
..
|
||||
},
|
||||
cx| {
|
||||
if !Self::scroll(state.clone(), position, delta, precise, scroll_max, cx) {
|
||||
if !Self::scroll(
|
||||
state.clone(),
|
||||
position,
|
||||
*delta.raw(),
|
||||
delta.precise(),
|
||||
scroll_max,
|
||||
cx,
|
||||
) {
|
||||
cx.propagate_event();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,21 +66,32 @@ struct DeterministicState {
|
||||
rng: rand::prelude::StdRng,
|
||||
seed: u64,
|
||||
scheduled_from_foreground: collections::HashMap<usize, Vec<ForegroundRunnable>>,
|
||||
scheduled_from_background: Vec<Runnable>,
|
||||
scheduled_from_background: Vec<BackgroundRunnable>,
|
||||
forbid_parking: bool,
|
||||
block_on_ticks: std::ops::RangeInclusive<usize>,
|
||||
now: std::time::Instant,
|
||||
next_timer_id: usize,
|
||||
pending_timers: Vec<(usize, std::time::Instant, postage::barrier::Sender)>,
|
||||
waiting_backtrace: Option<backtrace::Backtrace>,
|
||||
next_runnable_id: usize,
|
||||
poll_history: Vec<usize>,
|
||||
enable_runnable_backtraces: bool,
|
||||
runnable_backtraces: collections::HashMap<usize, backtrace::Backtrace>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
struct ForegroundRunnable {
|
||||
id: usize,
|
||||
runnable: Runnable,
|
||||
main: bool,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
struct BackgroundRunnable {
|
||||
id: usize,
|
||||
runnable: Runnable,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct Deterministic {
|
||||
state: Arc<parking_lot::Mutex<DeterministicState>>,
|
||||
@@ -117,11 +128,29 @@ impl Deterministic {
|
||||
next_timer_id: Default::default(),
|
||||
pending_timers: Default::default(),
|
||||
waiting_backtrace: None,
|
||||
next_runnable_id: 0,
|
||||
poll_history: Default::default(),
|
||||
enable_runnable_backtraces: false,
|
||||
runnable_backtraces: Default::default(),
|
||||
})),
|
||||
parker: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn runnable_history(&self) -> Vec<usize> {
|
||||
self.state.lock().poll_history.clone()
|
||||
}
|
||||
|
||||
pub fn enable_runnable_backtrace(&self) {
|
||||
self.state.lock().enable_runnable_backtraces = true;
|
||||
}
|
||||
|
||||
pub fn runnable_backtrace(&self, runnable_id: usize) -> backtrace::Backtrace {
|
||||
let mut backtrace = self.state.lock().runnable_backtraces[&runnable_id].clone();
|
||||
backtrace.resolve();
|
||||
backtrace
|
||||
}
|
||||
|
||||
pub fn build_background(self: &Arc<Self>) -> Arc<Background> {
|
||||
Arc::new(Background::Deterministic {
|
||||
executor: self.clone(),
|
||||
@@ -142,6 +171,17 @@ impl Deterministic {
|
||||
main: bool,
|
||||
) -> AnyLocalTask {
|
||||
let state = self.state.clone();
|
||||
let id;
|
||||
{
|
||||
let mut state = state.lock();
|
||||
id = util::post_inc(&mut state.next_runnable_id);
|
||||
if state.enable_runnable_backtraces {
|
||||
state
|
||||
.runnable_backtraces
|
||||
.insert(id, backtrace::Backtrace::new_unresolved());
|
||||
}
|
||||
}
|
||||
|
||||
let unparker = self.parker.lock().unparker();
|
||||
let (runnable, task) = async_task::spawn_local(future, move |runnable| {
|
||||
let mut state = state.lock();
|
||||
@@ -149,7 +189,7 @@ impl Deterministic {
|
||||
.scheduled_from_foreground
|
||||
.entry(cx_id)
|
||||
.or_default()
|
||||
.push(ForegroundRunnable { runnable, main });
|
||||
.push(ForegroundRunnable { id, runnable, main });
|
||||
unparker.unpark();
|
||||
});
|
||||
runnable.schedule();
|
||||
@@ -158,10 +198,23 @@ impl Deterministic {
|
||||
|
||||
fn spawn(&self, future: AnyFuture) -> AnyTask {
|
||||
let state = self.state.clone();
|
||||
let id;
|
||||
{
|
||||
let mut state = state.lock();
|
||||
id = util::post_inc(&mut state.next_runnable_id);
|
||||
if state.enable_runnable_backtraces {
|
||||
state
|
||||
.runnable_backtraces
|
||||
.insert(id, backtrace::Backtrace::new_unresolved());
|
||||
}
|
||||
}
|
||||
|
||||
let unparker = self.parker.lock().unparker();
|
||||
let (runnable, task) = async_task::spawn(future, move |runnable| {
|
||||
let mut state = state.lock();
|
||||
state.scheduled_from_background.push(runnable);
|
||||
state
|
||||
.scheduled_from_background
|
||||
.push(BackgroundRunnable { id, runnable });
|
||||
unparker.unpark();
|
||||
});
|
||||
runnable.schedule();
|
||||
@@ -178,15 +231,27 @@ impl Deterministic {
|
||||
let woken = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let state = self.state.clone();
|
||||
let id;
|
||||
{
|
||||
let mut state = state.lock();
|
||||
id = util::post_inc(&mut state.next_runnable_id);
|
||||
if state.enable_runnable_backtraces {
|
||||
state
|
||||
.runnable_backtraces
|
||||
.insert(id, backtrace::Backtrace::new_unresolved());
|
||||
}
|
||||
}
|
||||
|
||||
let unparker = self.parker.lock().unparker();
|
||||
let (runnable, mut main_task) = unsafe {
|
||||
async_task::spawn_unchecked(main_future, move |runnable| {
|
||||
let mut state = state.lock();
|
||||
let state = &mut *state.lock();
|
||||
state
|
||||
.scheduled_from_foreground
|
||||
.entry(cx_id)
|
||||
.or_default()
|
||||
.push(ForegroundRunnable {
|
||||
id: util::post_inc(&mut state.next_runnable_id),
|
||||
runnable,
|
||||
main: true,
|
||||
});
|
||||
@@ -248,9 +313,10 @@ impl Deterministic {
|
||||
if !state.scheduled_from_background.is_empty() && state.rng.gen() {
|
||||
let background_len = state.scheduled_from_background.len();
|
||||
let ix = state.rng.gen_range(0..background_len);
|
||||
let runnable = state.scheduled_from_background.remove(ix);
|
||||
let background_runnable = state.scheduled_from_background.remove(ix);
|
||||
state.poll_history.push(background_runnable.id);
|
||||
drop(state);
|
||||
runnable.run();
|
||||
background_runnable.runnable.run();
|
||||
} else if !state.scheduled_from_foreground.is_empty() {
|
||||
let available_cx_ids = state
|
||||
.scheduled_from_foreground
|
||||
@@ -266,6 +332,7 @@ impl Deterministic {
|
||||
if scheduled_from_cx.is_empty() {
|
||||
state.scheduled_from_foreground.remove(&cx_id_to_run);
|
||||
}
|
||||
state.poll_history.push(foreground_runnable.id);
|
||||
|
||||
drop(state);
|
||||
|
||||
@@ -298,9 +365,10 @@ impl Deterministic {
|
||||
let runnable_count = state.scheduled_from_background.len();
|
||||
let ix = state.rng.gen_range(0..=runnable_count);
|
||||
if ix < state.scheduled_from_background.len() {
|
||||
let runnable = state.scheduled_from_background.remove(ix);
|
||||
let background_runnable = state.scheduled_from_background.remove(ix);
|
||||
state.poll_history.push(background_runnable.id);
|
||||
drop(state);
|
||||
runnable.run();
|
||||
background_runnable.runnable.run();
|
||||
} else {
|
||||
drop(state);
|
||||
if let Poll::Ready(result) = future.poll(&mut cx) {
|
||||
|
||||
@@ -112,6 +112,21 @@ impl PartialEq for MatchResult {
|
||||
|
||||
impl Eq for MatchResult {}
|
||||
|
||||
impl Clone for MatchResult {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
MatchResult::None => MatchResult::None,
|
||||
MatchResult::Pending => MatchResult::Pending,
|
||||
MatchResult::Matches(matches) => MatchResult::Matches(
|
||||
matches
|
||||
.iter()
|
||||
.map(|(view_id, action)| (*view_id, Action::boxed_clone(action.as_ref())))
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Matcher {
|
||||
pub fn new(keymap: Keymap) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
|
||||
use crate::{geometry::vector::Vector2F, keymap::Keystroke};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -44,11 +46,45 @@ pub enum TouchPhase {
|
||||
Ended,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ScrollDelta {
|
||||
Pixels(Vector2F),
|
||||
Lines(Vector2F),
|
||||
}
|
||||
|
||||
impl Default for ScrollDelta {
|
||||
fn default() -> Self {
|
||||
Self::Lines(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollDelta {
|
||||
pub fn raw(&self) -> &Vector2F {
|
||||
match self {
|
||||
ScrollDelta::Pixels(v) => v,
|
||||
ScrollDelta::Lines(v) => v,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn precise(&self) -> bool {
|
||||
match self {
|
||||
ScrollDelta::Pixels(_) => true,
|
||||
ScrollDelta::Lines(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pixel_delta(&self, line_height: f32) -> Vector2F {
|
||||
match self {
|
||||
ScrollDelta::Pixels(delta) => *delta,
|
||||
ScrollDelta::Lines(delta) => vec2f(delta.x() * line_height, delta.y() * line_height),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct ScrollWheelEvent {
|
||||
pub position: Vector2F,
|
||||
pub delta: Vector2F,
|
||||
pub precise: bool,
|
||||
pub delta: ScrollDelta,
|
||||
pub modifiers: Modifiers,
|
||||
/// If the platform supports returning the phase of a scroll wheel event, it will be stored here
|
||||
pub phase: Option<TouchPhase>,
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
keymap::Keystroke,
|
||||
platform::{Event, NavigationDirection},
|
||||
KeyDownEvent, KeyUpEvent, Modifiers, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
|
||||
MouseMovedEvent, ScrollWheelEvent, TouchPhase,
|
||||
MouseMovedEvent, ScrollDelta, ScrollWheelEvent, TouchPhase,
|
||||
};
|
||||
use cocoa::{
|
||||
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
|
||||
@@ -164,17 +164,24 @@ impl Event {
|
||||
_ => Some(TouchPhase::Moved),
|
||||
};
|
||||
|
||||
let raw_data = vec2f(
|
||||
native_event.scrollingDeltaX() as f32,
|
||||
native_event.scrollingDeltaY() as f32,
|
||||
);
|
||||
|
||||
let delta = if native_event.hasPreciseScrollingDeltas() == YES {
|
||||
ScrollDelta::Pixels(raw_data)
|
||||
} else {
|
||||
ScrollDelta::Lines(raw_data)
|
||||
};
|
||||
|
||||
Self::ScrollWheel(ScrollWheelEvent {
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
),
|
||||
delta: vec2f(
|
||||
native_event.scrollingDeltaX() as f32,
|
||||
native_event.scrollingDeltaY() as f32,
|
||||
),
|
||||
delta,
|
||||
phase,
|
||||
precise: native_event.hasPreciseScrollingDeltas() == YES,
|
||||
modifiers: read_modifiers(native_event),
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -17,10 +17,15 @@ use crate::{
|
||||
SceneBuilder, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle,
|
||||
WeakViewHandle,
|
||||
};
|
||||
use anyhow::bail;
|
||||
use collections::{HashMap, HashSet};
|
||||
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
||||
use serde_json::json;
|
||||
use smallvec::SmallVec;
|
||||
use sqlez::{
|
||||
bindable::{Bind, Column},
|
||||
statement::Statement,
|
||||
};
|
||||
use std::{
|
||||
marker::PhantomData,
|
||||
ops::{Deref, DerefMut, Range},
|
||||
@@ -475,27 +480,35 @@ impl Presenter {
|
||||
if let MouseEvent::Down(e) = &mouse_event {
|
||||
if valid_region
|
||||
.handlers
|
||||
.contains_handler(MouseEvent::click_disc(), Some(e.button))
|
||||
.contains(MouseEvent::click_disc(), Some(e.button))
|
||||
|| valid_region
|
||||
.handlers
|
||||
.contains_handler(MouseEvent::drag_disc(), Some(e.button))
|
||||
.contains(MouseEvent::drag_disc(), Some(e.button))
|
||||
{
|
||||
event_cx.handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(callback) = valid_region.handlers.get(&mouse_event.handler_key()) {
|
||||
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` 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;
|
||||
any_event_handled |= event_cx.handled;
|
||||
}
|
||||
}
|
||||
|
||||
any_event_handled = any_event_handled || event_cx.handled;
|
||||
// For bubbling events, if the event was handled, don't continue dispatching
|
||||
// This only makes sense for local events.
|
||||
if event_cx.handled && mouse_event.is_capturable() {
|
||||
any_event_handled |= event_cx.handled;
|
||||
|
||||
// For bubbling events, if the event was handled, don't continue dispatching.
|
||||
// This only makes sense for local events which return false from is_capturable.
|
||||
if event_consumed && mouse_event.is_capturable() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -855,8 +868,9 @@ pub struct DebugContext<'a> {
|
||||
pub app: &'a AppContext,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum Axis {
|
||||
#[default]
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
@@ -886,6 +900,31 @@ impl ToJson for Axis {
|
||||
}
|
||||
}
|
||||
|
||||
impl Bind for Axis {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
|
||||
match self {
|
||||
Axis::Horizontal => "Horizontal",
|
||||
Axis::Vertical => "Vertical",
|
||||
}
|
||||
.bind(statement, start_index)
|
||||
}
|
||||
}
|
||||
|
||||
impl Column for Axis {
|
||||
fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
|
||||
String::column(statement, start_index).and_then(|(axis_text, next_index)| {
|
||||
Ok((
|
||||
match axis_text.as_str() {
|
||||
"Horizontal" => Axis::Horizontal,
|
||||
"Vertical" => Axis::Vertical,
|
||||
_ => bail!("Stored serialized item kind is incorrect"),
|
||||
},
|
||||
next_index,
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Vector2FExt {
|
||||
fn along(self, axis: Axis) -> f32;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
|
||||
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
|
||||
|
||||
use crate::{MouseButton, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent};
|
||||
use crate::{scene::mouse_region::HandlerKey, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct MouseMove {
|
||||
@@ -217,17 +217,17 @@ impl MouseEvent {
|
||||
discriminant(&MouseEvent::ScrollWheel(Default::default()))
|
||||
}
|
||||
|
||||
pub fn handler_key(&self) -> (Discriminant<MouseEvent>, Option<MouseButton>) {
|
||||
pub fn handler_key(&self) -> HandlerKey {
|
||||
match self {
|
||||
MouseEvent::Move(_) => (Self::move_disc(), None),
|
||||
MouseEvent::Drag(e) => (Self::drag_disc(), e.pressed_button),
|
||||
MouseEvent::Hover(_) => (Self::hover_disc(), None),
|
||||
MouseEvent::Down(e) => (Self::down_disc(), Some(e.button)),
|
||||
MouseEvent::Up(e) => (Self::up_disc(), Some(e.button)),
|
||||
MouseEvent::Click(e) => (Self::click_disc(), Some(e.button)),
|
||||
MouseEvent::UpOut(e) => (Self::up_out_disc(), Some(e.button)),
|
||||
MouseEvent::DownOut(e) => (Self::down_out_disc(), Some(e.button)),
|
||||
MouseEvent::ScrollWheel(_) => (Self::scroll_wheel_disc(), None),
|
||||
MouseEvent::Move(_) => HandlerKey::new(Self::move_disc(), None),
|
||||
MouseEvent::Drag(e) => HandlerKey::new(Self::drag_disc(), e.pressed_button),
|
||||
MouseEvent::Hover(_) => HandlerKey::new(Self::hover_disc(), None),
|
||||
MouseEvent::Down(e) => HandlerKey::new(Self::down_disc(), Some(e.button)),
|
||||
MouseEvent::Up(e) => HandlerKey::new(Self::up_disc(), Some(e.button)),
|
||||
MouseEvent::Click(e) => HandlerKey::new(Self::click_disc(), Some(e.button)),
|
||||
MouseEvent::UpOut(e) => HandlerKey::new(Self::up_out_disc(), Some(e.button)),
|
||||
MouseEvent::DownOut(e) => HandlerKey::new(Self::down_out_disc(), Some(e.button)),
|
||||
MouseEvent::ScrollWheel(_) => HandlerKey::new(Self::scroll_wheel_disc(), None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::{any::TypeId, fmt::Debug, mem::Discriminant, rc::Rc};
|
||||
use collections::HashMap;
|
||||
|
||||
use pathfinder_geometry::rect::RectF;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{EventContext, MouseButton};
|
||||
|
||||
@@ -177,61 +178,105 @@ impl MouseRegionId {
|
||||
}
|
||||
}
|
||||
|
||||
pub type HandlerCallback = Rc<dyn Fn(MouseEvent, &mut EventContext)>;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
pub struct HandlerKey {
|
||||
event_kind: Discriminant<MouseEvent>,
|
||||
button: Option<MouseButton>,
|
||||
}
|
||||
|
||||
impl HandlerKey {
|
||||
pub fn new(event_kind: Discriminant<MouseEvent>, button: Option<MouseButton>) -> HandlerKey {
|
||||
HandlerKey { event_kind, button }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct HandlerSet {
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub set: HashMap<
|
||||
(Discriminant<MouseEvent>, Option<MouseButton>),
|
||||
Rc<dyn Fn(MouseEvent, &mut EventContext)>,
|
||||
>,
|
||||
set: HashMap<HandlerKey, SmallVec<[HandlerCallback; 1]>>,
|
||||
}
|
||||
|
||||
impl HandlerSet {
|
||||
pub fn capture_all() -> Self {
|
||||
#[allow(clippy::type_complexity)]
|
||||
let mut set: HashMap<
|
||||
(Discriminant<MouseEvent>, Option<MouseButton>),
|
||||
Rc<dyn Fn(MouseEvent, &mut EventContext)>,
|
||||
> = Default::default();
|
||||
let mut set: HashMap<HandlerKey, SmallVec<[HandlerCallback; 1]>> = HashMap::default();
|
||||
|
||||
set.insert((MouseEvent::move_disc(), None), Rc::new(|_, _| {}));
|
||||
set.insert((MouseEvent::hover_disc(), None), Rc::new(|_, _| {}));
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::move_disc(), None),
|
||||
SmallVec::from_buf([Rc::new(|_, _| {})]),
|
||||
);
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::hover_disc(), None),
|
||||
SmallVec::from_buf([Rc::new(|_, _| {})]),
|
||||
);
|
||||
for button in MouseButton::all() {
|
||||
set.insert((MouseEvent::drag_disc(), Some(button)), Rc::new(|_, _| {}));
|
||||
set.insert((MouseEvent::down_disc(), Some(button)), Rc::new(|_, _| {}));
|
||||
set.insert((MouseEvent::up_disc(), Some(button)), Rc::new(|_, _| {}));
|
||||
set.insert((MouseEvent::click_disc(), Some(button)), Rc::new(|_, _| {}));
|
||||
set.insert(
|
||||
(MouseEvent::down_out_disc(), Some(button)),
|
||||
Rc::new(|_, _| {}),
|
||||
HandlerKey::new(MouseEvent::drag_disc(), Some(button)),
|
||||
SmallVec::from_buf([Rc::new(|_, _| {})]),
|
||||
);
|
||||
set.insert(
|
||||
(MouseEvent::up_out_disc(), Some(button)),
|
||||
Rc::new(|_, _| {}),
|
||||
HandlerKey::new(MouseEvent::down_disc(), Some(button)),
|
||||
SmallVec::from_buf([Rc::new(|_, _| {})]),
|
||||
);
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::up_disc(), Some(button)),
|
||||
SmallVec::from_buf([Rc::new(|_, _| {})]),
|
||||
);
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::click_disc(), Some(button)),
|
||||
SmallVec::from_buf([Rc::new(|_, _| {})]),
|
||||
);
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::down_out_disc(), Some(button)),
|
||||
SmallVec::from_buf([Rc::new(|_, _| {})]),
|
||||
);
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::up_out_disc(), Some(button)),
|
||||
SmallVec::from_buf([Rc::new(|_, _| {})]),
|
||||
);
|
||||
}
|
||||
set.insert((MouseEvent::scroll_wheel_disc(), None), Rc::new(|_, _| {}));
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::scroll_wheel_disc(), None),
|
||||
SmallVec::from_buf([Rc::new(|_, _| {})]),
|
||||
);
|
||||
|
||||
HandlerSet { set }
|
||||
}
|
||||
|
||||
pub fn get(
|
||||
&self,
|
||||
key: &(Discriminant<MouseEvent>, Option<MouseButton>),
|
||||
) -> Option<Rc<dyn Fn(MouseEvent, &mut EventContext)>> {
|
||||
self.set.get(key).cloned()
|
||||
pub fn get(&self, key: &HandlerKey) -> Option<&[HandlerCallback]> {
|
||||
self.set.get(key).map(|vec| vec.as_slice())
|
||||
}
|
||||
|
||||
pub fn contains_handler(
|
||||
pub fn contains(
|
||||
&self,
|
||||
event: Discriminant<MouseEvent>,
|
||||
discriminant: Discriminant<MouseEvent>,
|
||||
button: Option<MouseButton>,
|
||||
) -> bool {
|
||||
self.set.contains_key(&(event, button))
|
||||
self.set
|
||||
.contains_key(&HandlerKey::new(discriminant, button))
|
||||
}
|
||||
|
||||
fn insert(
|
||||
&mut self,
|
||||
event_kind: Discriminant<MouseEvent>,
|
||||
button: Option<MouseButton>,
|
||||
callback: HandlerCallback,
|
||||
) {
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
match self.set.entry(HandlerKey::new(event_kind, button)) {
|
||||
Entry::Occupied(mut vec) => {
|
||||
vec.get_mut().push(callback);
|
||||
}
|
||||
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(SmallVec::from_buf([callback]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_move(mut self, handler: impl Fn(MouseMove, &mut EventContext) + 'static) -> Self {
|
||||
self.set.insert((MouseEvent::move_disc(), None),
|
||||
self.insert(MouseEvent::move_disc(), None,
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseEvent::Move(e) = region_event {
|
||||
handler(e, cx);
|
||||
@@ -249,7 +294,7 @@ impl HandlerSet {
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseDown, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.set.insert((MouseEvent::down_disc(), Some(button)),
|
||||
self.insert(MouseEvent::down_disc(), Some(button),
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseEvent::Down(e) = region_event {
|
||||
handler(e, cx);
|
||||
@@ -267,7 +312,7 @@ impl HandlerSet {
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseUp, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.set.insert((MouseEvent::up_disc(), Some(button)),
|
||||
self.insert(MouseEvent::up_disc(), Some(button),
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseEvent::Up(e) = region_event {
|
||||
handler(e, cx);
|
||||
@@ -285,7 +330,7 @@ impl HandlerSet {
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseClick, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.set.insert((MouseEvent::click_disc(), Some(button)),
|
||||
self.insert(MouseEvent::click_disc(), Some(button),
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseEvent::Click(e) = region_event {
|
||||
handler(e, cx);
|
||||
@@ -303,7 +348,7 @@ impl HandlerSet {
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseDownOut, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.set.insert((MouseEvent::down_out_disc(), Some(button)),
|
||||
self.insert(MouseEvent::down_out_disc(), Some(button),
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseEvent::DownOut(e) = region_event {
|
||||
handler(e, cx);
|
||||
@@ -321,7 +366,7 @@ impl HandlerSet {
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseUpOut, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.set.insert((MouseEvent::up_out_disc(), Some(button)),
|
||||
self.insert(MouseEvent::up_out_disc(), Some(button),
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseEvent::UpOut(e) = region_event {
|
||||
handler(e, cx);
|
||||
@@ -339,7 +384,7 @@ impl HandlerSet {
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseDrag, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.set.insert((MouseEvent::drag_disc(), Some(button)),
|
||||
self.insert(MouseEvent::drag_disc(), Some(button),
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseEvent::Drag(e) = region_event {
|
||||
handler(e, cx);
|
||||
@@ -353,7 +398,7 @@ impl HandlerSet {
|
||||
}
|
||||
|
||||
pub fn on_hover(mut self, handler: impl Fn(MouseHover, &mut EventContext) + 'static) -> Self {
|
||||
self.set.insert((MouseEvent::hover_disc(), None),
|
||||
self.insert(MouseEvent::hover_disc(), None,
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseEvent::Hover(e) = region_event {
|
||||
handler(e, cx);
|
||||
@@ -370,7 +415,7 @@ impl HandlerSet {
|
||||
mut self,
|
||||
handler: impl Fn(MouseScrollWheel, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.set.insert((MouseEvent::scroll_wheel_disc(), None),
|
||||
self.insert(MouseEvent::scroll_wheel_disc(), None,
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseEvent::ScrollWheel(e) = region_event {
|
||||
handler(e, cx);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user