Compare commits
340 Commits
v0.60.1
...
v0.62.0-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e13012c48e | ||
|
|
df708465d1 | ||
|
|
aa9ccf3411 | ||
|
|
6410fdc474 | ||
|
|
499d947e69 | ||
|
|
c093516351 | ||
|
|
41699224ff | ||
|
|
8886cb5786 | ||
|
|
f56f0b7bbb | ||
|
|
ae79b50101 | ||
|
|
fcfc4a4298 | ||
|
|
d355bd3372 | ||
|
|
2bfd46d48c | ||
|
|
f1b41389b3 | ||
|
|
92a4998ddc | ||
|
|
23d7209f82 | ||
|
|
cf3c610eba | ||
|
|
6a010f58be | ||
|
|
0f1b0a4a78 | ||
|
|
a4a8596a29 | ||
|
|
1cdd3c0e28 | ||
|
|
22db5bffe8 | ||
|
|
a61f3b715b | ||
|
|
949a28d49c | ||
|
|
88be4fe77e | ||
|
|
625a62626e | ||
|
|
ee440cf300 | ||
|
|
cf2ec99a4d | ||
|
|
bb0f6e85a8 | ||
|
|
4412217f51 | ||
|
|
1e85361914 | ||
|
|
f611b443c0 | ||
|
|
5984be3d84 | ||
|
|
5a8061ac7b | ||
|
|
509c327b3b | ||
|
|
56a66b348d | ||
|
|
a7d86a164c | ||
|
|
383334633f | ||
|
|
6a2dc444c6 | ||
|
|
e9073310c4 | ||
|
|
3b67602b13 | ||
|
|
04477e9f97 | ||
|
|
990c83eabd | ||
|
|
ddc71653ad | ||
|
|
e5e5cf1314 | ||
|
|
f364a15d89 | ||
|
|
2b4fd53202 | ||
|
|
dfe2fd0386 | ||
|
|
2055f05b09 | ||
|
|
33ebfc3f10 | ||
|
|
6a4f3aaa56 | ||
|
|
c1f7ac0d8c | ||
|
|
19adfdf8bb | ||
|
|
af74d5409a | ||
|
|
2a3773240d | ||
|
|
782676dc67 | ||
|
|
68717d0fe8 | ||
|
|
8bd9577318 | ||
|
|
2ac537393d | ||
|
|
82956b618a | ||
|
|
a725ded95e | ||
|
|
113b7f6f97 | ||
|
|
aed085b168 | ||
|
|
345544646a | ||
|
|
4520227e98 | ||
|
|
f5795ffc6f | ||
|
|
8cde64d3f6 | ||
|
|
d7b8a189e4 | ||
|
|
cfde3e348c | ||
|
|
70e2951e35 | ||
|
|
ba35536664 | ||
|
|
b9f9819637 | ||
|
|
076d353e84 | ||
|
|
64e9b9f893 | ||
|
|
21ad375b42 | ||
|
|
cb9534eae0 | ||
|
|
8b43368bf9 | ||
|
|
c96c8fd782 | ||
|
|
c295f943ba | ||
|
|
e527474dd9 | ||
|
|
73f267167f | ||
|
|
40290a9a42 | ||
|
|
bd35468d18 | ||
|
|
c80395fc18 | ||
|
|
95be2c6070 | ||
|
|
fb7a92242b | ||
|
|
8c2ff69515 | ||
|
|
011085a93f | ||
|
|
dce21900a7 | ||
|
|
2b5ac535b9 | ||
|
|
484c8f7cbe | ||
|
|
7e4d582d1e | ||
|
|
50c4783333 | ||
|
|
9860dbbbea | ||
|
|
874a3605f8 | ||
|
|
088c5bac1f | ||
|
|
e135b982c1 | ||
|
|
a8bd234aa4 | ||
|
|
f99d70500c | ||
|
|
476020ae84 | ||
|
|
2f1ddc0d0f | ||
|
|
ef5844bc79 | ||
|
|
0c9ceb51e6 | ||
|
|
cedc0f64d5 | ||
|
|
9952f08cce | ||
|
|
efa6745035 | ||
|
|
4816a587c3 | ||
|
|
6514eb5209 | ||
|
|
2a38c4938d | ||
|
|
b015761131 | ||
|
|
99e6ecc466 | ||
|
|
7e411ae098 | ||
|
|
1bbb7dd126 | ||
|
|
78969d0938 | ||
|
|
bac3dc1ccd | ||
|
|
ae44a38285 | ||
|
|
77b13b1356 | ||
|
|
2e97e2dbfd | ||
|
|
75ec5c3b1b | ||
|
|
3a456b09cb | ||
|
|
022f70b1de | ||
|
|
c1e23fc6d9 | ||
|
|
a6e9d0d061 | ||
|
|
b700ea84a5 | ||
|
|
0ef62fc334 | ||
|
|
c3900565b9 | ||
|
|
a86756ed20 | ||
|
|
e3ef6d35ab | ||
|
|
038670cc6f | ||
|
|
5d87a04dc3 | ||
|
|
fbfe8a2311 | ||
|
|
bd8509990a | ||
|
|
6bdb08ab9c | ||
|
|
db8b8ef66b | ||
|
|
ac5d5e2451 | ||
|
|
fad6cfef05 | ||
|
|
c83cae60f6 | ||
|
|
9b8e6cce02 | ||
|
|
9858906463 | ||
|
|
be1dc01d9e | ||
|
|
de24b4b4e8 | ||
|
|
629d3d473c | ||
|
|
5dc82d3df8 | ||
|
|
76a1b81e45 | ||
|
|
99aa1219d2 | ||
|
|
69472f7823 | ||
|
|
723fa83909 | ||
|
|
2f064d5ccc | ||
|
|
ae9a0a99ea | ||
|
|
c2b9b08944 | ||
|
|
2aa2e5af7a | ||
|
|
b7c439f4c4 | ||
|
|
e6b29086a9 | ||
|
|
83e4e26989 | ||
|
|
caec9c1f45 | ||
|
|
e3809c267d | ||
|
|
0d9eecd2ed | ||
|
|
d7915840d0 | ||
|
|
8098697847 | ||
|
|
4c2f8406c7 | ||
|
|
e0a477265d | ||
|
|
364c3f2f00 | ||
|
|
75c79d60fe | ||
|
|
5b2dd8e4d0 | ||
|
|
9e8e227b46 | ||
|
|
adf7578007 | ||
|
|
b6e5aa3bb0 | ||
|
|
288c039929 | ||
|
|
fb5c6493cf | ||
|
|
3160d07b9c | ||
|
|
e49fc9f4b1 | ||
|
|
ed6f482e68 | ||
|
|
773f569385 | ||
|
|
219793afcc | ||
|
|
571636c526 | ||
|
|
cbc15b6b58 | ||
|
|
c410935c9c | ||
|
|
79cf5dbd4b | ||
|
|
da5203011c | ||
|
|
84c7aa9cad | ||
|
|
f8e5a08324 | ||
|
|
5e57a33df7 | ||
|
|
38bdf7ad92 | ||
|
|
5447f63e9d | ||
|
|
50ba8bdc9b | ||
|
|
6f279c0239 | ||
|
|
26ccd70e77 | ||
|
|
b0ddbeb0ad | ||
|
|
826eb113e7 | ||
|
|
2661a9cc98 | ||
|
|
b06366ebb7 | ||
|
|
c7a629ba6b | ||
|
|
d155c11729 | ||
|
|
0c3c1e1f68 | ||
|
|
6c322dc835 | ||
|
|
6019e4c37b | ||
|
|
9c8dd66b20 | ||
|
|
0c0e8688ed | ||
|
|
6146923dbb | ||
|
|
2c4f003897 | ||
|
|
0491747eed | ||
|
|
29b9651ebd | ||
|
|
48a1dd1588 | ||
|
|
9cf39b1da6 | ||
|
|
47be340cac | ||
|
|
bf98300547 | ||
|
|
46635956f4 | ||
|
|
8c6de99159 | ||
|
|
a42a703b35 | ||
|
|
59fab0bb2d | ||
|
|
c73e2c2d0f | ||
|
|
8c1c98a0bf | ||
|
|
d99a074bc0 | ||
|
|
05b4b443d9 | ||
|
|
4b09f77950 | ||
|
|
dbea3cf20c | ||
|
|
aa8fa4a6d5 | ||
|
|
dbc03e2668 | ||
|
|
4ef69c8361 | ||
|
|
895aeb033f | ||
|
|
e15cc376b0 | ||
|
|
54428ca6f6 | ||
|
|
54cf6fa838 | ||
|
|
09a0b3eb55 | ||
|
|
40c3e925ad | ||
|
|
5ef5147780 | ||
|
|
318b923bac | ||
|
|
93a30ea940 | ||
|
|
5bb2edca8b | ||
|
|
1789dfb8b1 | ||
|
|
f473eadf2d | ||
|
|
1f161b9aa1 | ||
|
|
354fefe61b | ||
|
|
19c98bb5ad | ||
|
|
2149c17a0a | ||
|
|
1716aff969 | ||
|
|
2a5d7ea2de | ||
|
|
be34c50c72 | ||
|
|
50ae3e03f7 | ||
|
|
499b8f5f55 | ||
|
|
81d83841ab | ||
|
|
cce00526b9 | ||
|
|
c9225bb87c | ||
|
|
75c339851f | ||
|
|
e39c7c62e4 | ||
|
|
b6bb2985f5 | ||
|
|
6bdbab2faf | ||
|
|
f09d6b7b95 | ||
|
|
19a2752674 | ||
|
|
5d433b1666 | ||
|
|
caeae38e3a | ||
|
|
c25acc155d | ||
|
|
4222f86537 | ||
|
|
9569323f93 | ||
|
|
0bbba90f30 | ||
|
|
f1ff557a25 | ||
|
|
23d7143298 | ||
|
|
12eab6551f | ||
|
|
d25c6b15a6 | ||
|
|
b9308ad80d | ||
|
|
6e363e464c | ||
|
|
6e53deb1b2 | ||
|
|
0717c168d9 | ||
|
|
6d020a3ee9 | ||
|
|
9a381c1803 | ||
|
|
3e23d1f48d | ||
|
|
1750fcf833 | ||
|
|
646d344a11 | ||
|
|
bc03592912 | ||
|
|
a4b518ec72 | ||
|
|
b541ac313c | ||
|
|
934474f87e | ||
|
|
3a4e802093 | ||
|
|
b3eb5f7cdf | ||
|
|
c21e0e916c | ||
|
|
d301a215f7 | ||
|
|
8044beffc7 | ||
|
|
8df84e0341 | ||
|
|
137a9cefbd | ||
|
|
55576f879b | ||
|
|
78aee53411 | ||
|
|
864020463f | ||
|
|
2d3d07d4d7 | ||
|
|
ad6f9b2499 | ||
|
|
330968434f | ||
|
|
4b12fb6b3b | ||
|
|
eef086f60f | ||
|
|
6ac0b81778 | ||
|
|
8d82702da2 | ||
|
|
dde3dfdbf6 | ||
|
|
8d609959f1 | ||
|
|
16f854b636 | ||
|
|
9c47325c25 | ||
|
|
a6a7e85894 | ||
|
|
e75dcc853b | ||
|
|
e744520d90 | ||
|
|
a6910584b6 | ||
|
|
6dfa34fcf8 | ||
|
|
b626ec3bf9 | ||
|
|
5708879b5a | ||
|
|
95bc18a995 | ||
|
|
61dc703a58 | ||
|
|
a87d9d3578 | ||
|
|
fc770c6ea5 | ||
|
|
0c68abbe17 | ||
|
|
2d25e25ec3 | ||
|
|
c4028ef116 | ||
|
|
393d728769 | ||
|
|
431ac1267a | ||
|
|
5bc074005c | ||
|
|
4a61b1011e | ||
|
|
84847ff181 | ||
|
|
0bbc02e10d | ||
|
|
0ed811b81b | ||
|
|
ce2112df43 | ||
|
|
7080dc9c23 | ||
|
|
3c62de34f7 | ||
|
|
a6cccf82f7 | ||
|
|
f8da5ab2e7 | ||
|
|
fbe5f9225c | ||
|
|
773423fcf4 | ||
|
|
a62e2a38d7 | ||
|
|
48dcc465f2 | ||
|
|
d0c50b4fbf | ||
|
|
ab3a6f775e | ||
|
|
0674ca14d9 | ||
|
|
d0b35b5e19 | ||
|
|
01570504ad | ||
|
|
506c28d2b6 | ||
|
|
53f58f72f2 | ||
|
|
c9786fe464 | ||
|
|
c2ffc7086c | ||
|
|
96f9ee784d | ||
|
|
962f087ac2 | ||
|
|
ebe8c952e4 | ||
|
|
eabd687cbc | ||
|
|
593c7a8cd1 | ||
|
|
79b9420017 | ||
|
|
db5c83eb36 | ||
|
|
56f9543a95 |
@@ -1,3 +1,11 @@
|
||||
/target
|
||||
/manifest.yml
|
||||
/migrate.yml
|
||||
**/target
|
||||
zed.xcworkspace
|
||||
.DS_Store
|
||||
plugins/bin
|
||||
script/node_modules
|
||||
styles/node_modules
|
||||
crates/collab/static/styles.css
|
||||
vendor/bin
|
||||
assets/themes/*.json
|
||||
assets/themes/internal/*.json
|
||||
assets/themes/experiments/*.json
|
||||
|
||||
34
.github/workflows/ci.yml
vendored
34
.github/workflows/ci.yml
vendored
@@ -39,6 +39,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
clean: false
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --workspace --no-fail-fast
|
||||
@@ -57,6 +58,7 @@ jobs:
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
ZED_AMPLITUDE_API_KEY: ${{ secrets.ZED_AMPLITUDE_API_KEY }}
|
||||
ZED_MIXPANEL_TOKEN: ${{ secrets.ZED_MIXPANEL_TOKEN }}
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
@@ -75,10 +77,32 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
clean: false
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Validate version
|
||||
- name: Determine version and release channel
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: script/validate-version
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
version=$(script/get-crate-version zed)
|
||||
channel=$(cat crates/zed/RELEASE_CHANNEL)
|
||||
echo "Publishing version: ${version} on release channel ${channel}"
|
||||
echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
|
||||
|
||||
expected_tag_name=""
|
||||
case ${channel} in
|
||||
stable)
|
||||
expected_tag_name="v${version}";;
|
||||
preview)
|
||||
expected_tag_name="v${version}-pre";;
|
||||
*)
|
||||
echo "can't publish a release on channel ${channel}"
|
||||
exit 1;;
|
||||
esac
|
||||
if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
|
||||
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle
|
||||
@@ -91,12 +115,12 @@ jobs:
|
||||
path: target/release/Zed.dmg
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
name: Upload app bundle to release if release tag
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
name: Upload app bundle to release
|
||||
if: ${{ env.RELEASE_CHANNEL }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
files: target/release/Zed.dmg
|
||||
overwrite: true
|
||||
body: ""
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
46
.github/workflows/publish_collab_image.yml
vendored
Normal file
46
.github/workflows/publish_collab_image.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Publish Collab Server Image
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- collab-v*
|
||||
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish collab server image
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- deploy
|
||||
steps:
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Sign into DigitalOcean docker registry
|
||||
run: doctl registry login
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Determine version
|
||||
run: |
|
||||
set -eu
|
||||
version=$(script/get-crate-version collab)
|
||||
if [[ $GITHUB_REF_NAME != "collab-v${version}" ]]; then
|
||||
echo "release tag ${GITHUB_REF_NAME} does not match version ${version}"
|
||||
exit 1
|
||||
fi
|
||||
echo "Publishing collab version: ${version}"
|
||||
echo "COLLAB_VERSION=${version}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build docker image
|
||||
run: docker build . --tag registry.digitalocean.com/zed/collab:v${COLLAB_VERSION}
|
||||
|
||||
- name: Publish docker image
|
||||
run: docker push registry.digitalocean.com/zed/collab:v${COLLAB_VERSION}
|
||||
2
.github/workflows/release_actions.yml
vendored
2
.github/workflows/release_actions.yml
vendored
@@ -30,4 +30,4 @@ jobs:
|
||||
architecture: "x64"
|
||||
cache: "pip"
|
||||
- run: pip install -r script/amplitude_release/requirements.txt
|
||||
- run: python script/amplitude_release/main.py ${{ github.event.release.tag_name }} ${{ secrets.ZED_AMPLITUDE_API_KEY }} ${{ secrets.ZED_AMPLITUDE_SECRET_KEY }}
|
||||
- run: python script/amplitude_release/main.py ${{ github.event.release.tag_name }} ${{ secrets.ZED_AMPLITUDE_API_KEY }} ${{ secrets.ZED_AMPLITUDE_SECRET_KEY }}
|
||||
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -7,6 +7,14 @@
|
||||
/crates/collab/static/styles.css
|
||||
/vendor/bin
|
||||
/assets/themes/*.json
|
||||
/assets/themes/internal/*.json
|
||||
/assets/themes/experiments/*.json
|
||||
**/venv
|
||||
/assets/themes/Internal/*.json
|
||||
/assets/themes/Experiments/*.json
|
||||
**/venv
|
||||
.build
|
||||
Packages
|
||||
*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "crates/live_kit_server/protocol"]
|
||||
path = crates/live_kit_server/protocol
|
||||
url = https://github.com/livekit/protocol
|
||||
335
Cargo.lock
generated
335
Cargo.lock
generated
@@ -172,13 +172,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.3.4"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90622698a1218e0b2fb846c97b5f19a0831f6baddee73d9454156365ccfa473b"
|
||||
checksum = "6d26004fe83b2d1cd3a97609b21e39f9a31535822210fe83205d2ce48866ea61"
|
||||
dependencies = [
|
||||
"easy-parallel",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
"parking_lot 0.12.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -705,17 +705,6 @@ version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
|
||||
|
||||
[[package]]
|
||||
name = "bzip2-sys"
|
||||
version = "0.1.11+1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cache-padded"
|
||||
version = "1.2.0"
|
||||
@@ -727,10 +716,13 @@ name = "call"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
"client",
|
||||
"collections",
|
||||
"futures 0.3.24",
|
||||
"gpui",
|
||||
"live_kit_client",
|
||||
"media",
|
||||
"postage",
|
||||
"project",
|
||||
"util",
|
||||
@@ -802,34 +794,6 @@ dependencies = [
|
||||
"winx",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "capture"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bindgen",
|
||||
"block",
|
||||
"byteorder",
|
||||
"bytes 1.2.1",
|
||||
"cocoa",
|
||||
"core-foundation",
|
||||
"core-graphics",
|
||||
"foreign-types",
|
||||
"futures 0.3.24",
|
||||
"gpui",
|
||||
"hmac 0.12.1",
|
||||
"jwt",
|
||||
"live_kit",
|
||||
"log",
|
||||
"media",
|
||||
"objc",
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"serde",
|
||||
"sha2 0.10.6",
|
||||
"simplelog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.1.2"
|
||||
@@ -866,22 +830,6 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chat_panel"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"client",
|
||||
"editor",
|
||||
"gpui",
|
||||
"menu",
|
||||
"postage",
|
||||
"settings",
|
||||
"theme",
|
||||
"time 0.3.15",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.22"
|
||||
@@ -1011,6 +959,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"rpc",
|
||||
"serde",
|
||||
"settings",
|
||||
"smol",
|
||||
"sum_tree",
|
||||
"tempfile",
|
||||
@@ -1079,7 +1028,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1103,6 +1052,8 @@ dependencies = [
|
||||
"language",
|
||||
"lazy_static",
|
||||
"lipsum",
|
||||
"live_kit_client",
|
||||
"live_kit_server",
|
||||
"log",
|
||||
"lsp",
|
||||
"nanoid",
|
||||
@@ -1543,9 +1494,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cxx"
|
||||
version = "1.0.78"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f39818dcfc97d45b03953c1292efc4e80954e1583c4aa770bac1383e2310a4"
|
||||
checksum = "3f83d0ebf42c6eafb8d7c52f7e5f2d3003b89c7aa4fd2b79229209459a849af8"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cxxbridge-flags",
|
||||
@@ -1555,9 +1506,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cxx-build"
|
||||
version = "1.0.78"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e580d70777c116df50c390d1211993f62d40302881e54d4b79727acb83d0199"
|
||||
checksum = "07d050484b55975889284352b0ffc2ecbda25c0c55978017c132b29ba0818a86"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"codespan-reporting",
|
||||
@@ -1570,15 +1521,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-flags"
|
||||
version = "1.0.78"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56a46460b88d1cec95112c8c363f0e2c39afdb237f60583b0b36343bf627ea9c"
|
||||
checksum = "99d2199b00553eda8012dfec8d3b1c75fce747cf27c169a270b3b99e3448ab78"
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-macro"
|
||||
version = "1.0.78"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "747b608fecf06b0d72d440f27acc99288207324b793be2c17991839f3d4995ea"
|
||||
checksum = "dcb67a6de1f602736dd7eaead0080cf3435df806c61b24b13328db128c58868f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1602,8 +1553,13 @@ dependencies = [
|
||||
"async-trait",
|
||||
"collections",
|
||||
"gpui",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"parking_lot 0.11.2",
|
||||
"rocksdb",
|
||||
"rusqlite",
|
||||
"rusqlite_migration",
|
||||
"serde",
|
||||
"serde_rusqlite",
|
||||
"tempdir",
|
||||
]
|
||||
|
||||
@@ -1735,12 +1691,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dotenvy"
|
||||
version = "0.15.5"
|
||||
version = "0.15.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed9155c8f4dc55c7470ae9da3f63c6785245093b3f6aeb0f5bf2e968efbba314"
|
||||
dependencies = [
|
||||
"dirs 4.0.0",
|
||||
]
|
||||
checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0"
|
||||
|
||||
[[package]]
|
||||
name = "drag_and_drop"
|
||||
@@ -1784,6 +1737,7 @@ dependencies = [
|
||||
"collections",
|
||||
"context_menu",
|
||||
"ctor",
|
||||
"drag_and_drop",
|
||||
"env_logger",
|
||||
"futures 0.3.24",
|
||||
"fuzzy",
|
||||
@@ -1926,6 +1880,12 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-streaming-iterator"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.8.0"
|
||||
@@ -2730,9 +2690,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fde6edd6cef363e9359ed3c98ba64590ba9eecba2293eb5a723ab32aee8926aa"
|
||||
checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
|
||||
dependencies = [
|
||||
"cxx",
|
||||
"cxx-build",
|
||||
@@ -2946,6 +2906,8 @@ dependencies = [
|
||||
"editor",
|
||||
"gpui",
|
||||
"log",
|
||||
"settings",
|
||||
"shellexpand",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
@@ -3119,17 +3081,14 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "librocksdb-sys"
|
||||
version = "0.7.1+7.3.1"
|
||||
source = "git+https://github.com/rust-rocksdb/rust-rocksdb?rev=39dc822dde743b2a26eb160b660e8fbdab079d49#39dc822dde743b2a26eb160b660e8fbdab079d49"
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f0455f2c1bc9a7caa792907026e469c1d91761fb0ea37cbb16427c77280cf35"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"bzip2-sys",
|
||||
"cc",
|
||||
"glob",
|
||||
"libc",
|
||||
"libz-sys",
|
||||
"zstd-sys",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3185,17 +3144,54 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "live_kit"
|
||||
name = "live_kit_client"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
"async-trait",
|
||||
"block",
|
||||
"byteorder",
|
||||
"bytes 1.2.1",
|
||||
"cocoa",
|
||||
"collections",
|
||||
"core-foundation",
|
||||
"core-graphics",
|
||||
"foreign-types",
|
||||
"futures 0.3.24",
|
||||
"gpui",
|
||||
"hmac 0.12.1",
|
||||
"jwt",
|
||||
"lazy_static",
|
||||
"live_kit_server",
|
||||
"log",
|
||||
"media",
|
||||
"nanoid",
|
||||
"objc",
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.6",
|
||||
"simplelog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "live_kit_server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"futures 0.3.24",
|
||||
"hmac 0.12.1",
|
||||
"jwt",
|
||||
"log",
|
||||
"prost 0.8.0",
|
||||
"prost-build",
|
||||
"prost-types 0.8.0",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"sha2 0.10.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3450,7 +3446,7 @@ dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys",
|
||||
"windows-sys 0.36.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3879,7 +3875,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core 0.9.3",
|
||||
"parking_lot_core 0.9.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3898,15 +3894,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.3"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
|
||||
checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-sys",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4201,9 +4197,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.46"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
|
||||
checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -4246,7 +4242,6 @@ dependencies = [
|
||||
"pulldown-cmark",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"rocksdb",
|
||||
"rpc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4353,7 +4348,7 @@ dependencies = [
|
||||
"multimap",
|
||||
"petgraph",
|
||||
"prost 0.9.0",
|
||||
"prost-types",
|
||||
"prost-types 0.9.0",
|
||||
"regex",
|
||||
"tempfile",
|
||||
"which",
|
||||
@@ -4385,6 +4380,16 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-types"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "603bbd6394701d13f3f25aada59c7de9d35a6a5887cfc156181234a44002771b"
|
||||
dependencies = [
|
||||
"bytes 1.2.1",
|
||||
"prost 0.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-types"
|
||||
version = "0.9.0"
|
||||
@@ -4745,15 +4750,6 @@ dependencies = [
|
||||
"rmp",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rocksdb"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/rust-rocksdb/rust-rocksdb?rev=39dc822dde743b2a26eb160b660e8fbdab079d49#39dc822dde743b2a26eb160b660e8fbdab079d49"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"librocksdb-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rope"
|
||||
version = "0.1.0"
|
||||
@@ -4825,6 +4821,31 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
"libsqlite3-sys",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite_migration"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eda44233be97aea786691f9f6f7ef230bcf905061f4012e90f4f39e6dcf31163"
|
||||
dependencies = [
|
||||
"log",
|
||||
"rusqlite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "6.4.1"
|
||||
@@ -4912,9 +4933,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.20.6"
|
||||
version = "0.20.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033"
|
||||
checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring",
|
||||
@@ -4993,7 +5014,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys",
|
||||
"windows-sys 0.36.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5220,6 +5241,16 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_rusqlite"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "538b51f10ee271375cbd9caa04fa6e3e50af431a21db97caae48da92a074244a"
|
||||
dependencies = [
|
||||
"rusqlite",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
@@ -5594,7 +5625,7 @@ dependencies = [
|
||||
"paste",
|
||||
"percent-encoding",
|
||||
"rand 0.8.5",
|
||||
"rustls 0.20.6",
|
||||
"rustls 0.20.7",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -5826,6 +5857,7 @@ dependencies = [
|
||||
"futures 0.3.24",
|
||||
"gpui",
|
||||
"itertools",
|
||||
"language",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"mio-extras",
|
||||
@@ -5914,6 +5946,18 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "theme_testbench"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"gpui",
|
||||
"project",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"theme",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.37"
|
||||
@@ -6100,7 +6144,7 @@ version = "0.23.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
|
||||
dependencies = [
|
||||
"rustls 0.20.6",
|
||||
"rustls 0.20.7",
|
||||
"tokio",
|
||||
"webpki 0.22.0",
|
||||
]
|
||||
@@ -7384,43 +7428,100 @@ version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
|
||||
dependencies = [
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_msvc",
|
||||
"windows_aarch64_msvc 0.36.1",
|
||||
"windows_i686_gnu 0.36.1",
|
||||
"windows_i686_msvc 0.36.1",
|
||||
"windows_x86_64_gnu 0.36.1",
|
||||
"windows_x86_64_msvc 0.36.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc 0.42.0",
|
||||
"windows_i686_gnu 0.42.0",
|
||||
"windows_i686_msvc 0.42.0",
|
||||
"windows_x86_64_gnu 0.42.0",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.10.1"
|
||||
@@ -7507,9 +7608,9 @@ checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
|
||||
|
||||
[[package]]
|
||||
name = "xmlparser"
|
||||
version = "0.13.3"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "114ba2b24d2167ef6d67d7d04c8cc86522b87f490025f39f0303b7db5bf5e3d8"
|
||||
checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd"
|
||||
|
||||
[[package]]
|
||||
name = "xmlwriter"
|
||||
@@ -7528,7 +7629,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.60.1"
|
||||
version = "0.62.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -7540,7 +7641,6 @@ dependencies = [
|
||||
"backtrace",
|
||||
"breadcrumbs",
|
||||
"call",
|
||||
"chat_panel",
|
||||
"chrono",
|
||||
"cli",
|
||||
"client",
|
||||
@@ -7599,6 +7699,7 @@ dependencies = [
|
||||
"text",
|
||||
"theme",
|
||||
"theme_selector",
|
||||
"theme_testbench",
|
||||
"thiserror",
|
||||
"tiny_http",
|
||||
"toml",
|
||||
|
||||
60
Cargo.toml
60
Cargo.toml
@@ -1,5 +1,61 @@
|
||||
[workspace]
|
||||
members = ["crates/*"]
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/assets",
|
||||
"crates/auto_update",
|
||||
"crates/breadcrumbs",
|
||||
"crates/call",
|
||||
"crates/cli",
|
||||
"crates/client",
|
||||
"crates/clock",
|
||||
"crates/collab",
|
||||
"crates/collab_ui",
|
||||
"crates/collections",
|
||||
"crates/command_palette",
|
||||
"crates/context_menu",
|
||||
"crates/db",
|
||||
"crates/diagnostics",
|
||||
"crates/drag_and_drop",
|
||||
"crates/editor",
|
||||
"crates/file_finder",
|
||||
"crates/fs",
|
||||
"crates/fsevent",
|
||||
"crates/fuzzy",
|
||||
"crates/git",
|
||||
"crates/go_to_line",
|
||||
"crates/gpui",
|
||||
"crates/gpui_macros",
|
||||
"crates/journal",
|
||||
"crates/language",
|
||||
"crates/live_kit_client",
|
||||
"crates/live_kit_server",
|
||||
"crates/lsp",
|
||||
"crates/media",
|
||||
"crates/menu",
|
||||
"crates/outline",
|
||||
"crates/picker",
|
||||
"crates/plugin",
|
||||
"crates/plugin_macros",
|
||||
"crates/plugin_runtime",
|
||||
"crates/project",
|
||||
"crates/project_panel",
|
||||
"crates/project_symbols",
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
"crates/search",
|
||||
"crates/settings",
|
||||
"crates/snippet",
|
||||
"crates/sum_tree",
|
||||
"crates/terminal",
|
||||
"crates/text",
|
||||
"crates/theme",
|
||||
"crates/theme_selector",
|
||||
"crates/theme_testbench",
|
||||
"crates/util",
|
||||
"crates/vim",
|
||||
"crates/workspace",
|
||||
"crates/zed",
|
||||
]
|
||||
default-members = ["crates/zed"]
|
||||
resolver = "2"
|
||||
|
||||
@@ -18,8 +74,6 @@ cocoa-foundation = { git = "https://github.com/servo/core-foundation-rs", rev =
|
||||
core-foundation = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" }
|
||||
core-foundation-sys = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" }
|
||||
core-graphics = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" }
|
||||
# TODO - Remove when a new version of RustRocksDB is released
|
||||
rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "39dc822dde743b2a26eb160b660e8fbdab079d49" }
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
|
||||
@@ -19,5 +19,7 @@ FROM debian:bullseye-slim as runtime
|
||||
RUN apt-get update; \
|
||||
apt-get install -y --no-install-recommends libcurl4-openssl-dev ca-certificates
|
||||
WORKDIR app
|
||||
COPY --from=builder /app/collab /app
|
||||
COPY --from=builder /app/collab /app/collab
|
||||
COPY --from=builder /app/crates/collab/migrations /app/migrations
|
||||
ENV MIGRATIONS_PATH=/app/migrations
|
||||
ENTRYPOINT ["/app/collab"]
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.64-bullseye as builder
|
||||
WORKDIR app
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=./target \
|
||||
cargo install sqlx-cli --root=/app --target-dir=/app/target --version 0.5.7
|
||||
|
||||
FROM debian:bullseye-slim as runtime
|
||||
RUN apt-get update; \
|
||||
apt-get install -y --no-install-recommends libssl1.1
|
||||
WORKDIR app
|
||||
COPY --from=builder /app/bin/sqlx /app
|
||||
COPY ./crates/collab/migrations /app/migrations
|
||||
ENTRYPOINT ["/app/sqlx", "migrate", "run"]
|
||||
4
Procfile
4
Procfile
@@ -1,2 +1,2 @@
|
||||
web: cd ../zed.dev && PORT=3000 npx next dev
|
||||
collab: cd crates/collab && cargo run
|
||||
web: cd ../zed.dev && PORT=3000 npx vercel dev
|
||||
collab: cd crates/collab && cargo run serve
|
||||
|
||||
3
assets/icons/disable_screen_sharing_12.svg
Normal file
3
assets/icons/disable_screen_sharing_12.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 0.666656H1C0.447917 0.666656 0 1.11457 0 1.66666V8.33332C0 8.88541 0.447917 9.33332 1 9.33332H5L4.66667 10.3333H3.16667C2.89167 10.3333 2.66667 10.5583 2.66667 10.8333C2.66667 11.1083 2.89167 11.3333 3.16667 11.3333H8.83333C9.10938 11.3333 9.33333 11.1094 9.33333 10.8333C9.33333 10.5573 9.10938 10.3333 8.83333 10.3333H7.33333L7 9.33332H11C11.5521 9.33332 12 8.88541 12 8.33332V1.66666C12 1.11457 11.5521 0.666656 11 0.666656ZM10.6667 7.99999H1.33333V1.99999H10.6667V7.99999Z" fill="#979DB4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 611 B |
3
assets/icons/enable_screen_sharing_12.svg
Normal file
3
assets/icons/enable_screen_sharing_12.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.53324 9.90014H7.18324L6.88324 9.00014H7.63305L6.10211 7.80014H1.78324V4.41577L0.583236 3.47452V8.10014C0.583217 8.59702 0.986361 9.00014 1.46636 9.00014H5.04949L4.74949 9.90014H3.43324C3.1848 9.90014 2.98324 10.1017 2.98324 10.3501C2.98324 10.5986 3.1848 10.8001 3.43324 10.8001H8.51637C8.7648 10.8001 8.96637 10.5986 8.96637 10.3501C8.96637 10.1017 8.79762 9.90014 8.53324 9.90014ZM11.8276 9.99577L10.5507 8.99489C11.0234 8.96789 11.3999 8.57939 11.3999 8.09996V2.09995C11.3999 1.60308 10.9968 1.19995 10.4999 1.19995H1.5168C1.28617 1.19995 1.07786 1.28939 0.918674 1.43208L0.727799 1.29595C0.645299 1.23145 0.547423 1.19995 0.450673 1.19995C0.316986 1.19995 0.184611 1.2592 0.0961106 1.37226C-0.057452 1.56801 -0.023327 1.85095 0.172236 2.00414L11.2724 10.7041C11.4693 10.8579 11.7519 10.8226 11.9041 10.6276C12.0581 10.4321 12.0224 10.149 11.8274 9.99521L11.8276 9.99577ZM10.1832 7.80014H9.00968L2.11905 2.40014H10.1816L10.1832 7.80014Z" fill="#93A1A1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -3,8 +3,12 @@
|
||||
{
|
||||
"bindings": {
|
||||
"up": "menu::SelectPrev",
|
||||
"pageup": "menu::SelectFirst",
|
||||
"shift-pageup": "menu::SelectFirst",
|
||||
"ctrl-p": "menu::SelectPrev",
|
||||
"down": "menu::SelectNext",
|
||||
"pagedown": "menu::SelectLast",
|
||||
"shift-pagedown": "menu::SelectFirst",
|
||||
"ctrl-n": "menu::SelectNext",
|
||||
"cmd-up": "menu::SelectFirst",
|
||||
"cmd-down": "menu::SelectLast",
|
||||
@@ -60,13 +64,18 @@
|
||||
"cmd-z": "editor::Undo",
|
||||
"cmd-shift-z": "editor::Redo",
|
||||
"up": "editor::MoveUp",
|
||||
"pageup": "editor::PageUp",
|
||||
"shift-pageup": "editor::MovePageUp",
|
||||
"down": "editor::MoveDown",
|
||||
"pagedown": "editor::PageDown",
|
||||
"shift-pagedown": "editor::MovePageDown",
|
||||
"left": "editor::MoveLeft",
|
||||
"right": "editor::MoveRight",
|
||||
"ctrl-p": "editor::MoveUp",
|
||||
"ctrl-n": "editor::MoveDown",
|
||||
"ctrl-b": "editor::MoveLeft",
|
||||
"ctrl-f": "editor::MoveRight",
|
||||
"ctrl-l": "editor::CenterScreen",
|
||||
"alt-left": "editor::MoveToPreviousWordStart",
|
||||
"alt-b": "editor::MoveToPreviousWordStart",
|
||||
"alt-right": "editor::MoveToNextWordEnd",
|
||||
@@ -118,8 +127,18 @@
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"pageup": "editor::PageUp",
|
||||
"pagedown": "editor::PageDown",
|
||||
"ctrl-v": [
|
||||
"editor::MovePageDown",
|
||||
{
|
||||
"center_cursor": true
|
||||
}
|
||||
],
|
||||
"alt-v": [
|
||||
"editor::MovePageUp",
|
||||
{
|
||||
"center_cursor": true
|
||||
}
|
||||
],
|
||||
"ctrl-cmd-space": "editor::ShowCharacterPalette"
|
||||
}
|
||||
},
|
||||
@@ -412,6 +431,12 @@
|
||||
"shift-escape": "dock::HideDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"cmd-escape": "dock::MoveActiveItemToDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
@@ -451,10 +476,18 @@
|
||||
"terminal::SendKeystroke",
|
||||
"up"
|
||||
],
|
||||
"pageup": [
|
||||
"terminal::SendKeystroke",
|
||||
"pageup"
|
||||
],
|
||||
"down": [
|
||||
"terminal::SendKeystroke",
|
||||
"down"
|
||||
],
|
||||
"pagedown": [
|
||||
"terminal::SendKeystroke",
|
||||
"pagedown"
|
||||
],
|
||||
"escape": [
|
||||
"terminal::SendKeystroke",
|
||||
"escape"
|
||||
|
||||
@@ -1,218 +1,230 @@
|
||||
{
|
||||
// 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 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}"]
|
||||
// }
|
||||
// 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"]
|
||||
// }
|
||||
// }
|
||||
"formatter": "language_server",
|
||||
// How to soft-wrap long lines of text. This setting can take
|
||||
// three values:
|
||||
"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/"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// 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 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"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
"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"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ impl ActivityIndicator {
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> ViewHandle<ActivityIndicator> {
|
||||
let project = workspace.project().clone();
|
||||
let auto_updater = AutoUpdater::get(cx);
|
||||
let this = cx.add_view(|cx: &mut ViewContext<Self>| {
|
||||
let mut status_events = languages.language_server_binary_statuses();
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
@@ -66,11 +67,14 @@ impl ActivityIndicator {
|
||||
})
|
||||
.detach();
|
||||
cx.observe(&project, |_, _, cx| cx.notify()).detach();
|
||||
if let Some(auto_updater) = auto_updater.as_ref() {
|
||||
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
|
||||
}
|
||||
|
||||
Self {
|
||||
statuses: Default::default(),
|
||||
project: project.clone(),
|
||||
auto_updater: AutoUpdater::get(cx),
|
||||
auto_updater,
|
||||
}
|
||||
});
|
||||
cx.subscribe(&this, move |workspace, _, event, cx| match event {
|
||||
@@ -285,7 +289,7 @@ impl View for ActivityIndicator {
|
||||
.workspace
|
||||
.status_bar
|
||||
.lsp_status;
|
||||
let style = if state.hovered && action.is_some() {
|
||||
let style = if state.hovered() && action.is_some() {
|
||||
theme.hover.as_ref().unwrap_or(&theme.default)
|
||||
} else {
|
||||
&theme.default
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
mod update_notification;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN};
|
||||
use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
|
||||
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;
|
||||
@@ -40,7 +41,7 @@ pub struct AutoUpdater {
|
||||
current_version: AppVersion,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
pending_poll: Option<Task<()>>,
|
||||
db: Arc<project::Db>,
|
||||
db: project::Db,
|
||||
server_url: String,
|
||||
}
|
||||
|
||||
@@ -54,13 +55,9 @@ impl Entity for AutoUpdater {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
pub fn init(
|
||||
db: Arc<project::Db>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
server_url: String,
|
||||
cx: &mut MutableAppContext,
|
||||
) {
|
||||
pub fn init(db: project::Db, http_client: Arc<dyn HttpClient>, 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 auto_updater = cx.add_model(|cx| {
|
||||
let updater = AutoUpdater::new(version, db.clone(), http_client, server_url.clone());
|
||||
updater.start_polling(cx).detach();
|
||||
@@ -116,7 +113,7 @@ impl AutoUpdater {
|
||||
|
||||
fn new(
|
||||
current_version: AppVersion,
|
||||
db: Arc<project::Db>,
|
||||
db: project::Db,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
server_url: String,
|
||||
) -> Self {
|
||||
@@ -177,9 +174,19 @@ impl AutoUpdater {
|
||||
this.current_version,
|
||||
)
|
||||
});
|
||||
|
||||
let preview_param = cx.read(|cx| {
|
||||
if cx.has_global::<ReleaseChannel>() {
|
||||
if *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview {
|
||||
return "&preview=1";
|
||||
}
|
||||
}
|
||||
""
|
||||
});
|
||||
|
||||
let mut response = client
|
||||
.get(
|
||||
&format!("{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg"),
|
||||
&format!("{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg{preview_param}"),
|
||||
Default::default(),
|
||||
true,
|
||||
)
|
||||
@@ -283,9 +290,9 @@ impl AutoUpdater {
|
||||
let db = self.db.clone();
|
||||
cx.background().spawn(async move {
|
||||
if should_show {
|
||||
db.write([(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY, "")])?;
|
||||
db.write_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY, "")?;
|
||||
} else {
|
||||
db.delete([(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)])?;
|
||||
db.delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
@@ -293,8 +300,7 @@ impl AutoUpdater {
|
||||
|
||||
fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
|
||||
let db = self.db.clone();
|
||||
cx.background().spawn(async move {
|
||||
Ok(db.read([(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)])?[0].is_some())
|
||||
})
|
||||
cx.background()
|
||||
.spawn(async move { Ok(db.read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?.is_some()) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ test-support = [
|
||||
"client/test-support",
|
||||
"collections/test-support",
|
||||
"gpui/test-support",
|
||||
"live_kit_client/test-support",
|
||||
"project/test-support",
|
||||
"util/test-support"
|
||||
]
|
||||
@@ -20,10 +21,13 @@ test-support = [
|
||||
client = { path = "../client" }
|
||||
collections = { path = "../collections" }
|
||||
gpui = { path = "../gpui" }
|
||||
live_kit_client = { path = "../live_kit_client" }
|
||||
media = { path = "../media" }
|
||||
project = { path = "../project" }
|
||||
util = { path = "../util" }
|
||||
|
||||
anyhow = "1.0.38"
|
||||
async-broadcast = "0.4"
|
||||
futures = "0.3"
|
||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||
|
||||
@@ -31,5 +35,6 @@ postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
collections = { path = "../collections", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
mod participant;
|
||||
pub mod participant;
|
||||
pub mod room;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::{proto, Client, TypedEnvelope, User, UserStore};
|
||||
use collections::HashSet;
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
|
||||
Subscription, Task,
|
||||
Subscription, Task, WeakModelHandle,
|
||||
};
|
||||
pub use participant::ParticipantLocation;
|
||||
use postage::watch;
|
||||
@@ -28,6 +29,8 @@ pub struct IncomingCall {
|
||||
|
||||
pub struct ActiveCall {
|
||||
room: Option<(ModelHandle<Room>, Vec<Subscription>)>,
|
||||
location: Option<WeakModelHandle<Project>>,
|
||||
pending_invites: HashSet<u64>,
|
||||
incoming_call: (
|
||||
watch::Sender<Option<IncomingCall>>,
|
||||
watch::Receiver<Option<IncomingCall>>,
|
||||
@@ -49,6 +52,8 @@ impl ActiveCall {
|
||||
) -> Self {
|
||||
Self {
|
||||
room: None,
|
||||
location: None,
|
||||
pending_invites: Default::default(),
|
||||
incoming_call: watch::channel(),
|
||||
_subscriptions: vec![
|
||||
client.add_request_handler(cx.handle(), Self::handle_incoming_call),
|
||||
@@ -111,31 +116,49 @@ impl ActiveCall {
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if let Some(room) = this.read_with(&cx, |this, _| this.room().cloned()) {
|
||||
let initial_project_id = if let Some(initial_project) = initial_project {
|
||||
Some(
|
||||
room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if !self.pending_invites.insert(recipient_user_id) {
|
||||
return Task::ready(Err(anyhow!("user was already invited")));
|
||||
}
|
||||
|
||||
room.update(&mut cx, |room, cx| {
|
||||
room.call(recipient_user_id, initial_project_id, cx)
|
||||
})
|
||||
.await?;
|
||||
} else {
|
||||
let room = cx
|
||||
.update(|cx| {
|
||||
Room::create(recipient_user_id, initial_project, client, user_store, cx)
|
||||
cx.notify();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let invite = async {
|
||||
if let Some(room) = this.read_with(&cx, |this, _| this.room().cloned()) {
|
||||
let initial_project_id = if let Some(initial_project) = initial_project {
|
||||
Some(
|
||||
room.update(&mut cx, |room, cx| {
|
||||
room.share_project(initial_project, cx)
|
||||
})
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
room.update(&mut cx, |room, cx| {
|
||||
room.call(recipient_user_id, initial_project_id, cx)
|
||||
})
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| this.set_room(Some(room), cx));
|
||||
} else {
|
||||
let room = cx
|
||||
.update(|cx| {
|
||||
Room::create(recipient_user_id, initial_project, client, user_store, cx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| this.set_room(Some(room), cx))
|
||||
.await?;
|
||||
};
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
Ok(())
|
||||
let result = invite.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.pending_invites.remove(&recipient_user_id);
|
||||
cx.notify();
|
||||
});
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
@@ -180,7 +203,8 @@ impl ActiveCall {
|
||||
let join = Room::join(&call, self.client.clone(), self.user_store.clone(), cx);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let room = join.await?;
|
||||
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx));
|
||||
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -223,39 +247,54 @@ impl ActiveCall {
|
||||
project: Option<&ModelHandle<Project>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.location = project.map(|project| project.downgrade());
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
room.update(cx, |room, cx| room.set_location(project, cx))
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("no active call")))
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
fn set_room(&mut self, room: Option<ModelHandle<Room>>, cx: &mut ModelContext<Self>) {
|
||||
fn set_room(
|
||||
&mut self,
|
||||
room: Option<ModelHandle<Room>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if room.as_ref() != self.room.as_ref().map(|room| &room.0) {
|
||||
cx.notify();
|
||||
if let Some(room) = room {
|
||||
if room.read(cx).status().is_offline() {
|
||||
self.room = None;
|
||||
Task::ready(Ok(()))
|
||||
} else {
|
||||
let subscriptions = vec![
|
||||
cx.observe(&room, |this, room, cx| {
|
||||
if room.read(cx).status().is_offline() {
|
||||
this.set_room(None, cx);
|
||||
this.set_room(None, cx).detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}),
|
||||
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
|
||||
];
|
||||
self.room = Some((room, subscriptions));
|
||||
self.room = Some((room.clone(), subscriptions));
|
||||
let location = self.location.and_then(|location| location.upgrade(cx));
|
||||
room.update(cx, |room, cx| room.set_location(location.as_ref(), cx))
|
||||
}
|
||||
} else {
|
||||
self.room = None;
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
cx.notify();
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn room(&self) -> Option<&ModelHandle<Room>> {
|
||||
self.room.as_ref().map(|(room, _)| room)
|
||||
}
|
||||
|
||||
pub fn pending_invites(&self) -> &HashSet<u64> {
|
||||
&self.pending_invites
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::{proto, User};
|
||||
use collections::HashMap;
|
||||
use gpui::WeakModelHandle;
|
||||
pub use live_kit_client::Frame;
|
||||
use project::Project;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -34,9 +36,21 @@ pub struct LocalParticipant {
|
||||
pub active_project: Option<WeakModelHandle<Project>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone)]
|
||||
pub struct RemoteParticipant {
|
||||
pub user: Arc<User>,
|
||||
pub projects: Vec<proto::ParticipantProject>,
|
||||
pub location: ParticipantLocation,
|
||||
pub tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RemoteVideoTrack {
|
||||
pub(crate) live_kit_track: Arc<live_kit_client::RemoteVideoTrack>,
|
||||
}
|
||||
|
||||
impl RemoteVideoTrack {
|
||||
pub fn frames(&self) -> async_broadcast::Receiver<Frame> {
|
||||
self.live_kit_track.frames()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
participant::{LocalParticipant, ParticipantLocation, RemoteParticipant},
|
||||
participant::{LocalParticipant, ParticipantLocation, RemoteParticipant, RemoteVideoTrack},
|
||||
IncomingCall,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
@@ -7,12 +7,20 @@ use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
|
||||
use collections::{BTreeMap, HashSet};
|
||||
use futures::StreamExt;
|
||||
use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task};
|
||||
use live_kit_client::{LocalTrackPublication, LocalVideoTrack, RemoteVideoTrackUpdate};
|
||||
use postage::stream::Stream;
|
||||
use project::Project;
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
use std::{mem, os::unix::prelude::OsStrExt, sync::Arc};
|
||||
use util::{post_inc, ResultExt};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Event {
|
||||
ParticipantLocationChanged {
|
||||
participant_id: PeerId,
|
||||
},
|
||||
RemoteVideoTracksChanged {
|
||||
participant_id: PeerId,
|
||||
},
|
||||
RemoteProjectShared {
|
||||
owner: Arc<User>,
|
||||
project_id: u64,
|
||||
@@ -26,6 +34,7 @@ pub enum Event {
|
||||
|
||||
pub struct Room {
|
||||
id: u64,
|
||||
live_kit: Option<LiveKitRoom>,
|
||||
status: RoomStatus,
|
||||
local_participant: LocalParticipant,
|
||||
remote_participants: BTreeMap<PeerId, RemoteParticipant>,
|
||||
@@ -43,13 +52,16 @@ impl Entity for Room {
|
||||
type Event = Event;
|
||||
|
||||
fn release(&mut self, _: &mut MutableAppContext) {
|
||||
self.client.send(proto::LeaveRoom { id: self.id }).log_err();
|
||||
if self.status.is_online() {
|
||||
self.client.send(proto::LeaveRoom { id: self.id }).log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Room {
|
||||
fn new(
|
||||
id: u64,
|
||||
live_kit_connection_info: Option<proto::LiveKitConnectionInfo>,
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
@@ -69,8 +81,59 @@ impl Room {
|
||||
})
|
||||
.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();
|
||||
// Consume the initial status of the room.
|
||||
let _ = status.try_recv();
|
||||
let _maintain_room = cx.spawn_weak(|this, mut cx| async move {
|
||||
while let Some(status) = status.next().await {
|
||||
let this = if let Some(this) = this.upgrade(&cx) {
|
||||
this
|
||||
} else {
|
||||
break;
|
||||
};
|
||||
|
||||
if status == live_kit_client::ConnectionState::Disconnected {
|
||||
this.update(&mut cx, |this, cx| this.leave(cx).log_err());
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut track_changes = room.remote_video_track_updates();
|
||||
let _maintain_tracks = cx.spawn_weak(|this, mut cx| async move {
|
||||
while let Some(track_change) = track_changes.next().await {
|
||||
let this = if let Some(this) = this.upgrade(&cx) {
|
||||
this
|
||||
} else {
|
||||
break;
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.remote_video_track_updated(track_change, cx).log_err()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
cx.foreground()
|
||||
.spawn(room.connect(&connection_info.server_url, &connection_info.token))
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
Some(LiveKitRoom {
|
||||
room,
|
||||
screen_track: ScreenTrack::None,
|
||||
next_publish_id: 0,
|
||||
_maintain_room,
|
||||
_maintain_tracks,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Self {
|
||||
id,
|
||||
live_kit: live_kit_room,
|
||||
status: RoomStatus::Online,
|
||||
participant_user_ids: Default::default(),
|
||||
local_participant: Default::default(),
|
||||
@@ -94,7 +157,16 @@ impl Room {
|
||||
) -> Task<Result<ModelHandle<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let response = client.request(proto::CreateRoom {}).await?;
|
||||
let room = cx.add_model(|cx| Self::new(response.id, client, user_store, cx));
|
||||
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
|
||||
let room = cx.add_model(|cx| {
|
||||
Self::new(
|
||||
room_proto.id,
|
||||
response.live_kit_connection_info,
|
||||
client,
|
||||
user_store,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let initial_project_id = if let Some(initial_project) = initial_project {
|
||||
let initial_project_id = room
|
||||
@@ -130,7 +202,15 @@ impl Room {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let response = client.request(proto::JoinRoom { id: room_id }).await?;
|
||||
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
|
||||
let room = cx.add_model(|cx| Self::new(room_id, client, user_store, cx));
|
||||
let room = cx.add_model(|cx| {
|
||||
Self::new(
|
||||
room_id,
|
||||
response.live_kit_connection_info,
|
||||
client,
|
||||
user_store,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
room.update(&mut cx, |room, cx| {
|
||||
room.leave_when_empty = true;
|
||||
room.apply_room_update(room_proto, cx)?;
|
||||
@@ -160,6 +240,7 @@ impl Room {
|
||||
self.pending_participants.clear();
|
||||
self.participant_user_ids.clear();
|
||||
self.subscriptions.clear();
|
||||
self.live_kit.take();
|
||||
self.client.send(proto::LeaveRoom { id: self.id })?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -272,15 +353,40 @@ impl Room {
|
||||
});
|
||||
}
|
||||
|
||||
this.remote_participants.insert(
|
||||
peer_id,
|
||||
RemoteParticipant {
|
||||
user: user.clone(),
|
||||
projects: participant.projects,
|
||||
location: ParticipantLocation::from_proto(participant.location)
|
||||
.unwrap_or(ParticipantLocation::External),
|
||||
},
|
||||
);
|
||||
let location = ParticipantLocation::from_proto(participant.location)
|
||||
.unwrap_or(ParticipantLocation::External);
|
||||
if let Some(remote_participant) = this.remote_participants.get_mut(&peer_id)
|
||||
{
|
||||
remote_participant.projects = participant.projects;
|
||||
if location != remote_participant.location {
|
||||
remote_participant.location = location;
|
||||
cx.emit(Event::ParticipantLocationChanged {
|
||||
participant_id: peer_id,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.remote_participants.insert(
|
||||
peer_id,
|
||||
RemoteParticipant {
|
||||
user: user.clone(),
|
||||
projects: participant.projects,
|
||||
location,
|
||||
tracks: Default::default(),
|
||||
},
|
||||
);
|
||||
|
||||
if let Some(live_kit) = this.live_kit.as_ref() {
|
||||
let tracks =
|
||||
live_kit.room.remote_video_tracks(&peer_id.0.to_string());
|
||||
for track in tracks {
|
||||
this.remote_video_track_updated(
|
||||
RemoteVideoTrackUpdate::Subscribed(track),
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.remote_participants.retain(|_, participant| {
|
||||
@@ -318,6 +424,49 @@ impl Room {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remote_video_track_updated(
|
||||
&mut self,
|
||||
change: RemoteVideoTrackUpdate,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
match change {
|
||||
RemoteVideoTrackUpdate::Subscribed(track) => {
|
||||
let peer_id = PeerId(track.publisher_id().parse()?);
|
||||
let track_id = track.sid().to_string();
|
||||
let participant = self
|
||||
.remote_participants
|
||||
.get_mut(&peer_id)
|
||||
.ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
|
||||
participant.tracks.insert(
|
||||
track_id.clone(),
|
||||
Arc::new(RemoteVideoTrack {
|
||||
live_kit_track: track,
|
||||
}),
|
||||
);
|
||||
cx.emit(Event::RemoteVideoTracksChanged {
|
||||
participant_id: peer_id,
|
||||
});
|
||||
}
|
||||
RemoteVideoTrackUpdate::Unsubscribed {
|
||||
publisher_id,
|
||||
track_id,
|
||||
} => {
|
||||
let peer_id = PeerId(publisher_id.parse()?);
|
||||
let participant = self
|
||||
.remote_participants
|
||||
.get_mut(&peer_id)
|
||||
.ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
|
||||
participant.tracks.remove(&track_id);
|
||||
cx.emit(Event::RemoteVideoTracksChanged {
|
||||
participant_id: peer_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_invariants(&self) {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
{
|
||||
@@ -389,6 +538,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(),
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
@@ -417,7 +567,7 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_location(
|
||||
pub(crate) fn set_location(
|
||||
&mut self,
|
||||
project: Option<&ModelHandle<Project>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
@@ -457,6 +607,140 @@ impl Room {
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_screen_sharing(&self) -> bool {
|
||||
self.live_kit.as_ref().map_or(false, |live_kit| {
|
||||
!matches!(live_kit.screen_track, ScreenTrack::None)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn share_screen(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
if self.status.is_offline() {
|
||||
return Task::ready(Err(anyhow!("room is offline")));
|
||||
} else if self.is_screen_sharing() {
|
||||
return Task::ready(Err(anyhow!("screen was already shared")));
|
||||
}
|
||||
|
||||
let (displays, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() {
|
||||
let publish_id = post_inc(&mut live_kit.next_publish_id);
|
||||
live_kit.screen_track = ScreenTrack::Pending { publish_id };
|
||||
cx.notify();
|
||||
(live_kit.room.display_sources(), publish_id)
|
||||
} else {
|
||||
return Task::ready(Err(anyhow!("live-kit was not initialized")));
|
||||
};
|
||||
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
let publish_track = async {
|
||||
let displays = displays.await?;
|
||||
let display = displays
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("no display found"))?;
|
||||
let track = LocalVideoTrack::screen_share_for_display(&display);
|
||||
this.upgrade(&cx)
|
||||
.ok_or_else(|| anyhow!("room was dropped"))?
|
||||
.read_with(&cx, |this, _| {
|
||||
this.live_kit
|
||||
.as_ref()
|
||||
.map(|live_kit| live_kit.room.publish_video_track(&track))
|
||||
})
|
||||
.ok_or_else(|| anyhow!("live-kit was not initialized"))?
|
||||
.await
|
||||
};
|
||||
|
||||
let publication = publish_track.await;
|
||||
this.upgrade(&cx)
|
||||
.ok_or_else(|| anyhow!("room was dropped"))?
|
||||
.update(&mut cx, |this, cx| {
|
||||
let live_kit = this
|
||||
.live_kit
|
||||
.as_mut()
|
||||
.ok_or_else(|| anyhow!("live-kit was not initialized"))?;
|
||||
|
||||
let canceled = if let ScreenTrack::Pending {
|
||||
publish_id: cur_publish_id,
|
||||
} = &live_kit.screen_track
|
||||
{
|
||||
*cur_publish_id != publish_id
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
match publication {
|
||||
Ok(publication) => {
|
||||
if canceled {
|
||||
live_kit.room.unpublish_track(publication);
|
||||
} else {
|
||||
live_kit.screen_track = ScreenTrack::Published(publication);
|
||||
cx.notify();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(error) => {
|
||||
if canceled {
|
||||
Ok(())
|
||||
} else {
|
||||
live_kit.screen_track = ScreenTrack::None;
|
||||
cx.notify();
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn unshare_screen(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
|
||||
if self.status.is_offline() {
|
||||
return Err(anyhow!("room is offline"));
|
||||
}
|
||||
|
||||
let live_kit = self
|
||||
.live_kit
|
||||
.as_mut()
|
||||
.ok_or_else(|| anyhow!("live-kit was not initialized"))?;
|
||||
match mem::take(&mut live_kit.screen_track) {
|
||||
ScreenTrack::None => Err(anyhow!("screen was not shared")),
|
||||
ScreenTrack::Pending { .. } => {
|
||||
cx.notify();
|
||||
Ok(())
|
||||
}
|
||||
ScreenTrack::Published(track) => {
|
||||
live_kit.room.unpublish_track(track);
|
||||
cx.notify();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn set_display_sources(&self, sources: Vec<live_kit_client::MacOSDisplay>) {
|
||||
self.live_kit
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.room
|
||||
.set_display_sources(sources);
|
||||
}
|
||||
}
|
||||
|
||||
struct LiveKitRoom {
|
||||
room: Arc<live_kit_client::Room>,
|
||||
screen_track: ScreenTrack,
|
||||
next_publish_id: usize,
|
||||
_maintain_room: Task<()>,
|
||||
_maintain_tracks: Task<()>,
|
||||
}
|
||||
|
||||
enum ScreenTrack {
|
||||
None,
|
||||
Pending { publish_id: usize },
|
||||
Published(LocalTrackPublication),
|
||||
}
|
||||
|
||||
impl Default for ScreenTrack {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
@@ -469,4 +753,8 @@ impl RoomStatus {
|
||||
pub fn is_offline(&self) -> bool {
|
||||
matches!(self, RoomStatus::Offline)
|
||||
}
|
||||
|
||||
pub fn is_online(&self) -> bool {
|
||||
matches!(self, RoomStatus::Online)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
[package]
|
||||
name = "capture"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "An example of screen capture"
|
||||
|
||||
[dependencies]
|
||||
gpui = { path = "../gpui" }
|
||||
live_kit = { path = "../live_kit" }
|
||||
media = { path = "../media" }
|
||||
|
||||
anyhow = "1.0.38"
|
||||
block = "0.1"
|
||||
bytes = "1.2"
|
||||
byteorder = "1.4"
|
||||
cocoa = "0.24"
|
||||
core-foundation = "0.9.3"
|
||||
core-graphics = "0.22.3"
|
||||
foreign-types = "0.3"
|
||||
futures = "0.3"
|
||||
hmac = "0.12"
|
||||
jwt = "0.16"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
objc = "0.2"
|
||||
parking_lot = "0.11.1"
|
||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
sha2 = "0.10"
|
||||
simplelog = "0.9"
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "0.59.2"
|
||||
@@ -1,7 +0,0 @@
|
||||
fn main() {
|
||||
// Find WebRTC.framework as a sibling of the executable when running outside of an application bundle
|
||||
println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
|
||||
|
||||
// Register exported Objective-C selectors, protocols, etc
|
||||
println!("cargo:rustc-link-arg=-Wl,-ObjC");
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use hmac::{Hmac, Mac};
|
||||
use jwt::SignWithKey;
|
||||
use serde::Serialize;
|
||||
use sha2::Sha256;
|
||||
use std::{
|
||||
ops::Add,
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
static DEFAULT_TTL: Duration = Duration::from_secs(6 * 60 * 60); // 6 hours
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ClaimGrants<'a> {
|
||||
iss: &'a str,
|
||||
sub: &'a str,
|
||||
iat: u64,
|
||||
exp: u64,
|
||||
nbf: u64,
|
||||
jwtid: &'a str,
|
||||
video: VideoGrant<'a>,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct VideoGrant<'a> {
|
||||
room_create: Option<bool>,
|
||||
room_join: Option<bool>,
|
||||
room_list: Option<bool>,
|
||||
room_record: Option<bool>,
|
||||
room_admin: Option<bool>,
|
||||
room: Option<&'a str>,
|
||||
can_publish: Option<bool>,
|
||||
can_subscribe: Option<bool>,
|
||||
can_publish_data: Option<bool>,
|
||||
hidden: Option<bool>,
|
||||
recorder: Option<bool>,
|
||||
}
|
||||
|
||||
pub fn create_token(
|
||||
api_key: &str,
|
||||
secret_key: &str,
|
||||
room_name: &str,
|
||||
participant_name: &str,
|
||||
) -> Result<String> {
|
||||
let secret_key: Hmac<Sha256> = Hmac::new_from_slice(secret_key.as_bytes())?;
|
||||
|
||||
let now = SystemTime::now();
|
||||
|
||||
let claims = ClaimGrants {
|
||||
iss: api_key,
|
||||
sub: participant_name,
|
||||
iat: now.duration_since(UNIX_EPOCH).unwrap().as_secs(),
|
||||
exp: now
|
||||
.add(DEFAULT_TTL)
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs(),
|
||||
nbf: 0,
|
||||
jwtid: participant_name,
|
||||
video: VideoGrant {
|
||||
room: Some(room_name),
|
||||
room_join: Some(true),
|
||||
can_publish: Some(true),
|
||||
can_subscribe: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
Ok(claims.sign_with_key(&secret_key)?)
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
mod live_kit_token;
|
||||
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{Canvas, *},
|
||||
keymap::Binding,
|
||||
platform::current::Surface,
|
||||
Menu, MenuItem, ViewContext,
|
||||
};
|
||||
use live_kit::{LocalVideoTrack, Room};
|
||||
use log::LevelFilter;
|
||||
use media::core_video::CVImageBuffer;
|
||||
use postage::watch;
|
||||
use simplelog::SimpleLogger;
|
||||
use std::sync::Arc;
|
||||
|
||||
actions!(capture, [Quit]);
|
||||
|
||||
fn main() {
|
||||
SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
|
||||
|
||||
gpui::App::new(()).unwrap().run(|cx| {
|
||||
cx.platform().activate(true);
|
||||
cx.add_global_action(quit);
|
||||
|
||||
cx.add_bindings([Binding::new("cmd-q", Quit, None)]);
|
||||
cx.set_menus(vec![Menu {
|
||||
name: "Zed",
|
||||
items: vec![MenuItem::Action {
|
||||
name: "Quit",
|
||||
action: Box::new(Quit),
|
||||
}],
|
||||
}]);
|
||||
|
||||
let live_kit_url = std::env::var("LIVE_KIT_URL").unwrap();
|
||||
let live_kit_key = std::env::var("LIVE_KIT_KEY").unwrap();
|
||||
let live_kit_secret = std::env::var("LIVE_KIT_SECRET").unwrap();
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
let user1_token = live_kit_token::create_token(
|
||||
&live_kit_key,
|
||||
&live_kit_secret,
|
||||
"test-room",
|
||||
"test-participant-1",
|
||||
)
|
||||
.unwrap();
|
||||
let room1 = Room::new();
|
||||
room1.connect(&live_kit_url, &user1_token).await.unwrap();
|
||||
|
||||
let user2_token = live_kit_token::create_token(
|
||||
&live_kit_key,
|
||||
&live_kit_secret,
|
||||
"test-room",
|
||||
"test-participant-2",
|
||||
)
|
||||
.unwrap();
|
||||
let room2 = Room::new();
|
||||
room2.connect(&live_kit_url, &user2_token).await.unwrap();
|
||||
cx.add_window(Default::default(), |cx| ScreenCaptureView::new(room2, cx));
|
||||
|
||||
let windows = live_kit::list_windows();
|
||||
let window = windows
|
||||
.iter()
|
||||
.find(|w| w.owner_name.as_deref() == Some("Safari"))
|
||||
.unwrap();
|
||||
let track = LocalVideoTrack::screen_share_for_window(window.id);
|
||||
room1.publish_video_track(&track).await.unwrap();
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
|
||||
struct ScreenCaptureView {
|
||||
image_buffer: Option<CVImageBuffer>,
|
||||
_room: Arc<Room>,
|
||||
}
|
||||
|
||||
impl gpui::Entity for ScreenCaptureView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl ScreenCaptureView {
|
||||
pub fn new(room: Arc<Room>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let mut remote_video_tracks = room.remote_video_tracks();
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
if let Some(video_track) = remote_video_tracks.next().await {
|
||||
let (mut frames_tx, mut frames_rx) = watch::channel_with(None);
|
||||
video_track.add_renderer(move |frame| *frames_tx.borrow_mut() = Some(frame));
|
||||
|
||||
while let Some(frame) = frames_rx.next().await {
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.image_buffer = frame;
|
||||
cx.notify();
|
||||
});
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
image_buffer: None,
|
||||
_room: room,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::View for ScreenCaptureView {
|
||||
fn ui_name() -> &'static str {
|
||||
"View"
|
||||
}
|
||||
|
||||
fn render(&mut self, _: &mut gpui::RenderContext<Self>) -> gpui::ElementBox {
|
||||
let image_buffer = self.image_buffer.clone();
|
||||
let canvas = Canvas::new(move |bounds, _, cx| {
|
||||
if let Some(image_buffer) = image_buffer.clone() {
|
||||
cx.scene.push_surface(Surface {
|
||||
bounds,
|
||||
image_buffer,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(image_buffer) = self.image_buffer.as_ref() {
|
||||
canvas
|
||||
.constrained()
|
||||
.with_width(image_buffer.width() as f32)
|
||||
.with_height(image_buffer.height() as f32)
|
||||
.aligned()
|
||||
.boxed()
|
||||
} else {
|
||||
canvas.boxed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
|
||||
cx.platform().quit();
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
[package]
|
||||
name = "chat_panel"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/chat_panel.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
client = { path = "../client" }
|
||||
editor = { path = "../editor" }
|
||||
gpui = { path = "../gpui" }
|
||||
menu = { path = "../menu" }
|
||||
settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||
time = { version = "0.3", features = ["serde", "serde-well-known"] }
|
||||
@@ -1,433 +0,0 @@
|
||||
use client::{
|
||||
channel::{Channel, ChannelEvent, ChannelList, ChannelMessage},
|
||||
Client,
|
||||
};
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::*,
|
||||
platform::CursorStyle,
|
||||
views::{ItemType, Select, SelectStyle},
|
||||
AnyViewHandle, AppContext, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
|
||||
Subscription, Task, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use menu::Confirm;
|
||||
use postage::prelude::Stream;
|
||||
use settings::{Settings, SoftWrap};
|
||||
use std::sync::Arc;
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
const MESSAGE_LOADING_THRESHOLD: usize = 50;
|
||||
|
||||
pub struct ChatPanel {
|
||||
rpc: Arc<Client>,
|
||||
channel_list: ModelHandle<ChannelList>,
|
||||
active_channel: Option<(ModelHandle<Channel>, Subscription)>,
|
||||
message_list: ListState,
|
||||
input_editor: ViewHandle<Editor>,
|
||||
channel_select: ViewHandle<Select>,
|
||||
local_timezone: UtcOffset,
|
||||
_observe_status: Task<()>,
|
||||
}
|
||||
|
||||
pub enum Event {}
|
||||
|
||||
actions!(chat_panel, [LoadMoreMessages]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(ChatPanel::send);
|
||||
cx.add_action(ChatPanel::load_more_messages);
|
||||
}
|
||||
|
||||
impl ChatPanel {
|
||||
pub fn new(
|
||||
rpc: Arc<Client>,
|
||||
channel_list: ModelHandle<ChannelList>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let input_editor = cx.add_view(|cx| {
|
||||
let mut editor =
|
||||
Editor::auto_height(4, Some(|theme| theme.chat_panel.input_editor.clone()), cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor
|
||||
});
|
||||
let channel_select = cx.add_view(|cx| {
|
||||
let channel_list = channel_list.clone();
|
||||
Select::new(0, cx, {
|
||||
move |ix, item_type, is_hovered, cx| {
|
||||
Self::render_channel_name(
|
||||
&channel_list,
|
||||
ix,
|
||||
item_type,
|
||||
is_hovered,
|
||||
&cx.global::<Settings>().theme.chat_panel.channel_select,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.with_style(move |cx| {
|
||||
let theme = &cx.global::<Settings>().theme.chat_panel.channel_select;
|
||||
SelectStyle {
|
||||
header: theme.header.container,
|
||||
menu: theme.menu,
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let mut message_list = ListState::new(0, Orientation::Bottom, 1000., cx, {
|
||||
let this = cx.weak_handle();
|
||||
move |_, ix, cx| {
|
||||
let this = this.upgrade(cx).unwrap().read(cx);
|
||||
let message = this.active_channel.as_ref().unwrap().0.read(cx).message(ix);
|
||||
this.render_message(message, cx)
|
||||
}
|
||||
});
|
||||
message_list.set_scroll_handler(|visible_range, cx| {
|
||||
if visible_range.start < MESSAGE_LOADING_THRESHOLD {
|
||||
cx.dispatch_action(LoadMoreMessages);
|
||||
}
|
||||
});
|
||||
let _observe_status = cx.spawn_weak(|this, mut cx| {
|
||||
let mut status = rpc.status();
|
||||
async move {
|
||||
while (status.recv().await).is_some() {
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |_, cx| cx.notify());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut this = Self {
|
||||
rpc,
|
||||
channel_list,
|
||||
active_channel: Default::default(),
|
||||
message_list,
|
||||
input_editor,
|
||||
channel_select,
|
||||
local_timezone: cx.platform().local_timezone(),
|
||||
_observe_status,
|
||||
};
|
||||
|
||||
this.init_active_channel(cx);
|
||||
cx.observe(&this.channel_list, |this, _, cx| {
|
||||
this.init_active_channel(cx);
|
||||
})
|
||||
.detach();
|
||||
cx.observe(&this.channel_select, |this, channel_select, cx| {
|
||||
let selected_ix = channel_select.read(cx).selected_index();
|
||||
let selected_channel = this.channel_list.update(cx, |channel_list, cx| {
|
||||
let available_channels = channel_list.available_channels()?;
|
||||
let channel_id = available_channels.get(selected_ix)?.id;
|
||||
channel_list.get_channel(channel_id, cx)
|
||||
});
|
||||
if let Some(selected_channel) = selected_channel {
|
||||
this.set_active_channel(selected_channel, cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
fn init_active_channel(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let (active_channel, channel_count) = self.channel_list.update(cx, |list, cx| {
|
||||
let channel_count;
|
||||
let mut active_channel = None;
|
||||
|
||||
if let Some(available_channels) = list.available_channels() {
|
||||
channel_count = available_channels.len();
|
||||
if self.active_channel.is_none() {
|
||||
if let Some(channel_id) = available_channels.first().map(|channel| channel.id) {
|
||||
active_channel = list.get_channel(channel_id, cx);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel_count = 0;
|
||||
}
|
||||
|
||||
(active_channel, channel_count)
|
||||
});
|
||||
|
||||
if let Some(active_channel) = active_channel {
|
||||
self.set_active_channel(active_channel, cx);
|
||||
} else {
|
||||
self.message_list.reset(0);
|
||||
self.active_channel = None;
|
||||
}
|
||||
|
||||
self.channel_select.update(cx, |select, cx| {
|
||||
select.set_item_count(channel_count, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn set_active_channel(&mut self, channel: ModelHandle<Channel>, cx: &mut ViewContext<Self>) {
|
||||
if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) {
|
||||
{
|
||||
let channel = channel.read(cx);
|
||||
self.message_list.reset(channel.message_count());
|
||||
let placeholder = format!("Message #{}", channel.name());
|
||||
self.input_editor.update(cx, move |editor, cx| {
|
||||
editor.set_placeholder_text(placeholder, cx);
|
||||
});
|
||||
}
|
||||
let subscription = cx.subscribe(&channel, Self::channel_did_change);
|
||||
self.active_channel = Some((channel, subscription));
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_did_change(
|
||||
&mut self,
|
||||
_: ModelHandle<Channel>,
|
||||
event: &ChannelEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
ChannelEvent::MessagesUpdated {
|
||||
old_range,
|
||||
new_count,
|
||||
} => {
|
||||
self.message_list.splice(old_range.clone(), *new_count);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_channel(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = &cx.global::<Settings>().theme;
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Container::new(ChildView::new(&self.channel_select, cx).boxed())
|
||||
.with_style(theme.chat_panel.channel_select.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(self.render_active_channel_messages())
|
||||
.with_child(self.render_input_box(cx))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_active_channel_messages(&self) -> ElementBox {
|
||||
let messages = if self.active_channel.is_some() {
|
||||
List::new(self.message_list.clone()).boxed()
|
||||
} else {
|
||||
Empty::new().boxed()
|
||||
};
|
||||
|
||||
FlexItem::new(messages).flex(1., true).boxed()
|
||||
}
|
||||
|
||||
fn render_message(&self, message: &ChannelMessage, cx: &AppContext) -> ElementBox {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let settings = cx.global::<Settings>();
|
||||
let theme = if message.is_pending() {
|
||||
&settings.theme.chat_panel.pending_message
|
||||
} else {
|
||||
&settings.theme.chat_panel.message
|
||||
};
|
||||
|
||||
Container::new(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Container::new(
|
||||
Label::new(
|
||||
message.sender.github_login.clone(),
|
||||
theme.sender.text.clone(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(theme.sender.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(
|
||||
Label::new(
|
||||
format_timestamp(message.timestamp, now, self.local_timezone),
|
||||
theme.timestamp.text.clone(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(theme.timestamp.container)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(Text::new(message.body.clone(), theme.body.clone()).boxed())
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(theme.container)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_input_box(&self, cx: &AppContext) -> ElementBox {
|
||||
let theme = &cx.global::<Settings>().theme;
|
||||
Container::new(ChildView::new(&self.input_editor, cx).boxed())
|
||||
.with_style(theme.chat_panel.input_editor.container)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_channel_name(
|
||||
channel_list: &ModelHandle<ChannelList>,
|
||||
ix: usize,
|
||||
item_type: ItemType,
|
||||
is_hovered: bool,
|
||||
theme: &theme::ChannelSelect,
|
||||
cx: &AppContext,
|
||||
) -> ElementBox {
|
||||
let channel = &channel_list.read(cx).available_channels().unwrap()[ix];
|
||||
let theme = match (item_type, is_hovered) {
|
||||
(ItemType::Header, _) => &theme.header,
|
||||
(ItemType::Selected, false) => &theme.active_item,
|
||||
(ItemType::Selected, true) => &theme.hovered_active_item,
|
||||
(ItemType::Unselected, false) => &theme.item,
|
||||
(ItemType::Unselected, true) => &theme.hovered_item,
|
||||
};
|
||||
Container::new(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Container::new(Label::new("#".to_string(), theme.hash.text.clone()).boxed())
|
||||
.with_style(theme.hash.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(Label::new(channel.name.clone(), theme.name.clone()).boxed())
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(theme.container)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_sign_in_prompt(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let rpc = self.rpc.clone();
|
||||
let this = cx.handle();
|
||||
|
||||
enum SignInPromptLabel {}
|
||||
|
||||
Align::new(
|
||||
MouseEventHandler::<SignInPromptLabel>::new(0, cx, |mouse_state, _| {
|
||||
Label::new(
|
||||
"Sign in to use chat".to_string(),
|
||||
if mouse_state.hovered {
|
||||
theme.chat_panel.hovered_sign_in_prompt.clone()
|
||||
} else {
|
||||
theme.chat_panel.sign_in_prompt.clone()
|
||||
},
|
||||
)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
let rpc = rpc.clone();
|
||||
let this = this.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
if rpc
|
||||
.authenticate_and_connect(true, &cx)
|
||||
.log_err()
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
cx.update(|cx| {
|
||||
if let Some(this) = this.upgrade(cx) {
|
||||
if this.is_focused(cx) {
|
||||
this.update(cx, |this, cx| cx.focus(&this.input_editor));
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||
if let Some((channel, _)) = self.active_channel.as_ref() {
|
||||
let body = self.input_editor.update(cx, |editor, cx| {
|
||||
let body = editor.text(cx);
|
||||
editor.clear(cx);
|
||||
body
|
||||
});
|
||||
|
||||
if let Some(task) = channel
|
||||
.update(cx, |channel, cx| channel.send_message(body, cx))
|
||||
.log_err()
|
||||
{
|
||||
task.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
|
||||
if let Some((channel, _)) = self.active_channel.as_ref() {
|
||||
channel.update(cx, |channel, cx| {
|
||||
channel.load_more_messages(cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ChatPanel {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for ChatPanel {
|
||||
fn ui_name() -> &'static str {
|
||||
"ChatPanel"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let element = if self.rpc.user_id().is_some() {
|
||||
self.render_channel(cx)
|
||||
} else {
|
||||
self.render_sign_in_prompt(cx)
|
||||
};
|
||||
let theme = &cx.global::<Settings>().theme;
|
||||
ConstrainedBox::new(
|
||||
Container::new(element)
|
||||
.with_style(theme.chat_panel.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_min_width(150.)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if matches!(
|
||||
*self.rpc.status().borrow(),
|
||||
client::Status::Connected { .. }
|
||||
) {
|
||||
cx.focus(&self.input_editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_timestamp(
|
||||
mut timestamp: OffsetDateTime,
|
||||
mut now: OffsetDateTime,
|
||||
local_timezone: UtcOffset,
|
||||
) -> String {
|
||||
timestamp = timestamp.to_offset(local_timezone);
|
||||
now = now.to_offset(local_timezone);
|
||||
|
||||
let today = now.date();
|
||||
let date = timestamp.date();
|
||||
let mut hour = timestamp.hour();
|
||||
let mut part = "am";
|
||||
if hour > 12 {
|
||||
hour -= 12;
|
||||
part = "pm";
|
||||
}
|
||||
if date == today {
|
||||
format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
|
||||
} else if date.next_day() == Some(today) {
|
||||
format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
|
||||
} else {
|
||||
format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
|
||||
}
|
||||
}
|
||||
@@ -35,9 +35,11 @@ tiny_http = "0.8"
|
||||
uuid = { version = "1.1.2", features = ["v4"] }
|
||||
url = "2.2"
|
||||
serde = { version = "*", features = ["derive"] }
|
||||
settings = { path = "../settings" }
|
||||
tempfile = "3"
|
||||
|
||||
[dev-dependencies]
|
||||
collections = { path = "../collections", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
|
||||
277
crates/client/src/amplitude_telemetry.rs
Normal file
277
crates/client/src/amplitude_telemetry.rs
Normal file
@@ -0,0 +1,277 @@
|
||||
use crate::http::HttpClient;
|
||||
use db::Db;
|
||||
use gpui::{
|
||||
executor::Background,
|
||||
serde_json::{self, value::Map, Value},
|
||||
AppContext, Task,
|
||||
};
|
||||
use isahc::Request;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
io::Write,
|
||||
mem,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tempfile::NamedTempFile;
|
||||
use util::{post_inc, ResultExt, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct AmplitudeTelemetry {
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
executor: Arc<Background>,
|
||||
session_id: u128,
|
||||
state: Mutex<AmplitudeTelemetryState>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct AmplitudeTelemetryState {
|
||||
metrics_id: Option<Arc<str>>,
|
||||
device_id: Option<Arc<str>>,
|
||||
app_version: Option<Arc<str>>,
|
||||
os_version: Option<Arc<str>>,
|
||||
os_name: &'static str,
|
||||
queue: Vec<AmplitudeEvent>,
|
||||
next_event_id: usize,
|
||||
flush_task: Option<Task<()>>,
|
||||
log_file: Option<NamedTempFile>,
|
||||
}
|
||||
|
||||
const AMPLITUDE_EVENTS_URL: &'static str = "https://api2.amplitude.com/batch";
|
||||
|
||||
lazy_static! {
|
||||
static ref AMPLITUDE_API_KEY: Option<String> = std::env::var("ZED_AMPLITUDE_API_KEY")
|
||||
.ok()
|
||||
.or_else(|| option_env!("ZED_AMPLITUDE_API_KEY").map(|key| key.to_string()));
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AmplitudeEventBatch {
|
||||
api_key: &'static str,
|
||||
events: Vec<AmplitudeEvent>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AmplitudeEvent {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
user_id: Option<Arc<str>>,
|
||||
device_id: Option<Arc<str>>,
|
||||
event_type: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
event_properties: Option<Map<String, Value>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
user_properties: Option<Map<String, Value>>,
|
||||
os_name: &'static str,
|
||||
os_version: Option<Arc<str>>,
|
||||
app_version: Option<Arc<str>>,
|
||||
platform: &'static str,
|
||||
event_id: usize,
|
||||
session_id: u128,
|
||||
time: u128,
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
const MAX_QUEUE_LEN: usize = 1;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
const MAX_QUEUE_LEN: usize = 10;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30);
|
||||
|
||||
impl AmplitudeTelemetry {
|
||||
pub fn new(client: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
|
||||
let platform = cx.platform();
|
||||
let this = Arc::new(Self {
|
||||
http_client: client,
|
||||
executor: cx.background().clone(),
|
||||
session_id: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis(),
|
||||
state: Mutex::new(AmplitudeTelemetryState {
|
||||
os_version: platform
|
||||
.os_version()
|
||||
.log_err()
|
||||
.map(|v| v.to_string().into()),
|
||||
os_name: platform.os_name().into(),
|
||||
app_version: platform
|
||||
.app_version()
|
||||
.log_err()
|
||||
.map(|v| v.to_string().into()),
|
||||
device_id: None,
|
||||
queue: Default::default(),
|
||||
flush_task: Default::default(),
|
||||
next_event_id: 0,
|
||||
log_file: None,
|
||||
metrics_id: None,
|
||||
}),
|
||||
});
|
||||
|
||||
if AMPLITUDE_API_KEY.is_some() {
|
||||
this.executor
|
||||
.spawn({
|
||||
let this = this.clone();
|
||||
async move {
|
||||
if let Some(tempfile) = NamedTempFile::new().log_err() {
|
||||
this.state.lock().log_file = Some(tempfile);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
pub fn log_file_path(&self) -> Option<PathBuf> {
|
||||
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
|
||||
}
|
||||
|
||||
pub fn start(self: &Arc<Self>, db: Db) {
|
||||
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 = Some(Arc::from(device_id));
|
||||
let mut state = this.state.lock();
|
||||
state.device_id = device_id.clone();
|
||||
for event in &mut state.queue {
|
||||
event.device_id = device_id.clone();
|
||||
}
|
||||
if !state.queue.is_empty() {
|
||||
drop(state);
|
||||
this.flush();
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn set_authenticated_user_info(
|
||||
self: &Arc<Self>,
|
||||
metrics_id: Option<String>,
|
||||
is_staff: bool,
|
||||
) {
|
||||
let is_signed_in = metrics_id.is_some();
|
||||
self.state.lock().metrics_id = metrics_id.map(|s| s.into());
|
||||
if is_signed_in {
|
||||
self.report_event_with_user_properties(
|
||||
"$identify",
|
||||
Default::default(),
|
||||
json!({ "$set": { "staff": is_staff } }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_event(self: &Arc<Self>, kind: &str, properties: Value) {
|
||||
self.report_event_with_user_properties(kind, properties, Default::default());
|
||||
}
|
||||
|
||||
fn report_event_with_user_properties(
|
||||
self: &Arc<Self>,
|
||||
kind: &str,
|
||||
properties: Value,
|
||||
user_properties: Value,
|
||||
) {
|
||||
if AMPLITUDE_API_KEY.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut state = self.state.lock();
|
||||
let event = AmplitudeEvent {
|
||||
event_type: kind.to_string(),
|
||||
time: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis(),
|
||||
session_id: self.session_id,
|
||||
event_properties: if let Value::Object(properties) = properties {
|
||||
Some(properties)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
user_properties: if let Value::Object(user_properties) = user_properties {
|
||||
Some(user_properties)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
user_id: state.metrics_id.clone(),
|
||||
device_id: state.device_id.clone(),
|
||||
os_name: state.os_name,
|
||||
platform: "Zed",
|
||||
os_version: state.os_version.clone(),
|
||||
app_version: state.app_version.clone(),
|
||||
event_id: post_inc(&mut state.next_event_id),
|
||||
};
|
||||
state.queue.push(event);
|
||||
if state.device_id.is_some() {
|
||||
if state.queue.len() >= MAX_QUEUE_LEN {
|
||||
drop(state);
|
||||
self.flush();
|
||||
} else {
|
||||
let this = self.clone();
|
||||
let executor = self.executor.clone();
|
||||
state.flush_task = Some(self.executor.spawn(async move {
|
||||
executor.timer(DEBOUNCE_INTERVAL).await;
|
||||
this.flush();
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(self: &Arc<Self>) {
|
||||
let mut state = self.state.lock();
|
||||
let events = mem::take(&mut state.queue);
|
||||
state.flush_task.take();
|
||||
drop(state);
|
||||
|
||||
if let Some(api_key) = AMPLITUDE_API_KEY.as_ref() {
|
||||
let this = self.clone();
|
||||
self.executor
|
||||
.spawn(
|
||||
async move {
|
||||
let mut json_bytes = Vec::new();
|
||||
|
||||
if let Some(file) = &mut this.state.lock().log_file {
|
||||
let file = file.as_file_mut();
|
||||
for event in &events {
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(&mut json_bytes, event)?;
|
||||
file.write_all(&json_bytes)?;
|
||||
file.write(b"\n")?;
|
||||
}
|
||||
}
|
||||
|
||||
let batch = AmplitudeEventBatch { api_key, events };
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(&mut json_bytes, &batch)?;
|
||||
let request =
|
||||
Request::post(AMPLITUDE_EVENTS_URL).body(json_bytes.into())?;
|
||||
this.http_client.send(request).await?;
|
||||
Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
|
||||
pub mod amplitude_telemetry;
|
||||
pub mod channel;
|
||||
pub mod http;
|
||||
pub mod telemetry;
|
||||
pub mod user;
|
||||
|
||||
use amplitude_telemetry::AmplitudeTelemetry;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use async_recursion::async_recursion;
|
||||
use async_tungstenite::tungstenite::{
|
||||
@@ -13,11 +15,13 @@ use async_tungstenite::tungstenite::{
|
||||
http::{Request, StatusCode},
|
||||
};
|
||||
use db::Db;
|
||||
use futures::{future::LocalBoxFuture, FutureExt, SinkExt, StreamExt, TryStreamExt};
|
||||
use futures::{future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryStreamExt};
|
||||
use gpui::{
|
||||
actions, serde_json::Value, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle,
|
||||
AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||
MutableAppContext, Task, View, ViewContext, ViewHandle,
|
||||
actions,
|
||||
serde_json::{self, Value},
|
||||
AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext,
|
||||
AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext,
|
||||
ViewHandle,
|
||||
};
|
||||
use http::HttpClient;
|
||||
use lazy_static::lazy_static;
|
||||
@@ -25,6 +29,8 @@ use parking_lot::RwLock;
|
||||
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,
|
||||
@@ -50,6 +56,9 @@ lazy_static! {
|
||||
pub static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
|
||||
.ok()
|
||||
.and_then(|s| if s.is_empty() { None } else { Some(s) });
|
||||
pub static ref ADMIN_API_TOKEN: Option<String> = std::env::var("ZED_ADMIN_API_TOKEN")
|
||||
.ok()
|
||||
.and_then(|s| if s.is_empty() { None } else { Some(s) });
|
||||
}
|
||||
|
||||
pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
|
||||
@@ -76,6 +85,7 @@ pub struct Client {
|
||||
peer: Arc<Peer>,
|
||||
http: Arc<dyn HttpClient>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
amplitude_telemetry: Arc<AmplitudeTelemetry>,
|
||||
state: RwLock<ClientState>,
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
@@ -143,11 +153,16 @@ pub enum Status {
|
||||
Authenticating,
|
||||
Connecting,
|
||||
ConnectionError,
|
||||
Connected { connection_id: ConnectionId },
|
||||
Connected {
|
||||
peer_id: PeerId,
|
||||
connection_id: ConnectionId,
|
||||
},
|
||||
ConnectionLost,
|
||||
Reauthenticating,
|
||||
Reconnecting,
|
||||
ReconnectionError { next_reconnection: Instant },
|
||||
ReconnectionError {
|
||||
next_reconnection: Instant,
|
||||
},
|
||||
}
|
||||
|
||||
impl Status {
|
||||
@@ -250,6 +265,7 @@ impl Client {
|
||||
id: 0,
|
||||
peer: Peer::new(),
|
||||
telemetry: Telemetry::new(http.clone(), cx),
|
||||
amplitude_telemetry: AmplitudeTelemetry::new(http.clone(), cx),
|
||||
http,
|
||||
state: Default::default(),
|
||||
|
||||
@@ -314,6 +330,14 @@ impl Client {
|
||||
.map(|credentials| credentials.user_id)
|
||||
}
|
||||
|
||||
pub fn peer_id(&self) -> Option<PeerId> {
|
||||
if let Status::Connected { peer_id, .. } = &*self.status().borrow() {
|
||||
Some(*peer_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(&self) -> watch::Receiver<Status> {
|
||||
self.state.read().status.1.clone()
|
||||
}
|
||||
@@ -354,6 +378,8 @@ impl Client {
|
||||
}
|
||||
Status::SignedOut | Status::UpgradeRequired => {
|
||||
self.telemetry.set_authenticated_user_info(None, false);
|
||||
self.amplitude_telemetry
|
||||
.set_authenticated_user_info(None, false);
|
||||
state._reconnect_task.take();
|
||||
}
|
||||
_ => {}
|
||||
@@ -663,6 +689,7 @@ impl Client {
|
||||
self.set_status(Status::Reconnecting, cx);
|
||||
}
|
||||
|
||||
let mut timeout = cx.background().timer(CONNECTION_TIMEOUT).fuse();
|
||||
futures::select_biased! {
|
||||
connection = self.establish_connection(&credentials, cx).fuse() => {
|
||||
match connection {
|
||||
@@ -671,8 +698,14 @@ impl Client {
|
||||
if !read_from_keychain && IMPERSONATE_LOGIN.is_none() {
|
||||
write_credentials_to_keychain(&credentials, cx).log_err();
|
||||
}
|
||||
self.set_connection(conn, cx);
|
||||
Ok(())
|
||||
|
||||
futures::select_biased! {
|
||||
result = self.set_connection(conn, cx).fuse() => result,
|
||||
_ = timeout => {
|
||||
self.set_status(Status::ConnectionError, cx);
|
||||
Err(anyhow!("timed out waiting on hello message from server"))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(EstablishConnectionError::Unauthorized) => {
|
||||
self.state.write().credentials.take();
|
||||
@@ -695,21 +728,65 @@ impl Client {
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = cx.background().timer(CONNECTION_TIMEOUT).fuse() => {
|
||||
_ = &mut timeout => {
|
||||
self.set_status(Status::ConnectionError, cx);
|
||||
Err(anyhow!("timed out trying to establish connection"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_connection(self: &Arc<Self>, conn: Connection, cx: &AsyncAppContext) {
|
||||
async fn set_connection(
|
||||
self: &Arc<Self>,
|
||||
conn: Connection,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let executor = cx.background();
|
||||
log::info!("add connection to peer");
|
||||
let (connection_id, handle_io, mut incoming) = self
|
||||
.peer
|
||||
.add_connection(conn, move |duration| executor.timer(duration));
|
||||
log::info!("set status to connected {}", connection_id);
|
||||
self.set_status(Status::Connected { connection_id }, cx);
|
||||
let handle_io = cx.background().spawn(handle_io);
|
||||
|
||||
let peer_id = async {
|
||||
log::info!("waiting for server hello");
|
||||
let message = incoming
|
||||
.next()
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("no hello message received"))?;
|
||||
log::info!("got server hello");
|
||||
let hello_message_type_name = message.payload_type_name().to_string();
|
||||
let hello = message
|
||||
.into_any()
|
||||
.downcast::<TypedEnvelope<proto::Hello>>()
|
||||
.map_err(|_| {
|
||||
anyhow!(
|
||||
"invalid hello message received: {:?}",
|
||||
hello_message_type_name
|
||||
)
|
||||
})?;
|
||||
Ok(PeerId(hello.payload.peer_id))
|
||||
};
|
||||
|
||||
let peer_id = match peer_id.await {
|
||||
Ok(peer_id) => peer_id,
|
||||
Err(error) => {
|
||||
self.peer.disconnect(connection_id);
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
|
||||
log::info!(
|
||||
"set status to connected (connection id: {}, peer id: {})",
|
||||
connection_id,
|
||||
peer_id
|
||||
);
|
||||
self.set_status(
|
||||
Status::Connected {
|
||||
peer_id,
|
||||
connection_id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
cx.foreground()
|
||||
.spawn({
|
||||
let cx = cx.clone();
|
||||
@@ -807,14 +884,18 @@ impl Client {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let handle_io = cx.background().spawn(handle_io);
|
||||
let this = self.clone();
|
||||
let cx = cx.clone();
|
||||
cx.foreground()
|
||||
.spawn(async move {
|
||||
match handle_io.await {
|
||||
Ok(()) => {
|
||||
if *this.status().borrow() == (Status::Connected { connection_id }) {
|
||||
if *this.status().borrow()
|
||||
== (Status::Connected {
|
||||
connection_id,
|
||||
peer_id,
|
||||
})
|
||||
{
|
||||
this.set_status(Status::SignedOut, &cx);
|
||||
}
|
||||
}
|
||||
@@ -825,6 +906,8 @@ impl Client {
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn authenticate(self: &Arc<Self>, cx: &AsyncAppContext) -> Task<Result<Credentials>> {
|
||||
@@ -849,11 +932,51 @@ impl Client {
|
||||
self.establish_websocket_connection(credentials, cx)
|
||||
}
|
||||
|
||||
async fn get_rpc_url(http: Arc<dyn HttpClient>, is_preview: bool) -> Result<Url> {
|
||||
let preview_param = if is_preview { "?preview=1" } else { "" };
|
||||
let url = format!("{}/rpc{preview_param}", *ZED_SERVER_URL);
|
||||
let response = http.get(&url, Default::default(), false).await?;
|
||||
|
||||
// Normally, ZED_SERVER_URL is set to the URL of zed.dev website.
|
||||
// The website's /rpc endpoint redirects to a collab server's /rpc endpoint,
|
||||
// which requires authorization via an HTTP header.
|
||||
//
|
||||
// For testing purposes, ZED_SERVER_URL can also set to the direct URL of
|
||||
// of a collab server. In that case, a request to the /rpc endpoint will
|
||||
// return an 'unauthorized' response.
|
||||
let collab_url = if response.status().is_redirection() {
|
||||
response
|
||||
.headers()
|
||||
.get("Location")
|
||||
.ok_or_else(|| anyhow!("missing location header in /rpc response"))?
|
||||
.to_str()
|
||||
.map_err(EstablishConnectionError::other)?
|
||||
.to_string()
|
||||
} else if response.status() == StatusCode::UNAUTHORIZED {
|
||||
url
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"unexpected /rpc response status {}",
|
||||
response.status()
|
||||
))?
|
||||
};
|
||||
|
||||
Url::parse(&collab_url).context("invalid rpc url")
|
||||
}
|
||||
|
||||
fn establish_websocket_connection(
|
||||
self: &Arc<Self>,
|
||||
credentials: &Credentials,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Task<Result<Connection, EstablishConnectionError>> {
|
||||
let is_preview = cx.read(|cx| {
|
||||
if cx.has_global::<ReleaseChannel>() {
|
||||
*cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
let request = Request::builder()
|
||||
.header(
|
||||
"Authorization",
|
||||
@@ -863,28 +986,7 @@ impl Client {
|
||||
|
||||
let http = self.http.clone();
|
||||
cx.background().spawn(async move {
|
||||
let mut rpc_url = format!("{}/rpc", *ZED_SERVER_URL);
|
||||
let rpc_response = http.get(&rpc_url, Default::default(), false).await?;
|
||||
if rpc_response.status().is_redirection() {
|
||||
rpc_url = rpc_response
|
||||
.headers()
|
||||
.get("Location")
|
||||
.ok_or_else(|| anyhow!("missing location header in /rpc response"))?
|
||||
.to_str()
|
||||
.map_err(EstablishConnectionError::other)?
|
||||
.to_string();
|
||||
}
|
||||
// Until we switch the zed.dev domain to point to the new Next.js app, there
|
||||
// will be no redirect required, and the app will connect directly to
|
||||
// wss://zed.dev/rpc.
|
||||
else if rpc_response.status() != StatusCode::UPGRADE_REQUIRED {
|
||||
Err(anyhow!(
|
||||
"unexpected /rpc response status {}",
|
||||
rpc_response.status()
|
||||
))?
|
||||
}
|
||||
|
||||
let mut rpc_url = Url::parse(&rpc_url).context("invalid rpc url")?;
|
||||
let mut rpc_url = Self::get_rpc_url(http, is_preview).await?;
|
||||
let rpc_host = rpc_url
|
||||
.host_str()
|
||||
.zip(rpc_url.port_or_known_default())
|
||||
@@ -927,6 +1029,8 @@ impl Client {
|
||||
let platform = cx.platform();
|
||||
let executor = cx.background();
|
||||
let telemetry = self.telemetry.clone();
|
||||
let amplitude_telemetry = self.amplitude_telemetry.clone();
|
||||
let http = self.http.clone();
|
||||
executor.clone().spawn(async move {
|
||||
// Generate a pair of asymmetric encryption keys. The public key will be used by the
|
||||
// zed server to encrypt the user's access token, so that it can'be intercepted by
|
||||
@@ -936,6 +1040,10 @@ impl Client {
|
||||
let public_key_string =
|
||||
String::try_from(public_key).expect("failed to serialize public key for auth");
|
||||
|
||||
if let Some((login, token)) = IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) {
|
||||
return Self::authenticate_as_admin(http, login.clone(), token.clone()).await;
|
||||
}
|
||||
|
||||
// Start an HTTP server to receive the redirect from Zed's sign-in page.
|
||||
let server = tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port");
|
||||
let port = server.server_addr().port();
|
||||
@@ -1006,6 +1114,7 @@ impl Client {
|
||||
platform.activate(true);
|
||||
|
||||
telemetry.report_event("authenticate with browser", Default::default());
|
||||
amplitude_telemetry.report_event("authenticate with browser", Default::default());
|
||||
|
||||
Ok(Credentials {
|
||||
user_id: user_id.parse()?,
|
||||
@@ -1014,6 +1123,50 @@ impl Client {
|
||||
})
|
||||
}
|
||||
|
||||
async fn authenticate_as_admin(
|
||||
http: Arc<dyn HttpClient>,
|
||||
login: String,
|
||||
mut api_token: String,
|
||||
) -> Result<Credentials> {
|
||||
#[derive(Deserialize)]
|
||||
struct AuthenticatedUserResponse {
|
||||
user: User,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct User {
|
||||
id: u64,
|
||||
}
|
||||
|
||||
// Use the collab server's admin API to retrieve the id
|
||||
// of the impersonated user.
|
||||
let mut url = Self::get_rpc_url(http.clone(), false).await?;
|
||||
url.set_path("/user");
|
||||
url.set_query(Some(&format!("github_login={login}")));
|
||||
let request = Request::get(url.as_str())
|
||||
.header("Authorization", format!("token {api_token}"))
|
||||
.body("".into())?;
|
||||
|
||||
let mut response = http.send(request).await?;
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
if !response.status().is_success() {
|
||||
Err(anyhow!(
|
||||
"admin user request failed {} - {}",
|
||||
response.status().as_u16(),
|
||||
body,
|
||||
))?;
|
||||
}
|
||||
let response: AuthenticatedUserResponse = serde_json::from_str(&body)?;
|
||||
|
||||
// Use the admin API token to authenticate as the impersonated user.
|
||||
api_token.insert_str(0, "ADMIN_TOKEN:");
|
||||
Ok(Credentials {
|
||||
user_id: response.user.id,
|
||||
access_token: api_token,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) -> Result<()> {
|
||||
let conn_id = self.connection_id()?;
|
||||
self.peer.disconnect(conn_id);
|
||||
@@ -1072,15 +1225,18 @@ impl Client {
|
||||
self.peer.respond_with_error(receipt, error)
|
||||
}
|
||||
|
||||
pub fn start_telemetry(&self, db: Arc<Db>) {
|
||||
self.telemetry.start(db);
|
||||
pub fn start_telemetry(&self, db: Db) {
|
||||
self.telemetry.start(db.clone());
|
||||
self.amplitude_telemetry.start(db);
|
||||
}
|
||||
|
||||
pub fn report_event(&self, kind: &str, properties: Value) {
|
||||
self.telemetry.report_event(kind, properties)
|
||||
self.telemetry.report_event(kind, properties.clone());
|
||||
self.amplitude_telemetry.report_event(kind, properties);
|
||||
}
|
||||
|
||||
pub fn telemetry_log_file_path(&self) -> Option<PathBuf> {
|
||||
self.amplitude_telemetry.log_file_path();
|
||||
self.telemetry.log_file_path()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ use uuid::Uuid;
|
||||
pub struct Telemetry {
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
executor: Arc<Background>,
|
||||
session_id: u128,
|
||||
state: Mutex<TelemetryState>,
|
||||
}
|
||||
|
||||
@@ -35,43 +34,54 @@ struct TelemetryState {
|
||||
app_version: Option<Arc<str>>,
|
||||
os_version: Option<Arc<str>>,
|
||||
os_name: &'static str,
|
||||
queue: Vec<AmplitudeEvent>,
|
||||
queue: Vec<MixpanelEvent>,
|
||||
next_event_id: usize,
|
||||
flush_task: Option<Task<()>>,
|
||||
log_file: Option<NamedTempFile>,
|
||||
}
|
||||
|
||||
const AMPLITUDE_EVENTS_URL: &'static str = "https://api2.amplitude.com/batch";
|
||||
const MIXPANEL_EVENTS_URL: &'static str = "https://api.mixpanel.com/track";
|
||||
const MIXPANEL_ENGAGE_URL: &'static str = "https://api.mixpanel.com/engage#profile-set";
|
||||
|
||||
lazy_static! {
|
||||
static ref AMPLITUDE_API_KEY: Option<String> = std::env::var("ZED_AMPLITUDE_API_KEY")
|
||||
static ref MIXPANEL_TOKEN: Option<String> = std::env::var("ZED_MIXPANEL_TOKEN")
|
||||
.ok()
|
||||
.or_else(|| option_env!("ZED_AMPLITUDE_API_KEY").map(|key| key.to_string()));
|
||||
.or_else(|| option_env!("ZED_MIXPANEL_TOKEN").map(|key| key.to_string()));
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AmplitudeEventBatch {
|
||||
api_key: &'static str,
|
||||
events: Vec<AmplitudeEvent>,
|
||||
#[derive(Serialize, Debug)]
|
||||
struct MixpanelEvent {
|
||||
event: String,
|
||||
properties: MixpanelEventProperties,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AmplitudeEvent {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
user_id: Option<Arc<str>>,
|
||||
device_id: Option<Arc<str>>,
|
||||
event_type: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[derive(Serialize, Debug)]
|
||||
struct MixpanelEventProperties {
|
||||
// Mixpanel required fields
|
||||
#[serde(skip_serializing_if = "str::is_empty")]
|
||||
token: &'static str,
|
||||
time: u128,
|
||||
distinct_id: Option<Arc<str>>,
|
||||
#[serde(rename = "$insert_id")]
|
||||
insert_id: usize,
|
||||
// Custom fields
|
||||
#[serde(skip_serializing_if = "Option::is_none", flatten)]
|
||||
event_properties: Option<Map<String, Value>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
user_properties: Option<Map<String, Value>>,
|
||||
os_name: &'static str,
|
||||
os_version: Option<Arc<str>>,
|
||||
app_version: Option<Arc<str>>,
|
||||
signed_in: bool,
|
||||
platform: &'static str,
|
||||
event_id: usize,
|
||||
session_id: u128,
|
||||
time: u128,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MixpanelEngageRequest {
|
||||
#[serde(rename = "$token")]
|
||||
token: &'static str,
|
||||
#[serde(rename = "$distinct_id")]
|
||||
distinct_id: Arc<str>,
|
||||
#[serde(rename = "$set")]
|
||||
set: Value,
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -92,10 +102,6 @@ impl Telemetry {
|
||||
let this = Arc::new(Self {
|
||||
http_client: client,
|
||||
executor: cx.background().clone(),
|
||||
session_id: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis(),
|
||||
state: Mutex::new(TelemetryState {
|
||||
os_version: platform
|
||||
.os_version()
|
||||
@@ -107,15 +113,15 @@ impl Telemetry {
|
||||
.log_err()
|
||||
.map(|v| v.to_string().into()),
|
||||
device_id: None,
|
||||
metrics_id: None,
|
||||
queue: Default::default(),
|
||||
flush_task: Default::default(),
|
||||
next_event_id: 0,
|
||||
log_file: None,
|
||||
metrics_id: None,
|
||||
}),
|
||||
});
|
||||
|
||||
if AMPLITUDE_API_KEY.is_some() {
|
||||
if MIXPANEL_TOKEN.is_some() {
|
||||
this.executor
|
||||
.spawn({
|
||||
let this = this.clone();
|
||||
@@ -135,30 +141,27 @@ impl Telemetry {
|
||||
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
|
||||
}
|
||||
|
||||
pub fn start(self: &Arc<Self>, db: Arc<Db>) {
|
||||
pub fn start(self: &Arc<Self>, db: Db) {
|
||||
let this = self.clone();
|
||||
self.executor
|
||||
.spawn(
|
||||
async move {
|
||||
let device_id = if let Some(device_id) = db
|
||||
.read(["device_id"])?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.next()
|
||||
.and_then(|bytes| String::from_utf8(bytes).ok())
|
||||
{
|
||||
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([("device_id", device_id.as_bytes())])?;
|
||||
db.write_kvp("device_id", &device_id)?;
|
||||
device_id
|
||||
};
|
||||
|
||||
let device_id = Some(Arc::from(device_id));
|
||||
let device_id: Arc<str> = device_id.into();
|
||||
let mut state = this.state.lock();
|
||||
state.device_id = device_id.clone();
|
||||
state.device_id = Some(device_id.clone());
|
||||
for event in &mut state.queue {
|
||||
event.device_id = device_id.clone();
|
||||
event
|
||||
.properties
|
||||
.distinct_id
|
||||
.get_or_insert_with(|| device_id.clone());
|
||||
}
|
||||
if !state.queue.is_empty() {
|
||||
drop(state);
|
||||
@@ -177,56 +180,57 @@ impl Telemetry {
|
||||
metrics_id: Option<String>,
|
||||
is_staff: bool,
|
||||
) {
|
||||
let is_signed_in = metrics_id.is_some();
|
||||
self.state.lock().metrics_id = metrics_id.map(|s| s.into());
|
||||
if is_signed_in {
|
||||
self.report_event_with_user_properties(
|
||||
"$identify",
|
||||
Default::default(),
|
||||
json!({ "$set": { "staff": is_staff } }),
|
||||
)
|
||||
let this = self.clone();
|
||||
let mut state = self.state.lock();
|
||||
let device_id = state.device_id.clone();
|
||||
let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
|
||||
state.metrics_id = metrics_id.clone();
|
||||
drop(state);
|
||||
|
||||
if let Some((token, device_id)) = MIXPANEL_TOKEN.as_ref().zip(device_id) {
|
||||
self.executor
|
||||
.spawn(
|
||||
async move {
|
||||
let json_bytes = serde_json::to_vec(&[MixpanelEngageRequest {
|
||||
token,
|
||||
distinct_id: device_id,
|
||||
set: json!({ "staff": is_staff, "id": metrics_id }),
|
||||
}])?;
|
||||
let request = Request::post(MIXPANEL_ENGAGE_URL)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(json_bytes.into())?;
|
||||
this.http_client.send(request).await?;
|
||||
Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_event(self: &Arc<Self>, kind: &str, properties: Value) {
|
||||
self.report_event_with_user_properties(kind, properties, Default::default());
|
||||
}
|
||||
|
||||
fn report_event_with_user_properties(
|
||||
self: &Arc<Self>,
|
||||
kind: &str,
|
||||
properties: Value,
|
||||
user_properties: Value,
|
||||
) {
|
||||
if AMPLITUDE_API_KEY.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut state = self.state.lock();
|
||||
let event = AmplitudeEvent {
|
||||
event_type: kind.to_string(),
|
||||
time: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis(),
|
||||
session_id: self.session_id,
|
||||
event_properties: if let Value::Object(properties) = properties {
|
||||
Some(properties)
|
||||
} else {
|
||||
None
|
||||
let event = MixpanelEvent {
|
||||
event: kind.to_string(),
|
||||
properties: MixpanelEventProperties {
|
||||
token: "",
|
||||
time: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis(),
|
||||
distinct_id: state.device_id.clone(),
|
||||
insert_id: post_inc(&mut state.next_event_id),
|
||||
event_properties: if let Value::Object(properties) = properties {
|
||||
Some(properties)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
os_name: state.os_name,
|
||||
os_version: state.os_version.clone(),
|
||||
app_version: state.app_version.clone(),
|
||||
signed_in: state.metrics_id.is_some(),
|
||||
platform: "Zed",
|
||||
},
|
||||
user_properties: if let Value::Object(user_properties) = user_properties {
|
||||
Some(user_properties)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
user_id: state.metrics_id.clone(),
|
||||
device_id: state.device_id.clone(),
|
||||
os_name: state.os_name,
|
||||
platform: "Zed",
|
||||
os_version: state.os_version.clone(),
|
||||
app_version: state.app_version.clone(),
|
||||
event_id: post_inc(&mut state.next_event_id),
|
||||
};
|
||||
state.queue.push(event);
|
||||
if state.device_id.is_some() {
|
||||
@@ -246,11 +250,11 @@ impl Telemetry {
|
||||
|
||||
fn flush(self: &Arc<Self>) {
|
||||
let mut state = self.state.lock();
|
||||
let events = mem::take(&mut state.queue);
|
||||
let mut events = mem::take(&mut state.queue);
|
||||
state.flush_task.take();
|
||||
drop(state);
|
||||
|
||||
if let Some(api_key) = AMPLITUDE_API_KEY.as_ref() {
|
||||
if let Some(token) = MIXPANEL_TOKEN.as_ref() {
|
||||
let this = self.clone();
|
||||
self.executor
|
||||
.spawn(
|
||||
@@ -259,19 +263,21 @@ impl Telemetry {
|
||||
|
||||
if let Some(file) = &mut this.state.lock().log_file {
|
||||
let file = file.as_file_mut();
|
||||
for event in &events {
|
||||
for event in &mut events {
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(&mut json_bytes, event)?;
|
||||
file.write_all(&json_bytes)?;
|
||||
file.write(b"\n")?;
|
||||
|
||||
event.properties.token = token;
|
||||
}
|
||||
}
|
||||
|
||||
let batch = AmplitudeEventBatch { api_key, events };
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(&mut json_bytes, &batch)?;
|
||||
let request =
|
||||
Request::post(AMPLITUDE_EVENTS_URL).body(json_bytes.into())?;
|
||||
serde_json::to_writer(&mut json_bytes, &events)?;
|
||||
let request = Request::post(MIXPANEL_EVENTS_URL)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(json_bytes.into())?;
|
||||
this.http_client.send(request).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -84,9 +84,19 @@ impl FakeServer {
|
||||
let (connection_id, io, incoming) =
|
||||
peer.add_test_connection(server_conn, cx.background());
|
||||
cx.background().spawn(io).detach();
|
||||
let mut state = state.lock();
|
||||
state.connection_id = Some(connection_id);
|
||||
state.incoming = Some(incoming);
|
||||
{
|
||||
let mut state = state.lock();
|
||||
state.connection_id = Some(connection_id);
|
||||
state.incoming = Some(incoming);
|
||||
}
|
||||
peer.send(
|
||||
connection_id,
|
||||
proto::Hello {
|
||||
peer_id: connection_id.0,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Ok(client_conn)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -143,13 +143,24 @@ impl UserStore {
|
||||
let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
|
||||
if let Some(info) = info {
|
||||
client.telemetry.set_authenticated_user_info(
|
||||
Some(info.metrics_id.clone()),
|
||||
info.staff,
|
||||
);
|
||||
client.amplitude_telemetry.set_authenticated_user_info(
|
||||
Some(info.metrics_id),
|
||||
info.staff,
|
||||
);
|
||||
} else {
|
||||
client.telemetry.set_authenticated_user_info(None, false);
|
||||
client
|
||||
.amplitude_telemetry
|
||||
.set_authenticated_user_info(None, false);
|
||||
}
|
||||
|
||||
client.telemetry.report_event("sign in", Default::default());
|
||||
client
|
||||
.amplitude_telemetry
|
||||
.report_event("sign in", Default::default());
|
||||
current_user_tx.send(user).await.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ DATABASE_URL = "postgres://postgres@localhost/zed"
|
||||
HTTP_PORT = 8080
|
||||
API_TOKEN = "secret"
|
||||
INVITE_LINK_PREFIX = "http://localhost:3000/invites/"
|
||||
LIVE_KIT_SERVER = "http://localhost:7880"
|
||||
LIVE_KIT_KEY = "devkey"
|
||||
LIVE_KIT_SECRET = "secret"
|
||||
|
||||
# HONEYCOMB_API_KEY=
|
||||
# HONEYCOMB_DATASET=
|
||||
# RUST_LOG=info
|
||||
# LOG_JSON=true
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
[[bin]]
|
||||
name = "collab"
|
||||
@@ -14,8 +14,10 @@ required-features = ["seed-support"]
|
||||
|
||||
[dependencies]
|
||||
collections = { path = "../collections" }
|
||||
live_kit_server = { path = "../live_kit_server" }
|
||||
rpc = { path = "../rpc" }
|
||||
util = { path = "../util" }
|
||||
|
||||
anyhow = "1.0.40"
|
||||
async-trait = "0.1.50"
|
||||
async-tungstenite = "0.16"
|
||||
@@ -60,15 +62,17 @@ editor = { path = "../editor", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
fs = { path = "../fs", features = ["test-support"] }
|
||||
git = { path = "../git", features = ["test-support"] }
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
theme = { path = "../theme" }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
|
||||
ctor = "0.1"
|
||||
env_logger = "0.9"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
util = { path = "../util" }
|
||||
lazy_static = "1.4"
|
||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
collab: ./target/release/collab
|
||||
release: ./target/release/sqlx migrate run
|
||||
3
crates/collab/k8s/environments/preview.sh
Normal file
3
crates/collab/k8s/environments/preview.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
ZED_ENVIRONMENT=preview
|
||||
RUST_LOG=info
|
||||
INVITE_LINK_PREFIX=https://zed.dev/invites/
|
||||
@@ -11,7 +11,7 @@ metadata:
|
||||
name: collab
|
||||
annotations:
|
||||
service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443"
|
||||
service.beta.kubernetes.io/do-loadbalancer-certificate-id: "40879815-9a6b-4bbb-8207-8f2c7c0218f9"
|
||||
service.beta.kubernetes.io/do-loadbalancer-certificate-id: "08d9d8ce-761f-4ab3-bc78-4923ab5b0e33"
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
selector:
|
||||
@@ -54,6 +54,8 @@ spec:
|
||||
containers:
|
||||
- name: collab
|
||||
image: "${ZED_IMAGE_ID}"
|
||||
args:
|
||||
- serve
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
protocol: TCP
|
||||
@@ -65,49 +67,32 @@ spec:
|
||||
secretKeyRef:
|
||||
name: database
|
||||
key: url
|
||||
- name: SESSION_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: session
|
||||
key: secret
|
||||
- name: GITHUB_APP_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: github
|
||||
key: appId
|
||||
- name: GITHUB_CLIENT_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: github
|
||||
key: clientId
|
||||
- name: GITHUB_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: github
|
||||
key: clientSecret
|
||||
- name: GITHUB_PRIVATE_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: github
|
||||
key: privateKey
|
||||
- name: API_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: api
|
||||
key: token
|
||||
- name: LIVE_KIT_SERVER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: livekit
|
||||
key: server
|
||||
- name: LIVE_KIT_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: livekit
|
||||
key: key
|
||||
- name: LIVE_KIT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: livekit
|
||||
key: secret
|
||||
- name: INVITE_LINK_PREFIX
|
||||
value: ${INVITE_LINK_PREFIX}
|
||||
- name: RUST_LOG
|
||||
value: ${RUST_LOG}
|
||||
- name: LOG_JSON
|
||||
value: "true"
|
||||
- name: HONEYCOMB_DATASET
|
||||
value: "collab"
|
||||
- name: HONEYCOMB_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: honeycomb
|
||||
key: apiKey
|
||||
securityContext:
|
||||
capabilities:
|
||||
# FIXME - Switch to the more restrictive `PERFMON` capability.
|
||||
|
||||
@@ -9,7 +9,10 @@ spec:
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: migrator
|
||||
imagePullPolicy: Always
|
||||
image: ${ZED_IMAGE_ID}
|
||||
args:
|
||||
- migrate
|
||||
env:
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
|
||||
@@ -22,7 +22,7 @@ use time::OffsetDateTime;
|
||||
use tower::ServiceBuilder;
|
||||
use tracing::instrument;
|
||||
|
||||
pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> {
|
||||
pub fn routes(rpc_server: Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> {
|
||||
Router::new()
|
||||
.route("/user", get(get_authenticated_user))
|
||||
.route("/users", get(get_users).post(create_user))
|
||||
@@ -50,7 +50,7 @@ pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Bod
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
.layer(Extension(state))
|
||||
.layer(Extension(rpc_server.clone()))
|
||||
.layer(Extension(rpc_server))
|
||||
.layer(middleware::from_fn(validate_api_token)),
|
||||
)
|
||||
}
|
||||
@@ -76,7 +76,7 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
|
||||
|
||||
let state = req.extensions().get::<Arc<AppState>>().unwrap();
|
||||
|
||||
if token != state.api_token {
|
||||
if token != state.config.api_token {
|
||||
Err(Error::Http(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"invalid authorization token".to_string(),
|
||||
@@ -88,7 +88,7 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AuthenticatedUserParams {
|
||||
github_user_id: i32,
|
||||
github_user_id: Option<i32>,
|
||||
github_login: String,
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ async fn get_authenticated_user(
|
||||
) -> Result<Json<AuthenticatedUserResponse>> {
|
||||
let user = app
|
||||
.db
|
||||
.get_user_by_github_account(¶ms.github_login, Some(params.github_user_id))
|
||||
.get_user_by_github_account(¶ms.github_login, params.github_user_id)
|
||||
.await?
|
||||
.ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "user not found".into()))?;
|
||||
let metrics_id = app.db.get_user_metrics_id(user.id).await?;
|
||||
@@ -156,7 +156,7 @@ async fn create_user(
|
||||
Json(params): Json<CreateUserParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
Extension(rpc_server): Extension<Arc<rpc::Server>>,
|
||||
) -> Result<Json<CreateUserResponse>> {
|
||||
) -> Result<Json<Option<CreateUserResponse>>> {
|
||||
let user = NewUserParams {
|
||||
github_login: params.github_login,
|
||||
github_user_id: params.github_user_id,
|
||||
@@ -165,7 +165,8 @@ async fn create_user(
|
||||
|
||||
// Creating a user via the normal signup process
|
||||
let result = if let Some(email_confirmation_code) = params.email_confirmation_code {
|
||||
app.db
|
||||
if let Some(result) = app
|
||||
.db
|
||||
.create_user_from_invite(
|
||||
&Invite {
|
||||
email_address: params.email_address,
|
||||
@@ -174,6 +175,11 @@ async fn create_user(
|
||||
user,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
result
|
||||
} else {
|
||||
return Ok(Json(None));
|
||||
}
|
||||
}
|
||||
// Creating a user as an admin
|
||||
else if params.admin {
|
||||
@@ -200,11 +206,11 @@ async fn create_user(
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
|
||||
|
||||
Ok(Json(CreateUserResponse {
|
||||
Ok(Json(Some(CreateUserResponse {
|
||||
user,
|
||||
metrics_id: result.metrics_id,
|
||||
signup_device_id: result.signup_device_id,
|
||||
}))
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::db::{self, UserId};
|
||||
use crate::{AppState, Error, Result};
|
||||
use crate::{
|
||||
db::{self, UserId},
|
||||
AppState, Error, Result,
|
||||
};
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::{
|
||||
http::{self, Request, StatusCode},
|
||||
@@ -13,6 +13,7 @@ use scrypt::{
|
||||
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
||||
Scrypt,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl IntoResponse {
|
||||
let mut auth_header = req
|
||||
@@ -21,7 +22,7 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
|
||||
.and_then(|header| header.to_str().ok())
|
||||
.ok_or_else(|| {
|
||||
Error::Http(
|
||||
StatusCode::BAD_REQUEST,
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"missing authorization header".to_string(),
|
||||
)
|
||||
})?
|
||||
@@ -41,12 +42,18 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
|
||||
)
|
||||
})?;
|
||||
|
||||
let state = req.extensions().get::<Arc<AppState>>().unwrap();
|
||||
let mut credentials_valid = false;
|
||||
for password_hash in state.db.get_access_token_hashes(user_id).await? {
|
||||
if verify_access_token(access_token, &password_hash)? {
|
||||
let state = req.extensions().get::<Arc<AppState>>().unwrap();
|
||||
if let Some(admin_token) = access_token.strip_prefix("ADMIN_TOKEN:") {
|
||||
if state.config.api_token == admin_token {
|
||||
credentials_valid = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
for password_hash in state.db.get_access_token_hashes(user_id).await? {
|
||||
if verify_access_token(access_token, &password_hash)? {
|
||||
credentials_valid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,12 @@ use collections::HashMap;
|
||||
use futures::StreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use sqlx::postgres::PgPoolOptions as DbOptions;
|
||||
use sqlx::{types::Uuid, FromRow, QueryBuilder};
|
||||
use std::{cmp, ops::Range, time::Duration};
|
||||
use sqlx::{
|
||||
migrate::{Migrate as _, Migration, MigrationSource},
|
||||
types::Uuid,
|
||||
FromRow, QueryBuilder,
|
||||
};
|
||||
use std::{cmp, ops::Range, path::Path, time::Duration};
|
||||
use time::{OffsetDateTime, PrimitiveDateTime};
|
||||
|
||||
#[async_trait]
|
||||
@@ -51,7 +55,7 @@ pub trait Db: Send + Sync {
|
||||
&self,
|
||||
invite: &Invite,
|
||||
user: NewUserParams,
|
||||
) -> Result<NewUserResult>;
|
||||
) -> Result<Option<NewUserResult>>;
|
||||
|
||||
/// Registers a new project for the given user.
|
||||
async fn register_project(&self, host_user_id: UserId) -> Result<ProjectId>;
|
||||
@@ -173,6 +177,13 @@ pub trait Db: Send + Sync {
|
||||
fn as_fake(&self) -> Option<&FakeDb>;
|
||||
}
|
||||
|
||||
#[cfg(any(test, debug_assertions))]
|
||||
pub const DEFAULT_MIGRATIONS_PATH: Option<&'static str> =
|
||||
Some(concat!(env!("CARGO_MANIFEST_DIR"), "/migrations"));
|
||||
|
||||
#[cfg(not(any(test, debug_assertions)))]
|
||||
pub const DEFAULT_MIGRATIONS_PATH: Option<&'static str> = None;
|
||||
|
||||
pub struct PostgresDb {
|
||||
pool: sqlx::PgPool,
|
||||
}
|
||||
@@ -187,6 +198,47 @@ impl PostgresDb {
|
||||
Ok(Self { pool })
|
||||
}
|
||||
|
||||
pub async fn migrate(
|
||||
&self,
|
||||
migrations_path: &Path,
|
||||
ignore_checksum_mismatch: bool,
|
||||
) -> anyhow::Result<Vec<(Migration, Duration)>> {
|
||||
let migrations = MigrationSource::resolve(migrations_path)
|
||||
.await
|
||||
.map_err(|err| anyhow!("failed to load migrations: {err:?}"))?;
|
||||
|
||||
let mut conn = self.pool.acquire().await?;
|
||||
|
||||
conn.ensure_migrations_table().await?;
|
||||
let applied_migrations: HashMap<_, _> = conn
|
||||
.list_applied_migrations()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|m| (m.version, m))
|
||||
.collect();
|
||||
|
||||
let mut new_migrations = Vec::new();
|
||||
for migration in migrations {
|
||||
match applied_migrations.get(&migration.version) {
|
||||
Some(applied_migration) => {
|
||||
if migration.checksum != applied_migration.checksum && !ignore_checksum_mismatch
|
||||
{
|
||||
Err(anyhow!(
|
||||
"checksum mismatch for applied migration {}",
|
||||
migration.description
|
||||
))?;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let elapsed = conn.apply(&migration).await?;
|
||||
new_migrations.push((migration, elapsed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(new_migrations)
|
||||
}
|
||||
|
||||
pub fn fuzzy_like_string(string: &str) -> String {
|
||||
let mut result = String::with_capacity(string.len() * 2 + 1);
|
||||
for c in string.chars() {
|
||||
@@ -428,7 +480,8 @@ impl Db for PostgresDb {
|
||||
COUNT(*) as count,
|
||||
COALESCE(SUM(CASE WHEN platform_linux THEN 1 ELSE 0 END), 0) as linux_count,
|
||||
COALESCE(SUM(CASE WHEN platform_mac THEN 1 ELSE 0 END), 0) as mac_count,
|
||||
COALESCE(SUM(CASE WHEN platform_windows THEN 1 ELSE 0 END), 0) as windows_count
|
||||
COALESCE(SUM(CASE WHEN platform_windows THEN 1 ELSE 0 END), 0) as windows_count,
|
||||
COALESCE(SUM(CASE WHEN platform_unknown THEN 1 ELSE 0 END), 0) as unknown_count
|
||||
FROM (
|
||||
SELECT *
|
||||
FROM signups
|
||||
@@ -449,7 +502,7 @@ impl Db for PostgresDb {
|
||||
FROM signups
|
||||
WHERE
|
||||
NOT email_confirmation_sent AND
|
||||
platform_mac
|
||||
(platform_mac OR platform_unknown)
|
||||
LIMIT $1
|
||||
",
|
||||
)
|
||||
@@ -481,7 +534,7 @@ impl Db for PostgresDb {
|
||||
&self,
|
||||
invite: &Invite,
|
||||
user: NewUserParams,
|
||||
) -> Result<NewUserResult> {
|
||||
) -> Result<Option<NewUserResult>> {
|
||||
let mut tx = self.pool.begin().await?;
|
||||
|
||||
let (signup_id, existing_user_id, inviting_user_id, signup_device_id): (
|
||||
@@ -505,10 +558,7 @@ impl Db for PostgresDb {
|
||||
.ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "no such invite".to_string()))?;
|
||||
|
||||
if existing_user_id.is_some() {
|
||||
Err(Error::Http(
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
"invitation already redeemed".to_string(),
|
||||
))?;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let (user_id, metrics_id): (UserId, String) = sqlx::query_as(
|
||||
@@ -517,6 +567,10 @@ impl Db for PostgresDb {
|
||||
(email_address, github_login, github_user_id, admin, invite_count, invite_code)
|
||||
VALUES
|
||||
($1, $2, $3, 'f', $4, $5)
|
||||
ON CONFLICT (github_login) DO UPDATE SET
|
||||
email_address = excluded.email_address,
|
||||
github_user_id = excluded.github_user_id,
|
||||
admin = excluded.admin
|
||||
RETURNING id, metrics_id::text
|
||||
",
|
||||
)
|
||||
@@ -566,6 +620,7 @@ impl Db for PostgresDb {
|
||||
(user_id_a, user_id_b, a_to_b, should_notify, accepted)
|
||||
VALUES
|
||||
($1, $2, 't', 't', 't')
|
||||
ON CONFLICT DO NOTHING
|
||||
",
|
||||
)
|
||||
.bind(inviting_user_id)
|
||||
@@ -575,12 +630,12 @@ impl Db for PostgresDb {
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
Ok(NewUserResult {
|
||||
Ok(Some(NewUserResult {
|
||||
user_id,
|
||||
metrics_id,
|
||||
inviting_user_id,
|
||||
signup_device_id,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
// invite codes
|
||||
@@ -1720,6 +1775,8 @@ pub struct WaitlistSummary {
|
||||
pub mac_count: i64,
|
||||
#[sqlx(default)]
|
||||
pub windows_count: i64,
|
||||
#[sqlx(default)]
|
||||
pub unknown_count: i64,
|
||||
}
|
||||
|
||||
#[derive(FromRow, PartialEq, Debug, Serialize, Deserialize)]
|
||||
@@ -1763,11 +1820,8 @@ mod test {
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use rand::prelude::*;
|
||||
use sqlx::{
|
||||
migrate::{MigrateDatabase, Migrator},
|
||||
Postgres,
|
||||
};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use sqlx::{migrate::MigrateDatabase, Postgres};
|
||||
use std::sync::Arc;
|
||||
use util::post_inc;
|
||||
|
||||
pub struct FakeDb {
|
||||
@@ -1955,7 +2009,7 @@ mod test {
|
||||
&self,
|
||||
_invite: &Invite,
|
||||
_user: NewUserParams,
|
||||
) -> Result<NewUserResult> {
|
||||
) -> Result<Option<NewUserResult>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
@@ -2430,13 +2484,13 @@ mod test {
|
||||
let mut rng = StdRng::from_entropy();
|
||||
let name = format!("zed-test-{}", rng.gen::<u128>());
|
||||
let url = format!("postgres://postgres@localhost/{}", name);
|
||||
let migrations_path = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/migrations"));
|
||||
Postgres::create_database(&url)
|
||||
.await
|
||||
.expect("failed to create test db");
|
||||
let db = PostgresDb::new(&url, 5).await.unwrap();
|
||||
let migrator = Migrator::new(migrations_path).await.unwrap();
|
||||
migrator.run(&db.pool).await.unwrap();
|
||||
db.migrate(Path::new(DEFAULT_MIGRATIONS_PATH.unwrap()), false)
|
||||
.await
|
||||
.unwrap();
|
||||
Self {
|
||||
db: Some(Arc::new(db)),
|
||||
url,
|
||||
|
||||
@@ -852,6 +852,7 @@ async fn test_invite_codes() {
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
|
||||
assert_eq!(invite_count, 1);
|
||||
@@ -897,6 +898,7 @@ async fn test_invite_codes() {
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
|
||||
assert_eq!(invite_count, 0);
|
||||
@@ -954,6 +956,7 @@ async fn test_invite_codes() {
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
|
||||
@@ -1022,6 +1025,7 @@ async fn test_signups() {
|
||||
mac_count: 8,
|
||||
linux_count: 4,
|
||||
windows_count: 2,
|
||||
unknown_count: 0,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1074,6 +1078,7 @@ async fn test_signups() {
|
||||
mac_count: 5,
|
||||
linux_count: 2,
|
||||
windows_count: 1,
|
||||
unknown_count: 0,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1097,6 +1102,7 @@ async fn test_signups() {
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
|
||||
assert!(inviting_user_id.is_none());
|
||||
@@ -1106,19 +1112,21 @@ async fn test_signups() {
|
||||
assert_eq!(signup_device_id.unwrap(), "device_id_0");
|
||||
|
||||
// cannot redeem the same signup again.
|
||||
db.create_user_from_invite(
|
||||
&Invite {
|
||||
email_address: signups_batch1[0].email_address.clone(),
|
||||
email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
|
||||
},
|
||||
NewUserParams {
|
||||
github_login: "some-other-github_account".into(),
|
||||
github_user_id: 1,
|
||||
invite_count: 5,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(db
|
||||
.create_user_from_invite(
|
||||
&Invite {
|
||||
email_address: signups_batch1[0].email_address.clone(),
|
||||
email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
|
||||
},
|
||||
NewUserParams {
|
||||
github_login: "some-other-github_account".into(),
|
||||
github_user_id: 1,
|
||||
invite_count: 5,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
// cannot redeem a signup with the wrong confirmation code.
|
||||
db.create_user_from_invite(
|
||||
|
||||
@@ -8,15 +8,18 @@ use anyhow::anyhow;
|
||||
use call::{room, ActiveCall, ParticipantLocation, Room};
|
||||
use client::{
|
||||
self, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection,
|
||||
Credentials, EstablishConnectionError, User, UserStore, RECEIVE_TIMEOUT,
|
||||
Credentials, EstablishConnectionError, PeerId, User, UserStore, RECEIVE_TIMEOUT,
|
||||
};
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use editor::{
|
||||
self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, ToOffset,
|
||||
ToggleCodeActions, Undo,
|
||||
};
|
||||
use fs::{FakeFs, Fs as _, LineEnding};
|
||||
use futures::{channel::mpsc, Future, StreamExt as _};
|
||||
use fs::{FakeFs, Fs as _, HomeDir, LineEnding};
|
||||
use futures::{
|
||||
channel::{mpsc, oneshot},
|
||||
Future, StreamExt as _,
|
||||
};
|
||||
use gpui::{
|
||||
executor::{self, Deterministic},
|
||||
geometry::vector::vec2f,
|
||||
@@ -27,14 +30,13 @@ use language::{
|
||||
range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
|
||||
LanguageConfig, LanguageRegistry, OffsetRangeExt, Point, Rope,
|
||||
};
|
||||
use live_kit_client::MacOSDisplay;
|
||||
use lsp::{self, FakeLanguageServer};
|
||||
use parking_lot::Mutex;
|
||||
use project::{
|
||||
search::SearchQuery, worktree::WorktreeHandle, DiagnosticSummary, Project, ProjectPath,
|
||||
ProjectStore, WorktreeId,
|
||||
search::SearchQuery, DiagnosticSummary, Project, ProjectPath, ProjectStore, WorktreeId,
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use rpc::PeerId;
|
||||
use serde_json::json;
|
||||
use settings::{Formatter, Settings};
|
||||
use sqlx::types::time::OffsetDateTime;
|
||||
@@ -45,14 +47,14 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use theme::ThemeRegistry;
|
||||
use unindent::Unindent as _;
|
||||
use workspace::{Item, SplitDirection, ToggleFollow, Workspace};
|
||||
use workspace::{shared_screen::SharedScreen, Item, SplitDirection, ToggleFollow, Workspace};
|
||||
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
@@ -183,6 +185,37 @@ async fn test_basic_calls(
|
||||
}
|
||||
);
|
||||
|
||||
// User A shares their screen
|
||||
let display = MacOSDisplay::new();
|
||||
let events_b = active_call_events(cx_b);
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.room().unwrap().update(cx, |room, cx| {
|
||||
room.set_display_sources(vec![display.clone()]);
|
||||
room.share_screen(cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
assert_eq!(events_b.borrow().len(), 1);
|
||||
let event = events_b.borrow().first().unwrap().clone();
|
||||
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event {
|
||||
assert_eq!(participant_id, client_a.peer_id().unwrap());
|
||||
room_b.read_with(cx_b, |room, _| {
|
||||
assert_eq!(
|
||||
room.remote_participants()[&client_a.peer_id().unwrap()]
|
||||
.tracks
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
});
|
||||
} else {
|
||||
panic!("unexpected event")
|
||||
}
|
||||
|
||||
// User A leaves the room.
|
||||
active_call_a.update(cx_a, |call, cx| {
|
||||
call.hang_up(cx).unwrap();
|
||||
@@ -204,12 +237,13 @@ async fn test_basic_calls(
|
||||
}
|
||||
);
|
||||
|
||||
// User B leaves the room.
|
||||
active_call_b.update(cx_b, |call, cx| {
|
||||
call.hang_up(cx).unwrap();
|
||||
assert!(call.room().is_none());
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
// User B gets disconnected from the LiveKit server, which causes them
|
||||
// to automatically leave the room.
|
||||
server
|
||||
.test_live_kit_server
|
||||
.disconnect_client(client_b.peer_id().unwrap().to_string())
|
||||
.await;
|
||||
active_call_b.update(cx_b, |call, _| assert!(call.room().is_none()));
|
||||
assert_eq!(
|
||||
room_participants(&room_a, cx_a),
|
||||
RoomParticipants {
|
||||
@@ -385,7 +419,7 @@ async fn test_leaving_room_on_disconnection(
|
||||
);
|
||||
|
||||
// When user A disconnects, both client A and B clear their room on the active call.
|
||||
server.disconnect_client(client_a.current_user_id(cx_a));
|
||||
server.disconnect_client(client_a.peer_id().unwrap());
|
||||
cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
|
||||
active_call_a.read_with(cx_a, |call, _| assert!(call.room().is_none()));
|
||||
active_call_b.read_with(cx_b, |call, _| assert!(call.room().is_none()));
|
||||
@@ -403,6 +437,63 @@ async fn test_leaving_room_on_disconnection(
|
||||
pending: Default::default()
|
||||
}
|
||||
);
|
||||
|
||||
// Call user B again from client A.
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.invite(client_b.user_id().unwrap(), None, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
|
||||
|
||||
// User B receives the call and joins the room.
|
||||
let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
|
||||
incoming_call_b.next().await.unwrap().unwrap();
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.accept_incoming(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
|
||||
deterministic.run_until_parked();
|
||||
assert_eq!(
|
||||
room_participants(&room_a, cx_a),
|
||||
RoomParticipants {
|
||||
remote: vec!["user_b".to_string()],
|
||||
pending: Default::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
room_participants(&room_b, cx_b),
|
||||
RoomParticipants {
|
||||
remote: vec!["user_a".to_string()],
|
||||
pending: Default::default()
|
||||
}
|
||||
);
|
||||
|
||||
// User B gets disconnected from the LiveKit server, which causes it
|
||||
// to automatically leave the room.
|
||||
server
|
||||
.test_live_kit_server
|
||||
.disconnect_client(client_b.peer_id().unwrap().to_string())
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
active_call_a.update(cx_a, |call, _| assert!(call.room().is_none()));
|
||||
active_call_b.update(cx_b, |call, _| assert!(call.room().is_none()));
|
||||
assert_eq!(
|
||||
room_participants(&room_a, cx_a),
|
||||
RoomParticipants {
|
||||
remote: Default::default(),
|
||||
pending: Default::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
room_participants(&room_b, cx_b),
|
||||
RoomParticipants {
|
||||
remote: Default::default(),
|
||||
pending: Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
@@ -416,7 +507,7 @@ async fn test_calls_on_multiple_connections(
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b1 = server.create_client(cx_b1, "user_b").await;
|
||||
let _client_b2 = server.create_client(cx_b2, "user_b").await;
|
||||
let client_b2 = server.create_client(cx_b2, "user_b").await;
|
||||
server
|
||||
.make_contacts(&mut [(&client_a, cx_a), (&client_b1, cx_b1)])
|
||||
.await;
|
||||
@@ -468,6 +559,14 @@ async fn test_calls_on_multiple_connections(
|
||||
assert!(incoming_call_b1.next().await.unwrap().is_none());
|
||||
assert!(incoming_call_b2.next().await.unwrap().is_none());
|
||||
|
||||
// User B disconnects the client that is not on the call. Everything should be fine.
|
||||
client_b1.disconnect(&cx_b1.to_async()).unwrap();
|
||||
deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
|
||||
client_b1
|
||||
.authenticate_and_connect(false, &cx_b1.to_async())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// User B hangs up, and user A calls them again.
|
||||
active_call_b2.update(cx_b2, |call, cx| call.hang_up(cx).unwrap());
|
||||
deterministic.run_until_parked();
|
||||
@@ -520,11 +619,29 @@ async fn test_calls_on_multiple_connections(
|
||||
assert!(incoming_call_b1.next().await.unwrap().is_some());
|
||||
assert!(incoming_call_b2.next().await.unwrap().is_some());
|
||||
|
||||
// User A disconnects up, causing both connections to stop ringing.
|
||||
server.disconnect_client(client_a.current_user_id(cx_a));
|
||||
cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
|
||||
// User A disconnects, causing both connections to stop ringing.
|
||||
server.disconnect_client(client_a.peer_id().unwrap());
|
||||
deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
|
||||
assert!(incoming_call_b1.next().await.unwrap().is_none());
|
||||
assert!(incoming_call_b2.next().await.unwrap().is_none());
|
||||
|
||||
// User A reconnects automatically, then calls user B again.
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.invite(client_b1.user_id().unwrap(), None, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
assert!(incoming_call_b1.next().await.unwrap().is_some());
|
||||
assert!(incoming_call_b2.next().await.unwrap().is_some());
|
||||
|
||||
// User B disconnects all clients, causing user A to no longer see a pending call for them.
|
||||
server.forbid_connections();
|
||||
server.disconnect_client(client_b1.peer_id().unwrap());
|
||||
server.disconnect_client(client_b2.peer_id().unwrap());
|
||||
deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
|
||||
active_call_a.read_with(cx_a, |call, _| assert!(call.room().is_none()));
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
@@ -582,7 +699,7 @@ async fn test_share_project(
|
||||
.update(cx_b, |call, cx| call.accept_incoming(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let client_b_peer_id = client_b.peer_id;
|
||||
let client_b_peer_id = client_b.peer_id().unwrap();
|
||||
let project_b = client_b
|
||||
.build_remote_project(initial_project.id, cx_b)
|
||||
.await;
|
||||
@@ -806,8 +923,8 @@ async fn test_host_disconnect(
|
||||
assert!(cx_b.is_window_edited(workspace_b.window_id()));
|
||||
|
||||
// Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
|
||||
server.disconnect_client(client_a.current_user_id(cx_a));
|
||||
cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
|
||||
server.disconnect_client(client_a.peer_id().unwrap());
|
||||
deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
|
||||
project_a
|
||||
.condition(cx_a, |project, _| project.collaborators().is_empty())
|
||||
.await;
|
||||
@@ -829,6 +946,29 @@ async fn test_host_disconnect(
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(can_close);
|
||||
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| {
|
||||
call.invite(client_a.user_id().unwrap(), None, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.accept_incoming(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Drop client A's connection again. We should still unshare it successfully.
|
||||
server.disconnect_client(client_a.peer_id().unwrap());
|
||||
deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
|
||||
project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
@@ -903,21 +1043,21 @@ async fn test_active_call_events(
|
||||
deterministic.run_until_parked();
|
||||
assert_eq!(mem::take(&mut *events_a.borrow_mut()), vec![]);
|
||||
assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]);
|
||||
}
|
||||
|
||||
fn active_call_events(cx: &mut TestAppContext) -> Rc<RefCell<Vec<room::Event>>> {
|
||||
let events = Rc::new(RefCell::new(Vec::new()));
|
||||
let active_call = cx.read(ActiveCall::global);
|
||||
cx.update({
|
||||
let events = events.clone();
|
||||
|cx| {
|
||||
cx.subscribe(&active_call, move |_, event, _| {
|
||||
events.borrow_mut().push(event.clone())
|
||||
})
|
||||
.detach()
|
||||
}
|
||||
});
|
||||
events
|
||||
}
|
||||
fn active_call_events(cx: &mut TestAppContext) -> Rc<RefCell<Vec<room::Event>>> {
|
||||
let events = Rc::new(RefCell::new(Vec::new()));
|
||||
let active_call = cx.read(ActiveCall::global);
|
||||
cx.update({
|
||||
let events = events.clone();
|
||||
|cx| {
|
||||
cx.subscribe(&active_call, move |_, event, _| {
|
||||
events.borrow_mut().push(event.clone())
|
||||
})
|
||||
.detach()
|
||||
}
|
||||
});
|
||||
events
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
@@ -933,15 +1073,9 @@ async fn test_room_location(
|
||||
client_a.fs.insert_tree("/a", json!({})).await;
|
||||
client_b.fs.insert_tree("/b", json!({})).await;
|
||||
|
||||
let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
|
||||
let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
|
||||
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
let a_notified = Rc::new(Cell::new(false));
|
||||
cx_a.update({
|
||||
let notified = a_notified.clone();
|
||||
@@ -951,8 +1085,6 @@ async fn test_room_location(
|
||||
}
|
||||
});
|
||||
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
|
||||
let b_notified = Rc::new(Cell::new(false));
|
||||
cx_b.update({
|
||||
let b_notified = b_notified.clone();
|
||||
@@ -962,10 +1094,18 @@ async fn test_room_location(
|
||||
}
|
||||
});
|
||||
|
||||
room_a
|
||||
.update(cx_a, |room, cx| room.set_location(Some(&project_a), cx))
|
||||
let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
|
||||
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
|
||||
let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
|
||||
deterministic.run_until_parked();
|
||||
assert!(a_notified.take());
|
||||
assert_eq!(
|
||||
@@ -1020,8 +1160,8 @@ async fn test_room_location(
|
||||
)]
|
||||
);
|
||||
|
||||
room_b
|
||||
.update(cx_b, |room, cx| room.set_location(Some(&project_b), cx))
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
@@ -1046,8 +1186,8 @@ async fn test_room_location(
|
||||
)]
|
||||
);
|
||||
|
||||
room_b
|
||||
.update(cx_b, |room, cx| room.set_location(None, cx))
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.set_location(None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
@@ -1087,26 +1227,49 @@ async fn test_room_location(
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_propagate_saves_and_fs_changes(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
let rust = Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
));
|
||||
let javascript = Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "JavaScript".into(),
|
||||
path_suffixes: vec!["js".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
));
|
||||
for client in [&client_a, &client_b, &client_c] {
|
||||
client.language_registry.add(rust.clone());
|
||||
client.language_registry.add(javascript.clone());
|
||||
}
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
"file1": "",
|
||||
"file1.rs": "",
|
||||
"file2": ""
|
||||
}),
|
||||
)
|
||||
@@ -1126,19 +1289,25 @@ async fn test_propagate_saves_and_fs_changes(
|
||||
|
||||
// Open and edit a buffer as both guests B and C.
|
||||
let buffer_b = project_b
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let buffer_c = project_c
|
||||
.update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
|
||||
.update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
buffer_b.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(&*buffer.language().unwrap().name(), "Rust");
|
||||
});
|
||||
buffer_c.read_with(cx_c, |buffer, _| {
|
||||
assert_eq!(&*buffer.language().unwrap().name(), "Rust");
|
||||
});
|
||||
buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "i-am-b, ")], None, cx));
|
||||
buffer_c.update(cx_c, |buf, cx| buf.edit([(0..0, "i-am-c, ")], None, cx));
|
||||
|
||||
// Open and edit that buffer as the host.
|
||||
let buffer_a = project_a
|
||||
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
|
||||
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1149,90 +1318,87 @@ async fn test_propagate_saves_and_fs_changes(
|
||||
buf.edit([(buf.len()..buf.len(), "i-am-a")], None, cx)
|
||||
});
|
||||
|
||||
// Wait for edits to propagate
|
||||
buffer_a
|
||||
.condition(cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
|
||||
.await;
|
||||
buffer_b
|
||||
.condition(cx_b, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
|
||||
.await;
|
||||
buffer_c
|
||||
.condition(cx_c, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
buffer_a.read_with(cx_a, |buf, _| {
|
||||
assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
|
||||
});
|
||||
buffer_b.read_with(cx_b, |buf, _| {
|
||||
assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
|
||||
});
|
||||
buffer_c.read_with(cx_c, |buf, _| {
|
||||
assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
|
||||
});
|
||||
|
||||
// Edit the buffer as the host and concurrently save as guest B.
|
||||
let save_b = buffer_b.update(cx_b, |buf, cx| buf.save(cx));
|
||||
buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx));
|
||||
save_b.await.unwrap();
|
||||
assert_eq!(
|
||||
client_a.fs.load("/a/file1".as_ref()).await.unwrap(),
|
||||
client_a.fs.load("/a/file1.rs".as_ref()).await.unwrap(),
|
||||
"hi-a, i-am-c, i-am-b, i-am-a"
|
||||
);
|
||||
|
||||
deterministic.run_until_parked();
|
||||
buffer_a.read_with(cx_a, |buf, _| assert!(!buf.is_dirty()));
|
||||
buffer_b.read_with(cx_b, |buf, _| assert!(!buf.is_dirty()));
|
||||
buffer_c.condition(cx_c, |buf, _| !buf.is_dirty()).await;
|
||||
|
||||
worktree_a.flush_fs_events(cx_a).await;
|
||||
buffer_c.read_with(cx_c, |buf, _| assert!(!buf.is_dirty()));
|
||||
|
||||
// Make changes on host's file system, see those changes on guest worktrees.
|
||||
client_a
|
||||
.fs
|
||||
.rename(
|
||||
"/a/file1".as_ref(),
|
||||
"/a/file1-renamed".as_ref(),
|
||||
"/a/file1.rs".as_ref(),
|
||||
"/a/file1.js".as_ref(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
client_a.fs.insert_file("/a/file4", "4".into()).await;
|
||||
deterministic.run_until_parked();
|
||||
|
||||
worktree_a
|
||||
.condition(cx_a, |tree, _| {
|
||||
worktree_a.read_with(cx_a, |tree, _| {
|
||||
assert_eq!(
|
||||
tree.paths()
|
||||
.map(|p| p.to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
== ["file1-renamed", "file3", "file4"]
|
||||
})
|
||||
.await;
|
||||
worktree_b
|
||||
.condition(cx_b, |tree, _| {
|
||||
.collect::<Vec<_>>(),
|
||||
["file1.js", "file3", "file4"]
|
||||
)
|
||||
});
|
||||
worktree_b.read_with(cx_b, |tree, _| {
|
||||
assert_eq!(
|
||||
tree.paths()
|
||||
.map(|p| p.to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
== ["file1-renamed", "file3", "file4"]
|
||||
})
|
||||
.await;
|
||||
worktree_c
|
||||
.condition(cx_c, |tree, _| {
|
||||
.collect::<Vec<_>>(),
|
||||
["file1.js", "file3", "file4"]
|
||||
)
|
||||
});
|
||||
worktree_c.read_with(cx_c, |tree, _| {
|
||||
assert_eq!(
|
||||
tree.paths()
|
||||
.map(|p| p.to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
== ["file1-renamed", "file3", "file4"]
|
||||
})
|
||||
.await;
|
||||
.collect::<Vec<_>>(),
|
||||
["file1.js", "file3", "file4"]
|
||||
)
|
||||
});
|
||||
|
||||
// Ensure buffer files are updated as well.
|
||||
buffer_a
|
||||
.condition(cx_a, |buf, _| {
|
||||
buf.file().unwrap().path().to_str() == Some("file1-renamed")
|
||||
})
|
||||
.await;
|
||||
buffer_b
|
||||
.condition(cx_b, |buf, _| {
|
||||
buf.file().unwrap().path().to_str() == Some("file1-renamed")
|
||||
})
|
||||
.await;
|
||||
buffer_c
|
||||
.condition(cx_c, |buf, _| {
|
||||
buf.file().unwrap().path().to_str() == Some("file1-renamed")
|
||||
})
|
||||
.await;
|
||||
buffer_a.read_with(cx_a, |buffer, _| {
|
||||
assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
|
||||
assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
|
||||
});
|
||||
buffer_b.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
|
||||
assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
|
||||
});
|
||||
buffer_c.read_with(cx_c, |buffer, _| {
|
||||
assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
|
||||
assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
@@ -2127,7 +2293,7 @@ async fn test_leaving_project(
|
||||
|
||||
// Simulate connection loss for client C and ensure client A observes client C leaving the project.
|
||||
client_c.wait_for_current_user(cx_c).await;
|
||||
server.disconnect_client(client_c.current_user_id(cx_c));
|
||||
server.disconnect_client(client_c.peer_id().unwrap());
|
||||
cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
|
||||
deterministic.run_until_parked();
|
||||
project_a.read_with(cx_a, |project, _| {
|
||||
@@ -3015,7 +3181,7 @@ async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
assert_eq!(references[1].buffer, references[0].buffer);
|
||||
assert_eq!(
|
||||
three_buffer.file().unwrap().full_path(cx),
|
||||
Path::new("three.rs")
|
||||
Path::new("/root/dir-2/three.rs")
|
||||
);
|
||||
|
||||
assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
|
||||
@@ -4290,7 +4456,7 @@ async fn test_chat_reconnection(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon
|
||||
|
||||
// Disconnect client B, ensuring we can still access its cached channel data.
|
||||
server.forbid_connections();
|
||||
server.disconnect_client(client_b.current_user_id(cx_b));
|
||||
server.disconnect_client(client_b.peer_id().unwrap());
|
||||
cx_b.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
|
||||
while !matches!(
|
||||
status_b.next().await,
|
||||
@@ -4453,7 +4619,7 @@ async fn test_contacts(
|
||||
]
|
||||
);
|
||||
|
||||
server.disconnect_client(client_c.current_user_id(cx_c));
|
||||
server.disconnect_client(client_c.peer_id().unwrap());
|
||||
server.forbid_connections();
|
||||
deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
|
||||
assert_eq!(
|
||||
@@ -4693,7 +4859,7 @@ async fn test_contacts(
|
||||
);
|
||||
|
||||
server.forbid_connections();
|
||||
server.disconnect_client(client_a.current_user_id(cx_a));
|
||||
server.disconnect_client(client_a.peer_id().unwrap());
|
||||
deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
|
||||
assert_eq!(contacts(&client_a, cx_a), []);
|
||||
assert_eq!(
|
||||
@@ -4917,7 +5083,11 @@ async fn test_contact_requests(
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
async fn test_following(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
cx_a.update(editor::init);
|
||||
cx_b.update(editor::init);
|
||||
@@ -4929,6 +5099,7 @@ async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
@@ -4942,11 +5113,20 @@ async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
)
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client A opens some editors.
|
||||
let workspace_a = client_a.build_workspace(&project_a, cx_a);
|
||||
@@ -5088,7 +5268,7 @@ async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace.activate_item(&editor_a2, cx)
|
||||
});
|
||||
cx_a.foreground().run_until_parked();
|
||||
deterministic.run_until_parked();
|
||||
assert_eq!(
|
||||
workspace_b.read_with(cx_b, |workspace, cx| workspace
|
||||
.active_item(cx)
|
||||
@@ -5118,9 +5298,62 @@ async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
editor_a1.id()
|
||||
);
|
||||
|
||||
// Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
|
||||
let display = MacOSDisplay::new();
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.set_location(None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| {
|
||||
call.room().unwrap().update(cx, |room, cx| {
|
||||
room.set_display_sources(vec![display.clone()]);
|
||||
room.share_screen(cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.downcast::<SharedScreen>()
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
// Client B activates Zed again, which causes the previous editor to become focused again.
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
assert_eq!(
|
||||
workspace_a.read_with(cx_a, |workspace, cx| workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.id()),
|
||||
editor_a1.id()
|
||||
);
|
||||
|
||||
// Client B activates an external window again, and the previously-opened screen-sharing item
|
||||
// gets activated.
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.set_location(None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
assert_eq!(
|
||||
workspace_a.read_with(cx_a, |workspace, cx| workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.id()),
|
||||
shared_screen.id()
|
||||
);
|
||||
|
||||
// Following interrupts when client B disconnects.
|
||||
client_b.disconnect(&cx_b.to_async()).unwrap();
|
||||
cx_a.foreground().run_until_parked();
|
||||
deterministic.run_until_parked();
|
||||
assert_eq!(
|
||||
workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||
None
|
||||
@@ -5140,6 +5373,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
// Client A shares a project.
|
||||
client_a
|
||||
@@ -5155,6 +5389,10 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
)
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
@@ -5162,6 +5400,10 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
|
||||
// Client B joins the project.
|
||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client A opens some editors.
|
||||
let workspace_a = client_a.build_workspace(&project_a, cx_a);
|
||||
@@ -5309,6 +5551,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
// Client A shares a project.
|
||||
client_a
|
||||
@@ -5323,11 +5566,20 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
|
||||
)
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client A opens some editors.
|
||||
let workspace_a = client_a.build_workspace(&project_a, cx_a);
|
||||
@@ -5603,6 +5855,7 @@ async fn test_random_collaboration(
|
||||
|
||||
let mut clients = Vec::new();
|
||||
let mut user_ids = Vec::new();
|
||||
let mut peer_ids = Vec::new();
|
||||
let mut op_start_signals = Vec::new();
|
||||
|
||||
let mut next_entity_id = 100000;
|
||||
@@ -5791,6 +6044,7 @@ async fn test_random_collaboration(
|
||||
|
||||
let op_start_signal = futures::channel::mpsc::unbounded();
|
||||
user_ids.push(host_user_id);
|
||||
peer_ids.push(host.peer_id().unwrap());
|
||||
op_start_signals.push(op_start_signal.0);
|
||||
clients.push(host_cx.foreground().spawn(host.simulate_host(
|
||||
host_project,
|
||||
@@ -5808,7 +6062,7 @@ async fn test_random_collaboration(
|
||||
let mut operations = 0;
|
||||
while operations < max_operations {
|
||||
if operations == disconnect_host_at {
|
||||
server.disconnect_client(user_ids[0]);
|
||||
server.disconnect_client(peer_ids[0]);
|
||||
deterministic.advance_clock(RECEIVE_TIMEOUT);
|
||||
drop(op_start_signals);
|
||||
|
||||
@@ -5891,6 +6145,7 @@ async fn test_random_collaboration(
|
||||
|
||||
let op_start_signal = futures::channel::mpsc::unbounded();
|
||||
user_ids.push(guest_user_id);
|
||||
peer_ids.push(guest.peer_id().unwrap());
|
||||
op_start_signals.push(op_start_signal.0);
|
||||
clients.push(guest_cx.foreground().spawn(guest.simulate_guest(
|
||||
guest_username.clone(),
|
||||
@@ -5907,10 +6162,11 @@ async fn test_random_collaboration(
|
||||
let guest_ix = rng.lock().gen_range(1..clients.len());
|
||||
log::info!("Removing guest {}", user_ids[guest_ix]);
|
||||
let removed_guest_id = user_ids.remove(guest_ix);
|
||||
let removed_peer_id = peer_ids.remove(guest_ix);
|
||||
let guest = clients.remove(guest_ix);
|
||||
op_start_signals.remove(guest_ix);
|
||||
server.forbid_connections();
|
||||
server.disconnect_client(removed_guest_id);
|
||||
server.disconnect_client(removed_peer_id);
|
||||
deterministic.advance_clock(RECEIVE_TIMEOUT);
|
||||
deterministic.start_waiting();
|
||||
log::info!("Waiting for guest {} to exit...", removed_guest_id);
|
||||
@@ -6034,8 +6290,10 @@ async fn test_random_collaboration(
|
||||
let host_buffer = host_project.read_with(&host_cx, |project, cx| {
|
||||
project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"host does not have buffer for guest:{}, peer:{}, id:{}",
|
||||
guest_client.username, guest_client.peer_id, buffer_id
|
||||
"host does not have buffer for guest:{}, peer:{:?}, id:{}",
|
||||
guest_client.username,
|
||||
guest_client.peer_id(),
|
||||
buffer_id
|
||||
)
|
||||
})
|
||||
});
|
||||
@@ -6078,9 +6336,10 @@ struct TestServer {
|
||||
server: Arc<Server>,
|
||||
foreground: Rc<executor::Foreground>,
|
||||
notifications: mpsc::UnboundedReceiver<()>,
|
||||
connection_killers: Arc<Mutex<HashMap<UserId, Arc<AtomicBool>>>>,
|
||||
connection_killers: Arc<Mutex<HashMap<PeerId, Arc<AtomicBool>>>>,
|
||||
forbid_connections: Arc<AtomicBool>,
|
||||
_test_db: TestDb,
|
||||
test_live_kit_server: Arc<live_kit_client::TestServer>,
|
||||
}
|
||||
|
||||
impl TestServer {
|
||||
@@ -6088,8 +6347,18 @@ impl TestServer {
|
||||
foreground: Rc<executor::Foreground>,
|
||||
background: Arc<executor::Background>,
|
||||
) -> Self {
|
||||
static NEXT_LIVE_KIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
let test_db = TestDb::fake(background.clone());
|
||||
let app_state = Self::build_app_state(&test_db).await;
|
||||
let live_kit_server_id = NEXT_LIVE_KIT_SERVER_ID.fetch_add(1, SeqCst);
|
||||
let live_kit_server = live_kit_client::TestServer::create(
|
||||
format!("http://livekit.{}.test", live_kit_server_id),
|
||||
format!("devkey-{}", live_kit_server_id),
|
||||
format!("secret-{}", live_kit_server_id),
|
||||
background.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
let app_state = Self::build_app_state(&test_db, &live_kit_server).await;
|
||||
let peer = Peer::new();
|
||||
let notifications = mpsc::unbounded();
|
||||
let server = Server::new(app_state.clone(), Some(notifications.0));
|
||||
@@ -6102,11 +6371,14 @@ impl TestServer {
|
||||
connection_killers: Default::default(),
|
||||
forbid_connections: Default::default(),
|
||||
_test_db: test_db,
|
||||
test_live_kit_server: live_kit_server,
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
|
||||
cx.update(|cx| {
|
||||
cx.set_global(HomeDir(Path::new("/tmp/").to_path_buf()));
|
||||
|
||||
let mut settings = Settings::test(cx);
|
||||
settings.projects_online_by_default = false;
|
||||
cx.set_global(settings);
|
||||
@@ -6142,7 +6414,6 @@ impl TestServer {
|
||||
let db = self.app_state.db.clone();
|
||||
let connection_killers = self.connection_killers.clone();
|
||||
let forbid_connections = self.forbid_connections.clone();
|
||||
let (connection_id_tx, mut connection_id_rx) = mpsc::channel(16);
|
||||
|
||||
Arc::get_mut(&mut client)
|
||||
.unwrap()
|
||||
@@ -6165,7 +6436,6 @@ impl TestServer {
|
||||
let connection_killers = connection_killers.clone();
|
||||
let forbid_connections = forbid_connections.clone();
|
||||
let client_name = client_name.clone();
|
||||
let connection_id_tx = connection_id_tx.clone();
|
||||
cx.spawn(move |cx| async move {
|
||||
if forbid_connections.load(SeqCst) {
|
||||
Err(EstablishConnectionError::other(anyhow!(
|
||||
@@ -6174,7 +6444,7 @@ impl TestServer {
|
||||
} else {
|
||||
let (client_conn, server_conn, killed) =
|
||||
Connection::in_memory(cx.background());
|
||||
connection_killers.lock().insert(user_id, killed);
|
||||
let (connection_id_tx, connection_id_rx) = oneshot::channel();
|
||||
let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
|
||||
cx.background()
|
||||
.spawn(server.handle_connection(
|
||||
@@ -6185,6 +6455,10 @@ impl TestServer {
|
||||
cx.background(),
|
||||
))
|
||||
.detach();
|
||||
let connection_id = connection_id_rx.await.unwrap();
|
||||
connection_killers
|
||||
.lock()
|
||||
.insert(PeerId(connection_id.0), killed);
|
||||
Ok(client_conn)
|
||||
}
|
||||
})
|
||||
@@ -6216,11 +6490,9 @@ impl TestServer {
|
||||
.authenticate_and_connect(false, &cx.to_async())
|
||||
.await
|
||||
.unwrap();
|
||||
let peer_id = PeerId(connection_id_rx.next().await.unwrap().0);
|
||||
|
||||
let client = TestClient {
|
||||
client,
|
||||
peer_id,
|
||||
username: name.to_string(),
|
||||
user_store,
|
||||
project_store,
|
||||
@@ -6232,10 +6504,10 @@ impl TestServer {
|
||||
client
|
||||
}
|
||||
|
||||
fn disconnect_client(&self, user_id: UserId) {
|
||||
fn disconnect_client(&self, peer_id: PeerId) {
|
||||
self.connection_killers
|
||||
.lock()
|
||||
.remove(&user_id)
|
||||
.remove(&peer_id)
|
||||
.unwrap()
|
||||
.store(true, SeqCst);
|
||||
}
|
||||
@@ -6295,11 +6567,14 @@ impl TestServer {
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_app_state(test_db: &TestDb) -> Arc<AppState> {
|
||||
async fn build_app_state(
|
||||
test_db: &TestDb,
|
||||
fake_server: &live_kit_client::TestServer,
|
||||
) -> Arc<AppState> {
|
||||
Arc::new(AppState {
|
||||
db: test_db.db().clone(),
|
||||
api_token: Default::default(),
|
||||
invite_link_prefix: Default::default(),
|
||||
live_kit_client: Some(Arc::new(fake_server.create_api_client())),
|
||||
config: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6330,13 +6605,13 @@ impl Deref for TestServer {
|
||||
impl Drop for TestServer {
|
||||
fn drop(&mut self) {
|
||||
self.peer.reset();
|
||||
self.test_live_kit_server.teardown().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
struct TestClient {
|
||||
client: Arc<Client>,
|
||||
username: String,
|
||||
pub peer_id: PeerId,
|
||||
pub user_store: ModelHandle<UserStore>,
|
||||
pub project_store: ModelHandle<ProjectStore>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
|
||||
@@ -9,44 +9,73 @@ mod db_tests;
|
||||
#[cfg(test)]
|
||||
mod integration_tests;
|
||||
|
||||
use axum::{body::Body, Router};
|
||||
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 std::{
|
||||
env::args,
|
||||
net::{SocketAddr, TcpListener},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::signal;
|
||||
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 honeycomb_api_key: Option<String>,
|
||||
pub honeycomb_dataset: Option<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>,
|
||||
api_token: String,
|
||||
invite_link_prefix: String,
|
||||
live_kit_client: Option<Arc<dyn live_kit_server::api::Client>>,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
async fn new(config: &Config) -> Result<Arc<Self>> {
|
||||
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),
|
||||
api_token: config.api_token.clone(),
|
||||
invite_link_prefix: config.invite_link_prefix.clone(),
|
||||
live_kit_client,
|
||||
config,
|
||||
};
|
||||
Ok(Arc::new(this))
|
||||
}
|
||||
@@ -61,27 +90,62 @@ async fn main() -> Result<()> {
|
||||
);
|
||||
}
|
||||
|
||||
let config = envy::from_env::<Config>().expect("error loading config");
|
||||
init_tracing(&config);
|
||||
let state = AppState::new(&config).await?;
|
||||
match args().skip(1).next().as_deref() {
|
||||
Some("version") => {
|
||||
println!("collab v{VERSION}");
|
||||
}
|
||||
Some("migrate") => {
|
||||
let config = envy::from_env::<MigrateConfig>().expect("error loading config");
|
||||
let db = PostgresDb::new(&config.database_url, 5).await?;
|
||||
|
||||
let listener = TcpListener::bind(&format!("0.0.0.0:{}", config.http_port))
|
||||
.expect("failed to bind TCP listener");
|
||||
let rpc_server = rpc::Server::new(state.clone(), None);
|
||||
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"))?;
|
||||
|
||||
rpc_server.start_recording_project_activity(Duration::from_secs(5 * 60), rpc::RealExecutor);
|
||||
let migrations = db.migrate(&migrations_path, false).await?;
|
||||
for (migration, duration) in migrations {
|
||||
println!(
|
||||
"Ran {} {} {:?}",
|
||||
migration.version, migration.description, duration
|
||||
);
|
||||
}
|
||||
|
||||
let app = Router::<Body>::new()
|
||||
.merge(api::routes(&rpc_server, state.clone()))
|
||||
.merge(rpc::routes(rpc_server));
|
||||
return Ok(());
|
||||
}
|
||||
Some("serve") => {
|
||||
let config = envy::from_env::<Config>().expect("error loading config");
|
||||
init_tracing(&config);
|
||||
|
||||
axum::Server::from_tcp(listener)?
|
||||
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
|
||||
.await?;
|
||||
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 app = api::routes(rpc_server.clone(), state.clone())
|
||||
.merge(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))
|
||||
.await?;
|
||||
}
|
||||
_ => {
|
||||
Err(anyhow!("usage: collab <version | migrate | serve>"))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_root() -> String {
|
||||
format!("collab v{VERSION}")
|
||||
}
|
||||
|
||||
pub fn init_tracing(config: &Config) -> Option<()> {
|
||||
use std::str::FromStr;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
@@ -113,3 +177,52 @@ 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ use axum::{
|
||||
};
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::{
|
||||
channel::mpsc,
|
||||
channel::{mpsc, oneshot},
|
||||
future::{self, BoxFuture},
|
||||
stream::FuturesUnordered,
|
||||
FutureExt, SinkExt, StreamExt, TryStreamExt,
|
||||
@@ -42,6 +42,7 @@ use std::{
|
||||
marker::PhantomData,
|
||||
net::SocketAddr,
|
||||
ops::{Deref, DerefMut},
|
||||
os::unix::prelude::OsStrExt,
|
||||
rc::Rc,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
@@ -49,6 +50,7 @@ use std::{
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
pub use store::{Store, Worktree};
|
||||
use time::OffsetDateTime;
|
||||
use tokio::{
|
||||
sync::{Mutex, MutexGuard},
|
||||
@@ -57,8 +59,6 @@ use tokio::{
|
||||
use tower::ServiceBuilder;
|
||||
use tracing::{info_span, instrument, Instrument};
|
||||
|
||||
pub use store::{Store, Worktree};
|
||||
|
||||
lazy_static! {
|
||||
static ref METRIC_CONNECTIONS: IntGauge =
|
||||
register_int_gauge!("connections", "number of connections").unwrap();
|
||||
@@ -347,7 +347,7 @@ impl Server {
|
||||
connection: Connection,
|
||||
address: String,
|
||||
user: User,
|
||||
mut send_connection_id: Option<mpsc::Sender<ConnectionId>>,
|
||||
mut send_connection_id: Option<oneshot::Sender<ConnectionId>>,
|
||||
executor: E,
|
||||
) -> impl Future<Output = Result<()>> {
|
||||
let mut this = self.clone();
|
||||
@@ -368,9 +368,11 @@ impl Server {
|
||||
});
|
||||
|
||||
tracing::info!(%user_id, %login, %connection_id, %address, "connection opened");
|
||||
this.peer.send(connection_id, proto::Hello { peer_id: connection_id.0 })?;
|
||||
tracing::info!(%user_id, %login, %connection_id, %address, "sent hello message");
|
||||
|
||||
if let Some(send_connection_id) = send_connection_id.as_mut() {
|
||||
let _ = send_connection_id.send(connection_id).await;
|
||||
if let Some(send_connection_id) = send_connection_id.take() {
|
||||
let _ = send_connection_id.send(connection_id);
|
||||
}
|
||||
|
||||
if !user.connected_once {
|
||||
@@ -394,7 +396,7 @@ impl Server {
|
||||
|
||||
if let Some((code, count)) = invite_code {
|
||||
this.peer.send(connection_id, proto::UpdateInviteInfo {
|
||||
url: format!("{}{}", this.app_state.invite_link_prefix, code),
|
||||
url: format!("{}{}", this.app_state.config.invite_link_prefix, code),
|
||||
count,
|
||||
})?;
|
||||
}
|
||||
@@ -474,8 +476,13 @@ impl Server {
|
||||
|
||||
let mut projects_to_unshare = Vec::new();
|
||||
let mut contacts_to_update = HashSet::default();
|
||||
let mut room_left = None;
|
||||
{
|
||||
let mut store = self.store().await;
|
||||
|
||||
#[cfg(test)]
|
||||
let removed_connection = store.remove_connection(connection_id).unwrap();
|
||||
#[cfg(not(test))]
|
||||
let removed_connection = store.remove_connection(connection_id)?;
|
||||
|
||||
for project in removed_connection.hosted_projects {
|
||||
@@ -502,23 +509,24 @@ impl Server {
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(room) = removed_connection.room {
|
||||
self.room_updated(&room);
|
||||
room_left = Some(self.room_left(&room, connection_id));
|
||||
}
|
||||
|
||||
contacts_to_update.insert(removed_connection.user_id);
|
||||
for connection_id in removed_connection.canceled_call_connection_ids {
|
||||
self.peer
|
||||
.send(connection_id, proto::CallCanceled {})
|
||||
.trace_err();
|
||||
contacts_to_update.extend(store.user_id_for_connection(connection_id).ok());
|
||||
}
|
||||
|
||||
if let Some(room) = removed_connection
|
||||
.room_id
|
||||
.and_then(|room_id| store.room(room_id))
|
||||
{
|
||||
self.room_updated(room);
|
||||
}
|
||||
|
||||
contacts_to_update.insert(removed_connection.user_id);
|
||||
};
|
||||
|
||||
if let Some(room_left) = room_left {
|
||||
room_left.await.trace_err();
|
||||
}
|
||||
|
||||
for user_id in contacts_to_update {
|
||||
self.update_user_contacts(user_id).await.trace_err();
|
||||
}
|
||||
@@ -554,7 +562,7 @@ impl Server {
|
||||
self.peer.send(
|
||||
connection_id,
|
||||
proto::UpdateInviteInfo {
|
||||
url: format!("{}{}", self.app_state.invite_link_prefix, &code),
|
||||
url: format!("{}{}", self.app_state.config.invite_link_prefix, &code),
|
||||
count: user.invite_count as u32,
|
||||
},
|
||||
)?;
|
||||
@@ -572,7 +580,10 @@ impl Server {
|
||||
self.peer.send(
|
||||
connection_id,
|
||||
proto::UpdateInviteInfo {
|
||||
url: format!("{}{}", self.app_state.invite_link_prefix, invite_code),
|
||||
url: format!(
|
||||
"{}{}",
|
||||
self.app_state.config.invite_link_prefix, invite_code
|
||||
),
|
||||
count: user.invite_count as u32,
|
||||
},
|
||||
)?;
|
||||
@@ -597,13 +608,42 @@ impl Server {
|
||||
response: Response<proto::CreateRoom>,
|
||||
) -> Result<()> {
|
||||
let user_id;
|
||||
let room_id;
|
||||
let room;
|
||||
{
|
||||
let mut store = self.store().await;
|
||||
user_id = store.user_id_for_connection(request.sender_id)?;
|
||||
room_id = store.create_room(request.sender_id)?;
|
||||
room = store.create_room(request.sender_id)?.clone();
|
||||
}
|
||||
response.send(proto::CreateRoomResponse { id: room_id })?;
|
||||
|
||||
let live_kit_connection_info =
|
||||
if let Some(live_kit) = self.app_state.live_kit_client.as_ref() {
|
||||
if let Some(_) = live_kit
|
||||
.create_room(room.live_kit_room.clone())
|
||||
.await
|
||||
.trace_err()
|
||||
{
|
||||
if let Some(token) = live_kit
|
||||
.room_token(&room.live_kit_room, &request.sender_id.to_string())
|
||||
.trace_err()
|
||||
{
|
||||
Some(proto::LiveKitConnectionInfo {
|
||||
server_url: live_kit.url().into(),
|
||||
token,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
response.send(proto::CreateRoomResponse {
|
||||
room: Some(room),
|
||||
live_kit_connection_info,
|
||||
})?;
|
||||
self.update_user_contacts(user_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -624,8 +664,27 @@ impl Server {
|
||||
.send(recipient_id, proto::CallCanceled {})
|
||||
.trace_err();
|
||||
}
|
||||
|
||||
let live_kit_connection_info =
|
||||
if let Some(live_kit) = self.app_state.live_kit_client.as_ref() {
|
||||
if let Some(token) = live_kit
|
||||
.room_token(&room.live_kit_room, &request.sender_id.to_string())
|
||||
.trace_err()
|
||||
{
|
||||
Some(proto::LiveKitConnectionInfo {
|
||||
server_url: live_kit.url().into(),
|
||||
token,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
response.send(proto::JoinRoomResponse {
|
||||
room: Some(room.clone()),
|
||||
live_kit_connection_info,
|
||||
})?;
|
||||
self.room_updated(room);
|
||||
}
|
||||
@@ -635,6 +694,7 @@ impl Server {
|
||||
|
||||
async fn leave_room(self: Arc<Server>, message: TypedEnvelope<proto::LeaveRoom>) -> Result<()> {
|
||||
let mut contacts_to_update = HashSet::default();
|
||||
let room_left;
|
||||
{
|
||||
let mut store = self.store().await;
|
||||
let user_id = store.user_id_for_connection(message.sender_id)?;
|
||||
@@ -673,9 +733,8 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(room) = left_room.room {
|
||||
self.room_updated(room);
|
||||
}
|
||||
self.room_updated(&left_room.room);
|
||||
room_left = self.room_left(&left_room.room, message.sender_id);
|
||||
|
||||
for connection_id in left_room.canceled_call_connection_ids {
|
||||
self.peer
|
||||
@@ -685,6 +744,7 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
room_left.await.trace_err();
|
||||
for user_id in contacts_to_update {
|
||||
self.update_user_contacts(user_id).await?;
|
||||
}
|
||||
@@ -833,6 +893,29 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
fn room_left(
|
||||
&self,
|
||||
room: &proto::Room,
|
||||
connection_id: ConnectionId,
|
||||
) -> impl Future<Output = Result<()>> {
|
||||
let client = self.app_state.live_kit_client.clone();
|
||||
let room_name = room.live_kit_room.clone();
|
||||
let participant_count = room.participants.len();
|
||||
async move {
|
||||
if let Some(client) = client {
|
||||
client
|
||||
.remove_participant(room_name.clone(), connection_id.to_string())
|
||||
.await?;
|
||||
|
||||
if participant_count == 0 {
|
||||
client.delete_room(room_name).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn share_project(
|
||||
self: Arc<Server>,
|
||||
request: TypedEnvelope<proto::ShareProject>,
|
||||
@@ -941,6 +1024,7 @@ impl Server {
|
||||
id: *id,
|
||||
root_name: worktree.root_name.clone(),
|
||||
visible: worktree.visible,
|
||||
abs_path: worktree.abs_path.as_os_str().as_bytes().to_vec(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -989,6 +1073,7 @@ impl Server {
|
||||
let message = proto::UpdateWorktree {
|
||||
project_id: project_id.to_proto(),
|
||||
worktree_id: *worktree_id,
|
||||
abs_path: worktree.abs_path.as_os_str().as_bytes().to_vec(),
|
||||
root_name: worktree.root_name.clone(),
|
||||
updated_entries: worktree.entries.values().cloned().collect(),
|
||||
removed_entries: Default::default(),
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use crate::db::{self, ChannelId, ProjectId, UserId};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet};
|
||||
use nanoid::nanoid;
|
||||
use rpc::{proto, ConnectionId};
|
||||
use serde::Serialize;
|
||||
use std::{mem, path::PathBuf, str, time::Duration};
|
||||
use std::{borrow::Cow, mem, path::PathBuf, str, time::Duration};
|
||||
use time::OffsetDateTime;
|
||||
use tracing::instrument;
|
||||
use util::post_inc;
|
||||
@@ -66,6 +67,7 @@ pub struct Collaborator {
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
pub struct Worktree {
|
||||
pub abs_path: PathBuf,
|
||||
pub root_name: String,
|
||||
pub visible: bool,
|
||||
#[serde(skip)]
|
||||
@@ -84,12 +86,12 @@ pub struct Channel {
|
||||
pub type ReplicaId = u16;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RemovedConnectionState {
|
||||
pub struct RemovedConnectionState<'a> {
|
||||
pub user_id: UserId,
|
||||
pub hosted_projects: Vec<Project>,
|
||||
pub guest_projects: Vec<LeftProject>,
|
||||
pub contact_ids: HashSet<UserId>,
|
||||
pub room_id: Option<RoomId>,
|
||||
pub room: Option<Cow<'a, proto::Room>>,
|
||||
pub canceled_call_connection_ids: Vec<ConnectionId>,
|
||||
}
|
||||
|
||||
@@ -102,7 +104,7 @@ pub struct LeftProject {
|
||||
}
|
||||
|
||||
pub struct LeftRoom<'a> {
|
||||
pub room: Option<&'a proto::Room>,
|
||||
pub room: Cow<'a, proto::Room>,
|
||||
pub unshared_projects: Vec<Project>,
|
||||
pub left_projects: Vec<LeftProject>,
|
||||
pub canceled_call_connection_ids: Vec<ConnectionId>,
|
||||
@@ -214,11 +216,16 @@ impl Store {
|
||||
let connected_user = self.connected_users.get(&user_id).unwrap();
|
||||
if let Some(active_call) = connected_user.active_call.as_ref() {
|
||||
let room_id = active_call.room_id;
|
||||
let left_room = self.leave_room(room_id, connection_id)?;
|
||||
result.hosted_projects = left_room.unshared_projects;
|
||||
result.guest_projects = left_room.left_projects;
|
||||
result.room_id = Some(room_id);
|
||||
result.canceled_call_connection_ids = left_room.canceled_call_connection_ids;
|
||||
if active_call.connection_id == Some(connection_id) {
|
||||
let left_room = self.leave_room(room_id, connection_id)?;
|
||||
result.hosted_projects = left_room.unshared_projects;
|
||||
result.guest_projects = left_room.left_projects;
|
||||
result.room = Some(Cow::Owned(left_room.room.into_owned()));
|
||||
result.canceled_call_connection_ids = left_room.canceled_call_connection_ids;
|
||||
} else if connected_user.connection_ids.len() == 1 {
|
||||
let (room, _) = self.decline_call(room_id, connection_id)?;
|
||||
result.room = Some(Cow::Owned(room.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
let connected_user = self.connected_users.get_mut(&user_id).unwrap();
|
||||
@@ -339,7 +346,7 @@ impl Store {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_room(&mut self, creator_connection_id: ConnectionId) -> Result<RoomId> {
|
||||
pub fn create_room(&mut self, creator_connection_id: ConnectionId) -> Result<&proto::Room> {
|
||||
let connection = self
|
||||
.connections
|
||||
.get_mut(&creator_connection_id)
|
||||
@@ -353,19 +360,23 @@ impl Store {
|
||||
"can't create a room with an active call"
|
||||
);
|
||||
|
||||
let mut room = proto::Room::default();
|
||||
room.participants.push(proto::Participant {
|
||||
user_id: connection.user_id.to_proto(),
|
||||
peer_id: creator_connection_id.0,
|
||||
projects: Default::default(),
|
||||
location: Some(proto::ParticipantLocation {
|
||||
variant: Some(proto::participant_location::Variant::External(
|
||||
proto::participant_location::External {},
|
||||
)),
|
||||
}),
|
||||
});
|
||||
|
||||
let room_id = post_inc(&mut self.next_room_id);
|
||||
let room = proto::Room {
|
||||
id: room_id,
|
||||
participants: vec![proto::Participant {
|
||||
user_id: connection.user_id.to_proto(),
|
||||
peer_id: creator_connection_id.0,
|
||||
projects: Default::default(),
|
||||
location: Some(proto::ParticipantLocation {
|
||||
variant: Some(proto::participant_location::Variant::External(
|
||||
proto::participant_location::External {},
|
||||
)),
|
||||
}),
|
||||
}],
|
||||
pending_participant_user_ids: Default::default(),
|
||||
live_kit_room: nanoid!(30),
|
||||
};
|
||||
|
||||
self.rooms.insert(room_id, room);
|
||||
connected_user.active_call = Some(Call {
|
||||
caller_user_id: connection.user_id,
|
||||
@@ -373,7 +384,7 @@ impl Store {
|
||||
connection_id: Some(creator_connection_id),
|
||||
initial_project_id: None,
|
||||
});
|
||||
Ok(room_id)
|
||||
Ok(self.rooms.get(&room_id).unwrap())
|
||||
}
|
||||
|
||||
pub fn join_room(
|
||||
@@ -490,12 +501,14 @@ impl Store {
|
||||
}
|
||||
});
|
||||
|
||||
if room.participants.is_empty() && room.pending_participant_user_ids.is_empty() {
|
||||
self.rooms.remove(&room_id);
|
||||
}
|
||||
let room = if room.participants.is_empty() {
|
||||
Cow::Owned(self.rooms.remove(&room_id).unwrap())
|
||||
} else {
|
||||
Cow::Borrowed(self.rooms.get(&room_id).unwrap())
|
||||
};
|
||||
|
||||
Ok(LeftRoom {
|
||||
room: self.rooms.get(&room_id),
|
||||
room,
|
||||
unshared_projects,
|
||||
left_projects,
|
||||
canceled_call_connection_ids,
|
||||
@@ -506,6 +519,10 @@ impl Store {
|
||||
self.rooms.get(&room_id)
|
||||
}
|
||||
|
||||
pub fn rooms(&self) -> &BTreeMap<RoomId, proto::Room> {
|
||||
&self.rooms
|
||||
}
|
||||
|
||||
pub fn call(
|
||||
&mut self,
|
||||
room_id: RoomId,
|
||||
|
||||
@@ -10,17 +10,21 @@ use gpui::{
|
||||
geometry::{rect::RectF, vector::vec2f, PathBuilder},
|
||||
json::{self, ToJson},
|
||||
Border, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
|
||||
Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::ops::Range;
|
||||
use theme::Theme;
|
||||
use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace};
|
||||
|
||||
actions!(collab, [ToggleCollaborationMenu, ShareProject]);
|
||||
actions!(
|
||||
collab,
|
||||
[ToggleCollaborationMenu, ToggleScreenSharing, ShareProject]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
|
||||
cx.add_action(CollabTitlebarItem::toggle_screen_sharing);
|
||||
cx.add_action(CollabTitlebarItem::share_project);
|
||||
}
|
||||
|
||||
@@ -48,10 +52,12 @@ impl View for CollabTitlebarItem {
|
||||
};
|
||||
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let project = workspace.read(cx).project().read(cx);
|
||||
|
||||
let mut container = Flex::row();
|
||||
container.add_children(self.render_toggle_screen_sharing_button(&theme, cx));
|
||||
|
||||
if workspace.read(cx).client().status().borrow().is_connected() {
|
||||
let project = workspace.read(cx).project().read(cx);
|
||||
if project.is_shared()
|
||||
|| project.is_remote()
|
||||
|| ActiveCall::global(cx).read(cx).room().is_none()
|
||||
@@ -114,19 +120,15 @@ impl CollabTitlebarItem {
|
||||
}
|
||||
|
||||
fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||
let workspace = self.workspace.upgrade(cx);
|
||||
let room = ActiveCall::global(cx).read(cx).room().cloned();
|
||||
if let Some((workspace, room)) = workspace.zip(room) {
|
||||
let workspace = workspace.read(cx);
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let project = if active {
|
||||
Some(workspace.project().clone())
|
||||
Some(workspace.read(cx).project().clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
room.update(cx, |room, cx| {
|
||||
room.set_location(project.as_ref(), cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| call.set_location(project.as_ref(), cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +171,19 @@ impl CollabTitlebarItem {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn toggle_screen_sharing(&mut self, _: &ToggleScreenSharing, cx: &mut ViewContext<Self>) {
|
||||
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||
let toggle_screen_sharing = room.update(cx, |room, cx| {
|
||||
if room.is_screen_sharing() {
|
||||
Task::ready(room.unshare_screen(cx))
|
||||
} else {
|
||||
room.share_screen(cx)
|
||||
}
|
||||
});
|
||||
toggle_screen_sharing.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_toggle_contacts_button(
|
||||
&self,
|
||||
theme: &Theme,
|
||||
@@ -232,11 +247,62 @@ impl CollabTitlebarItem {
|
||||
)
|
||||
.with_fit_mode(OverlayFitMode::SwitchAnchor)
|
||||
.with_anchor_corner(AnchorCorner::BottomLeft)
|
||||
.with_z_index(999)
|
||||
.boxed()
|
||||
}))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_toggle_screen_sharing_button(
|
||||
&self,
|
||||
theme: &Theme,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> Option<ElementBox> {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
let room = active_call.read(cx).room().cloned()?;
|
||||
let icon;
|
||||
let tooltip;
|
||||
|
||||
if room.read(cx).is_screen_sharing() {
|
||||
icon = "icons/disable_screen_sharing_12.svg";
|
||||
tooltip = "Stop Sharing Screen"
|
||||
} else {
|
||||
icon = "icons/enable_screen_sharing_12.svg";
|
||||
tooltip = "Share Screen";
|
||||
}
|
||||
|
||||
let titlebar = &theme.workspace.titlebar;
|
||||
Some(
|
||||
MouseEventHandler::<ToggleScreenSharing>::new(0, cx, |state, _| {
|
||||
let style = titlebar.call_control.style_for(state, false);
|
||||
Svg::new(icon)
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleScreenSharing);
|
||||
})
|
||||
.with_tooltip::<ToggleScreenSharing, _>(
|
||||
0,
|
||||
tooltip.into(),
|
||||
Some(Box::new(ToggleScreenSharing)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_share_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
enum Share {}
|
||||
|
||||
@@ -526,18 +592,6 @@ impl Element for AvatarRibbon {
|
||||
cx.scene.push_path(path.build(self.color, None));
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
_: &gpui::Event,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
_: &mut Self::PaintState,
|
||||
_: &mut gpui::EventContext,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
_: Range<usize>,
|
||||
|
||||
@@ -36,7 +36,7 @@ impl View for ContactFinder {
|
||||
ChildView::new(self.picker.clone(), cx).boxed()
|
||||
}
|
||||
|
||||
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.picker);
|
||||
}
|
||||
@@ -101,7 +101,7 @@ impl PickerDelegate for ContactFinder {
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
mouse_state: MouseState,
|
||||
mouse_state: &mut MouseState,
|
||||
selected: bool,
|
||||
cx: &gpui::AppContext,
|
||||
) -> ElementBox {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::sync::Arc;
|
||||
use std::{mem, sync::Arc};
|
||||
|
||||
use crate::contacts_popover;
|
||||
use call::ActiveCall;
|
||||
@@ -17,7 +17,7 @@ use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use theme::IconButton;
|
||||
use util::ResultExt;
|
||||
use workspace::JoinProject;
|
||||
use workspace::{JoinProject, OpenSharedScreen};
|
||||
|
||||
impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]);
|
||||
impl_internal_actions!(contact_list, [ToggleExpanded, Call, LeaveCall]);
|
||||
@@ -67,9 +67,16 @@ enum ContactEntry {
|
||||
host_user_id: u64,
|
||||
is_last: bool,
|
||||
},
|
||||
ParticipantScreen {
|
||||
peer_id: PeerId,
|
||||
is_last: bool,
|
||||
},
|
||||
IncomingRequest(Arc<User>),
|
||||
OutgoingRequest(Arc<User>),
|
||||
Contact(Arc<Contact>),
|
||||
Contact {
|
||||
contact: Arc<Contact>,
|
||||
calling: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl PartialEq for ContactEntry {
|
||||
@@ -97,6 +104,16 @@ impl PartialEq for ContactEntry {
|
||||
return project_id_1 == project_id_2;
|
||||
}
|
||||
}
|
||||
ContactEntry::ParticipantScreen {
|
||||
peer_id: peer_id_1, ..
|
||||
} => {
|
||||
if let ContactEntry::ParticipantScreen {
|
||||
peer_id: peer_id_2, ..
|
||||
} = other
|
||||
{
|
||||
return peer_id_1 == peer_id_2;
|
||||
}
|
||||
}
|
||||
ContactEntry::IncomingRequest(user_1) => {
|
||||
if let ContactEntry::IncomingRequest(user_2) = other {
|
||||
return user_1.id == user_2.id;
|
||||
@@ -107,8 +124,13 @@ impl PartialEq for ContactEntry {
|
||||
return user_1.id == user_2.id;
|
||||
}
|
||||
}
|
||||
ContactEntry::Contact(contact_1) => {
|
||||
if let ContactEntry::Contact(contact_2) = other {
|
||||
ContactEntry::Contact {
|
||||
contact: contact_1, ..
|
||||
} => {
|
||||
if let ContactEntry::Contact {
|
||||
contact: contact_2, ..
|
||||
} = other
|
||||
{
|
||||
return contact_1.user.id == contact_2.user.id;
|
||||
}
|
||||
}
|
||||
@@ -216,6 +238,15 @@ impl ContactList {
|
||||
&theme.contact_list,
|
||||
cx,
|
||||
),
|
||||
ContactEntry::ParticipantScreen { peer_id, is_last } => {
|
||||
Self::render_participant_screen(
|
||||
*peer_id,
|
||||
*is_last,
|
||||
is_selected,
|
||||
&theme.contact_list,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
ContactEntry::IncomingRequest(user) => Self::render_contact_request(
|
||||
user.clone(),
|
||||
this.user_store.clone(),
|
||||
@@ -232,8 +263,9 @@ impl ContactList {
|
||||
is_selected,
|
||||
cx,
|
||||
),
|
||||
ContactEntry::Contact(contact) => Self::render_contact(
|
||||
ContactEntry::Contact { contact, calling } => Self::render_contact(
|
||||
contact,
|
||||
*calling,
|
||||
&this.project,
|
||||
&theme.contact_list,
|
||||
is_selected,
|
||||
@@ -302,8 +334,14 @@ impl ContactList {
|
||||
} else if !self.entries.is_empty() {
|
||||
self.selection = Some(0);
|
||||
}
|
||||
cx.notify();
|
||||
self.list_state.reset(self.entries.len());
|
||||
if let Some(ix) = self.selection {
|
||||
self.list_state.scroll_to(ListOffset {
|
||||
item_ix: ix,
|
||||
offset_in_item: 0.,
|
||||
});
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
|
||||
@@ -314,8 +352,14 @@ impl ContactList {
|
||||
self.selection = None;
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
self.list_state.reset(self.entries.len());
|
||||
if let Some(ix) = self.selection {
|
||||
self.list_state.scroll_to(ListOffset {
|
||||
item_ix: ix,
|
||||
offset_in_item: 0.,
|
||||
});
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||
@@ -326,8 +370,8 @@ impl ContactList {
|
||||
let section = *section;
|
||||
self.toggle_expanded(&ToggleExpanded(section), cx);
|
||||
}
|
||||
ContactEntry::Contact(contact) => {
|
||||
if contact.online && !contact.busy {
|
||||
ContactEntry::Contact { contact, calling } => {
|
||||
if contact.online && !contact.busy && !calling {
|
||||
self.call(
|
||||
&Call {
|
||||
recipient_user_id: contact.user.id,
|
||||
@@ -347,6 +391,9 @@ impl ContactList {
|
||||
follow_user_id: *host_user_id,
|
||||
});
|
||||
}
|
||||
ContactEntry::ParticipantScreen { peer_id, .. } => {
|
||||
cx.dispatch_action(OpenSharedScreen { peer_id: *peer_id });
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -369,7 +416,7 @@ impl ContactList {
|
||||
let executor = cx.background().clone();
|
||||
|
||||
let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
|
||||
self.entries.clear();
|
||||
let old_entries = mem::take(&mut self.entries);
|
||||
|
||||
if let Some(room) = ActiveCall::global(cx).read(cx).room() {
|
||||
let room = room.read(cx);
|
||||
@@ -430,11 +477,10 @@ impl ContactList {
|
||||
executor.clone(),
|
||||
));
|
||||
for mat in matches {
|
||||
let participant = &room.remote_participants()[&PeerId(mat.candidate_id as u32)];
|
||||
let peer_id = PeerId(mat.candidate_id as u32);
|
||||
let participant = &room.remote_participants()[&peer_id];
|
||||
participant_entries.push(ContactEntry::CallParticipant {
|
||||
user: room.remote_participants()[&PeerId(mat.candidate_id as u32)]
|
||||
.user
|
||||
.clone(),
|
||||
user: participant.user.clone(),
|
||||
is_pending: false,
|
||||
});
|
||||
let mut projects = participant.projects.iter().peekable();
|
||||
@@ -443,7 +489,13 @@ impl ContactList {
|
||||
project_id: project.id,
|
||||
worktree_root_names: project.worktree_root_names.clone(),
|
||||
host_user_id: participant.user.id,
|
||||
is_last: projects.peek().is_none(),
|
||||
is_last: projects.peek().is_none() && participant.tracks.is_empty(),
|
||||
});
|
||||
}
|
||||
if !participant.tracks.is_empty() {
|
||||
participant_entries.push(ContactEntry::ParticipantScreen {
|
||||
peer_id,
|
||||
is_last: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -590,9 +642,13 @@ impl ContactList {
|
||||
if !matches.is_empty() {
|
||||
self.entries.push(ContactEntry::Header(section));
|
||||
if !self.collapsed_sections.contains(§ion) {
|
||||
let active_call = &ActiveCall::global(cx).read(cx);
|
||||
for mat in matches {
|
||||
let contact = &contacts[mat.candidate_id];
|
||||
self.entries.push(ContactEntry::Contact(contact.clone()));
|
||||
self.entries.push(ContactEntry::Contact {
|
||||
contact: contact.clone(),
|
||||
calling: active_call.pending_invites().contains(&contact.user.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -609,7 +665,47 @@ impl ContactList {
|
||||
}
|
||||
}
|
||||
|
||||
let old_scroll_top = self.list_state.logical_scroll_top();
|
||||
self.list_state.reset(self.entries.len());
|
||||
|
||||
// Attempt to maintain the same scroll position.
|
||||
if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
|
||||
let new_scroll_top = self
|
||||
.entries
|
||||
.iter()
|
||||
.position(|entry| entry == old_top_entry)
|
||||
.map(|item_ix| ListOffset {
|
||||
item_ix,
|
||||
offset_in_item: old_scroll_top.offset_in_item,
|
||||
})
|
||||
.or_else(|| {
|
||||
let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
|
||||
let item_ix = self
|
||||
.entries
|
||||
.iter()
|
||||
.position(|entry| entry == entry_after_old_top)?;
|
||||
Some(ListOffset {
|
||||
item_ix,
|
||||
offset_in_item: 0.,
|
||||
})
|
||||
})
|
||||
.or_else(|| {
|
||||
let entry_before_old_top =
|
||||
old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
|
||||
let item_ix = self
|
||||
.entries
|
||||
.iter()
|
||||
.position(|entry| entry == entry_before_old_top)?;
|
||||
Some(ListOffset {
|
||||
item_ix,
|
||||
offset_in_item: 0.,
|
||||
})
|
||||
});
|
||||
|
||||
self.list_state
|
||||
.scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -653,7 +749,11 @@ impl ContactList {
|
||||
.constrained()
|
||||
.with_height(theme.row_height)
|
||||
.contained()
|
||||
.with_style(*theme.contact_row.style_for(Default::default(), is_selected))
|
||||
.with_style(
|
||||
*theme
|
||||
.contact_row
|
||||
.style_for(&mut Default::default(), is_selected),
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
@@ -759,6 +859,102 @@ impl ContactList {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_participant_screen(
|
||||
peer_id: PeerId,
|
||||
is_last: bool,
|
||||
is_selected: bool,
|
||||
theme: &theme::ContactList,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
let font_cache = cx.font_cache();
|
||||
let host_avatar_height = theme
|
||||
.contact_avatar
|
||||
.width
|
||||
.or(theme.contact_avatar.height)
|
||||
.unwrap_or(0.);
|
||||
let row = &theme.project_row.default;
|
||||
let tree_branch = theme.tree_branch;
|
||||
let line_height = row.name.text.line_height(font_cache);
|
||||
let cap_height = row.name.text.cap_height(font_cache);
|
||||
let baseline_offset =
|
||||
row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
|
||||
|
||||
MouseEventHandler::<OpenSharedScreen>::new(peer_id.0 as usize, cx, |mouse_state, _| {
|
||||
let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
|
||||
let row = theme.project_row.style_for(mouse_state, is_selected);
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Stack::new()
|
||||
.with_child(
|
||||
Canvas::new(move |bounds, _, cx| {
|
||||
let start_x = bounds.min_x() + (bounds.width() / 2.)
|
||||
- (tree_branch.width / 2.);
|
||||
let end_x = bounds.max_x();
|
||||
let start_y = bounds.min_y();
|
||||
let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
|
||||
|
||||
cx.scene.push_quad(gpui::Quad {
|
||||
bounds: RectF::from_points(
|
||||
vec2f(start_x, start_y),
|
||||
vec2f(
|
||||
start_x + tree_branch.width,
|
||||
if is_last { end_y } else { bounds.max_y() },
|
||||
),
|
||||
),
|
||||
background: Some(tree_branch.color),
|
||||
border: gpui::Border::default(),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
cx.scene.push_quad(gpui::Quad {
|
||||
bounds: RectF::from_points(
|
||||
vec2f(start_x, end_y),
|
||||
vec2f(end_x, end_y + tree_branch.width),
|
||||
),
|
||||
background: Some(tree_branch.color),
|
||||
border: gpui::Border::default(),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.constrained()
|
||||
.with_width(host_avatar_height)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Svg::new("icons/disable_screen_sharing_12.svg")
|
||||
.with_color(row.icon.color)
|
||||
.constrained()
|
||||
.with_width(row.icon.width)
|
||||
.aligned()
|
||||
.left()
|
||||
.contained()
|
||||
.with_style(row.icon.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Label::new("Screen".into(), row.name.text.clone())
|
||||
.aligned()
|
||||
.left()
|
||||
.contained()
|
||||
.with_style(row.name.container)
|
||||
.flex(1., false)
|
||||
.boxed(),
|
||||
)
|
||||
.constrained()
|
||||
.with_height(theme.row_height)
|
||||
.contained()
|
||||
.with_style(row.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(OpenSharedScreen { peer_id });
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_header(
|
||||
section: Section,
|
||||
theme: &theme::ContactList,
|
||||
@@ -768,7 +964,9 @@ impl ContactList {
|
||||
) -> ElementBox {
|
||||
enum Header {}
|
||||
|
||||
let header_style = theme.header_row.style_for(Default::default(), is_selected);
|
||||
let header_style = theme
|
||||
.header_row
|
||||
.style_for(&mut Default::default(), is_selected);
|
||||
let text = match section {
|
||||
Section::ActiveCall => "Collaborators",
|
||||
Section::Requests => "Contact Requests",
|
||||
@@ -835,13 +1033,14 @@ impl ContactList {
|
||||
|
||||
fn render_contact(
|
||||
contact: &Contact,
|
||||
calling: bool,
|
||||
project: &ModelHandle<Project>,
|
||||
theme: &theme::ContactList,
|
||||
is_selected: bool,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
let online = contact.online;
|
||||
let busy = contact.busy;
|
||||
let busy = contact.busy || calling;
|
||||
let user_id = contact.user.id;
|
||||
let initial_project = project.clone();
|
||||
let mut element =
|
||||
@@ -853,7 +1052,7 @@ impl ContactList {
|
||||
Empty::new()
|
||||
.collapsed()
|
||||
.contained()
|
||||
.with_style(if contact.busy {
|
||||
.with_style(if busy {
|
||||
theme.contact_status_busy
|
||||
} else {
|
||||
theme.contact_status_free
|
||||
@@ -887,10 +1086,25 @@ impl ContactList {
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(if calling {
|
||||
Some(
|
||||
Label::new("Calling".to_string(), theme.calling_indicator.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.calling_indicator.container)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.constrained()
|
||||
.with_height(theme.row_height)
|
||||
.contained()
|
||||
.with_style(*theme.contact_row.style_for(Default::default(), is_selected))
|
||||
.with_style(
|
||||
*theme
|
||||
.contact_row
|
||||
.style_for(&mut Default::default(), is_selected),
|
||||
)
|
||||
.boxed()
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
@@ -1014,32 +1228,22 @@ impl ContactList {
|
||||
row.constrained()
|
||||
.with_height(theme.row_height)
|
||||
.contained()
|
||||
.with_style(*theme.contact_row.style_for(Default::default(), is_selected))
|
||||
.with_style(
|
||||
*theme
|
||||
.contact_row
|
||||
.style_for(&mut Default::default(), is_selected),
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn call(&mut self, action: &Call, cx: &mut ViewContext<Self>) {
|
||||
let recipient_user_id = action.recipient_user_id;
|
||||
let initial_project = action.initial_project.clone();
|
||||
let window_id = cx.window_id();
|
||||
|
||||
let active_call = ActiveCall::global(cx);
|
||||
cx.spawn_weak(|_, mut cx| async move {
|
||||
active_call
|
||||
.update(&mut cx, |active_call, cx| {
|
||||
active_call.invite(recipient_user_id, initial_project.clone(), cx)
|
||||
})
|
||||
.await?;
|
||||
if cx.update(|cx| cx.window_is_active(window_id)) {
|
||||
active_call
|
||||
.update(&mut cx, |call, cx| {
|
||||
call.set_location(initial_project.as_ref(), cx)
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| {
|
||||
call.invite(recipient_user_id, initial_project, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
|
||||
@@ -1107,13 +1311,13 @@ impl View for ContactList {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn on_focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if !self.filter_editor.is_focused(cx) {
|
||||
cx.focus(&self.filter_editor);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if !self.filter_editor.is_focused(cx) {
|
||||
cx.emit(Event::Dismissed);
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ impl View for ContactsPopover {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn on_focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.is_self_focused() {
|
||||
match &self.child {
|
||||
Child::ContactList(child) => cx.focus(child),
|
||||
|
||||
@@ -18,34 +18,37 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
|
||||
let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let mut notification_window = None;
|
||||
let mut notification_windows = Vec::new();
|
||||
while let Some(incoming_call) = incoming_call.next().await {
|
||||
if let Some(window_id) = notification_window.take() {
|
||||
for window_id in notification_windows.drain(..) {
|
||||
cx.remove_window(window_id);
|
||||
}
|
||||
|
||||
if let Some(incoming_call) = incoming_call {
|
||||
const PADDING: f32 = 16.;
|
||||
let screen_size = cx.platform().screen_size();
|
||||
|
||||
let window_size = cx.read(|cx| {
|
||||
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
|
||||
vec2f(theme.window_width, theme.window_height)
|
||||
});
|
||||
let (window_id, _) = cx.add_window(
|
||||
WindowOptions {
|
||||
bounds: WindowBounds::Fixed(RectF::new(
|
||||
vec2f(screen_size.x() - window_size.x() - PADDING, PADDING),
|
||||
window_size,
|
||||
)),
|
||||
titlebar: None,
|
||||
center: false,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
},
|
||||
|_| IncomingCallNotification::new(incoming_call),
|
||||
);
|
||||
notification_window = Some(window_id);
|
||||
|
||||
for screen in cx.platform().screens() {
|
||||
let screen_size = screen.size();
|
||||
let (window_id, _) = cx.add_window(
|
||||
WindowOptions {
|
||||
bounds: WindowBounds::Fixed(RectF::new(
|
||||
vec2f(screen_size.x() - window_size.x() - PADDING, PADDING),
|
||||
window_size,
|
||||
)),
|
||||
titlebar: None,
|
||||
center: false,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
screen: Some(screen),
|
||||
},
|
||||
|_| IncomingCallNotification::new(incoming_call.clone()),
|
||||
);
|
||||
notification_windows.push(window_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -27,41 +27,52 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
worktree_root_names,
|
||||
} => {
|
||||
const PADDING: f32 = 16.;
|
||||
let screen_size = cx.platform().screen_size();
|
||||
|
||||
let theme = &cx.global::<Settings>().theme.project_shared_notification;
|
||||
let window_size = vec2f(theme.window_width, theme.window_height);
|
||||
let (window_id, _) = cx.add_window(
|
||||
WindowOptions {
|
||||
bounds: WindowBounds::Fixed(RectF::new(
|
||||
vec2f(screen_size.x() - window_size.x() - PADDING, PADDING),
|
||||
window_size,
|
||||
)),
|
||||
titlebar: None,
|
||||
center: false,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
},
|
||||
|_| {
|
||||
ProjectSharedNotification::new(
|
||||
owner.clone(),
|
||||
*project_id,
|
||||
worktree_root_names.clone(),
|
||||
)
|
||||
},
|
||||
);
|
||||
notification_windows.insert(*project_id, window_id);
|
||||
|
||||
for screen in cx.platform().screens() {
|
||||
let screen_size = screen.size();
|
||||
let (window_id, _) = cx.add_window(
|
||||
WindowOptions {
|
||||
bounds: WindowBounds::Fixed(RectF::new(
|
||||
vec2f(screen_size.x() - window_size.x() - PADDING, PADDING),
|
||||
window_size,
|
||||
)),
|
||||
titlebar: None,
|
||||
center: false,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
screen: Some(screen),
|
||||
},
|
||||
|_| {
|
||||
ProjectSharedNotification::new(
|
||||
owner.clone(),
|
||||
*project_id,
|
||||
worktree_root_names.clone(),
|
||||
)
|
||||
},
|
||||
);
|
||||
notification_windows
|
||||
.entry(*project_id)
|
||||
.or_insert(Vec::new())
|
||||
.push(window_id);
|
||||
}
|
||||
}
|
||||
room::Event::RemoteProjectUnshared { project_id } => {
|
||||
if let Some(window_id) = notification_windows.remove(&project_id) {
|
||||
cx.remove_window(window_id);
|
||||
if let Some(window_ids) = notification_windows.remove(&project_id) {
|
||||
for window_id in window_ids {
|
||||
cx.remove_window(window_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
room::Event::Left => {
|
||||
for (_, window_id) in notification_windows.drain() {
|
||||
cx.remove_window(window_id);
|
||||
for (_, window_ids) in notification_windows.drain() {
|
||||
for window_id in window_ids {
|
||||
cx.remove_window(window_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ impl View for CommandPalette {
|
||||
ChildView::new(self.picker.clone(), cx).boxed()
|
||||
}
|
||||
|
||||
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.picker);
|
||||
}
|
||||
@@ -224,7 +224,7 @@ impl PickerDelegate for CommandPalette {
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
mouse_state: MouseState,
|
||||
mouse_state: &mut MouseState,
|
||||
selected: bool,
|
||||
cx: &gpui::AppContext,
|
||||
) -> gpui::ElementBox {
|
||||
|
||||
@@ -107,7 +107,7 @@ impl View for ContextMenu {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn on_focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
self.reset(cx);
|
||||
}
|
||||
}
|
||||
@@ -258,9 +258,10 @@ impl ContextMenu {
|
||||
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
|
||||
match item {
|
||||
ContextMenuItem::Item { label, .. } => {
|
||||
let style = style
|
||||
.item
|
||||
.style_for(Default::default(), Some(ix) == self.selected_index);
|
||||
let style = style.item.style_for(
|
||||
&mut Default::default(),
|
||||
Some(ix) == self.selected_index,
|
||||
);
|
||||
|
||||
Label::new(label.to_string(), style.label.clone())
|
||||
.contained()
|
||||
@@ -283,9 +284,10 @@ impl ContextMenu {
|
||||
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
|
||||
match item {
|
||||
ContextMenuItem::Item { action, .. } => {
|
||||
let style = style
|
||||
.item
|
||||
.style_for(Default::default(), Some(ix) == self.selected_index);
|
||||
let style = style.item.style_for(
|
||||
&mut Default::default(),
|
||||
Some(ix) == self.selected_index,
|
||||
);
|
||||
KeystrokeLabel::new(
|
||||
action.boxed_clone(),
|
||||
style.keystroke.container,
|
||||
@@ -313,13 +315,16 @@ impl ContextMenu {
|
||||
fn render_menu(&self, cx: &mut RenderContext<Self>) -> impl Element {
|
||||
enum Menu {}
|
||||
enum MenuItem {}
|
||||
|
||||
let style = cx.global::<Settings>().theme.context_menu.clone();
|
||||
|
||||
MouseEventHandler::<Menu>::new(0, cx, |_, cx| {
|
||||
Flex::column()
|
||||
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
|
||||
match item {
|
||||
ContextMenuItem::Item { label, action } => {
|
||||
let action = action.boxed_clone();
|
||||
|
||||
MouseEventHandler::<MenuItem>::new(ix, cx, |state, _| {
|
||||
let style =
|
||||
style.item.style_for(state, Some(ix) == self.selected_index);
|
||||
@@ -348,6 +353,7 @@ impl ContextMenu {
|
||||
cx.dispatch_action(Clicked);
|
||||
cx.dispatch_any_action(action.boxed_clone());
|
||||
})
|
||||
.on_drag(MouseButton::Left, |_, _| {})
|
||||
.boxed()
|
||||
}
|
||||
ContextMenuItem::Separator => Empty::new()
|
||||
|
||||
@@ -14,8 +14,13 @@ test-support = []
|
||||
collections = { path = "../collections" }
|
||||
anyhow = "1.0.57"
|
||||
async-trait = "0.1"
|
||||
lazy_static = "1.4.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
parking_lot = "0.11.1"
|
||||
rocksdb = "0.18"
|
||||
rusqlite = { version = "0.28.0", features = ["bundled", "serde_json"] }
|
||||
rusqlite_migration = "1.0.0"
|
||||
serde = { workspace = true }
|
||||
serde_rusqlite = "0.31.0"
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
|
||||
@@ -1,161 +1,119 @@
|
||||
use anyhow::Result;
|
||||
use std::path::Path;
|
||||
mod kvp;
|
||||
mod migrations;
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct Db(DbStore);
|
||||
use anyhow::Result;
|
||||
use log::error;
|
||||
use parking_lot::Mutex;
|
||||
use rusqlite::Connection;
|
||||
|
||||
enum DbStore {
|
||||
use migrations::MIGRATIONS;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Db {
|
||||
Real(Arc<RealDb>),
|
||||
Null,
|
||||
Real(rocksdb::DB),
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
Fake {
|
||||
data: parking_lot::Mutex<collections::HashMap<Vec<u8>, Vec<u8>>>,
|
||||
},
|
||||
pub struct RealDb {
|
||||
connection: Mutex<Connection>,
|
||||
path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Db {
|
||||
/// Open or create a database at the given file path.
|
||||
pub fn open(path: &Path) -> Result<Arc<Self>> {
|
||||
let db = rocksdb::DB::open_default(path)?;
|
||||
Ok(Arc::new(Self(DbStore::Real(db))))
|
||||
/// Open or create a database at the given directory path.
|
||||
pub fn open(db_dir: &Path) -> 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("0"));
|
||||
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 null database that stores no data, for use as a fallback
|
||||
/// when there is an error opening the real database.
|
||||
pub fn null() -> Arc<Self> {
|
||||
Arc::new(Self(DbStore::Null))
|
||||
}
|
||||
|
||||
/// Open a fake database for testing.
|
||||
/// Open a in memory database for testing and as a fallback.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn open_fake() -> Arc<Self> {
|
||||
Arc::new(Self(DbStore::Fake {
|
||||
data: Default::default(),
|
||||
}))
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read<K, I>(&self, keys: I) -> Result<Vec<Option<Vec<u8>>>>
|
||||
where
|
||||
K: AsRef<[u8]>,
|
||||
I: IntoIterator<Item = K>,
|
||||
{
|
||||
match &self.0 {
|
||||
DbStore::Real(db) => db
|
||||
.multi_get(keys)
|
||||
.into_iter()
|
||||
.map(|e| e.map_err(Into::into))
|
||||
.collect(),
|
||||
fn initialize(mut conn: Connection) -> Result<Mutex<Connection>> {
|
||||
MIGRATIONS.to_latest(&mut conn)?;
|
||||
|
||||
DbStore::Null => Ok(keys.into_iter().map(|_| None).collect()),
|
||||
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)?;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
DbStore::Fake { data: db } => {
|
||||
let db = db.lock();
|
||||
Ok(keys
|
||||
.into_iter()
|
||||
.map(|key| db.get(key.as_ref()).cloned())
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
Ok(Mutex::new(conn))
|
||||
}
|
||||
|
||||
pub fn delete<K, I>(&self, keys: I) -> Result<()>
|
||||
where
|
||||
K: AsRef<[u8]>,
|
||||
I: IntoIterator<Item = K>,
|
||||
{
|
||||
match &self.0 {
|
||||
DbStore::Real(db) => {
|
||||
let mut batch = rocksdb::WriteBatch::default();
|
||||
for key in keys {
|
||||
batch.delete(key);
|
||||
}
|
||||
db.write(batch)?;
|
||||
}
|
||||
|
||||
DbStore::Null => {}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
DbStore::Fake { data: db } => {
|
||||
let mut db = db.lock();
|
||||
for key in keys {
|
||||
db.remove(key.as_ref());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
pub fn persisting(&self) -> bool {
|
||||
self.real().and_then(|db| db.path.as_ref()).is_some()
|
||||
}
|
||||
|
||||
pub fn write<K, V, I>(&self, entries: I) -> Result<()>
|
||||
where
|
||||
K: AsRef<[u8]>,
|
||||
V: AsRef<[u8]>,
|
||||
I: IntoIterator<Item = (K, V)>,
|
||||
{
|
||||
match &self.0 {
|
||||
DbStore::Real(db) => {
|
||||
let mut batch = rocksdb::WriteBatch::default();
|
||||
for (key, value) in entries {
|
||||
batch.put(key, value);
|
||||
}
|
||||
db.write(batch)?;
|
||||
}
|
||||
|
||||
DbStore::Null => {}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
DbStore::Fake { data: db } => {
|
||||
let mut db = db.lock();
|
||||
for (key, value) in entries {
|
||||
db.insert(key.as_ref().into(), value.as_ref().into());
|
||||
}
|
||||
}
|
||||
pub fn real(&self) -> Option<&RealDb> {
|
||||
match self {
|
||||
Db::Real(db) => Some(&db),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Db {
|
||||
fn drop(&mut self) {
|
||||
match self {
|
||||
Db::Real(real_db) => {
|
||||
let lock = real_db.connection.lock();
|
||||
|
||||
let _ = lock.pragma_update(None, "analysis_limit", "500");
|
||||
let _ = lock.pragma_update(None, "optimize", "");
|
||||
}
|
||||
Db::Null => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempdir::TempDir;
|
||||
use crate::migrations::MIGRATIONS;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_db() {
|
||||
let dir = TempDir::new("db-test").unwrap();
|
||||
let fake_db = Db::open_fake();
|
||||
let real_db = Db::open(&dir.path().join("test.db")).unwrap();
|
||||
|
||||
for db in [&real_db, &fake_db] {
|
||||
assert_eq!(
|
||||
db.read(["key-1", "key-2", "key-3"]).unwrap(),
|
||||
&[None, None, None]
|
||||
);
|
||||
|
||||
db.write([("key-1", "one"), ("key-3", "three")]).unwrap();
|
||||
assert_eq!(
|
||||
db.read(["key-1", "key-2", "key-3"]).unwrap(),
|
||||
&[
|
||||
Some("one".as_bytes().to_vec()),
|
||||
None,
|
||||
Some("three".as_bytes().to_vec())
|
||||
]
|
||||
);
|
||||
|
||||
db.delete(["key-3", "key-4"]).unwrap();
|
||||
assert_eq!(
|
||||
db.read(["key-1", "key-2", "key-3"]).unwrap(),
|
||||
&[Some("one".as_bytes().to_vec()), None, None,]
|
||||
);
|
||||
}
|
||||
|
||||
drop(real_db);
|
||||
|
||||
let real_db = Db::open(&dir.path().join("test.db")).unwrap();
|
||||
assert_eq!(
|
||||
real_db.read(["key-1", "key-2", "key-3"]).unwrap(),
|
||||
&[Some("one".as_bytes().to_vec()), None, None,]
|
||||
);
|
||||
#[test]
|
||||
fn test_migrations() {
|
||||
assert!(MIGRATIONS.validate().is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
311
crates/db/src/items.rs
Normal file
311
crates/db/src/items.rs
Normal file
@@ -0,0 +1,311 @@
|
||||
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(())
|
||||
}
|
||||
}
|
||||
82
crates/db/src/kvp.rs
Normal file
82
crates/db/src/kvp.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use anyhow::Result;
|
||||
use rusqlite::OptionalExtension;
|
||||
|
||||
use super::Db;
|
||||
|
||||
pub(crate) const KVP_M_1_UP: &str = "
|
||||
CREATE TABLE 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))
|
||||
}
|
||||
|
||||
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(()))
|
||||
}
|
||||
|
||||
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(()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_kvp() -> Result<()> {
|
||||
let db = Db::open_in_memory();
|
||||
|
||||
assert_eq!(db.read_kvp("key-1")?, None);
|
||||
|
||||
db.write_kvp("key-1", "one")?;
|
||||
assert_eq!(db.read_kvp("key-1")?, Some("one".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", "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(())
|
||||
}
|
||||
}
|
||||
15
crates/db/src/migrations.rs
Normal file
15
crates/db/src/migrations.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
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),
|
||||
]);
|
||||
}
|
||||
@@ -99,7 +99,7 @@ impl View for ProjectDiagnosticsEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if !self.path_states.is_empty() {
|
||||
cx.focus(&self.editor);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use collections::HashSet;
|
||||
use gpui::{
|
||||
elements::{MouseEventHandler, Overlay},
|
||||
geometry::vector::Vector2F,
|
||||
scene::DragRegionEvent,
|
||||
scene::MouseDrag,
|
||||
CursorStyle, Element, ElementBox, EventContext, MouseButton, MutableAppContext, RenderContext,
|
||||
View, WeakViewHandle,
|
||||
};
|
||||
@@ -70,7 +70,7 @@ impl<V: View> DragAndDrop<V> {
|
||||
}
|
||||
|
||||
pub fn dragging<T: Any>(
|
||||
event: DragRegionEvent,
|
||||
event: MouseDrag,
|
||||
payload: Rc<T>,
|
||||
cx: &mut EventContext,
|
||||
render: Rc<impl 'static + Fn(&T, &mut RenderContext<V>) -> ElementBox>,
|
||||
@@ -125,7 +125,7 @@ impl<V: View> DragAndDrop<V> {
|
||||
cx.defer(|cx| {
|
||||
cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
|
||||
});
|
||||
cx.propogate_event();
|
||||
cx.propagate_event();
|
||||
})
|
||||
.on_up_out(MouseButton::Left, |_, cx| {
|
||||
cx.defer(|cx| {
|
||||
|
||||
@@ -20,6 +20,7 @@ test-support = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
drag_and_drop = { path = "../drag_and_drop" }
|
||||
text = { path = "../text" }
|
||||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections" }
|
||||
|
||||
111
crates/editor/src/blink_manager.rs
Normal file
111
crates/editor/src/blink_manager.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{Entity, ModelContext};
|
||||
use settings::Settings;
|
||||
use smol::Timer;
|
||||
|
||||
pub struct BlinkManager {
|
||||
blink_interval: Duration,
|
||||
|
||||
blink_epoch: usize,
|
||||
blinking_paused: bool,
|
||||
visible: bool,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
impl BlinkManager {
|
||||
pub fn new(blink_interval: Duration, cx: &mut ModelContext<Self>) -> Self {
|
||||
let weak_handle = cx.weak_handle();
|
||||
cx.observe_global::<Settings, _>(move |_, cx| {
|
||||
if let Some(this) = weak_handle.upgrade(cx) {
|
||||
// Make sure we blink the cursors if the setting is re-enabled
|
||||
this.update(cx, |this, cx| this.blink_cursors(this.blink_epoch, cx));
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
blink_interval,
|
||||
|
||||
blink_epoch: 0,
|
||||
blinking_paused: false,
|
||||
visible: true,
|
||||
enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn next_blink_epoch(&mut self) -> usize {
|
||||
self.blink_epoch += 1;
|
||||
self.blink_epoch
|
||||
}
|
||||
|
||||
pub fn pause_blinking(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if !self.visible {
|
||||
self.visible = true;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
let epoch = self.next_blink_epoch();
|
||||
let interval = self.blink_interval;
|
||||
cx.spawn(|this, mut cx| {
|
||||
let this = this.downgrade();
|
||||
async move {
|
||||
Timer::after(interval).await;
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
|
||||
if epoch == self.blink_epoch {
|
||||
self.blinking_paused = false;
|
||||
self.blink_cursors(epoch, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn blink_cursors(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
|
||||
if cx.global::<Settings>().cursor_blink {
|
||||
if epoch == self.blink_epoch && self.enabled && !self.blinking_paused {
|
||||
self.visible = !self.visible;
|
||||
cx.notify();
|
||||
dbg!(cx.handle());
|
||||
|
||||
let epoch = self.next_blink_epoch();
|
||||
let interval = self.blink_interval;
|
||||
cx.spawn(|this, mut cx| {
|
||||
let this = this.downgrade();
|
||||
async move {
|
||||
Timer::after(interval).await;
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
} else if !self.visible {
|
||||
self.visible = true;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enable(&mut self, cx: &mut ModelContext<Self>) {
|
||||
self.enabled = true;
|
||||
self.blink_cursors(self.blink_epoch, cx);
|
||||
}
|
||||
|
||||
pub fn disable(&mut self, _cx: &mut ModelContext<Self>) {
|
||||
self.enabled = false;
|
||||
}
|
||||
|
||||
pub fn visible(&self) -> bool {
|
||||
self.visible
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for BlinkManager {
|
||||
type Event = ();
|
||||
}
|
||||
@@ -157,6 +157,7 @@ pub struct BlockChunks<'a> {
|
||||
max_output_row: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BlockBufferRows<'a> {
|
||||
transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>,
|
||||
input_buffer_rows: wrap_map::WrapBufferRows<'a>,
|
||||
|
||||
@@ -987,6 +987,7 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for usize {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FoldBufferRows<'a> {
|
||||
cursor: Cursor<'a, Transform, (FoldPoint, Point)>,
|
||||
input_buffer_rows: MultiBufferRows<'a>,
|
||||
|
||||
@@ -62,6 +62,7 @@ pub struct WrapChunks<'a> {
|
||||
transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WrapBufferRows<'a> {
|
||||
input_buffer_rows: fold_map::FoldBufferRows<'a>,
|
||||
input_buffer_row: Option<u32>,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod blink_manager;
|
||||
pub mod display_map;
|
||||
mod element;
|
||||
mod highlight_matching_bracket;
|
||||
@@ -16,6 +17,7 @@ pub mod test;
|
||||
|
||||
use aho_corasick::AhoCorasick;
|
||||
use anyhow::Result;
|
||||
use blink_manager::BlinkManager;
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
||||
pub use display_map::DisplayPoint;
|
||||
@@ -33,20 +35,22 @@ use gpui::{
|
||||
impl_actions, impl_internal_actions,
|
||||
platform::CursorStyle,
|
||||
serde_json::json,
|
||||
text_layout, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox,
|
||||
Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, Task, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
text_layout, AnyViewHandle, AppContext, AsyncAppContext, Axis, ClipboardItem, Element,
|
||||
ElementBox, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription,
|
||||
Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
use hover_popover::{hide_hover, HoverState};
|
||||
pub use items::MAX_TAB_TITLE_LEN;
|
||||
pub use language::{char_kind, CharKind};
|
||||
use language::{
|
||||
AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic,
|
||||
DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point,
|
||||
Selection, SelectionGoal, TransactionId,
|
||||
AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
|
||||
Diagnostic, DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16,
|
||||
Point, Selection, SelectionGoal, TransactionId,
|
||||
};
|
||||
use link_go_to_definition::{
|
||||
hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState,
|
||||
};
|
||||
use link_go_to_definition::{hide_link_definition, LinkGoToDefinitionState};
|
||||
pub use multi_buffer::{
|
||||
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
|
||||
ToPoint,
|
||||
@@ -80,6 +84,7 @@ const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
|
||||
const MAX_LINE_LEN: usize = 1024;
|
||||
const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
|
||||
const MAX_SELECTION_HISTORY_LEN: usize = 1024;
|
||||
pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
|
||||
|
||||
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
|
||||
@@ -90,7 +95,10 @@ pub struct SelectNext {
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct Scroll(pub Vector2F);
|
||||
pub struct Scroll {
|
||||
pub scroll_position: Vector2F,
|
||||
pub axis: Option<Axis>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct Select(pub SelectPhase);
|
||||
@@ -108,6 +116,18 @@ pub struct SelectToBeginningOfLine {
|
||||
stop_at_soft_wraps: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize, PartialEq)]
|
||||
pub struct MovePageUp {
|
||||
#[serde(default)]
|
||||
center_cursor: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize, PartialEq)]
|
||||
pub struct MovePageDown {
|
||||
#[serde(default)]
|
||||
center_cursor: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
pub struct SelectToEndOfLine {
|
||||
#[serde(default)]
|
||||
@@ -161,8 +181,11 @@ actions!(
|
||||
Paste,
|
||||
Undo,
|
||||
Redo,
|
||||
CenterScreen,
|
||||
MoveUp,
|
||||
PageUp,
|
||||
MoveDown,
|
||||
PageDown,
|
||||
MoveLeft,
|
||||
MoveRight,
|
||||
MoveToPreviousWordStart,
|
||||
@@ -202,8 +225,6 @@ actions!(
|
||||
FindAllReferences,
|
||||
Rename,
|
||||
ConfirmRename,
|
||||
PageUp,
|
||||
PageDown,
|
||||
Fold,
|
||||
UnfoldLines,
|
||||
FoldSelectedRanges,
|
||||
@@ -222,6 +243,8 @@ impl_actions!(
|
||||
SelectToBeginningOfLine,
|
||||
SelectToEndOfLine,
|
||||
ToggleCodeActions,
|
||||
MovePageUp,
|
||||
MovePageDown,
|
||||
ConfirmCompletion,
|
||||
ConfirmCodeAction,
|
||||
]
|
||||
@@ -244,7 +267,7 @@ struct ScrollbarAutoHide(bool);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(Editor::new_file);
|
||||
cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx));
|
||||
cx.add_action(Editor::scroll);
|
||||
cx.add_action(Editor::select);
|
||||
cx.add_action(Editor::cancel);
|
||||
cx.add_action(Editor::newline);
|
||||
@@ -273,7 +296,12 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(Editor::undo);
|
||||
cx.add_action(Editor::redo);
|
||||
cx.add_action(Editor::move_up);
|
||||
cx.add_action(Editor::move_page_up);
|
||||
cx.add_action(Editor::page_up);
|
||||
cx.add_action(Editor::move_down);
|
||||
cx.add_action(Editor::move_page_down);
|
||||
cx.add_action(Editor::page_down);
|
||||
cx.add_action(Editor::center_screen);
|
||||
cx.add_action(Editor::move_left);
|
||||
cx.add_action(Editor::move_right);
|
||||
cx.add_action(Editor::move_to_previous_word_start);
|
||||
@@ -312,8 +340,6 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(Editor::go_to_prev_diagnostic);
|
||||
cx.add_action(Editor::go_to_definition);
|
||||
cx.add_action(Editor::go_to_type_definition);
|
||||
cx.add_action(Editor::page_up);
|
||||
cx.add_action(Editor::page_down);
|
||||
cx.add_action(Editor::fold);
|
||||
cx.add_action(Editor::unfold_lines);
|
||||
cx.add_action(Editor::fold_selected_ranges);
|
||||
@@ -407,6 +433,69 @@ pub type GetFieldEditorTheme = fn(&theme::Theme) -> theme::FieldEditor;
|
||||
|
||||
type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct OngoingScroll {
|
||||
last_timestamp: Instant,
|
||||
axis: Option<Axis>,
|
||||
}
|
||||
|
||||
impl OngoingScroll {
|
||||
fn initial() -> OngoingScroll {
|
||||
OngoingScroll {
|
||||
last_timestamp: Instant::now() - SCROLL_EVENT_SEPARATION,
|
||||
axis: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, axis: Option<Axis>) {
|
||||
self.last_timestamp = Instant::now();
|
||||
self.axis = axis;
|
||||
}
|
||||
|
||||
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_timestamp);
|
||||
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 Editor {
|
||||
handle: WeakViewHandle<Self>,
|
||||
buffer: ModelHandle<MultiBuffer>,
|
||||
@@ -421,6 +510,7 @@ pub struct Editor {
|
||||
select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>,
|
||||
ime_transaction: Option<TransactionId>,
|
||||
active_diagnostics: Option<ActiveDiagnosticGroup>,
|
||||
ongoing_scroll: OngoingScroll,
|
||||
scroll_position: Vector2F,
|
||||
scroll_top_anchor: Anchor,
|
||||
autoscroll_request: Option<(Autoscroll, bool)>,
|
||||
@@ -429,12 +519,10 @@ pub struct Editor {
|
||||
override_text_style: Option<Box<OverrideTextStyle>>,
|
||||
project: Option<ModelHandle<Project>>,
|
||||
focused: bool,
|
||||
show_local_cursors: bool,
|
||||
blink_manager: ModelHandle<BlinkManager>,
|
||||
show_local_selections: bool,
|
||||
show_scrollbars: bool,
|
||||
hide_scrollbar_task: Option<Task<()>>,
|
||||
blink_epoch: usize,
|
||||
blinking_paused: bool,
|
||||
mode: EditorMode,
|
||||
vertical_scroll_margin: f32,
|
||||
placeholder_text: Option<Arc<str>>,
|
||||
@@ -466,6 +554,7 @@ pub struct EditorSnapshot {
|
||||
pub display_snapshot: DisplaySnapshot,
|
||||
pub placeholder_text: Option<Arc<str>>,
|
||||
is_focused: bool,
|
||||
ongoing_scroll: OngoingScroll,
|
||||
scroll_position: Vector2F,
|
||||
scroll_top_anchor: Anchor,
|
||||
}
|
||||
@@ -606,6 +695,18 @@ enum ContextMenu {
|
||||
}
|
||||
|
||||
impl ContextMenu {
|
||||
fn select_first(&mut self, cx: &mut ViewContext<Editor>) -> bool {
|
||||
if self.visible() {
|
||||
match self {
|
||||
ContextMenu::Completions(menu) => menu.select_first(cx),
|
||||
ContextMenu::CodeActions(menu) => menu.select_first(cx),
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn select_prev(&mut self, cx: &mut ViewContext<Editor>) -> bool {
|
||||
if self.visible() {
|
||||
match self {
|
||||
@@ -630,6 +731,18 @@ impl ContextMenu {
|
||||
}
|
||||
}
|
||||
|
||||
fn select_last(&mut self, cx: &mut ViewContext<Editor>) -> bool {
|
||||
if self.visible() {
|
||||
match self {
|
||||
ContextMenu::Completions(menu) => menu.select_last(cx),
|
||||
ContextMenu::CodeActions(menu) => menu.select_last(cx),
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn visible(&self) -> bool {
|
||||
match self {
|
||||
ContextMenu::Completions(menu) => menu.visible(),
|
||||
@@ -662,6 +775,12 @@ struct CompletionsMenu {
|
||||
}
|
||||
|
||||
impl CompletionsMenu {
|
||||
fn select_first(&mut self, cx: &mut ViewContext<Editor>) {
|
||||
self.selected_item = 0;
|
||||
self.list.scroll_to(ScrollTarget::Show(self.selected_item));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
|
||||
if self.selected_item > 0 {
|
||||
self.selected_item -= 1;
|
||||
@@ -678,6 +797,12 @@ impl CompletionsMenu {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_last(&mut self, cx: &mut ViewContext<Editor>) {
|
||||
self.selected_item = self.matches.len() - 1;
|
||||
self.list.scroll_to(ScrollTarget::Show(self.selected_item));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn visible(&self) -> bool {
|
||||
!self.matches.is_empty()
|
||||
}
|
||||
@@ -705,7 +830,7 @@ impl CompletionsMenu {
|
||||
|state, _| {
|
||||
let item_style = if item_ix == selected_item {
|
||||
style.autocomplete.selected_item
|
||||
} else if state.hovered {
|
||||
} else if state.hovered() {
|
||||
style.autocomplete.hovered_item
|
||||
} else {
|
||||
style.autocomplete.item
|
||||
@@ -809,6 +934,11 @@ struct CodeActionsMenu {
|
||||
}
|
||||
|
||||
impl CodeActionsMenu {
|
||||
fn select_first(&mut self, cx: &mut ViewContext<Editor>) {
|
||||
self.selected_item = 0;
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
|
||||
if self.selected_item > 0 {
|
||||
self.selected_item -= 1;
|
||||
@@ -823,6 +953,11 @@ impl CodeActionsMenu {
|
||||
}
|
||||
}
|
||||
|
||||
fn select_last(&mut self, cx: &mut ViewContext<Editor>) {
|
||||
self.selected_item = self.actions.len() - 1;
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn visible(&self) -> bool {
|
||||
!self.actions.is_empty()
|
||||
}
|
||||
@@ -850,7 +985,7 @@ impl CodeActionsMenu {
|
||||
MouseEventHandler::<ActionTag>::new(item_ix, cx, |state, _| {
|
||||
let item_style = if item_ix == selected_item {
|
||||
style.autocomplete.selected_item
|
||||
} else if state.hovered {
|
||||
} else if state.hovered() {
|
||||
style.autocomplete.hovered_item
|
||||
} else {
|
||||
style.autocomplete.item
|
||||
@@ -1012,6 +1147,8 @@ impl Editor {
|
||||
|
||||
let selections = SelectionsCollection::new(display_map.clone(), buffer.clone());
|
||||
|
||||
let blink_manager = cx.add_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx));
|
||||
|
||||
let mut this = Self {
|
||||
handle: cx.weak_handle(),
|
||||
buffer: buffer.clone(),
|
||||
@@ -1029,16 +1166,15 @@ impl Editor {
|
||||
soft_wrap_mode_override: None,
|
||||
get_field_editor_theme,
|
||||
project,
|
||||
ongoing_scroll: OngoingScroll::initial(),
|
||||
scroll_position: Vector2F::zero(),
|
||||
scroll_top_anchor: Anchor::min(),
|
||||
autoscroll_request: None,
|
||||
focused: false,
|
||||
show_local_cursors: false,
|
||||
blink_manager: blink_manager.clone(),
|
||||
show_local_selections: true,
|
||||
show_scrollbars: true,
|
||||
hide_scrollbar_task: None,
|
||||
blink_epoch: 0,
|
||||
blinking_paused: false,
|
||||
mode,
|
||||
vertical_scroll_margin: 3.0,
|
||||
placeholder_text: None,
|
||||
@@ -1066,6 +1202,7 @@ impl Editor {
|
||||
cx.observe(&buffer, Self::on_buffer_changed),
|
||||
cx.subscribe(&buffer, Self::on_buffer_event),
|
||||
cx.observe(&display_map, Self::on_display_map_changed),
|
||||
cx.observe(&blink_manager, |_, _, cx| cx.notify()),
|
||||
],
|
||||
};
|
||||
this.end_selection(cx);
|
||||
@@ -1122,6 +1259,7 @@ impl Editor {
|
||||
EditorSnapshot {
|
||||
mode: self.mode,
|
||||
display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
|
||||
ongoing_scroll: self.ongoing_scroll,
|
||||
scroll_position: self.scroll_position,
|
||||
scroll_top_anchor: self.scroll_top_anchor.clone(),
|
||||
placeholder_text: self.placeholder_text.clone(),
|
||||
@@ -1414,6 +1552,7 @@ impl Editor {
|
||||
buffer.set_active_selections(
|
||||
&self.selections.disjoint_anchors(),
|
||||
self.selections.line_mode,
|
||||
self.cursor_shape,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1477,7 +1616,7 @@ impl Editor {
|
||||
refresh_matching_bracket_highlights(self, cx);
|
||||
}
|
||||
|
||||
self.pause_cursor_blinking(cx);
|
||||
self.blink_manager.update(cx, BlinkManager::pause_blinking);
|
||||
cx.emit(Event::SelectionsChanged { local });
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1524,6 +1663,11 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
|
||||
fn scroll(&mut self, action: &Scroll, cx: &mut ViewContext<Self>) {
|
||||
self.ongoing_scroll.update(action.axis);
|
||||
self.set_scroll_position(action.scroll_position, cx);
|
||||
}
|
||||
|
||||
fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext<Self>) {
|
||||
self.hide_context_menu(cx);
|
||||
|
||||
@@ -1936,11 +2080,14 @@ impl Editor {
|
||||
));
|
||||
continue;
|
||||
}
|
||||
} else if let Some(region) = autoclose_region {
|
||||
}
|
||||
|
||||
if let Some(region) = autoclose_region {
|
||||
// If the selection is followed by an auto-inserted closing bracket,
|
||||
// then don't insert anything else; just move the selection past the
|
||||
// closing bracket.
|
||||
let should_skip = selection.end == region.range.end.to_point(&snapshot);
|
||||
// then don't insert that closing bracket again; just move the selection
|
||||
// past the closing bracket.
|
||||
let should_skip = selection.end == region.range.end.to_point(&snapshot)
|
||||
&& text.as_ref() == region.pair.end.as_str();
|
||||
if should_skip {
|
||||
let anchor = snapshot.anchor_after(selection.end);
|
||||
new_selections.push((
|
||||
@@ -2023,7 +2170,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
drop(snapshot);
|
||||
this.change_selections(None, cx, |s| s.select(new_selections));
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(new_selections));
|
||||
this.trigger_completion_on_input(&text, cx);
|
||||
});
|
||||
}
|
||||
@@ -3848,6 +3995,23 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn center_screen(&mut self, _: &CenterScreen, cx: &mut ViewContext<Self>) {
|
||||
if self.take_rename(true, cx).is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(_) = self.context_menu.as_mut() {
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate_action();
|
||||
return;
|
||||
}
|
||||
|
||||
self.request_autoscroll(Autoscroll::Center, cx);
|
||||
}
|
||||
|
||||
pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
|
||||
if self.take_rename(true, cx).is_some() {
|
||||
return;
|
||||
@@ -3876,6 +4040,72 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_page_up(&mut self, action: &MovePageUp, cx: &mut ViewContext<Self>) {
|
||||
if self.take_rename(true, cx).is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(context_menu) = self.context_menu.as_mut() {
|
||||
if context_menu.select_first(cx) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate_action();
|
||||
return;
|
||||
}
|
||||
|
||||
let row_count = match self.visible_line_count {
|
||||
Some(row_count) => row_count as u32 - 1,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let autoscroll = if action.center_cursor {
|
||||
Autoscroll::Center
|
||||
} else {
|
||||
Autoscroll::Fit
|
||||
};
|
||||
|
||||
self.change_selections(Some(autoscroll), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
if !selection.is_empty() && !line_mode {
|
||||
selection.goal = SelectionGoal::None;
|
||||
}
|
||||
let (cursor, goal) =
|
||||
movement::up_by_rows(map, selection.end, row_count, selection.goal, false);
|
||||
selection.collapse_to(cursor, goal);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn page_up(&mut self, _: &PageUp, cx: &mut ViewContext<Self>) {
|
||||
if self.take_rename(true, cx).is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(context_menu) = self.context_menu.as_mut() {
|
||||
if context_menu.select_first(cx) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate_action();
|
||||
return;
|
||||
}
|
||||
|
||||
let lines = match self.visible_line_count {
|
||||
Some(lines) => lines,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let cur_position = self.scroll_position(cx);
|
||||
let new_pos = cur_position - vec2f(0., lines + 1.);
|
||||
self.set_scroll_position(new_pos, cx);
|
||||
}
|
||||
|
||||
pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_heads_with(|map, head, goal| movement::up(map, head, goal, false))
|
||||
@@ -3908,6 +4138,72 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn move_page_down(&mut self, action: &MovePageDown, cx: &mut ViewContext<Self>) {
|
||||
if self.take_rename(true, cx).is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(context_menu) = self.context_menu.as_mut() {
|
||||
if context_menu.select_last(cx) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate_action();
|
||||
return;
|
||||
}
|
||||
|
||||
let row_count = match self.visible_line_count {
|
||||
Some(row_count) => row_count as u32 - 1,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let autoscroll = if action.center_cursor {
|
||||
Autoscroll::Center
|
||||
} else {
|
||||
Autoscroll::Fit
|
||||
};
|
||||
|
||||
self.change_selections(Some(autoscroll), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
if !selection.is_empty() && !line_mode {
|
||||
selection.goal = SelectionGoal::None;
|
||||
}
|
||||
let (cursor, goal) =
|
||||
movement::down_by_rows(map, selection.end, row_count, selection.goal, false);
|
||||
selection.collapse_to(cursor, goal);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn page_down(&mut self, _: &PageDown, cx: &mut ViewContext<Self>) {
|
||||
if self.take_rename(true, cx).is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(context_menu) = self.context_menu.as_mut() {
|
||||
if context_menu.select_last(cx) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate_action();
|
||||
return;
|
||||
}
|
||||
|
||||
let lines = match self.visible_line_count {
|
||||
Some(lines) => lines,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let cur_position = self.scroll_position(cx);
|
||||
let new_pos = cur_position + vec2f(0., lines - 1.);
|
||||
self.set_scroll_position(new_pos, cx);
|
||||
}
|
||||
|
||||
pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_heads_with(|map, head, goal| movement::down(map, head, goal, false))
|
||||
@@ -5535,28 +5831,6 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn page_up(&mut self, _: &PageUp, cx: &mut ViewContext<Self>) {
|
||||
let lines = match self.visible_line_count {
|
||||
Some(lines) => lines,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let cur_position = self.scroll_position(cx);
|
||||
let new_pos = cur_position - vec2f(0., lines + 1.);
|
||||
self.set_scroll_position(new_pos, cx);
|
||||
}
|
||||
|
||||
pub fn page_down(&mut self, _: &PageDown, cx: &mut ViewContext<Self>) {
|
||||
let lines = match self.visible_line_count {
|
||||
Some(lines) => lines,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let cur_position = self.scroll_position(cx);
|
||||
let new_pos = cur_position + vec2f(0., lines - 1.);
|
||||
self.set_scroll_position(new_pos, cx);
|
||||
}
|
||||
|
||||
pub fn fold(&mut self, _: &Fold, cx: &mut ViewContext<Self>) {
|
||||
let mut fold_ranges = Vec::new();
|
||||
|
||||
@@ -5791,8 +6065,11 @@ impl Editor {
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<(fn(&Theme) -> Color, Vec<Range<Anchor>>)> {
|
||||
cx.notify();
|
||||
self.background_highlights.remove(&TypeId::of::<T>())
|
||||
let highlights = self.background_highlights.remove(&TypeId::of::<T>());
|
||||
if highlights.is_some() {
|
||||
cx.notify();
|
||||
}
|
||||
highlights
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
@@ -5906,65 +6183,17 @@ impl Editor {
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
|
||||
cx.notify();
|
||||
self.display_map
|
||||
.update(cx, |map, _| map.clear_text_highlights(TypeId::of::<T>()))
|
||||
}
|
||||
|
||||
fn next_blink_epoch(&mut self) -> usize {
|
||||
self.blink_epoch += 1;
|
||||
self.blink_epoch
|
||||
}
|
||||
|
||||
fn pause_cursor_blinking(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if !self.focused {
|
||||
return;
|
||||
}
|
||||
|
||||
self.show_local_cursors = true;
|
||||
cx.notify();
|
||||
|
||||
let epoch = self.next_blink_epoch();
|
||||
cx.spawn(|this, mut cx| {
|
||||
let this = this.downgrade();
|
||||
async move {
|
||||
Timer::after(CURSOR_BLINK_INTERVAL).await;
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
|
||||
if epoch == self.blink_epoch {
|
||||
self.blinking_paused = false;
|
||||
self.blink_cursors(epoch, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
|
||||
if epoch == self.blink_epoch && self.focused && !self.blinking_paused {
|
||||
self.show_local_cursors = !self.show_local_cursors;
|
||||
let highlights = self
|
||||
.display_map
|
||||
.update(cx, |map, _| map.clear_text_highlights(TypeId::of::<T>()));
|
||||
if highlights.is_some() {
|
||||
cx.notify();
|
||||
|
||||
let epoch = self.next_blink_epoch();
|
||||
cx.spawn(|this, mut cx| {
|
||||
let this = this.downgrade();
|
||||
async move {
|
||||
Timer::after(CURSOR_BLINK_INTERVAL).await;
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
highlights
|
||||
}
|
||||
|
||||
pub fn show_local_cursors(&self) -> bool {
|
||||
self.show_local_cursors && self.focused
|
||||
pub fn show_local_cursors(&self, cx: &AppContext) -> bool {
|
||||
self.blink_manager.read(cx).visible() && self.focused
|
||||
}
|
||||
|
||||
pub fn show_scrollbars(&self) -> bool {
|
||||
@@ -6267,9 +6496,7 @@ impl View for Editor {
|
||||
}
|
||||
|
||||
Stack::new()
|
||||
.with_child(
|
||||
EditorElement::new(self.handle.clone(), style.clone(), self.cursor_shape).boxed(),
|
||||
)
|
||||
.with_child(EditorElement::new(self.handle.clone(), style.clone()).boxed())
|
||||
.with_child(ChildView::new(&self.mouse_context_menu, cx).boxed())
|
||||
.boxed()
|
||||
}
|
||||
@@ -6278,20 +6505,23 @@ impl View for Editor {
|
||||
"Editor"
|
||||
}
|
||||
|
||||
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
let focused_event = EditorFocused(cx.handle());
|
||||
cx.emit_global(focused_event);
|
||||
if let Some(rename) = self.pending_rename.as_ref() {
|
||||
cx.focus(&rename.editor);
|
||||
} else {
|
||||
if !self.focused {
|
||||
self.blink_manager.update(cx, BlinkManager::enable);
|
||||
}
|
||||
self.focused = true;
|
||||
self.blink_cursors(self.blink_epoch, cx);
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction(cx);
|
||||
if self.leader_replica_id.is_none() {
|
||||
buffer.set_active_selections(
|
||||
&self.selections.disjoint_anchors(),
|
||||
self.selections.line_mode,
|
||||
self.cursor_shape,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -6299,10 +6529,11 @@ impl View for Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
let blurred_event = EditorBlurred(cx.handle());
|
||||
cx.emit_global(blurred_event);
|
||||
self.focused = false;
|
||||
self.blink_manager.update(cx, BlinkManager::disable);
|
||||
self.buffer
|
||||
.update(cx, |buffer, cx| buffer.remove_active_selections(cx));
|
||||
self.hide_context_menu(cx);
|
||||
@@ -6311,6 +6542,44 @@ impl View for Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn modifiers_changed(
|
||||
&mut self,
|
||||
event: &gpui::ModifiersChangedEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> bool {
|
||||
let pending_selection = self.has_pending_selection();
|
||||
|
||||
if let Some(point) = self.link_go_to_definition_state.last_mouse_location.clone() {
|
||||
if event.cmd && !pending_selection {
|
||||
let snapshot = self.snapshot(cx);
|
||||
let kind = if event.shift {
|
||||
LinkDefinitionKind::Type
|
||||
} else {
|
||||
LinkDefinitionKind::Symbol
|
||||
};
|
||||
|
||||
show_link_definition(kind, self, point, snapshot, cx);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
if self.link_go_to_definition_state.symbol_range.is_some()
|
||||
|| !self.link_go_to_definition_state.definitions.is_empty()
|
||||
{
|
||||
self.link_go_to_definition_state.symbol_range.take();
|
||||
self.link_go_to_definition_state.definitions.clear();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
self.link_go_to_definition_state.task = None;
|
||||
|
||||
self.clear_text_highlights::<LinkGoToDefinitionState>(cx);
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context {
|
||||
let mut context = Self::default_keymap_context();
|
||||
let mode = match self.mode {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::{cell::RefCell, rc::Rc, time::Instant};
|
||||
|
||||
use drag_and_drop::DragAndDrop;
|
||||
use futures::StreamExt;
|
||||
use indoc::indoc;
|
||||
use unindent::Unindent;
|
||||
@@ -472,6 +473,7 @@ fn test_clone(cx: &mut gpui::MutableAppContext) {
|
||||
#[gpui::test]
|
||||
fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
cx.set_global(DragAndDrop::<Workspace>::default());
|
||||
use workspace::Item;
|
||||
let (_, pane) = cx.add_window(Default::default(), |cx| Pane::new(None, cx));
|
||||
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
|
||||
@@ -1194,6 +1196,120 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut gpui::MutableAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
|
||||
let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
|
||||
cx.simulate_window_resize(cx.window_id, vec2f(100., 4. * line_height));
|
||||
|
||||
cx.set_state(
|
||||
&r#"
|
||||
ˇone
|
||||
two
|
||||
threeˇ
|
||||
four
|
||||
five
|
||||
six
|
||||
seven
|
||||
eight
|
||||
nine
|
||||
ten
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
|
||||
cx.assert_editor_state(
|
||||
&r#"
|
||||
one
|
||||
two
|
||||
three
|
||||
ˇfour
|
||||
five
|
||||
sixˇ
|
||||
seven
|
||||
eight
|
||||
nine
|
||||
ten
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
|
||||
cx.assert_editor_state(
|
||||
&r#"
|
||||
one
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
ˇseven
|
||||
eight
|
||||
nineˇ
|
||||
ten
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
|
||||
cx.assert_editor_state(
|
||||
&r#"
|
||||
one
|
||||
two
|
||||
three
|
||||
ˇfour
|
||||
five
|
||||
sixˇ
|
||||
seven
|
||||
eight
|
||||
nine
|
||||
ten
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
|
||||
cx.assert_editor_state(
|
||||
&r#"
|
||||
ˇone
|
||||
two
|
||||
threeˇ
|
||||
four
|
||||
five
|
||||
six
|
||||
seven
|
||||
eight
|
||||
nine
|
||||
ten
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
// Test select collapsing
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.move_page_down(&MovePageDown::default(), cx);
|
||||
editor.move_page_down(&MovePageDown::default(), cx);
|
||||
editor.move_page_down(&MovePageDown::default(), cx);
|
||||
});
|
||||
cx.assert_editor_state(
|
||||
&r#"
|
||||
one
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
seven
|
||||
eight
|
||||
nine
|
||||
ˇten
|
||||
ˇ"#
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
@@ -2907,6 +3023,12 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
|
||||
close: true,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "(".to_string(),
|
||||
end: ")".to_string(),
|
||||
close: true,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "/*".to_string(),
|
||||
end: " */".to_string(),
|
||||
@@ -2919,6 +3041,12 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
|
||||
close: false,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "\"".to_string(),
|
||||
end: "\"".to_string(),
|
||||
close: true,
|
||||
newline: false,
|
||||
},
|
||||
],
|
||||
autoclose_before: "})]".to_string(),
|
||||
..Default::default()
|
||||
@@ -2957,6 +3085,19 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
// insert a different closing bracket
|
||||
cx.update_editor(|view, cx| {
|
||||
view.handle_input(")", cx);
|
||||
});
|
||||
cx.assert_editor_state(
|
||||
&"
|
||||
🏀{{{)ˇ}}}
|
||||
ε{{{)ˇ}}}
|
||||
❤️{{{)ˇ}}}
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
// skip over the auto-closed brackets when typing a closing bracket
|
||||
cx.update_editor(|view, cx| {
|
||||
view.move_right(&MoveRight, cx);
|
||||
@@ -2966,9 +3107,9 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
cx.assert_editor_state(
|
||||
&"
|
||||
🏀{{{}}}}ˇ
|
||||
ε{{{}}}}ˇ
|
||||
❤️{{{}}}}ˇ
|
||||
🏀{{{)}}}}ˇ
|
||||
ε{{{)}}}}ˇ
|
||||
❤️{{{)}}}}ˇ
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
@@ -3026,6 +3167,13 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
|
||||
cx.set_state("«aˇ» b");
|
||||
cx.update_editor(|view, cx| view.handle_input("{", cx));
|
||||
cx.assert_editor_state("{«aˇ»} b");
|
||||
|
||||
// Autclose pair where the start and end characters are the same
|
||||
cx.set_state("aˇ");
|
||||
cx.update_editor(|view, cx| view.handle_input("\"", cx));
|
||||
cx.assert_editor_state("a\"ˇ\"");
|
||||
cx.update_editor(|view, cx| view.handle_input("\"", cx));
|
||||
cx.assert_editor_state("a\"\"ˇ");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
@@ -9,14 +9,14 @@ use crate::{
|
||||
HoverAt, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
|
||||
},
|
||||
link_go_to_definition::{
|
||||
CmdShiftChanged, GoToFetchedDefinition, GoToFetchedTypeDefinition, UpdateGoToDefinitionLink,
|
||||
GoToFetchedDefinition, GoToFetchedTypeDefinition, UpdateGoToDefinitionLink,
|
||||
},
|
||||
mouse_context_menu::DeployMouseContextMenu,
|
||||
EditorStyle,
|
||||
AnchorRangeExt, EditorStyle,
|
||||
};
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use git::diff::{DiffHunk, DiffHunkStatus};
|
||||
use git::diff::DiffHunkStatus;
|
||||
use gpui::{
|
||||
color::Color,
|
||||
elements::*,
|
||||
@@ -29,13 +29,12 @@ use gpui::{
|
||||
json::{self, ToJson},
|
||||
platform::CursorStyle,
|
||||
text_layout::{self, Line, RunStyle, TextLayoutCache},
|
||||
AppContext, Axis, Border, CursorRegion, Element, ElementBox, Event, EventContext,
|
||||
LayoutContext, ModifiersChangedEvent, MouseButton, MouseButtonEvent, MouseMovedEvent,
|
||||
MouseRegion, MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext,
|
||||
WeakViewHandle,
|
||||
AppContext, Axis, Border, CursorRegion, Element, ElementBox, EventContext, LayoutContext,
|
||||
Modifiers, MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion, MutableAppContext,
|
||||
PaintContext, Quad, SceneBuilder, SizeConstraint, ViewContext, WeakViewHandle,
|
||||
};
|
||||
use json::json;
|
||||
use language::{Bias, DiagnosticSeverity, OffsetUtf16, Selection};
|
||||
use language::{Bias, CursorShape, DiagnosticSeverity, OffsetUtf16, Point, Selection};
|
||||
use project::ProjectPath;
|
||||
use settings::{GitGutter, Settings};
|
||||
use smallvec::SmallVec;
|
||||
@@ -46,10 +45,17 @@ use std::{
|
||||
ops::{DerefMut, Range},
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::DiffStyle;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DiffHunkLayout {
|
||||
visual_range: Range<u32>,
|
||||
status: DiffHunkStatus,
|
||||
is_folded: bool,
|
||||
}
|
||||
|
||||
struct SelectionLayout {
|
||||
head: DisplayPoint,
|
||||
cursor_shape: CursorShape,
|
||||
range: Range<DisplayPoint>,
|
||||
}
|
||||
|
||||
@@ -57,6 +63,7 @@ impl SelectionLayout {
|
||||
fn new<T: ToPoint + ToDisplayPoint + Clone>(
|
||||
selection: Selection<T>,
|
||||
line_mode: bool,
|
||||
cursor_shape: CursorShape,
|
||||
map: &DisplaySnapshot,
|
||||
) -> Self {
|
||||
if line_mode {
|
||||
@@ -64,6 +71,7 @@ impl SelectionLayout {
|
||||
let point_range = map.expand_to_line(selection.range());
|
||||
Self {
|
||||
head: selection.head().to_display_point(map),
|
||||
cursor_shape,
|
||||
range: point_range.start.to_display_point(map)
|
||||
..point_range.end.to_display_point(map),
|
||||
}
|
||||
@@ -71,6 +79,7 @@ impl SelectionLayout {
|
||||
let selection = selection.map(|p| p.to_display_point(map));
|
||||
Self {
|
||||
head: selection.head(),
|
||||
cursor_shape,
|
||||
range: selection.range(),
|
||||
}
|
||||
}
|
||||
@@ -81,19 +90,13 @@ impl SelectionLayout {
|
||||
pub struct EditorElement {
|
||||
view: WeakViewHandle<Editor>,
|
||||
style: Arc<EditorStyle>,
|
||||
cursor_shape: CursorShape,
|
||||
}
|
||||
|
||||
impl EditorElement {
|
||||
pub fn new(
|
||||
view: WeakViewHandle<Editor>,
|
||||
style: EditorStyle,
|
||||
cursor_shape: CursorShape,
|
||||
) -> Self {
|
||||
pub fn new(view: WeakViewHandle<Editor>, style: EditorStyle) -> Self {
|
||||
Self {
|
||||
view,
|
||||
style: Arc::new(style),
|
||||
cursor_shape,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +137,7 @@ impl EditorElement {
|
||||
gutter_bounds,
|
||||
cx,
|
||||
) {
|
||||
cx.propogate_event();
|
||||
cx.propagate_event();
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -147,7 +150,7 @@ impl EditorElement {
|
||||
text_bounds,
|
||||
cx,
|
||||
) {
|
||||
cx.propogate_event();
|
||||
cx.propagate_event();
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -164,7 +167,7 @@ impl EditorElement {
|
||||
text_bounds,
|
||||
cx,
|
||||
) {
|
||||
cx.propogate_event()
|
||||
cx.propagate_event()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -179,7 +182,7 @@ impl EditorElement {
|
||||
text_bounds,
|
||||
cx,
|
||||
) {
|
||||
cx.propogate_event()
|
||||
cx.propagate_event()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -187,7 +190,7 @@ impl EditorElement {
|
||||
let position_map = position_map.clone();
|
||||
move |e, cx| {
|
||||
if !Self::mouse_moved(e.platform_event, &position_map, text_bounds, cx) {
|
||||
cx.propogate_event()
|
||||
cx.propagate_event()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -196,7 +199,7 @@ impl EditorElement {
|
||||
move |e, cx| {
|
||||
if !Self::scroll(e.position, e.delta, e.precise, &position_map, bounds, cx)
|
||||
{
|
||||
cx.propogate_event()
|
||||
cx.propagate_event()
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -206,10 +209,14 @@ impl EditorElement {
|
||||
fn mouse_down(
|
||||
MouseButtonEvent {
|
||||
position,
|
||||
ctrl,
|
||||
alt,
|
||||
shift,
|
||||
cmd,
|
||||
modifiers:
|
||||
Modifiers {
|
||||
shift,
|
||||
ctrl,
|
||||
alt,
|
||||
cmd,
|
||||
..
|
||||
},
|
||||
mut click_count,
|
||||
..
|
||||
}: MouseButtonEvent,
|
||||
@@ -300,8 +307,7 @@ impl EditorElement {
|
||||
fn mouse_dragged(
|
||||
view: WeakViewHandle<Editor>,
|
||||
MouseMovedEvent {
|
||||
cmd,
|
||||
shift,
|
||||
modifiers: Modifiers { cmd, shift, .. },
|
||||
position,
|
||||
..
|
||||
}: MouseMovedEvent,
|
||||
@@ -376,8 +382,7 @@ impl EditorElement {
|
||||
|
||||
fn mouse_moved(
|
||||
MouseMovedEvent {
|
||||
cmd,
|
||||
shift,
|
||||
modifiers: Modifiers { shift, cmd, .. },
|
||||
position,
|
||||
..
|
||||
}: MouseMovedEvent,
|
||||
@@ -408,14 +413,6 @@ impl EditorElement {
|
||||
true
|
||||
}
|
||||
|
||||
fn modifiers_changed(&self, event: ModifiersChangedEvent, cx: &mut EventContext) -> bool {
|
||||
cx.dispatch_action(CmdShiftChanged {
|
||||
cmd_down: event.cmd,
|
||||
shift_down: event.shift,
|
||||
});
|
||||
false
|
||||
}
|
||||
|
||||
fn scroll(
|
||||
position: Vector2F,
|
||||
mut delta: Vector2F,
|
||||
@@ -428,18 +425,27 @@ impl EditorElement {
|
||||
return false;
|
||||
}
|
||||
|
||||
let line_height = position_map.line_height;
|
||||
let max_glyph_width = position_map.em_width;
|
||||
if !precise {
|
||||
delta *= vec2f(max_glyph_width, position_map.line_height);
|
||||
}
|
||||
|
||||
let axis = if precise {
|
||||
//Trackpad
|
||||
position_map.snapshot.ongoing_scroll.filter(&mut delta)
|
||||
} else {
|
||||
//Not trackpad
|
||||
delta *= vec2f(max_glyph_width, line_height);
|
||||
None //Resets ongoing scroll
|
||||
};
|
||||
|
||||
let scroll_position = position_map.snapshot.scroll_position();
|
||||
let x = (scroll_position.x() * max_glyph_width - delta.x()) / max_glyph_width;
|
||||
let y =
|
||||
(scroll_position.y() * position_map.line_height - delta.y()) / position_map.line_height;
|
||||
let y = (scroll_position.y() * line_height - delta.y()) / line_height;
|
||||
let scroll_position = vec2f(x, y).clamp(Vector2F::zero(), position_map.scroll_max);
|
||||
|
||||
cx.dispatch_action(Scroll(scroll_position));
|
||||
cx.dispatch_action(Scroll {
|
||||
scroll_position,
|
||||
axis,
|
||||
});
|
||||
|
||||
true
|
||||
}
|
||||
@@ -525,85 +531,11 @@ impl EditorElement {
|
||||
layout: &mut LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) {
|
||||
struct GutterLayout {
|
||||
line_height: f32,
|
||||
// scroll_position: Vector2F,
|
||||
scroll_top: f32,
|
||||
bounds: RectF,
|
||||
}
|
||||
|
||||
struct DiffLayout<'a> {
|
||||
buffer_row: u32,
|
||||
last_diff: Option<&'a DiffHunk<u32>>,
|
||||
}
|
||||
|
||||
fn diff_quad(
|
||||
hunk: &DiffHunk<u32>,
|
||||
gutter_layout: &GutterLayout,
|
||||
diff_style: &DiffStyle,
|
||||
) -> Quad {
|
||||
let color = match hunk.status() {
|
||||
DiffHunkStatus::Added => diff_style.inserted,
|
||||
DiffHunkStatus::Modified => diff_style.modified,
|
||||
|
||||
//TODO: This rendering is entirely a horrible hack
|
||||
DiffHunkStatus::Removed => {
|
||||
let row = hunk.buffer_range.start;
|
||||
|
||||
let offset = gutter_layout.line_height / 2.;
|
||||
let start_y =
|
||||
row as f32 * gutter_layout.line_height + offset - gutter_layout.scroll_top;
|
||||
let end_y = start_y + gutter_layout.line_height;
|
||||
|
||||
let width = diff_style.removed_width_em * gutter_layout.line_height;
|
||||
let highlight_origin = gutter_layout.bounds.origin() + vec2f(-width, start_y);
|
||||
let highlight_size = vec2f(width * 2., end_y - start_y);
|
||||
let highlight_bounds = RectF::new(highlight_origin, highlight_size);
|
||||
|
||||
return Quad {
|
||||
bounds: highlight_bounds,
|
||||
background: Some(diff_style.deleted),
|
||||
border: Border::new(0., Color::transparent_black()),
|
||||
corner_radius: 1. * gutter_layout.line_height,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let start_row = hunk.buffer_range.start;
|
||||
let end_row = hunk.buffer_range.end;
|
||||
|
||||
let start_y = start_row as f32 * gutter_layout.line_height - gutter_layout.scroll_top;
|
||||
let end_y = end_row as f32 * gutter_layout.line_height - gutter_layout.scroll_top;
|
||||
|
||||
let width = diff_style.width_em * gutter_layout.line_height;
|
||||
let highlight_origin = gutter_layout.bounds.origin() + vec2f(-width, start_y);
|
||||
let highlight_size = vec2f(width * 2., end_y - start_y);
|
||||
let highlight_bounds = RectF::new(highlight_origin, highlight_size);
|
||||
|
||||
Quad {
|
||||
bounds: highlight_bounds,
|
||||
background: Some(color),
|
||||
border: Border::new(0., Color::transparent_black()),
|
||||
corner_radius: diff_style.corner_radius * gutter_layout.line_height,
|
||||
}
|
||||
}
|
||||
let line_height = layout.position_map.line_height;
|
||||
|
||||
let scroll_position = layout.position_map.snapshot.scroll_position();
|
||||
let gutter_layout = {
|
||||
let line_height = layout.position_map.line_height;
|
||||
GutterLayout {
|
||||
scroll_top: scroll_position.y() * line_height,
|
||||
line_height,
|
||||
bounds,
|
||||
}
|
||||
};
|
||||
let scroll_top = scroll_position.y() * line_height;
|
||||
|
||||
let mut diff_layout = DiffLayout {
|
||||
buffer_row: scroll_position.y() as u32,
|
||||
last_diff: None,
|
||||
};
|
||||
|
||||
let diff_style = &cx.global::<Settings>().theme.editor.diff.clone();
|
||||
let show_gutter = matches!(
|
||||
&cx.global::<Settings>()
|
||||
.git_overrides
|
||||
@@ -612,58 +544,107 @@ impl EditorElement {
|
||||
GitGutter::TrackedFiles
|
||||
);
|
||||
|
||||
// line is `None` when there's a line wrap
|
||||
if show_gutter {
|
||||
Self::paint_diff_hunks(bounds, layout, cx);
|
||||
}
|
||||
|
||||
for (ix, line) in layout.line_number_layouts.iter().enumerate() {
|
||||
if let Some(line) = line {
|
||||
let line_origin = bounds.origin()
|
||||
+ vec2f(
|
||||
bounds.width() - line.width() - layout.gutter_padding,
|
||||
ix as f32 * gutter_layout.line_height
|
||||
- (gutter_layout.scroll_top % gutter_layout.line_height),
|
||||
ix as f32 * line_height - (scroll_top % line_height),
|
||||
);
|
||||
|
||||
line.paint(line_origin, visible_bounds, gutter_layout.line_height, cx);
|
||||
|
||||
if show_gutter {
|
||||
//This line starts a buffer line, so let's do the diff calculation
|
||||
let new_hunk = get_hunk(diff_layout.buffer_row, &layout.diff_hunks);
|
||||
|
||||
let (is_ending, is_starting) = match (diff_layout.last_diff, new_hunk) {
|
||||
(Some(old_hunk), Some(new_hunk)) if new_hunk == old_hunk => (false, false),
|
||||
(a, b) => (a.is_some(), b.is_some()),
|
||||
};
|
||||
|
||||
if is_ending {
|
||||
let last_hunk = diff_layout.last_diff.take().unwrap();
|
||||
cx.scene
|
||||
.push_quad(diff_quad(last_hunk, &gutter_layout, diff_style));
|
||||
}
|
||||
|
||||
if is_starting {
|
||||
let new_hunk = new_hunk.unwrap();
|
||||
diff_layout.last_diff = Some(new_hunk);
|
||||
};
|
||||
|
||||
diff_layout.buffer_row += 1;
|
||||
}
|
||||
line.paint(line_origin, visible_bounds, line_height, cx);
|
||||
}
|
||||
}
|
||||
|
||||
// If we ran out with a diff hunk still being prepped, paint it now
|
||||
if let Some(last_hunk) = diff_layout.last_diff {
|
||||
cx.scene
|
||||
.push_quad(diff_quad(last_hunk, &gutter_layout, diff_style))
|
||||
}
|
||||
|
||||
if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
|
||||
let mut x = bounds.width() - layout.gutter_padding;
|
||||
let mut y = *row as f32 * gutter_layout.line_height - gutter_layout.scroll_top;
|
||||
let mut y = *row as f32 * line_height - scroll_top;
|
||||
x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
|
||||
y += (gutter_layout.line_height - indicator.size().y()) / 2.;
|
||||
y += (line_height - indicator.size().y()) / 2.;
|
||||
indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_diff_hunks(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
|
||||
let diff_style = &cx.global::<Settings>().theme.editor.diff.clone();
|
||||
let line_height = layout.position_map.line_height;
|
||||
|
||||
let scroll_position = layout.position_map.snapshot.scroll_position();
|
||||
let scroll_top = scroll_position.y() * line_height;
|
||||
|
||||
for hunk in &layout.hunk_layouts {
|
||||
let color = match (hunk.status, hunk.is_folded) {
|
||||
(DiffHunkStatus::Added, false) => diff_style.inserted,
|
||||
(DiffHunkStatus::Modified, false) => diff_style.modified,
|
||||
|
||||
//TODO: This rendering is entirely a horrible hack
|
||||
(DiffHunkStatus::Removed, false) => {
|
||||
let row = hunk.visual_range.start;
|
||||
|
||||
let offset = line_height / 2.;
|
||||
let start_y = row as f32 * line_height - offset - scroll_top;
|
||||
let end_y = start_y + line_height;
|
||||
|
||||
let width = diff_style.removed_width_em * line_height;
|
||||
let highlight_origin = bounds.origin() + vec2f(-width, start_y);
|
||||
let highlight_size = vec2f(width * 2., end_y - start_y);
|
||||
let highlight_bounds = RectF::new(highlight_origin, highlight_size);
|
||||
|
||||
cx.scene.push_quad(Quad {
|
||||
bounds: highlight_bounds,
|
||||
background: Some(diff_style.deleted),
|
||||
border: Border::new(0., Color::transparent_black()),
|
||||
corner_radius: 1. * line_height,
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
(_, true) => {
|
||||
let row = hunk.visual_range.start;
|
||||
let start_y = row as f32 * line_height - scroll_top;
|
||||
let end_y = start_y + line_height;
|
||||
|
||||
let width = diff_style.removed_width_em * line_height;
|
||||
let highlight_origin = bounds.origin() + vec2f(-width, start_y);
|
||||
let highlight_size = vec2f(width * 2., end_y - start_y);
|
||||
let highlight_bounds = RectF::new(highlight_origin, highlight_size);
|
||||
|
||||
cx.scene.push_quad(Quad {
|
||||
bounds: highlight_bounds,
|
||||
background: Some(diff_style.modified),
|
||||
border: Border::new(0., Color::transparent_black()),
|
||||
corner_radius: 1. * line_height,
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let start_row = hunk.visual_range.start;
|
||||
let end_row = hunk.visual_range.end;
|
||||
|
||||
let start_y = start_row as f32 * line_height - scroll_top;
|
||||
let end_y = end_row as f32 * line_height - scroll_top;
|
||||
|
||||
let width = diff_style.width_em * line_height;
|
||||
let highlight_origin = bounds.origin() + vec2f(-width, start_y);
|
||||
let highlight_size = vec2f(width * 2., end_y - start_y);
|
||||
let highlight_bounds = RectF::new(highlight_origin, highlight_size);
|
||||
|
||||
cx.scene.push_quad(Quad {
|
||||
bounds: highlight_bounds,
|
||||
background: Some(color),
|
||||
border: Border::new(0., Color::transparent_black()),
|
||||
corner_radius: diff_style.corner_radius * line_height,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_text(
|
||||
&mut self,
|
||||
bounds: RectF,
|
||||
@@ -675,10 +656,8 @@ impl EditorElement {
|
||||
let style = &self.style;
|
||||
let local_replica_id = view.replica_id(cx);
|
||||
let scroll_position = layout.position_map.snapshot.scroll_position();
|
||||
let start_row = scroll_position.y() as u32;
|
||||
let start_row = layout.visible_display_row_range.start;
|
||||
let scroll_top = scroll_position.y() * layout.position_map.line_height;
|
||||
let end_row =
|
||||
((scroll_top + bounds.height()) / layout.position_map.line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen
|
||||
let max_glyph_width = layout.position_map.em_width;
|
||||
let scroll_left = scroll_position.x() * max_glyph_width;
|
||||
let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.);
|
||||
@@ -697,8 +676,6 @@ impl EditorElement {
|
||||
for (range, color) in &layout.highlighted_ranges {
|
||||
self.paint_highlighted_range(
|
||||
range.clone(),
|
||||
start_row,
|
||||
end_row,
|
||||
*color,
|
||||
0.,
|
||||
0.15 * layout.position_map.line_height,
|
||||
@@ -719,8 +696,6 @@ impl EditorElement {
|
||||
for selection in selections {
|
||||
self.paint_highlighted_range(
|
||||
selection.range.clone(),
|
||||
start_row,
|
||||
end_row,
|
||||
selection_style.selection,
|
||||
corner_radius,
|
||||
corner_radius * 2.,
|
||||
@@ -732,9 +707,12 @@ impl EditorElement {
|
||||
cx,
|
||||
);
|
||||
|
||||
if view.show_local_cursors() || *replica_id != local_replica_id {
|
||||
if view.show_local_cursors(cx) || *replica_id != local_replica_id {
|
||||
let cursor_position = selection.head;
|
||||
if (start_row..end_row).contains(&cursor_position.row()) {
|
||||
if layout
|
||||
.visible_display_row_range
|
||||
.contains(&cursor_position.row())
|
||||
{
|
||||
let cursor_row_layout = &layout.position_map.line_layouts
|
||||
[(cursor_position.row() - start_row) as usize];
|
||||
let cursor_column = cursor_position.column() as usize;
|
||||
@@ -745,7 +723,7 @@ impl EditorElement {
|
||||
if block_width == 0.0 {
|
||||
block_width = layout.position_map.em_width;
|
||||
}
|
||||
let block_text = if let CursorShape::Block = self.cursor_shape {
|
||||
let block_text = if let CursorShape::Block = selection.cursor_shape {
|
||||
layout
|
||||
.position_map
|
||||
.snapshot
|
||||
@@ -781,7 +759,7 @@ impl EditorElement {
|
||||
block_width,
|
||||
origin: vec2f(x, y),
|
||||
line_height: layout.position_map.line_height,
|
||||
shape: self.cursor_shape,
|
||||
shape: selection.cursor_shape,
|
||||
block_text,
|
||||
});
|
||||
}
|
||||
@@ -813,7 +791,7 @@ impl EditorElement {
|
||||
cx.scene.pop_layer();
|
||||
|
||||
if let Some((position, context_menu)) = layout.context_menu.as_mut() {
|
||||
cx.scene.push_stacking_context(None);
|
||||
cx.scene.push_stacking_context(None, None);
|
||||
let cursor_row_layout =
|
||||
&layout.position_map.line_layouts[(position.row() - start_row) as usize];
|
||||
let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left;
|
||||
@@ -842,7 +820,7 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() {
|
||||
cx.scene.push_stacking_context(None);
|
||||
cx.scene.push_stacking_context(None, None);
|
||||
|
||||
// This is safe because we check on layout whether the required row is available
|
||||
let hovered_row_layout =
|
||||
@@ -1025,8 +1003,6 @@ impl EditorElement {
|
||||
fn paint_highlighted_range(
|
||||
&self,
|
||||
range: Range<DisplayPoint>,
|
||||
start_row: u32,
|
||||
end_row: u32,
|
||||
color: Color,
|
||||
corner_radius: f32,
|
||||
line_end_overshoot: f32,
|
||||
@@ -1037,6 +1013,8 @@ impl EditorElement {
|
||||
bounds: RectF,
|
||||
cx: &mut PaintContext,
|
||||
) {
|
||||
let start_row = layout.visible_display_row_range.start;
|
||||
let end_row = layout.visible_display_row_range.end;
|
||||
if range.start != range.end {
|
||||
let row_range = if range.end.column() == 0 {
|
||||
cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
|
||||
@@ -1125,6 +1103,75 @@ impl EditorElement {
|
||||
.width()
|
||||
}
|
||||
|
||||
//Folds contained in a hunk are ignored apart from shrinking visual size
|
||||
//If a fold contains any hunks then that fold line is marked as modified
|
||||
fn layout_git_gutters(
|
||||
&self,
|
||||
rows: Range<u32>,
|
||||
snapshot: &EditorSnapshot,
|
||||
) -> Vec<DiffHunkLayout> {
|
||||
let buffer_snapshot = &snapshot.buffer_snapshot;
|
||||
let visual_start = DisplayPoint::new(rows.start, 0).to_point(snapshot).row;
|
||||
let visual_end = DisplayPoint::new(rows.end, 0).to_point(snapshot).row;
|
||||
let hunks = buffer_snapshot.git_diff_hunks_in_range(visual_start..visual_end);
|
||||
|
||||
let mut layouts = Vec::<DiffHunkLayout>::new();
|
||||
|
||||
for hunk in hunks {
|
||||
let hunk_start_point = Point::new(hunk.buffer_range.start, 0);
|
||||
let hunk_end_point = Point::new(hunk.buffer_range.end, 0);
|
||||
let hunk_start_point_sub = Point::new(hunk.buffer_range.start.saturating_sub(1), 0);
|
||||
let hunk_end_point_sub = Point::new(
|
||||
hunk.buffer_range
|
||||
.end
|
||||
.saturating_sub(1)
|
||||
.max(hunk.buffer_range.start),
|
||||
0,
|
||||
);
|
||||
|
||||
let is_removal = hunk.status() == DiffHunkStatus::Removed;
|
||||
|
||||
let folds_start = Point::new(hunk.buffer_range.start.saturating_sub(1), 0);
|
||||
let folds_end = Point::new(hunk.buffer_range.end + 1, 0);
|
||||
let folds_range = folds_start..folds_end;
|
||||
|
||||
let containing_fold = snapshot.folds_in_range(folds_range).find(|fold_range| {
|
||||
let fold_point_range = fold_range.to_point(buffer_snapshot);
|
||||
let fold_point_range = fold_point_range.start..=fold_point_range.end;
|
||||
|
||||
let folded_start = fold_point_range.contains(&hunk_start_point);
|
||||
let folded_end = fold_point_range.contains(&hunk_end_point_sub);
|
||||
let folded_start_sub = fold_point_range.contains(&hunk_start_point_sub);
|
||||
|
||||
(folded_start && folded_end) || (is_removal && folded_start_sub)
|
||||
});
|
||||
|
||||
let visual_range = if let Some(fold) = containing_fold {
|
||||
let row = fold.start.to_display_point(snapshot).row();
|
||||
row..row
|
||||
} else {
|
||||
let start = hunk_start_point.to_display_point(snapshot).row();
|
||||
let end = hunk_end_point.to_display_point(snapshot).row();
|
||||
start..end
|
||||
};
|
||||
|
||||
let has_existing_layout = match layouts.last() {
|
||||
Some(e) => visual_range == e.visual_range && e.status == hunk.status(),
|
||||
None => false,
|
||||
};
|
||||
|
||||
if !has_existing_layout {
|
||||
layouts.push(DiffHunkLayout {
|
||||
visual_range,
|
||||
status: hunk.status(),
|
||||
is_folded: containing_fold.is_some(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
|
||||
fn layout_line_numbers(
|
||||
&self,
|
||||
rows: Range<u32>,
|
||||
@@ -1279,6 +1326,7 @@ impl EditorElement {
|
||||
line_height: f32,
|
||||
style: &EditorStyle,
|
||||
line_layouts: &[text_layout::Line],
|
||||
include_root: bool,
|
||||
cx: &mut LayoutContext,
|
||||
) -> (f32, Vec<BlockLayout>) {
|
||||
let editor = if let Some(editor) = self.view.upgrade(cx) {
|
||||
@@ -1382,10 +1430,11 @@ impl EditorElement {
|
||||
let font_size =
|
||||
(style.text_scale_factor * self.style.text.font_size).round();
|
||||
|
||||
let path = buffer.resolve_file_path(cx, include_root);
|
||||
let mut filename = None;
|
||||
let mut parent_path = None;
|
||||
if let Some(file) = buffer.file() {
|
||||
let path = file.path();
|
||||
// Can't use .and_then() because `.file_name()` and `.parent()` return references :(
|
||||
if let Some(path) = path {
|
||||
filename = path.file_name().map(|f| f.to_string_lossy().to_string());
|
||||
parent_path =
|
||||
path.parent().map(|p| p.to_string_lossy().to_string() + "/");
|
||||
@@ -1477,27 +1526,6 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the hunk that contains buffer_line, starting from start_idx
|
||||
/// Returns none if there is none found, and
|
||||
fn get_hunk(buffer_line: u32, hunks: &[DiffHunk<u32>]) -> Option<&DiffHunk<u32>> {
|
||||
for i in 0..hunks.len() {
|
||||
// Safety: Index out of bounds is handled by the check above
|
||||
let hunk = hunks.get(i).unwrap();
|
||||
if hunk.buffer_range.contains(&(buffer_line as u32)) {
|
||||
return Some(hunk);
|
||||
} else if hunk.status() == DiffHunkStatus::Removed && buffer_line == hunk.buffer_range.start
|
||||
{
|
||||
return Some(hunk);
|
||||
} else if hunk.buffer_range.start > buffer_line as u32 {
|
||||
// If we've passed the buffer_line, just stop
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
// We reached the end of the array without finding a hunk, just return none.
|
||||
return None;
|
||||
}
|
||||
|
||||
impl Element for EditorElement {
|
||||
type LayoutState = LayoutState;
|
||||
type PaintState = ();
|
||||
@@ -1610,6 +1638,7 @@ impl Element for EditorElement {
|
||||
let mut highlighted_rows = None;
|
||||
let mut highlighted_ranges = Vec::new();
|
||||
let mut show_scrollbars = false;
|
||||
let mut include_root = false;
|
||||
self.update_view(cx.app, |view, cx| {
|
||||
let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
@@ -1622,7 +1651,7 @@ impl Element for EditorElement {
|
||||
);
|
||||
|
||||
let mut remote_selections = HashMap::default();
|
||||
for (replica_id, line_mode, selection) in display_map
|
||||
for (replica_id, line_mode, cursor_shape, selection) in display_map
|
||||
.buffer_snapshot
|
||||
.remote_selections_in_range(&(start_anchor.clone()..end_anchor.clone()))
|
||||
{
|
||||
@@ -1633,7 +1662,12 @@ impl Element for EditorElement {
|
||||
remote_selections
|
||||
.entry(replica_id)
|
||||
.or_insert(Vec::new())
|
||||
.push(SelectionLayout::new(selection, line_mode, &display_map));
|
||||
.push(SelectionLayout::new(
|
||||
selection,
|
||||
line_mode,
|
||||
cursor_shape,
|
||||
&display_map,
|
||||
));
|
||||
}
|
||||
selections.extend(remote_selections);
|
||||
|
||||
@@ -1665,22 +1699,29 @@ impl Element for EditorElement {
|
||||
local_selections
|
||||
.into_iter()
|
||||
.map(|selection| {
|
||||
SelectionLayout::new(selection, view.selections.line_mode, &display_map)
|
||||
SelectionLayout::new(
|
||||
selection,
|
||||
view.selections.line_mode,
|
||||
view.cursor_shape,
|
||||
&display_map,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
));
|
||||
}
|
||||
|
||||
show_scrollbars = view.show_scrollbars();
|
||||
include_root = view
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
let line_number_layouts =
|
||||
self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx);
|
||||
|
||||
let diff_hunks = snapshot
|
||||
.buffer_snapshot
|
||||
.git_diff_hunks_in_range(start_row..end_row)
|
||||
.collect();
|
||||
let hunk_layouts = self.layout_git_gutters(start_row..end_row, &snapshot);
|
||||
|
||||
let scrollbar_row_range = scroll_position.y()..(scroll_position.y() + height_in_lines);
|
||||
|
||||
@@ -1714,6 +1755,7 @@ impl Element for EditorElement {
|
||||
line_height,
|
||||
&style,
|
||||
&line_layouts,
|
||||
include_root,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -1826,6 +1868,7 @@ impl Element for EditorElement {
|
||||
em_advance,
|
||||
snapshot,
|
||||
}),
|
||||
visible_display_row_range: start_row..end_row,
|
||||
gutter_size,
|
||||
gutter_padding,
|
||||
text_size,
|
||||
@@ -1837,7 +1880,7 @@ impl Element for EditorElement {
|
||||
highlighted_rows,
|
||||
highlighted_ranges,
|
||||
line_number_layouts,
|
||||
diff_hunks,
|
||||
hunk_layouts,
|
||||
blocks,
|
||||
selections,
|
||||
context_menu,
|
||||
@@ -1889,22 +1932,6 @@ impl Element for EditorElement {
|
||||
cx.scene.pop_layer();
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &mut LayoutState,
|
||||
_: &mut (),
|
||||
cx: &mut EventContext,
|
||||
) -> bool {
|
||||
if let Event::ModifiersChanged(event) = event {
|
||||
self.modifiers_changed(*event, cx);
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
range_utf16: Range<usize>,
|
||||
@@ -1971,9 +1998,11 @@ pub struct LayoutState {
|
||||
gutter_margin: f32,
|
||||
text_size: Vector2F,
|
||||
mode: EditorMode,
|
||||
visible_display_row_range: Range<u32>,
|
||||
active_rows: BTreeMap<u32, bool>,
|
||||
highlighted_rows: Option<Range<u32>>,
|
||||
line_number_layouts: Vec<Option<text_layout::Line>>,
|
||||
hunk_layouts: Vec<DiffHunkLayout>,
|
||||
blocks: Vec<BlockLayout>,
|
||||
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
||||
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
|
||||
@@ -1981,7 +2010,6 @@ pub struct LayoutState {
|
||||
show_scrollbars: bool,
|
||||
max_row: u32,
|
||||
context_menu: Option<(DisplayPoint, ElementBox)>,
|
||||
diff_hunks: Vec<DiffHunk<u32>>,
|
||||
code_actions_indicator: Option<(u32, ElementBox)>,
|
||||
hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>,
|
||||
}
|
||||
@@ -2069,20 +2097,6 @@ fn layout_line(
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub enum CursorShape {
|
||||
Bar,
|
||||
Block,
|
||||
Underscore,
|
||||
Hollow,
|
||||
}
|
||||
|
||||
impl Default for CursorShape {
|
||||
fn default() -> Self {
|
||||
CursorShape::Bar
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Cursor {
|
||||
origin: Vector2F,
|
||||
@@ -2175,7 +2189,7 @@ pub struct HighlightedRangeLine {
|
||||
}
|
||||
|
||||
impl HighlightedRange {
|
||||
pub fn paint(&self, bounds: RectF, scene: &mut Scene) {
|
||||
pub fn paint(&self, bounds: RectF, scene: &mut SceneBuilder) {
|
||||
if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x {
|
||||
self.paint_lines(self.start_y, &self.lines[0..1], bounds, scene);
|
||||
self.paint_lines(
|
||||
@@ -2194,7 +2208,7 @@ impl HighlightedRange {
|
||||
start_y: f32,
|
||||
lines: &[HighlightedRangeLine],
|
||||
bounds: RectF,
|
||||
scene: &mut Scene,
|
||||
scene: &mut SceneBuilder,
|
||||
) {
|
||||
if lines.is_empty() {
|
||||
return;
|
||||
@@ -2323,11 +2337,7 @@ mod tests {
|
||||
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
|
||||
Editor::new(EditorMode::Full, buffer, None, None, cx)
|
||||
});
|
||||
let element = EditorElement::new(
|
||||
editor.downgrade(),
|
||||
editor.read(cx).style(cx),
|
||||
CursorShape::Bar,
|
||||
);
|
||||
let element = EditorElement::new(editor.downgrade(), editor.read(cx).style(cx));
|
||||
|
||||
let layouts = editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
@@ -2363,13 +2373,9 @@ mod tests {
|
||||
cx.blur();
|
||||
});
|
||||
|
||||
let mut element = EditorElement::new(
|
||||
editor.downgrade(),
|
||||
editor.read(cx).style(cx),
|
||||
CursorShape::Bar,
|
||||
);
|
||||
let mut element = EditorElement::new(editor.downgrade(), editor.read(cx).style(cx));
|
||||
|
||||
let mut scene = Scene::new(1.0);
|
||||
let mut scene = SceneBuilder::new(1.0);
|
||||
let mut presenter = cx.build_presenter(window_id, 30., Default::default());
|
||||
let mut layout_cx = presenter.build_layout_context(Vector2F::zero(), false, cx);
|
||||
let (size, mut state) = element.layout(
|
||||
|
||||
@@ -354,7 +354,7 @@ impl InfoPopover {
|
||||
.with_style(style.hover_popover.container)
|
||||
.boxed()
|
||||
})
|
||||
.on_move(|_, _| {})
|
||||
.on_move(|_, _| {}) // Consume move events so they don't reach regions underneath.
|
||||
.with_cursor_style(CursorStyle::Arrow)
|
||||
.with_padding(Padding {
|
||||
bottom: HOVER_POPOVER_GAP,
|
||||
@@ -400,7 +400,7 @@ impl DiagnosticPopover {
|
||||
bottom: HOVER_POPOVER_GAP,
|
||||
..Default::default()
|
||||
})
|
||||
.on_move(|_, _| {})
|
||||
.on_move(|_, _| {}) // Consume move events so they don't reach regions underneath.
|
||||
.on_click(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(GoToDiagnostic)
|
||||
})
|
||||
|
||||
@@ -120,6 +120,7 @@ impl FollowableItem for Editor {
|
||||
buffer.set_active_selections(
|
||||
&self.selections.disjoint_anchors(),
|
||||
self.selections.line_mode,
|
||||
self.cursor_shape,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -531,21 +532,17 @@ impl Item for Editor {
|
||||
let buffer = multibuffer.buffer(buffer_id)?;
|
||||
|
||||
let buffer = buffer.read(cx);
|
||||
let filename = if let Some(file) = buffer.file() {
|
||||
if file.path().file_name().is_none()
|
||||
|| self
|
||||
.project
|
||||
let filename = buffer
|
||||
.snapshot()
|
||||
.resolve_file_path(
|
||||
cx,
|
||||
self.project
|
||||
.as_ref()
|
||||
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
file.full_path(cx).to_string_lossy().to_string()
|
||||
} else {
|
||||
file.path().to_string_lossy().to_string()
|
||||
}
|
||||
} else {
|
||||
"untitled".to_string()
|
||||
};
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
|
||||
let mut breadcrumbs = vec![Label::new(filename, theme.breadcrumbs.text.clone()).boxed()];
|
||||
breadcrumbs.extend(symbols.into_iter().map(|symbol| {
|
||||
|
||||
@@ -19,12 +19,6 @@ pub struct UpdateGoToDefinitionLink {
|
||||
pub shift_held: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct CmdShiftChanged {
|
||||
pub cmd_down: bool,
|
||||
pub shift_down: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct GoToFetchedDefinition {
|
||||
pub point: DisplayPoint,
|
||||
@@ -39,7 +33,6 @@ impl_internal_actions!(
|
||||
editor,
|
||||
[
|
||||
UpdateGoToDefinitionLink,
|
||||
CmdShiftChanged,
|
||||
GoToFetchedDefinition,
|
||||
GoToFetchedTypeDefinition
|
||||
]
|
||||
@@ -47,7 +40,6 @@ impl_internal_actions!(
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(update_go_to_definition_link);
|
||||
cx.add_action(cmd_shift_changed);
|
||||
cx.add_action(go_to_fetched_definition);
|
||||
cx.add_action(go_to_fetched_type_definition);
|
||||
}
|
||||
@@ -113,37 +105,6 @@ pub fn update_go_to_definition_link(
|
||||
hide_link_definition(editor, cx);
|
||||
}
|
||||
|
||||
pub fn cmd_shift_changed(
|
||||
editor: &mut Editor,
|
||||
&CmdShiftChanged {
|
||||
cmd_down,
|
||||
shift_down,
|
||||
}: &CmdShiftChanged,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
let pending_selection = editor.has_pending_selection();
|
||||
|
||||
if let Some(point) = editor
|
||||
.link_go_to_definition_state
|
||||
.last_mouse_location
|
||||
.clone()
|
||||
{
|
||||
if cmd_down && !pending_selection {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let kind = if shift_down {
|
||||
LinkDefinitionKind::Type
|
||||
} else {
|
||||
LinkDefinitionKind::Symbol
|
||||
};
|
||||
|
||||
show_link_definition(kind, editor, point, snapshot, cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
hide_link_definition(editor, cx)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum LinkDefinitionKind {
|
||||
Symbol,
|
||||
@@ -397,6 +358,7 @@ fn go_to_fetched_definition_of_kind(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use futures::StreamExt;
|
||||
use gpui::{Modifiers, ModifiersChangedEvent, View};
|
||||
use indoc::indoc;
|
||||
use lsp::request::{GotoDefinition, GotoTypeDefinition};
|
||||
|
||||
@@ -467,11 +429,13 @@ mod tests {
|
||||
|
||||
// Unpress shift causes highlight to go away (normal goto-definition is not valid here)
|
||||
cx.update_editor(|editor, cx| {
|
||||
cmd_shift_changed(
|
||||
editor,
|
||||
&CmdShiftChanged {
|
||||
cmd_down: true,
|
||||
shift_down: false,
|
||||
editor.modifiers_changed(
|
||||
&gpui::ModifiersChangedEvent {
|
||||
modifiers: Modifiers {
|
||||
cmd: true,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
@@ -581,14 +545,7 @@ mod tests {
|
||||
|
||||
// Unpress cmd causes highlight to go away
|
||||
cx.update_editor(|editor, cx| {
|
||||
cmd_shift_changed(
|
||||
editor,
|
||||
&CmdShiftChanged {
|
||||
cmd_down: false,
|
||||
shift_down: false,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
editor.modifiers_changed(&Default::default(), cx);
|
||||
});
|
||||
|
||||
// Assert no link highlights
|
||||
@@ -704,11 +661,12 @@ mod tests {
|
||||
])))
|
||||
});
|
||||
cx.update_editor(|editor, cx| {
|
||||
cmd_shift_changed(
|
||||
editor,
|
||||
&CmdShiftChanged {
|
||||
cmd_down: true,
|
||||
shift_down: false,
|
||||
editor.modifiers_changed(
|
||||
&ModifiersChangedEvent {
|
||||
modifiers: Modifiers {
|
||||
cmd: true,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -29,6 +29,25 @@ pub fn up(
|
||||
start: DisplayPoint,
|
||||
goal: SelectionGoal,
|
||||
preserve_column_at_start: bool,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
up_by_rows(map, start, 1, goal, preserve_column_at_start)
|
||||
}
|
||||
|
||||
pub fn down(
|
||||
map: &DisplaySnapshot,
|
||||
start: DisplayPoint,
|
||||
goal: SelectionGoal,
|
||||
preserve_column_at_end: bool,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
down_by_rows(map, start, 1, goal, preserve_column_at_end)
|
||||
}
|
||||
|
||||
pub fn up_by_rows(
|
||||
map: &DisplaySnapshot,
|
||||
start: DisplayPoint,
|
||||
row_count: u32,
|
||||
goal: SelectionGoal,
|
||||
preserve_column_at_start: bool,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let mut goal_column = if let SelectionGoal::Column(column) = goal {
|
||||
column
|
||||
@@ -36,7 +55,7 @@ pub fn up(
|
||||
map.column_to_chars(start.row(), start.column())
|
||||
};
|
||||
|
||||
let prev_row = start.row().saturating_sub(1);
|
||||
let prev_row = start.row().saturating_sub(row_count);
|
||||
let mut point = map.clip_point(
|
||||
DisplayPoint::new(prev_row, map.line_len(prev_row)),
|
||||
Bias::Left,
|
||||
@@ -62,9 +81,10 @@ pub fn up(
|
||||
)
|
||||
}
|
||||
|
||||
pub fn down(
|
||||
pub fn down_by_rows(
|
||||
map: &DisplaySnapshot,
|
||||
start: DisplayPoint,
|
||||
row_count: u32,
|
||||
goal: SelectionGoal,
|
||||
preserve_column_at_end: bool,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
@@ -74,8 +94,8 @@ pub fn down(
|
||||
map.column_to_chars(start.row(), start.column())
|
||||
};
|
||||
|
||||
let next_row = start.row() + 1;
|
||||
let mut point = map.clip_point(DisplayPoint::new(next_row, 0), Bias::Right);
|
||||
let new_row = start.row() + row_count;
|
||||
let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
|
||||
if point.row() > start.row() {
|
||||
*point.column_mut() = map.column_from_chars(point.row(), goal_column);
|
||||
} else if preserve_column_at_end {
|
||||
|
||||
@@ -8,7 +8,7 @@ use git::diff::DiffHunk;
|
||||
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
|
||||
pub use language::Completion;
|
||||
use language::{
|
||||
char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk,
|
||||
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,
|
||||
@@ -143,6 +143,7 @@ struct ExcerptSummary {
|
||||
text: TextSummary,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MultiBufferRows<'a> {
|
||||
buffer_row_range: Range<u32>,
|
||||
excerpts: Cursor<'a, Excerpt, Point>,
|
||||
@@ -603,6 +604,7 @@ impl MultiBuffer {
|
||||
&mut self,
|
||||
selections: &[Selection<Anchor>],
|
||||
line_mode: bool,
|
||||
cursor_shape: CursorShape,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let mut selections_by_buffer: HashMap<usize, Vec<Selection<text::Anchor>>> =
|
||||
@@ -667,7 +669,7 @@ impl MultiBuffer {
|
||||
}
|
||||
Some(selection)
|
||||
}));
|
||||
buffer.set_active_selections(merged_selections, line_mode, cx);
|
||||
buffer.set_active_selections(merged_selections, line_mode, cursor_shape, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2697,7 +2699,7 @@ impl MultiBufferSnapshot {
|
||||
pub fn remote_selections_in_range<'a>(
|
||||
&'a self,
|
||||
range: &'a Range<Anchor>,
|
||||
) -> impl 'a + Iterator<Item = (ReplicaId, bool, Selection<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, &());
|
||||
cursor
|
||||
@@ -2714,7 +2716,7 @@ impl MultiBufferSnapshot {
|
||||
excerpt
|
||||
.buffer
|
||||
.remote_selections_in_range(query_range)
|
||||
.flat_map(move |(replica_id, line_mode, selections)| {
|
||||
.flat_map(move |(replica_id, line_mode, cursor_shape, selections)| {
|
||||
selections.map(move |selection| {
|
||||
let mut start = Anchor {
|
||||
buffer_id: Some(excerpt.buffer_id),
|
||||
@@ -2736,6 +2738,7 @@ impl MultiBufferSnapshot {
|
||||
(
|
||||
replica_id,
|
||||
line_mode,
|
||||
cursor_shape,
|
||||
Selection {
|
||||
id: selection.id,
|
||||
start,
|
||||
|
||||
@@ -53,7 +53,7 @@ impl View for FileFinder {
|
||||
ChildView::new(self.picker.clone(), cx).boxed()
|
||||
}
|
||||
|
||||
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.picker);
|
||||
}
|
||||
@@ -251,7 +251,7 @@ impl PickerDelegate for FileFinder {
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
mouse_state: MouseState,
|
||||
mouse_state: &mut MouseState,
|
||||
selected: bool,
|
||||
cx: &AppContext,
|
||||
) -> ElementBox {
|
||||
|
||||
@@ -13,6 +13,7 @@ use smol::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use std::borrow::Cow;
|
||||
use std::cmp;
|
||||
use std::io::Write;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
io,
|
||||
@@ -92,6 +93,17 @@ impl LineEnding {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HomeDir(pub PathBuf);
|
||||
|
||||
impl Deref for HomeDir {
|
||||
type Target = PathBuf;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait Fs: Send + Sync {
|
||||
async fn create_dir(&self, path: &Path) -> Result<()>;
|
||||
|
||||
@@ -190,7 +190,6 @@ impl BufferDiff {
|
||||
}
|
||||
|
||||
if kind == GitDiffLineType::Deletion {
|
||||
*buffer_row_divergence -= 1;
|
||||
let end = content_offset + content_len;
|
||||
|
||||
match &mut head_byte_range {
|
||||
@@ -203,6 +202,8 @@ impl BufferDiff {
|
||||
let row = old_row as i64 + *buffer_row_divergence;
|
||||
first_deletion_buffer_row = Some(row as u32);
|
||||
}
|
||||
|
||||
*buffer_row_divergence -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ impl View for GoToLine {
|
||||
.named("go to line")
|
||||
}
|
||||
|
||||
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
cx.focus(&self.line_editor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ fn compile_metal_shaders() {
|
||||
"macosx",
|
||||
"metal",
|
||||
"-gline-tables-only",
|
||||
"-mmacosx-version-min=10.14",
|
||||
"-mmacosx-version-min=10.15.7",
|
||||
"-MO",
|
||||
"-c",
|
||||
shader_path,
|
||||
|
||||
@@ -101,18 +101,6 @@ impl gpui::Element for TextElement {
|
||||
line.paint(bounds.origin(), visible_bounds, bounds.height(), cx);
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
_: &gpui::Event,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
_: &mut Self::PaintState,
|
||||
_: &mut gpui::EventContext,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
_: Range<usize>,
|
||||
|
||||
@@ -41,8 +41,8 @@ use crate::{
|
||||
platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions},
|
||||
presenter::Presenter,
|
||||
util::post_inc,
|
||||
Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseButton,
|
||||
MouseRegionId, PathPromptOptions, TextLayoutCache,
|
||||
Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, KeyUpEvent,
|
||||
ModifiersChangedEvent, MouseButton, MouseRegionId, PathPromptOptions, TextLayoutCache,
|
||||
};
|
||||
|
||||
pub trait Entity: 'static {
|
||||
@@ -60,8 +60,18 @@ pub trait Entity: 'static {
|
||||
pub trait View: Entity + Sized {
|
||||
fn ui_name() -> &'static str;
|
||||
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox;
|
||||
fn on_focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {}
|
||||
fn on_focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {}
|
||||
fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {}
|
||||
fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {}
|
||||
fn key_down(&mut self, _: &KeyDownEvent, _: &mut ViewContext<Self>) -> bool {
|
||||
false
|
||||
}
|
||||
fn key_up(&mut self, _: &KeyUpEvent, _: &mut ViewContext<Self>) -> bool {
|
||||
false
|
||||
}
|
||||
fn modifiers_changed(&mut self, _: &ModifiersChangedEvent, _: &mut ViewContext<Self>) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
|
||||
Self::default_keymap_context()
|
||||
}
|
||||
@@ -1297,7 +1307,7 @@ impl MutableAppContext {
|
||||
) -> impl Iterator<Item = (&'static str, Box<dyn Action>, SmallVec<[&Binding; 1]>)> {
|
||||
let mut action_types: HashSet<_> = self.global_actions.keys().copied().collect();
|
||||
|
||||
for view_id in self.parents(window_id, view_id) {
|
||||
for view_id in self.ancestors(window_id, view_id) {
|
||||
if let Some(view) = self.views.get(&(window_id, view_id)) {
|
||||
let view_type = view.as_any().type_id();
|
||||
if let Some(actions) = self.actions.get(&view_type) {
|
||||
@@ -1327,7 +1337,7 @@ impl MutableAppContext {
|
||||
let action_type = action.as_any().type_id();
|
||||
if let Some(window_id) = self.cx.platform.key_window_id() {
|
||||
if let Some(focused_view_id) = self.focused_view_id(window_id) {
|
||||
for view_id in self.parents(window_id, focused_view_id) {
|
||||
for view_id in self.ancestors(window_id, focused_view_id) {
|
||||
if let Some(view) = self.views.get(&(window_id, view_id)) {
|
||||
let view_type = view.as_any().type_id();
|
||||
if let Some(actions) = self.actions.get(&view_type) {
|
||||
@@ -1376,7 +1386,7 @@ impl MutableAppContext {
|
||||
mut visit: impl FnMut(usize, bool, &mut MutableAppContext) -> bool,
|
||||
) -> bool {
|
||||
// List of view ids from the leaf to the root of the window
|
||||
let path = self.parents(window_id, view_id).collect::<Vec<_>>();
|
||||
let path = self.ancestors(window_id, view_id).collect::<Vec<_>>();
|
||||
|
||||
// Walk down from the root to the leaf calling visit with capture_phase = true
|
||||
for view_id in path.iter().rev() {
|
||||
@@ -1397,7 +1407,7 @@ impl MutableAppContext {
|
||||
|
||||
// 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 parents(&self, window_id: usize, mut view_id: usize) -> impl Iterator<Item = usize> + '_ {
|
||||
fn ancestors(&self, window_id: usize, mut view_id: usize) -> impl Iterator<Item = usize> + '_ {
|
||||
std::iter::once(view_id)
|
||||
.into_iter()
|
||||
.chain(std::iter::from_fn(move || {
|
||||
@@ -1445,40 +1455,111 @@ impl MutableAppContext {
|
||||
self.keystroke_matcher.clear_bindings();
|
||||
}
|
||||
|
||||
pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: &Keystroke) -> bool {
|
||||
let mut pending = false;
|
||||
|
||||
pub fn dispatch_key_down(&mut self, window_id: usize, event: &KeyDownEvent) -> bool {
|
||||
if let Some(focused_view_id) = self.focused_view_id(window_id) {
|
||||
for view_id in self.parents(window_id, focused_view_id).collect::<Vec<_>>() {
|
||||
let keymap_context = self
|
||||
.cx
|
||||
.views
|
||||
.get(&(window_id, view_id))
|
||||
.unwrap()
|
||||
.keymap_context(self.as_ref());
|
||||
|
||||
match self.keystroke_matcher.push_keystroke(
|
||||
keystroke.clone(),
|
||||
view_id,
|
||||
&keymap_context,
|
||||
) {
|
||||
MatchResult::None => {}
|
||||
MatchResult::Pending => pending = true,
|
||||
MatchResult::Action(action) => {
|
||||
if self.handle_dispatch_action_from_effect(
|
||||
window_id,
|
||||
Some(view_id),
|
||||
action.as_ref(),
|
||||
) {
|
||||
self.keystroke_matcher.clear_pending();
|
||||
return true;
|
||||
}
|
||||
for view_id in self
|
||||
.ancestors(window_id, focused_view_id)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
if let Some(mut view) = self.cx.views.remove(&(window_id, view_id)) {
|
||||
let handled = view.key_down(event, self, window_id, view_id);
|
||||
self.cx.views.insert((window_id, view_id), view);
|
||||
if handled {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
log::error!("view {} does not exist", view_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pending
|
||||
false
|
||||
}
|
||||
|
||||
pub fn dispatch_key_up(&mut self, window_id: usize, event: &KeyUpEvent) -> bool {
|
||||
if let Some(focused_view_id) = self.focused_view_id(window_id) {
|
||||
for view_id in self
|
||||
.ancestors(window_id, focused_view_id)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
if let Some(mut view) = self.cx.views.remove(&(window_id, view_id)) {
|
||||
let handled = view.key_up(event, self, window_id, view_id);
|
||||
self.cx.views.insert((window_id, view_id), view);
|
||||
if handled {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
log::error!("view {} does not exist", view_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn dispatch_modifiers_changed(
|
||||
&mut self,
|
||||
window_id: usize,
|
||||
event: &ModifiersChangedEvent,
|
||||
) -> bool {
|
||||
if let Some(focused_view_id) = self.focused_view_id(window_id) {
|
||||
for view_id in self
|
||||
.ancestors(window_id, focused_view_id)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
if let Some(mut view) = self.cx.views.remove(&(window_id, view_id)) {
|
||||
let handled = view.modifiers_changed(event, self, window_id, view_id);
|
||||
self.cx.views.insert((window_id, view_id), view);
|
||||
if handled {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
log::error!("view {} does not exist", view_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: &Keystroke) -> bool {
|
||||
if let Some(focused_view_id) = self.focused_view_id(window_id) {
|
||||
let dispatch_path = self
|
||||
.ancestors(window_id, focused_view_id)
|
||||
.map(|view_id| {
|
||||
(
|
||||
view_id,
|
||||
self.cx
|
||||
.views
|
||||
.get(&(window_id, view_id))
|
||||
.unwrap()
|
||||
.keymap_context(self.as_ref()),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
match self
|
||||
.keystroke_matcher
|
||||
.push_keystroke(keystroke.clone(), dispatch_path)
|
||||
{
|
||||
MatchResult::None => false,
|
||||
MatchResult::Pending => true,
|
||||
MatchResult::Match { view_id, action } => {
|
||||
if self.handle_dispatch_action_from_effect(
|
||||
window_id,
|
||||
Some(view_id),
|
||||
action.as_ref(),
|
||||
) {
|
||||
self.keystroke_matcher.clear_pending();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_global<T: 'static + Default>(&mut self) -> &T {
|
||||
@@ -1580,7 +1661,7 @@ impl MutableAppContext {
|
||||
is_fullscreen: false,
|
||||
},
|
||||
);
|
||||
root_view.update(this, |view, cx| view.on_focus_in(cx.handle().into(), cx));
|
||||
root_view.update(this, |view, cx| view.focus_in(cx.handle().into(), cx));
|
||||
|
||||
let window =
|
||||
this.cx
|
||||
@@ -1612,7 +1693,7 @@ impl MutableAppContext {
|
||||
is_fullscreen: false,
|
||||
},
|
||||
);
|
||||
root_view.update(this, |view, cx| view.on_focus_in(cx.handle().into(), cx));
|
||||
root_view.update(this, |view, cx| view.focus_in(cx.handle().into(), cx));
|
||||
|
||||
let status_item = this.cx.platform.add_status_item();
|
||||
this.register_platform_window(window_id, status_item);
|
||||
@@ -2235,12 +2316,12 @@ impl MutableAppContext {
|
||||
|
||||
//Handle focus
|
||||
let focused_id = window.focused_view_id?;
|
||||
for view_id in this.parents(window_id, focused_id).collect::<Vec<_>>() {
|
||||
for view_id in this.ancestors(window_id, focused_id).collect::<Vec<_>>() {
|
||||
if let Some(mut view) = this.cx.views.remove(&(window_id, view_id)) {
|
||||
if active {
|
||||
view.on_focus_in(this, window_id, view_id, focused_id);
|
||||
view.focus_in(this, window_id, view_id, focused_id);
|
||||
} else {
|
||||
view.on_focus_out(this, window_id, view_id, focused_id);
|
||||
view.focus_out(this, window_id, view_id, focused_id);
|
||||
}
|
||||
this.cx.views.insert((window_id, view_id), view);
|
||||
}
|
||||
@@ -2272,16 +2353,16 @@ impl MutableAppContext {
|
||||
});
|
||||
|
||||
let blurred_parents = blurred_id
|
||||
.map(|blurred_id| this.parents(window_id, blurred_id).collect::<Vec<_>>())
|
||||
.map(|blurred_id| this.ancestors(window_id, blurred_id).collect::<Vec<_>>())
|
||||
.unwrap_or_default();
|
||||
let focused_parents = focused_id
|
||||
.map(|focused_id| this.parents(window_id, focused_id).collect::<Vec<_>>())
|
||||
.map(|focused_id| this.ancestors(window_id, focused_id).collect::<Vec<_>>())
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(blurred_id) = blurred_id {
|
||||
for view_id in blurred_parents.iter().copied() {
|
||||
if let Some(mut view) = this.cx.views.remove(&(window_id, view_id)) {
|
||||
view.on_focus_out(this, window_id, view_id, blurred_id);
|
||||
view.focus_out(this, window_id, view_id, blurred_id);
|
||||
this.cx.views.insert((window_id, view_id), view);
|
||||
}
|
||||
}
|
||||
@@ -2294,7 +2375,7 @@ impl MutableAppContext {
|
||||
if let Some(focused_id) = focused_id {
|
||||
for view_id in focused_parents {
|
||||
if let Some(mut view) = this.cx.views.remove(&(window_id, view_id)) {
|
||||
view.on_focus_in(this, window_id, view_id, focused_id);
|
||||
view.focus_in(this, window_id, view_id, focused_id);
|
||||
this.cx.views.insert((window_id, view_id), view);
|
||||
}
|
||||
}
|
||||
@@ -2961,20 +3042,41 @@ pub trait AnyView {
|
||||
) -> Option<Pin<Box<dyn 'static + Future<Output = ()>>>>;
|
||||
fn ui_name(&self) -> &'static str;
|
||||
fn render(&mut self, params: RenderParams, cx: &mut MutableAppContext) -> ElementBox;
|
||||
fn on_focus_in(
|
||||
fn focus_in(
|
||||
&mut self,
|
||||
cx: &mut MutableAppContext,
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
focused_id: usize,
|
||||
);
|
||||
fn on_focus_out(
|
||||
fn focus_out(
|
||||
&mut self,
|
||||
cx: &mut MutableAppContext,
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
focused_id: usize,
|
||||
);
|
||||
fn key_down(
|
||||
&mut self,
|
||||
event: &KeyDownEvent,
|
||||
cx: &mut MutableAppContext,
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
) -> bool;
|
||||
fn key_up(
|
||||
&mut self,
|
||||
event: &KeyUpEvent,
|
||||
cx: &mut MutableAppContext,
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
) -> bool;
|
||||
fn modifiers_changed(
|
||||
&mut self,
|
||||
event: &ModifiersChangedEvent,
|
||||
cx: &mut MutableAppContext,
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
) -> bool;
|
||||
fn keymap_context(&self, cx: &AppContext) -> keymap::Context;
|
||||
fn debug_json(&self, cx: &AppContext) -> serde_json::Value;
|
||||
|
||||
@@ -3040,7 +3142,7 @@ where
|
||||
View::render(self, &mut RenderContext::new(params, cx))
|
||||
}
|
||||
|
||||
fn on_focus_in(
|
||||
fn focus_in(
|
||||
&mut self,
|
||||
cx: &mut MutableAppContext,
|
||||
window_id: usize,
|
||||
@@ -3059,10 +3161,10 @@ where
|
||||
.type_id();
|
||||
AnyViewHandle::new(window_id, focused_id, focused_type, cx.ref_counts.clone())
|
||||
};
|
||||
View::on_focus_in(self, focused_view_handle, &mut cx);
|
||||
View::focus_in(self, focused_view_handle, &mut cx);
|
||||
}
|
||||
|
||||
fn on_focus_out(
|
||||
fn focus_out(
|
||||
&mut self,
|
||||
cx: &mut MutableAppContext,
|
||||
window_id: usize,
|
||||
@@ -3081,7 +3183,40 @@ where
|
||||
.type_id();
|
||||
AnyViewHandle::new(window_id, blurred_id, blurred_type, cx.ref_counts.clone())
|
||||
};
|
||||
View::on_focus_out(self, blurred_view_handle, &mut cx);
|
||||
View::focus_out(self, blurred_view_handle, &mut cx);
|
||||
}
|
||||
|
||||
fn key_down(
|
||||
&mut self,
|
||||
event: &KeyDownEvent,
|
||||
cx: &mut MutableAppContext,
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
) -> bool {
|
||||
let mut cx = ViewContext::new(cx, window_id, view_id);
|
||||
View::key_down(self, event, &mut cx)
|
||||
}
|
||||
|
||||
fn key_up(
|
||||
&mut self,
|
||||
event: &KeyUpEvent,
|
||||
cx: &mut MutableAppContext,
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
) -> bool {
|
||||
let mut cx = ViewContext::new(cx, window_id, view_id);
|
||||
View::key_up(self, event, &mut cx)
|
||||
}
|
||||
|
||||
fn modifiers_changed(
|
||||
&mut self,
|
||||
event: &ModifiersChangedEvent,
|
||||
cx: &mut MutableAppContext,
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
) -> bool {
|
||||
let mut cx = ViewContext::new(cx, window_id, view_id);
|
||||
View::modifiers_changed(self, event, &mut cx)
|
||||
}
|
||||
|
||||
fn keymap_context(&self, cx: &AppContext) -> keymap::Context {
|
||||
@@ -3463,7 +3598,7 @@ impl<'a, T: View> ViewContext<'a, T> {
|
||||
if self.window_id != view.window_id {
|
||||
return false;
|
||||
}
|
||||
self.parents(view.window_id, view.view_id)
|
||||
self.ancestors(view.window_id, view.view_id)
|
||||
.any(|parent| parent == self.view_id)
|
||||
}
|
||||
|
||||
@@ -3701,6 +3836,11 @@ impl<'a, T: View> ViewContext<'a, T> {
|
||||
self.app.notify_view(self.window_id, self.view_id);
|
||||
}
|
||||
|
||||
pub fn dispatch_action(&mut self, action: impl Action) {
|
||||
self.app
|
||||
.dispatch_action_at(self.window_id, self.view_id, action)
|
||||
}
|
||||
|
||||
pub fn dispatch_any_action(&mut self, action: Box<dyn Action>) {
|
||||
self.app
|
||||
.dispatch_any_action_at(self.window_id, self.view_id, action)
|
||||
@@ -3774,10 +3914,32 @@ pub struct RenderContext<'a, T: View> {
|
||||
pub refreshing: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct MouseState {
|
||||
pub hovered: bool,
|
||||
pub clicked: Option<MouseButton>,
|
||||
hovered: bool,
|
||||
clicked: Option<MouseButton>,
|
||||
accessed_hovered: bool,
|
||||
accessed_clicked: bool,
|
||||
}
|
||||
|
||||
impl MouseState {
|
||||
pub fn hovered(&mut self) -> bool {
|
||||
self.accessed_hovered = true;
|
||||
self.hovered
|
||||
}
|
||||
|
||||
pub fn clicked(&mut self) -> Option<MouseButton> {
|
||||
self.accessed_clicked = true;
|
||||
self.clicked
|
||||
}
|
||||
|
||||
pub fn accessed_hovered(&self) -> bool {
|
||||
self.accessed_hovered
|
||||
}
|
||||
|
||||
pub fn accessed_clicked(&self) -> bool {
|
||||
self.accessed_clicked
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, V: View> RenderContext<'a, V> {
|
||||
@@ -3818,6 +3980,8 @@ impl<'a, V: View> RenderContext<'a, V> {
|
||||
None
|
||||
}
|
||||
}),
|
||||
accessed_hovered: false,
|
||||
accessed_clicked: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5614,10 +5778,7 @@ mod tests {
|
||||
Event::MouseDown(MouseButtonEvent {
|
||||
position: Default::default(),
|
||||
button: MouseButton::Left,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
shift: false,
|
||||
cmd: false,
|
||||
modifiers: Default::default(),
|
||||
click_count: 1,
|
||||
}),
|
||||
false,
|
||||
@@ -6330,13 +6491,13 @@ mod tests {
|
||||
"View"
|
||||
}
|
||||
|
||||
fn on_focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.handle().id() == focused.id() {
|
||||
self.events.lock().push(format!("{} focused", &self.name));
|
||||
}
|
||||
}
|
||||
|
||||
fn on_focus_out(&mut self, blurred: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
fn focus_out(&mut self, blurred: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.handle().id() == blurred.id() {
|
||||
self.events.lock().push(format!("{} blurred", &self.name));
|
||||
}
|
||||
|
||||
@@ -17,10 +17,11 @@ use parking_lot::{Mutex, RwLock};
|
||||
use smol::stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
executor, keymap::Keystroke, platform, Action, AnyViewHandle, AppContext, Appearance, Entity,
|
||||
Event, FontCache, InputHandler, KeyDownEvent, LeakDetector, ModelContext, ModelHandle,
|
||||
MutableAppContext, Platform, ReadModelWith, ReadViewWith, RenderContext, Task, UpdateModel,
|
||||
UpdateView, View, ViewContext, ViewHandle, WeakHandle, WindowInputHandler,
|
||||
executor, geometry::vector::Vector2F, keymap::Keystroke, platform, Action, AnyViewHandle,
|
||||
AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent, LeakDetector,
|
||||
ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith, ReadViewWith,
|
||||
RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle, WeakHandle,
|
||||
WindowInputHandler,
|
||||
};
|
||||
use collections::BTreeMap;
|
||||
|
||||
@@ -275,6 +276,17 @@ impl TestAppContext {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn simulate_window_resize(&self, window_id: usize, size: Vector2F) {
|
||||
let mut window = self.window_mut(window_id);
|
||||
window.size = size;
|
||||
let mut handlers = mem::take(&mut window.resize_handlers);
|
||||
drop(window);
|
||||
for handler in &mut handlers {
|
||||
handler();
|
||||
}
|
||||
self.window_mut(window_id).resize_handlers = handlers;
|
||||
}
|
||||
|
||||
pub fn simulate_window_activation(&self, to_activate: Option<usize>) {
|
||||
let mut handlers = BTreeMap::new();
|
||||
{
|
||||
|
||||
@@ -33,8 +33,8 @@ use crate::{
|
||||
},
|
||||
json,
|
||||
presenter::MeasurementContext,
|
||||
Action, DebugContext, Event, EventContext, LayoutContext, PaintContext, RenderContext,
|
||||
SizeConstraint, View,
|
||||
Action, DebugContext, EventContext, LayoutContext, PaintContext, RenderContext, SizeConstraint,
|
||||
View,
|
||||
};
|
||||
use core::panic;
|
||||
use json::ToJson;
|
||||
@@ -50,7 +50,6 @@ use std::{
|
||||
trait AnyElement {
|
||||
fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F;
|
||||
fn paint(&mut self, origin: Vector2F, visible_bounds: RectF, cx: &mut PaintContext);
|
||||
fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool;
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
range_utf16: Range<usize>,
|
||||
@@ -80,16 +79,6 @@ pub trait Element {
|
||||
cx: &mut PaintContext,
|
||||
) -> Self::PaintState;
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
bounds: RectF,
|
||||
visible_bounds: RectF,
|
||||
layout: &mut Self::LayoutState,
|
||||
paint: &mut Self::PaintState,
|
||||
cx: &mut EventContext,
|
||||
) -> bool;
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
range_utf16: Range<usize>,
|
||||
@@ -303,22 +292,6 @@ impl<T: Element> AnyElement for Lifecycle<T> {
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool {
|
||||
if let Lifecycle::PostPaint {
|
||||
element,
|
||||
bounds,
|
||||
visible_bounds,
|
||||
layout,
|
||||
paint,
|
||||
..
|
||||
} = self
|
||||
{
|
||||
element.dispatch_event(event, *bounds, *visible_bounds, layout, paint, cx)
|
||||
} else {
|
||||
panic!("invalid element lifecycle state");
|
||||
}
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
range_utf16: Range<usize>,
|
||||
@@ -433,10 +406,6 @@ impl ElementRc {
|
||||
self.element.borrow_mut().paint(origin, visible_bounds, cx);
|
||||
}
|
||||
|
||||
pub fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool {
|
||||
self.element.borrow_mut().dispatch_event(event, cx)
|
||||
}
|
||||
|
||||
pub fn rect_for_text_range(
|
||||
&self,
|
||||
range_utf16: Range<usize>,
|
||||
@@ -495,7 +464,7 @@ pub trait ParentElement<'a>: Extend<ElementBox> + Sized {
|
||||
|
||||
impl<'a, T> ParentElement<'a> for T where T: Extend<ElementBox> {}
|
||||
|
||||
fn constrain_size_preserving_aspect_ratio(max_size: Vector2F, size: Vector2F) -> Vector2F {
|
||||
pub fn constrain_size_preserving_aspect_ratio(max_size: Vector2F, size: Vector2F) -> Vector2F {
|
||||
if max_size.x().is_infinite() && max_size.y().is_infinite() {
|
||||
size
|
||||
} else if max_size.x().is_infinite() || max_size.x() / max_size.y() > size.x() / size.y() {
|
||||
|
||||
@@ -2,8 +2,7 @@ use crate::{
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
json,
|
||||
presenter::MeasurementContext,
|
||||
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
|
||||
SizeConstraint,
|
||||
DebugContext, Element, ElementBox, LayoutContext, PaintContext, SizeConstraint,
|
||||
};
|
||||
use json::ToJson;
|
||||
|
||||
@@ -84,18 +83,6 @@ impl Element for Align {
|
||||
);
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
_: &mut Self::PaintState,
|
||||
cx: &mut EventContext,
|
||||
) -> bool {
|
||||
self.child.dispatch_event(event, cx)
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
range_utf16: std::ops::Range<usize>,
|
||||
|
||||
@@ -56,18 +56,6 @@ where
|
||||
self.0(bounds, visible_bounds, cx)
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
_: &crate::Event,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
_: &mut Self::PaintState,
|
||||
_: &mut crate::EventContext,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
_: std::ops::Range<usize>,
|
||||
|
||||
@@ -7,8 +7,7 @@ use crate::{
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
json,
|
||||
presenter::MeasurementContext,
|
||||
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
|
||||
SizeConstraint,
|
||||
DebugContext, Element, ElementBox, LayoutContext, PaintContext, SizeConstraint,
|
||||
};
|
||||
|
||||
pub struct ConstrainedBox {
|
||||
@@ -157,18 +156,6 @@ impl Element for ConstrainedBox {
|
||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
_: &mut Self::PaintState,
|
||||
cx: &mut EventContext,
|
||||
) -> bool {
|
||||
self.child.dispatch_event(event, cx)
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
range_utf16: Range<usize>,
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
platform::CursorStyle,
|
||||
presenter::MeasurementContext,
|
||||
scene::{self, Border, CursorRegion, Quad},
|
||||
Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
|
||||
Element, ElementBox, LayoutContext, PaintContext, SizeConstraint,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
@@ -285,18 +285,6 @@ impl Element for Container {
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
_: &mut Self::PaintState,
|
||||
cx: &mut EventContext,
|
||||
) -> bool {
|
||||
self.child.dispatch_event(event, cx)
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
range_utf16: Range<usize>,
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::{
|
||||
presenter::MeasurementContext,
|
||||
DebugContext,
|
||||
};
|
||||
use crate::{Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint};
|
||||
use crate::{Element, LayoutContext, PaintContext, SizeConstraint};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Empty {
|
||||
@@ -59,18 +59,6 @@ impl Element for Empty {
|
||||
) -> Self::PaintState {
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
_: &Event,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
_: &mut Self::PaintState,
|
||||
_: &mut EventContext,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
_: Range<usize>,
|
||||
|
||||
@@ -4,8 +4,7 @@ use crate::{
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
json,
|
||||
presenter::MeasurementContext,
|
||||
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
|
||||
SizeConstraint,
|
||||
DebugContext, Element, ElementBox, LayoutContext, PaintContext, SizeConstraint,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -66,18 +65,6 @@ impl Element for Expanded {
|
||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
_: &mut Self::PaintState,
|
||||
cx: &mut EventContext,
|
||||
) -> bool {
|
||||
self.child.dispatch_event(event, cx)
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
range_utf16: Range<usize>,
|
||||
|
||||
@@ -3,8 +3,8 @@ use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc};
|
||||
use crate::{
|
||||
json::{self, ToJson, Value},
|
||||
presenter::MeasurementContext,
|
||||
Axis, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext,
|
||||
LayoutContext, PaintContext, RenderContext, SizeConstraint, Vector2FExt, View,
|
||||
Axis, DebugContext, Element, ElementBox, ElementStateHandle, LayoutContext, PaintContext,
|
||||
RenderContext, SizeConstraint, Vector2FExt, View,
|
||||
};
|
||||
use pathfinder_geometry::{
|
||||
rect::RectF,
|
||||
@@ -259,7 +259,7 @@ impl Element for Flex {
|
||||
if remaining_space < 0. {
|
||||
let mut delta = match axis {
|
||||
Axis::Horizontal => {
|
||||
if e.delta.x() != 0. {
|
||||
if e.delta.x().abs() >= e.delta.y().abs() {
|
||||
e.delta.x()
|
||||
} else {
|
||||
e.delta.y()
|
||||
@@ -277,7 +277,7 @@ impl Element for Flex {
|
||||
|
||||
cx.notify();
|
||||
} else {
|
||||
cx.propogate_event();
|
||||
cx.propagate_event();
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -318,23 +318,6 @@ impl Element for Flex {
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
_: &mut Self::PaintState,
|
||||
cx: &mut EventContext,
|
||||
) -> bool {
|
||||
let mut handled = false;
|
||||
for child in &mut self.children {
|
||||
handled = child.dispatch_event(event, cx) || handled;
|
||||
}
|
||||
|
||||
handled
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
range_utf16: Range<usize>,
|
||||
@@ -420,18 +403,6 @@ impl Element for FlexItem {
|
||||
self.child.paint(bounds.origin(), visible_bounds, cx)
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
_: &mut Self::PaintState,
|
||||
cx: &mut EventContext,
|
||||
) -> bool {
|
||||
self.child.dispatch_event(event, cx)
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
range_utf16: Range<usize>,
|
||||
|
||||
@@ -4,8 +4,7 @@ use crate::{
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
json::json,
|
||||
presenter::MeasurementContext,
|
||||
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
|
||||
SizeConstraint,
|
||||
DebugContext, Element, ElementBox, LayoutContext, PaintContext, SizeConstraint,
|
||||
};
|
||||
|
||||
pub struct Hook {
|
||||
@@ -56,18 +55,6 @@ impl Element for Hook {
|
||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
_: &mut Self::PaintState,
|
||||
cx: &mut EventContext,
|
||||
) -> bool {
|
||||
self.child.dispatch_event(event, cx)
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
range_utf16: Range<usize>,
|
||||
|
||||
@@ -6,8 +6,7 @@ use crate::{
|
||||
},
|
||||
json::{json, ToJson},
|
||||
presenter::MeasurementContext,
|
||||
scene, Border, DebugContext, Element, Event, EventContext, ImageData, LayoutContext,
|
||||
PaintContext, SizeConstraint,
|
||||
scene, Border, DebugContext, Element, ImageData, LayoutContext, PaintContext, SizeConstraint,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
@@ -81,18 +80,6 @@ impl Element for Image {
|
||||
});
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
_: &Event,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
_: &mut Self::PaintState,
|
||||
_: &mut EventContext,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
_: Range<usize>,
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
elements::*,
|
||||
fonts::TextStyle,
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
Action, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
|
||||
Action, ElementBox, LayoutContext, PaintContext, SizeConstraint,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -64,18 +64,6 @@ impl Element for KeystrokeLabel {
|
||||
element.paint(bounds.origin(), visible_bounds, cx);
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
element: &mut ElementBox,
|
||||
_: &mut (),
|
||||
cx: &mut EventContext,
|
||||
) -> bool {
|
||||
element.dispatch_event(event, cx)
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
_: Range<usize>,
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::{
|
||||
json::{ToJson, Value},
|
||||
presenter::MeasurementContext,
|
||||
text_layout::{Line, RunStyle},
|
||||
DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
|
||||
DebugContext, Element, LayoutContext, PaintContext, SizeConstraint,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
@@ -165,18 +165,6 @@ impl Element for Label {
|
||||
line.paint(bounds.origin(), visible_bounds, bounds.size().y(), cx)
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
_: &Event,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
_: &mut Self::PaintState,
|
||||
_: &mut EventContext,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
_: Range<usize>,
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
},
|
||||
json::json,
|
||||
presenter::MeasurementContext,
|
||||
DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, MouseRegion,
|
||||
DebugContext, Element, ElementBox, ElementRc, EventContext, LayoutContext, MouseRegion,
|
||||
PaintContext, RenderContext, SizeConstraint, View, ViewContext,
|
||||
};
|
||||
use std::{cell::RefCell, collections::VecDeque, ops::Range, rc::Rc};
|
||||
@@ -13,7 +13,6 @@ use sum_tree::{Bias, SumTree};
|
||||
|
||||
pub struct List {
|
||||
state: ListState,
|
||||
invalidated_elements: Vec<ElementRc>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -39,8 +38,8 @@ struct StateInner {
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct ListOffset {
|
||||
item_ix: usize,
|
||||
offset_in_item: f32,
|
||||
pub item_ix: usize,
|
||||
pub offset_in_item: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -82,10 +81,7 @@ struct Height(f32);
|
||||
|
||||
impl List {
|
||||
pub fn new(state: ListState) -> Self {
|
||||
Self {
|
||||
state,
|
||||
invalidated_elements: Default::default(),
|
||||
}
|
||||
Self { state }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,18 +112,7 @@ impl Element for List {
|
||||
let mut new_items = SumTree::new();
|
||||
let mut rendered_items = VecDeque::new();
|
||||
let mut rendered_height = 0.;
|
||||
let mut scroll_top = state
|
||||
.logical_scroll_top
|
||||
.unwrap_or_else(|| match state.orientation {
|
||||
Orientation::Top => ListOffset {
|
||||
item_ix: 0,
|
||||
offset_in_item: 0.,
|
||||
},
|
||||
Orientation::Bottom => ListOffset {
|
||||
item_ix: state.items.summary().count,
|
||||
offset_in_item: 0.,
|
||||
},
|
||||
});
|
||||
let mut scroll_top = state.logical_scroll_top();
|
||||
|
||||
// Render items after the scroll top, including those in the trailing overdraw.
|
||||
let mut cursor = old_items.cursor::<Count>();
|
||||
@@ -264,8 +249,8 @@ impl Element for List {
|
||||
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
|
||||
cx.scene.push_layer(Some(visible_bounds));
|
||||
|
||||
cx.scene
|
||||
.push_mouse_region(MouseRegion::new::<Self>(10, 0, bounds).on_scroll({
|
||||
cx.scene.push_mouse_region(
|
||||
MouseRegion::new::<Self>(cx.current_view_id(), 0, bounds).on_scroll({
|
||||
let state = self.state.clone();
|
||||
let height = bounds.height();
|
||||
let scroll_top = scroll_top.clone();
|
||||
@@ -278,7 +263,8 @@ impl Element for List {
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
let state = &mut *self.state.0.borrow_mut();
|
||||
for (mut element, origin) in state.visible_elements(bounds, scroll_top) {
|
||||
@@ -288,50 +274,6 @@ impl Element for List {
|
||||
cx.scene.pop_layer();
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
bounds: RectF,
|
||||
_: RectF,
|
||||
scroll_top: &mut ListOffset,
|
||||
_: &mut (),
|
||||
cx: &mut EventContext,
|
||||
) -> bool {
|
||||
let mut handled = false;
|
||||
|
||||
let mut state = self.state.0.borrow_mut();
|
||||
let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item);
|
||||
let mut cursor = state.items.cursor::<Count>();
|
||||
let mut new_items = cursor.slice(&Count(scroll_top.item_ix), Bias::Right, &());
|
||||
while let Some(item) = cursor.item() {
|
||||
if item_origin.y() > bounds.max_y() {
|
||||
break;
|
||||
}
|
||||
|
||||
if let ListItem::Rendered(element) = item {
|
||||
let prev_notify_count = cx.notify_count();
|
||||
let mut element = element.clone();
|
||||
handled = element.dispatch_event(event, cx) || handled;
|
||||
item_origin.set_y(item_origin.y() + element.size().y());
|
||||
if cx.notify_count() > prev_notify_count {
|
||||
new_items.push(ListItem::Unrendered, &());
|
||||
self.invalidated_elements.push(element);
|
||||
} else {
|
||||
new_items.push(item.clone(), &());
|
||||
}
|
||||
cursor.next(&());
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
|
||||
new_items.push_tree(cursor.suffix(&()), &());
|
||||
drop(cursor);
|
||||
state.items = new_items;
|
||||
|
||||
handled
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
range_utf16: Range<usize>,
|
||||
@@ -468,6 +410,20 @@ impl ListState {
|
||||
) {
|
||||
self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
|
||||
}
|
||||
|
||||
pub fn logical_scroll_top(&self) -> ListOffset {
|
||||
self.0.borrow().logical_scroll_top()
|
||||
}
|
||||
|
||||
pub fn scroll_to(&self, mut scroll_top: ListOffset) {
|
||||
let state = &mut *self.0.borrow_mut();
|
||||
let item_count = state.items.summary().count;
|
||||
if scroll_top.item_ix >= item_count {
|
||||
scroll_top.item_ix = item_count;
|
||||
scroll_top.offset_in_item = 0.;
|
||||
}
|
||||
state.logical_scroll_top = Some(scroll_top);
|
||||
}
|
||||
}
|
||||
|
||||
impl StateInner {
|
||||
@@ -557,6 +513,22 @@ impl StateInner {
|
||||
let visible_range = self.visible_range(height, scroll_top);
|
||||
self.scroll_handler.as_mut().unwrap()(visible_range, cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn logical_scroll_top(&self) -> ListOffset {
|
||||
self.logical_scroll_top
|
||||
.unwrap_or_else(|| match self.orientation {
|
||||
Orientation::Top => ListOffset {
|
||||
item_ix: 0,
|
||||
offset_in_item: 0.,
|
||||
},
|
||||
Orientation::Bottom => ListOffset {
|
||||
item_ix: self.items.summary().count,
|
||||
offset_in_item: 0.,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn scroll_top(&self, logical_scroll_top: &ListOffset) -> f32 {
|
||||
@@ -961,18 +933,6 @@ mod tests {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
_: &Event,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &mut (),
|
||||
_: &mut (),
|
||||
_: &mut EventContext,
|
||||
) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
_: Range<usize>,
|
||||
|
||||
@@ -6,11 +6,10 @@ use crate::{
|
||||
},
|
||||
platform::CursorStyle,
|
||||
scene::{
|
||||
ClickRegionEvent, CursorRegion, DownOutRegionEvent, DownRegionEvent, DragRegionEvent,
|
||||
HandlerSet, HoverRegionEvent, MoveRegionEvent, ScrollWheelRegionEvent, UpOutRegionEvent,
|
||||
UpRegionEvent,
|
||||
CursorRegion, HandlerSet, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseHover,
|
||||
MouseMove, MouseScrollWheel, MouseUp, MouseUpOut,
|
||||
},
|
||||
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MeasurementContext,
|
||||
DebugContext, Element, ElementBox, EventContext, LayoutContext, MeasurementContext,
|
||||
MouseButton, MouseRegion, MouseState, PaintContext, RenderContext, SizeConstraint, View,
|
||||
};
|
||||
use serde_json::json;
|
||||
@@ -22,27 +21,52 @@ pub struct MouseEventHandler<Tag: 'static> {
|
||||
cursor_style: Option<CursorStyle>,
|
||||
handlers: HandlerSet,
|
||||
hoverable: bool,
|
||||
notify_on_hover: bool,
|
||||
notify_on_click: bool,
|
||||
above: bool,
|
||||
padding: Padding,
|
||||
_tag: PhantomData<Tag>,
|
||||
}
|
||||
|
||||
/// Element which provides a render_child callback with a MouseState and paints a mouse
|
||||
/// region under (or above) it for easy mouse event handling.
|
||||
impl<Tag> MouseEventHandler<Tag> {
|
||||
pub fn new<V, F>(region_id: usize, cx: &mut RenderContext<V>, render_child: F) -> Self
|
||||
where
|
||||
V: View,
|
||||
F: FnOnce(MouseState, &mut RenderContext<V>) -> ElementBox,
|
||||
F: FnOnce(&mut MouseState, &mut RenderContext<V>) -> ElementBox,
|
||||
{
|
||||
let mut mouse_state = cx.mouse_state::<Tag>(region_id);
|
||||
let child = render_child(&mut mouse_state, cx);
|
||||
let notify_on_hover = mouse_state.accessed_hovered();
|
||||
let notify_on_click = mouse_state.accessed_clicked();
|
||||
Self {
|
||||
child: render_child(cx.mouse_state::<Tag>(region_id), cx),
|
||||
child,
|
||||
region_id,
|
||||
cursor_style: None,
|
||||
handlers: Default::default(),
|
||||
notify_on_hover,
|
||||
notify_on_click,
|
||||
hoverable: true,
|
||||
above: false,
|
||||
padding: Default::default(),
|
||||
_tag: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Modifies the MouseEventHandler to render the MouseRegion above the child element. Useful
|
||||
/// for drag and drop handling and similar events which should be captured before the child
|
||||
/// gets the opportunity
|
||||
pub fn above<V, F>(region_id: usize, cx: &mut RenderContext<V>, render_child: F) -> Self
|
||||
where
|
||||
V: View,
|
||||
F: FnOnce(&mut MouseState, &mut RenderContext<V>) -> ElementBox,
|
||||
{
|
||||
let mut handler = Self::new(region_id, cx, render_child);
|
||||
handler.above = true;
|
||||
handler
|
||||
}
|
||||
|
||||
pub fn with_cursor_style(mut self, cursor: CursorStyle) -> Self {
|
||||
self.cursor_style = Some(cursor);
|
||||
self
|
||||
@@ -53,10 +77,7 @@ impl<Tag> MouseEventHandler<Tag> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_move(
|
||||
mut self,
|
||||
handler: impl Fn(MoveRegionEvent, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
pub fn on_move(mut self, handler: impl Fn(MouseMove, &mut EventContext) + 'static) -> Self {
|
||||
self.handlers = self.handlers.on_move(handler);
|
||||
self
|
||||
}
|
||||
@@ -64,7 +85,7 @@ impl<Tag> MouseEventHandler<Tag> {
|
||||
pub fn on_down(
|
||||
mut self,
|
||||
button: MouseButton,
|
||||
handler: impl Fn(DownRegionEvent, &mut EventContext) + 'static,
|
||||
handler: impl Fn(MouseDown, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.handlers = self.handlers.on_down(button, handler);
|
||||
self
|
||||
@@ -73,7 +94,7 @@ impl<Tag> MouseEventHandler<Tag> {
|
||||
pub fn on_up(
|
||||
mut self,
|
||||
button: MouseButton,
|
||||
handler: impl Fn(UpRegionEvent, &mut EventContext) + 'static,
|
||||
handler: impl Fn(MouseUp, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.handlers = self.handlers.on_up(button, handler);
|
||||
self
|
||||
@@ -82,7 +103,7 @@ impl<Tag> MouseEventHandler<Tag> {
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
button: MouseButton,
|
||||
handler: impl Fn(ClickRegionEvent, &mut EventContext) + 'static,
|
||||
handler: impl Fn(MouseClick, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.handlers = self.handlers.on_click(button, handler);
|
||||
self
|
||||
@@ -91,7 +112,7 @@ impl<Tag> MouseEventHandler<Tag> {
|
||||
pub fn on_down_out(
|
||||
mut self,
|
||||
button: MouseButton,
|
||||
handler: impl Fn(DownOutRegionEvent, &mut EventContext) + 'static,
|
||||
handler: impl Fn(MouseDownOut, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.handlers = self.handlers.on_down_out(button, handler);
|
||||
self
|
||||
@@ -100,7 +121,7 @@ impl<Tag> MouseEventHandler<Tag> {
|
||||
pub fn on_up_out(
|
||||
mut self,
|
||||
button: MouseButton,
|
||||
handler: impl Fn(UpOutRegionEvent, &mut EventContext) + 'static,
|
||||
handler: impl Fn(MouseUpOut, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.handlers = self.handlers.on_up_out(button, handler);
|
||||
self
|
||||
@@ -109,23 +130,20 @@ impl<Tag> MouseEventHandler<Tag> {
|
||||
pub fn on_drag(
|
||||
mut self,
|
||||
button: MouseButton,
|
||||
handler: impl Fn(DragRegionEvent, &mut EventContext) + 'static,
|
||||
handler: impl Fn(MouseDrag, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.handlers = self.handlers.on_drag(button, handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_hover(
|
||||
mut self,
|
||||
handler: impl Fn(HoverRegionEvent, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
pub fn on_hover(mut self, handler: impl Fn(MouseHover, &mut EventContext) + 'static) -> Self {
|
||||
self.handlers = self.handlers.on_hover(handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_scroll(
|
||||
mut self,
|
||||
handler: impl Fn(ScrollWheelRegionEvent, &mut EventContext) + 'static,
|
||||
handler: impl Fn(MouseScrollWheel, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.handlers = self.handlers.on_scroll(handler);
|
||||
self
|
||||
@@ -148,6 +166,29 @@ impl<Tag> MouseEventHandler<Tag> {
|
||||
)
|
||||
.round_out()
|
||||
}
|
||||
|
||||
fn paint_regions(&self, bounds: RectF, visible_bounds: RectF, cx: &mut PaintContext) {
|
||||
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
|
||||
let hit_bounds = self.hit_bounds(visible_bounds);
|
||||
|
||||
if let Some(style) = self.cursor_style {
|
||||
cx.scene.push_cursor_region(CursorRegion {
|
||||
bounds: hit_bounds,
|
||||
style,
|
||||
});
|
||||
}
|
||||
cx.scene.push_mouse_region(
|
||||
MouseRegion::from_handlers::<Tag>(
|
||||
cx.current_view_id(),
|
||||
self.region_id,
|
||||
hit_bounds,
|
||||
self.handlers.clone(),
|
||||
)
|
||||
.with_hoverable(self.hoverable)
|
||||
.with_notify_on_hover(self.notify_on_hover)
|
||||
.with_notify_on_click(self.notify_on_click),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl<Tag> Element for MouseEventHandler<Tag> {
|
||||
@@ -169,38 +210,16 @@ impl<Tag> Element for MouseEventHandler<Tag> {
|
||||
_: &mut Self::LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) -> Self::PaintState {
|
||||
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
|
||||
let hit_bounds = self.hit_bounds(visible_bounds);
|
||||
if let Some(style) = self.cursor_style {
|
||||
cx.scene.push_cursor_region(CursorRegion {
|
||||
bounds: hit_bounds,
|
||||
style,
|
||||
if self.above {
|
||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||
|
||||
cx.paint_layer(None, |cx| {
|
||||
self.paint_regions(bounds, visible_bounds, cx);
|
||||
});
|
||||
} else {
|
||||
self.paint_regions(bounds, visible_bounds, cx);
|
||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||
}
|
||||
|
||||
cx.scene.push_mouse_region(
|
||||
MouseRegion::from_handlers::<Tag>(
|
||||
cx.current_view_id(),
|
||||
self.region_id,
|
||||
hit_bounds,
|
||||
self.handlers.clone(),
|
||||
)
|
||||
.with_hoverable(self.hoverable),
|
||||
);
|
||||
|
||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
_: &mut Self::PaintState,
|
||||
cx: &mut EventContext,
|
||||
) -> bool {
|
||||
self.child.dispatch_event(event, cx)
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
|
||||
@@ -4,8 +4,8 @@ use crate::{
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
json::ToJson,
|
||||
presenter::MeasurementContext,
|
||||
Axis, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion,
|
||||
PaintContext, SizeConstraint,
|
||||
Axis, DebugContext, Element, ElementBox, LayoutContext, MouseRegion, PaintContext,
|
||||
SizeConstraint,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -16,6 +16,7 @@ pub struct Overlay {
|
||||
fit_mode: OverlayFitMode,
|
||||
position_mode: OverlayPositionMode,
|
||||
hoverable: bool,
|
||||
z_index: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
@@ -82,6 +83,7 @@ impl Overlay {
|
||||
fit_mode: OverlayFitMode::None,
|
||||
position_mode: OverlayPositionMode::Window,
|
||||
hoverable: false,
|
||||
z_index: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +111,11 @@ impl Overlay {
|
||||
self.hoverable = hoverable;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_z_index(mut self, z_index: usize) -> Self {
|
||||
self.z_index = Some(z_index);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for Overlay {
|
||||
@@ -204,37 +211,24 @@ impl Element for Overlay {
|
||||
OverlayFitMode::None => {}
|
||||
}
|
||||
|
||||
cx.scene.push_stacking_context(None);
|
||||
cx.paint_stacking_context(None, self.z_index, |cx| {
|
||||
if self.hoverable {
|
||||
enum OverlayHoverCapture {}
|
||||
// Block hovers in lower stacking contexts
|
||||
cx.scene
|
||||
.push_mouse_region(MouseRegion::new::<OverlayHoverCapture>(
|
||||
cx.current_view_id(),
|
||||
cx.current_view_id(),
|
||||
bounds,
|
||||
));
|
||||
}
|
||||
|
||||
if self.hoverable {
|
||||
enum OverlayHoverCapture {}
|
||||
// Block hovers in lower stacking contexts
|
||||
cx.scene
|
||||
.push_mouse_region(MouseRegion::new::<OverlayHoverCapture>(
|
||||
cx.current_view_id(),
|
||||
cx.current_view_id(),
|
||||
bounds,
|
||||
));
|
||||
}
|
||||
|
||||
self.child.paint(
|
||||
bounds.origin(),
|
||||
RectF::new(Vector2F::zero(), cx.window_size),
|
||||
cx,
|
||||
);
|
||||
cx.scene.pop_stacking_context();
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
_: &mut Self::PaintState,
|
||||
cx: &mut EventContext,
|
||||
) -> bool {
|
||||
self.child.dispatch_event(event, cx)
|
||||
self.child.paint(
|
||||
bounds.origin(),
|
||||
RectF::new(Vector2F::zero(), cx.window_size),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user