Compare commits
151 Commits
add-theme-
...
v0.157.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6192aa1469 | ||
|
|
2b902c185e | ||
|
|
e2e95f2c49 | ||
|
|
5e3a02b3f3 | ||
|
|
bc768d8586 | ||
|
|
84caa0cf4c | ||
|
|
8445b4adfb | ||
|
|
86e2510414 | ||
|
|
5222a1162c | ||
|
|
be25c51c5b | ||
|
|
ef0eeb4853 | ||
|
|
4e0db8ba32 | ||
|
|
ed379fe233 | ||
|
|
eb933ce203 | ||
|
|
515f9a6c7d | ||
|
|
9c33d723f8 | ||
|
|
5b303e892a | ||
|
|
b6ba4fcc51 | ||
|
|
b703514d0e | ||
|
|
c674d73734 | ||
|
|
dbf986d37a | ||
|
|
a62a2fa8f7 | ||
|
|
f50bca7630 | ||
|
|
9c54bd1bd4 | ||
|
|
5d5c4b6677 | ||
|
|
e351148152 | ||
|
|
b0a9005163 | ||
|
|
801210cd50 | ||
|
|
f861479890 | ||
|
|
a95fb8f1f9 | ||
|
|
744891f15f | ||
|
|
f33019c885 | ||
|
|
7960468d8a | ||
|
|
5377674fc0 | ||
|
|
af9a595770 | ||
|
|
3f2de172ae | ||
|
|
77bf2ad0f1 | ||
|
|
3da1902e24 | ||
|
|
4139e2de23 | ||
|
|
ff7aa024ee | ||
|
|
d295c46433 | ||
|
|
4c7a6f5e7f | ||
|
|
dd44168cad | ||
|
|
5bb18adbe8 | ||
|
|
b2eb439f32 | ||
|
|
f0566d54eb | ||
|
|
be531653a4 | ||
|
|
fa85238c69 | ||
|
|
910a773b89 | ||
|
|
87cc208f9f | ||
|
|
b0a16a7601 | ||
|
|
3c91184726 | ||
|
|
d55f025906 | ||
|
|
c5d252b837 | ||
|
|
a15b10986a | ||
|
|
5387a6f7f9 | ||
|
|
8cdb9d6b85 | ||
|
|
7d380e9e18 | ||
|
|
60c12a8d06 | ||
|
|
11206a8444 | ||
|
|
c83690ff14 | ||
|
|
d1a758708d | ||
|
|
7c7151551a | ||
|
|
a3b63448df | ||
|
|
65c9b15796 | ||
|
|
25a97a6a2b | ||
|
|
5aa165c530 | ||
|
|
9c5bec5efb | ||
|
|
c03b8d6c48 | ||
|
|
67fbdbbed6 | ||
|
|
03c84466c2 | ||
|
|
59f0f4ac42 | ||
|
|
bd746145b0 | ||
|
|
1b06c70a76 | ||
|
|
06bd2431d2 | ||
|
|
200b2bf70a | ||
|
|
8376dd2011 | ||
|
|
c9bee9f81f | ||
|
|
1f31022cbe | ||
|
|
7608000df8 | ||
|
|
8f27ffda4d | ||
|
|
cee019b1ea | ||
|
|
01ad22683d | ||
|
|
dfe1e43832 | ||
|
|
e3a6f89e2d | ||
|
|
07e808d16f | ||
|
|
2f7430af70 | ||
|
|
d012e35b04 | ||
|
|
d695de4504 | ||
|
|
9702310737 | ||
|
|
bafd7ed000 | ||
|
|
37ded190cf | ||
|
|
a99750fd35 | ||
|
|
e2647025ac | ||
|
|
6635758009 | ||
|
|
8d6fa9526e | ||
|
|
fd22c9bef9 | ||
|
|
43d05a432b | ||
|
|
cac98b7bbf | ||
|
|
cddd7875a4 | ||
|
|
8c95b8d89a | ||
|
|
a9f816d5fb | ||
|
|
f7b3680e4d | ||
|
|
ded3d3fc14 | ||
|
|
ddcd45bb45 | ||
|
|
29796aa412 | ||
|
|
773ad6bfd1 | ||
|
|
dc85378b96 | ||
|
|
1e8297a469 | ||
|
|
9cd42427d8 | ||
|
|
df21fe174d | ||
|
|
c48d4dbc6b | ||
|
|
19b186671b | ||
|
|
e2d613a803 | ||
|
|
6f4385e737 | ||
|
|
9565a90528 | ||
|
|
3a5deb5c6f | ||
|
|
f809787275 | ||
|
|
778dedec6c | ||
|
|
7c4615519b | ||
|
|
0e8276560f | ||
|
|
209ebb0c65 | ||
|
|
a5f50e5c1e | ||
|
|
5aaaed52fc | ||
|
|
845991c0e5 | ||
|
|
167af4bc1d | ||
|
|
2cd12f84de | ||
|
|
028d7a624f | ||
|
|
cfd61f9337 | ||
|
|
21336eb124 | ||
|
|
8a18c94f33 | ||
|
|
82d3fcdf4b | ||
|
|
e01bc6765d | ||
|
|
fd94c2b3fd | ||
|
|
0ee1d7ab26 | ||
|
|
b3cdd2ccff | ||
|
|
e80cbab93f | ||
|
|
563a1dcbab | ||
|
|
7dcb0de28c | ||
|
|
9b148f3dcc | ||
|
|
d14e36b323 | ||
|
|
eb962b7bfc | ||
|
|
280b8a89ea | ||
|
|
051627c449 | ||
|
|
68d6177d37 | ||
|
|
1be24f7739 | ||
|
|
6336248c1a | ||
|
|
7ce8797d78 | ||
|
|
527c9097f8 | ||
|
|
72be8c5d14 | ||
|
|
8d795ff882 |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -7,9 +7,13 @@ on:
|
||||
- "v[0-9]+.[0-9]+.x"
|
||||
tags:
|
||||
- "v*"
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
|
||||
concurrency:
|
||||
# Allow only one workflow per any non-`main` branch.
|
||||
|
||||
6
.github/workflows/close_stale_issues.yml
vendored
6
.github/workflows/close_stale_issues.yml
vendored
@@ -14,16 +14,16 @@ jobs:
|
||||
stale-issue-message: >
|
||||
Hi there! 👋
|
||||
|
||||
We're working to clean up our issue tracker by closing older issues that might not be relevant anymore. Are you able to reproduce this issue in the latest version of Zed? If so, please let us know by commenting on this issue and we will keep it open; otherwise, we'll close it in 10 days. Feel free to open a new issue if you're seeing this message after the issue has been closed.
|
||||
We're working to clean up our issue tracker by closing older issues that might not be relevant anymore. Are you able to reproduce this issue in the latest version of Zed? If so, please let us know by commenting on this issue and we will keep it open; otherwise, we'll close it in 7 days. Feel free to open a new issue if you're seeing this message after the issue has been closed.
|
||||
|
||||
Thanks for your help!
|
||||
close-issue-message: "This issue was closed due to inactivity; feel free to open a new issue if you're still experiencing this problem!"
|
||||
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, feel free to ping a Zed team member to reopen this issue or open a new one."
|
||||
# We will increase `days-before-stale` to 365 on or after Jan 24th,
|
||||
# 2024. This date marks one year since migrating issues from
|
||||
# 'community' to 'zed' repository. The migration added activity to all
|
||||
# issues, preventing 365 days from working until then.
|
||||
days-before-stale: 180
|
||||
days-before-close: 10
|
||||
days-before-close: 7
|
||||
any-of-issue-labels: "defect,panic / crash"
|
||||
operations-per-run: 1000
|
||||
ascending: true
|
||||
|
||||
8
.github/workflows/deploy_cloudflare.yml
vendored
8
.github/workflows/deploy_cloudflare.yml
vendored
@@ -36,28 +36,28 @@ jobs:
|
||||
mdbook build ./docs --dest-dir=../target/deploy/docs/
|
||||
|
||||
- name: Deploy Docs
|
||||
uses: cloudflare/wrangler-action@f84a562284fc78278ff9052435d9526f9c718361 # v3
|
||||
uses: cloudflare/wrangler-action@168bc28b7078db16f6f1ecc26477fc2248592143 # v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: pages deploy target/deploy --project-name=docs
|
||||
|
||||
- name: Deploy Install
|
||||
uses: cloudflare/wrangler-action@f84a562284fc78278ff9052435d9526f9c718361 # v3
|
||||
uses: cloudflare/wrangler-action@168bc28b7078db16f6f1ecc26477fc2248592143 # v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: r2 object put -f script/install.sh zed-open-source-website-assets/install.sh
|
||||
|
||||
- name: Deploy Docs Workers
|
||||
uses: cloudflare/wrangler-action@f84a562284fc78278ff9052435d9526f9c718361 # v3
|
||||
uses: cloudflare/wrangler-action@168bc28b7078db16f6f1ecc26477fc2248592143 # v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: deploy .cloudflare/docs-proxy/src/worker.js
|
||||
|
||||
- name: Deploy Install Workers
|
||||
uses: cloudflare/wrangler-action@f84a562284fc78278ff9052435d9526f9c718361 # v3
|
||||
uses: cloudflare/wrangler-action@168bc28b7078db16f6f1ecc26477fc2248592143 # v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
|
||||
7
.github/workflows/docs.yml
vendored
7
.github/workflows/docs.yml
vendored
@@ -20,11 +20,14 @@ jobs:
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- run: |
|
||||
- name: Prettier Check on /docs
|
||||
working-directory: ./docs
|
||||
run: |
|
||||
pnpm dlx prettier . --check || {
|
||||
echo "To fix, run from the root of the zed repo:"
|
||||
echo " cd docs && pnpm dlx prettier . --write && cd .."
|
||||
false
|
||||
}
|
||||
|
||||
working-directory: ./docs
|
||||
- name: Check spelling
|
||||
run: script/check-spelling docs/
|
||||
|
||||
102
Cargo.lock
generated
102
Cargo.lock
generated
@@ -2282,9 +2282,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.18"
|
||||
version = "4.5.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3"
|
||||
checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -2292,9 +2292,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.18"
|
||||
version = "4.5.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b"
|
||||
checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -4145,6 +4145,7 @@ dependencies = [
|
||||
"snippet_provider",
|
||||
"task",
|
||||
"theme",
|
||||
"tokio",
|
||||
"toml 0.8.19",
|
||||
"ui",
|
||||
"url",
|
||||
@@ -5147,9 +5148,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "grid"
|
||||
version = "0.13.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d196ffc1627db18a531359249b2bf8416178d84b729f3cebeb278f285fb9b58c"
|
||||
checksum = "be136d9dacc2a13cc70bb6c8f902b414fb2641f8db1314637c6b7933411a8f82"
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
@@ -6301,6 +6302,7 @@ dependencies = [
|
||||
"strum 0.25.0",
|
||||
"text",
|
||||
"theme",
|
||||
"thiserror",
|
||||
"tiktoken-rs",
|
||||
"ui",
|
||||
"unindent",
|
||||
@@ -6367,7 +6369,6 @@ dependencies = [
|
||||
"node_runtime",
|
||||
"paths",
|
||||
"project",
|
||||
"protols-tree-sitter-proto",
|
||||
"regex",
|
||||
"rope",
|
||||
"rust-embed",
|
||||
@@ -7841,9 +7842,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.0"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
|
||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
@@ -8351,9 +8352,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "1.4.0"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
|
||||
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
|
||||
dependencies = [
|
||||
"diff",
|
||||
"yansi",
|
||||
@@ -8625,15 +8626,6 @@ version = "2.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94"
|
||||
|
||||
[[package]]
|
||||
name = "protols-tree-sitter-proto"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/zed-industries/tree-sitter-proto?rev=0848bd30a64be48772e15fbb9d5ba8c0cc5772ad#0848bd30a64be48772e15fbb9d5ba8c0cc5772ad"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psm"
|
||||
version = "0.1.21"
|
||||
@@ -8945,7 +8937,6 @@ dependencies = [
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"markdown",
|
||||
"menu",
|
||||
"ordered-float 2.10.1",
|
||||
"picker",
|
||||
@@ -9099,6 +9090,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"smol",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"util",
|
||||
]
|
||||
|
||||
@@ -9107,7 +9099,9 @@ name = "remote_server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"backtrace",
|
||||
"cargo_toml",
|
||||
"clap",
|
||||
"client",
|
||||
"clock",
|
||||
"env_logger",
|
||||
@@ -10061,9 +10055,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.127"
|
||||
version = "1.0.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad"
|
||||
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
|
||||
dependencies = [
|
||||
"indexmap 2.4.0",
|
||||
"itoa",
|
||||
@@ -10500,6 +10494,20 @@ dependencies = [
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "snippets_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
"paths",
|
||||
"picker",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.4.10"
|
||||
@@ -10590,6 +10598,7 @@ dependencies = [
|
||||
"libsqlite3-sys",
|
||||
"parking_lot",
|
||||
"smol",
|
||||
"sqlformat",
|
||||
"thread_local",
|
||||
"util",
|
||||
"uuid",
|
||||
@@ -10606,9 +10615,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlformat"
|
||||
version = "0.2.4"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f"
|
||||
checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790"
|
||||
dependencies = [
|
||||
"nom",
|
||||
"unicode_categories",
|
||||
@@ -10864,6 +10873,7 @@ dependencies = [
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"isahc_http_client",
|
||||
"language",
|
||||
"log",
|
||||
"menu",
|
||||
@@ -11262,6 +11272,7 @@ dependencies = [
|
||||
"project",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
@@ -11270,9 +11281,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "taffy"
|
||||
version = "0.4.4"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ec17858c2d465b2f734b798b920818a974faf0babb15d7fef81818a4b2d16f1"
|
||||
checksum = "9cb893bff0f80ae17d3a57e030622a967b8dbc90e38284d9b4b1442e23873c94"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"grid",
|
||||
@@ -11346,6 +11357,7 @@ dependencies = [
|
||||
name = "telemetry_events"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"language",
|
||||
"semantic_version",
|
||||
"serde",
|
||||
]
|
||||
@@ -11411,12 +11423,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
|
||||
checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef"
|
||||
dependencies = [
|
||||
"rustix 0.38.35",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -11488,7 +11500,6 @@ dependencies = [
|
||||
"palette",
|
||||
"parking_lot",
|
||||
"refineable",
|
||||
"regex",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -11496,7 +11507,6 @@ dependencies = [
|
||||
"serde_json_lenient",
|
||||
"serde_repr",
|
||||
"settings",
|
||||
"story",
|
||||
"util",
|
||||
"uuid",
|
||||
]
|
||||
@@ -11543,18 +11553,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.63"
|
||||
version = "1.0.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
|
||||
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.63"
|
||||
version = "1.0.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
|
||||
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -11733,6 +11743,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"recent_projects",
|
||||
"remote",
|
||||
"rpc",
|
||||
"serde",
|
||||
"settings",
|
||||
@@ -14105,6 +14116,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"postage",
|
||||
"project",
|
||||
"release_channel",
|
||||
"remote",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -14300,9 +14312,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "0.5.1"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||
|
||||
[[package]]
|
||||
name = "yazi"
|
||||
@@ -14385,7 +14397,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.156.0"
|
||||
version = "0.157.5"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -14469,6 +14481,7 @@ dependencies = [
|
||||
"simplelog",
|
||||
"smol",
|
||||
"snippet_provider",
|
||||
"snippets_ui",
|
||||
"supermaven",
|
||||
"sysinfo",
|
||||
"tab_switcher",
|
||||
@@ -14526,7 +14539,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_dart"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
@@ -14631,7 +14644,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_php"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
@@ -14643,6 +14656,13 @@ dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_proto"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_purescript"
|
||||
version = "0.0.1"
|
||||
|
||||
17
Cargo.toml
17
Cargo.toml
@@ -99,6 +99,7 @@ members = [
|
||||
"crates/settings_ui",
|
||||
"crates/snippet",
|
||||
"crates/snippet_provider",
|
||||
"crates/snippets_ui",
|
||||
"crates/sqlez",
|
||||
"crates/sqlez_macros",
|
||||
"crates/story",
|
||||
@@ -152,6 +153,7 @@ members = [
|
||||
"extensions/php",
|
||||
"extensions/perplexity",
|
||||
"extensions/prisma",
|
||||
"extensions/proto",
|
||||
"extensions/purescript",
|
||||
"extensions/ruff",
|
||||
"extensions/ruby",
|
||||
@@ -174,6 +176,7 @@ members = [
|
||||
default-members = ["crates/zed"]
|
||||
|
||||
[workspace.dependencies]
|
||||
|
||||
#
|
||||
# Workspace member crates
|
||||
#
|
||||
@@ -219,7 +222,6 @@ go_to_line = { path = "crates/go_to_line" }
|
||||
google_ai = { path = "crates/google_ai" }
|
||||
gpui = { path = "crates/gpui" }
|
||||
gpui_macros = { path = "crates/gpui_macros" }
|
||||
handlebars = "4.3"
|
||||
headless = { path = "crates/headless" }
|
||||
html_to_markdown = { path = "crates/html_to_markdown" }
|
||||
http_client = { path = "crates/http_client" }
|
||||
@@ -275,6 +277,7 @@ settings = { path = "crates/settings" }
|
||||
settings_ui = { path = "crates/settings_ui" }
|
||||
snippet = { path = "crates/snippet" }
|
||||
snippet_provider = { path = "crates/snippet_provider" }
|
||||
snippets_ui = { path = "crates/snippets_ui" }
|
||||
sqlez = { path = "crates/sqlez" }
|
||||
sqlez_macros = { path = "crates/sqlez_macros" }
|
||||
story = { path = "crates/story" }
|
||||
@@ -316,6 +319,7 @@ any_vec = "0.14"
|
||||
anyhow = "1.0.86"
|
||||
arrayvec = { version = "0.7.4", features = ["serde"] }
|
||||
ashpd = "0.9.1"
|
||||
async-compat = "0.2.1"
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
async-dispatcher = "0.1"
|
||||
async-fs = "1.6"
|
||||
@@ -354,10 +358,11 @@ futures-batch = "0.6.1"
|
||||
futures-lite = "1.13"
|
||||
git2 = { version = "0.19", default-features = false }
|
||||
globset = "0.4"
|
||||
handlebars = "4.3"
|
||||
heed = { version = "0.20.1", features = ["read-txn-no-tls"] }
|
||||
hex = "0.4.3"
|
||||
hyper = "0.14"
|
||||
html5ever = "0.27.0"
|
||||
hyper = "0.14"
|
||||
ignore = "0.4.22"
|
||||
image = "0.25.1"
|
||||
indexmap = { version = "1.6.2", features = ["serde"] }
|
||||
@@ -380,9 +385,9 @@ ordered-float = "2.1.1"
|
||||
palette = { version = "0.7.5", default-features = false, features = ["std"] }
|
||||
parking_lot = "0.12.1"
|
||||
pathdiff = "0.2"
|
||||
profiling = "1"
|
||||
postage = { version = "0.5", features = ["futures-traits"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
profiling = "1"
|
||||
prost = "0.9"
|
||||
prost-build = "0.9"
|
||||
prost-types = "0.9"
|
||||
@@ -416,6 +421,7 @@ similar = "1.3"
|
||||
simplelog = "0.12.2"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "1.2"
|
||||
sqlformat = "0.2"
|
||||
strsim = "0.11"
|
||||
strum = { version = "0.25.0", features = ["derive"] }
|
||||
subtle = "2.5.0"
|
||||
@@ -450,15 +456,14 @@ tree-sitter-html = "0.20"
|
||||
tree-sitter-jsdoc = "0.23"
|
||||
tree-sitter-json = "0.23"
|
||||
tree-sitter-md = { git = "https://github.com/zed-industries/tree-sitter-markdown", rev = "4cfa6aad6b75052a5077c80fd934757d9267d81b" }
|
||||
protols-tree-sitter-proto = { git = "https://github.com/zed-industries/tree-sitter-proto", rev = "0848bd30a64be48772e15fbb9d5ba8c0cc5772ad" }
|
||||
tree-sitter-python = "0.23"
|
||||
tree-sitter-regex = "0.23"
|
||||
tree-sitter-ruby = "0.23"
|
||||
tree-sitter-rust = "0.23"
|
||||
tree-sitter-typescript = "0.23"
|
||||
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" }
|
||||
unindent = "0.1.7"
|
||||
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" }
|
||||
unicase = "2.6"
|
||||
unindent = "0.1.7"
|
||||
unicode-segmentation = "1.10"
|
||||
url = "2.2"
|
||||
uuid = { version = "1.1.2", features = ["v4", "v5", "serde"] }
|
||||
|
||||
@@ -31,10 +31,12 @@ ENV GITHUB_SHA=$GITHUB_SHA
|
||||
# - Staging: `4f408ec65a3867278322a189b4eb20f1ab51f508`
|
||||
# - Production: `fc4c533d0a8c489e5636a4249d2b52a80039fbd7`
|
||||
#
|
||||
# Also add `cmake`, since we need it to build `wasmtime`.
|
||||
#
|
||||
# Installing these as a temporary workaround, but I think ideally we'd want to figure
|
||||
# out what caused them to be included in the first place.
|
||||
RUN apt-get update; \
|
||||
apt-get install -y --no-install-recommends libxkbcommon-dev libxkbcommon-x11-dev
|
||||
apt-get install -y --no-install-recommends libxkbcommon-dev libxkbcommon-x11-dev cmake
|
||||
|
||||
RUN --mount=type=cache,target=./script/node_modules \
|
||||
--mount=type=cache,target=/usr/local/cargo/registry \
|
||||
|
||||
1
assets/icons/trash_alt.svg
Normal file
1
assets/icons/trash_alt.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>
|
||||
|
After Width: | Height: | Size: 330 B |
@@ -440,7 +440,12 @@
|
||||
"cmd-k shift-right": ["workspace::SwapPaneInDirection", "Right"],
|
||||
"cmd-k shift-up": ["workspace::SwapPaneInDirection", "Up"],
|
||||
"cmd-k shift-down": ["workspace::SwapPaneInDirection", "Down"],
|
||||
"cmd-shift-x": "zed::Extensions",
|
||||
"cmd-shift-x": "zed::Extensions"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace && !Terminal",
|
||||
"bindings": {
|
||||
"alt-t": "task::Rerun",
|
||||
"alt-shift-t": "task::Spawn"
|
||||
}
|
||||
|
||||
@@ -50,6 +50,9 @@ And here's the section to rewrite based on that prompt again for reference:
|
||||
|
||||
{{#if diagnostic_errors}}
|
||||
{{#each diagnostic_errors}}
|
||||
|
||||
Below are the diagnostic errors visible to the user. If the user requests problems to be fixed, use this information, but do not try to fix these errors if the user hasn't asked you to.
|
||||
|
||||
<diagnostic_error>
|
||||
<line_number>{{line_number}}</line_number>
|
||||
<error_message>{{error_message}}</error_message>
|
||||
|
||||
@@ -356,9 +356,19 @@
|
||||
/// Scrollbar-related settings
|
||||
"scrollbar": {
|
||||
/// When to show the scrollbar in the project panel.
|
||||
/// This setting can take four values:
|
||||
///
|
||||
/// Default: always
|
||||
"show": "always"
|
||||
/// 1. null (default): Inherit editor settings
|
||||
/// 2. Show the scrollbar if there's important information or
|
||||
/// follow the system's configured behavior (default):
|
||||
/// "auto"
|
||||
/// 3. Match the system's configured behavior:
|
||||
/// "system"
|
||||
/// 4. Always show the scrollbar:
|
||||
/// "always"
|
||||
/// 5. Never show the scrollbar:
|
||||
/// "never"
|
||||
"show": null
|
||||
}
|
||||
},
|
||||
"outline_panel": {
|
||||
@@ -807,6 +817,7 @@
|
||||
// Different settings for specific languages.
|
||||
"languages": {
|
||||
"Astro": {
|
||||
"language_servers": ["astro-language-server", "..."],
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["prettier-plugin-astro"]
|
||||
@@ -830,6 +841,9 @@
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"Dart": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Elixir": {
|
||||
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
|
||||
},
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
allow-private-module-inception = true
|
||||
avoid-breaking-exported-api = false
|
||||
|
||||
@@ -10,7 +10,7 @@ use gpui::{
|
||||
use language::{
|
||||
LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId, LanguageServerName,
|
||||
};
|
||||
use project::{LanguageServerProgress, Project};
|
||||
use project::{EnvironmentErrorMessage, LanguageServerProgress, Project, WorktreeId};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
|
||||
use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle};
|
||||
@@ -175,7 +175,31 @@ impl ActivityIndicator {
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn pending_environment_errors<'a>(
|
||||
&'a self,
|
||||
cx: &'a AppContext,
|
||||
) -> impl Iterator<Item = (&'a WorktreeId, &'a EnvironmentErrorMessage)> {
|
||||
self.project.read(cx).shell_environment_errors(cx)
|
||||
}
|
||||
|
||||
fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Option<Content> {
|
||||
// Show if any direnv calls failed
|
||||
if let Some((&worktree_id, error)) = self.pending_environment_errors(cx).next() {
|
||||
return Some(Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::Warning)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: error.0.clone(),
|
||||
on_click: Some(Arc::new(move |this, cx| {
|
||||
this.project.update(cx, |project, cx| {
|
||||
project.remove_environment_error(cx, worktree_id);
|
||||
});
|
||||
cx.dispatch_action(Box::new(workspace::OpenLog));
|
||||
})),
|
||||
});
|
||||
}
|
||||
// Show any language server has pending activity.
|
||||
let mut pending_work = self.pending_language_server_work(cx);
|
||||
if let Some(PendingWork {
|
||||
|
||||
@@ -521,6 +521,10 @@ pub struct Usage {
|
||||
pub input_tokens: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub output_tokens: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_tokens: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_tokens: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
||||
@@ -77,7 +77,7 @@ use ui::TintColor;
|
||||
use ui::{
|
||||
prelude::*,
|
||||
utils::{format_distance_from_now, DateTimeType},
|
||||
Avatar, AvatarShape, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
|
||||
Avatar, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
|
||||
ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||
};
|
||||
use util::{maybe, ResultExt};
|
||||
@@ -262,9 +262,7 @@ impl PickerDelegate for SavedContextPickerDelegate {
|
||||
.gap_2()
|
||||
.children(if let Some(host_user) = host_user {
|
||||
vec![
|
||||
Avatar::new(host_user.avatar_uri.clone())
|
||||
.shape(AvatarShape::Circle)
|
||||
.into_any_element(),
|
||||
Avatar::new(host_user.avatar_uri.clone()).into_any_element(),
|
||||
Label::new(format!("Shared by @{}", host_user.github_login))
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
@@ -1498,6 +1496,13 @@ struct WorkflowAssist {
|
||||
|
||||
type MessageHeader = MessageMetadata;
|
||||
|
||||
#[derive(Clone)]
|
||||
enum AssistError {
|
||||
PaymentRequired,
|
||||
MaxMonthlySpendReached,
|
||||
Message(SharedString),
|
||||
}
|
||||
|
||||
pub struct ContextEditor {
|
||||
context: Model<Context>,
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -1516,7 +1521,7 @@ pub struct ContextEditor {
|
||||
workflow_steps: HashMap<Range<language::Anchor>, WorkflowStepViewState>,
|
||||
active_workflow_step: Option<ActiveWorkflowStep>,
|
||||
assistant_panel: WeakView<AssistantPanel>,
|
||||
error_message: Option<SharedString>,
|
||||
last_error: Option<AssistError>,
|
||||
show_accept_terms: bool,
|
||||
pub(crate) slash_menu_handle:
|
||||
PopoverMenuHandle<Picker<slash_command_picker::SlashCommandDelegate>>,
|
||||
@@ -1587,7 +1592,7 @@ impl ContextEditor {
|
||||
workflow_steps: HashMap::default(),
|
||||
active_workflow_step: None,
|
||||
assistant_panel,
|
||||
error_message: None,
|
||||
last_error: None,
|
||||
show_accept_terms: false,
|
||||
slash_menu_handle: Default::default(),
|
||||
dragged_file_worktrees: Vec::new(),
|
||||
@@ -1631,7 +1636,7 @@ impl ContextEditor {
|
||||
}
|
||||
|
||||
if !self.apply_active_workflow_step(cx) {
|
||||
self.error_message = None;
|
||||
self.last_error = None;
|
||||
self.send_to_model(cx);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1781,7 +1786,7 @@ impl ContextEditor {
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
|
||||
self.error_message = None;
|
||||
self.last_error = None;
|
||||
|
||||
if self
|
||||
.context
|
||||
@@ -2286,7 +2291,13 @@ impl ContextEditor {
|
||||
}
|
||||
ContextEvent::Operation(_) => {}
|
||||
ContextEvent::ShowAssistError(error_message) => {
|
||||
self.error_message = Some(error_message.clone());
|
||||
self.last_error = Some(AssistError::Message(error_message.clone()));
|
||||
}
|
||||
ContextEvent::ShowPaymentRequiredError => {
|
||||
self.last_error = Some(AssistError::PaymentRequired);
|
||||
}
|
||||
ContextEvent::ShowMaxMonthlySpendReachedError => {
|
||||
self.last_error = Some(AssistError::MaxMonthlySpendReached);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4300,6 +4311,154 @@ impl ContextEditor {
|
||||
focus_handle.dispatch_action(&Assist, cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
|
||||
let last_error = self.last_error.as_ref()?;
|
||||
|
||||
Some(
|
||||
div()
|
||||
.absolute()
|
||||
.right_3()
|
||||
.bottom_12()
|
||||
.max_w_96()
|
||||
.py_2()
|
||||
.px_3()
|
||||
.elevation_2(cx)
|
||||
.occlude()
|
||||
.child(match last_error {
|
||||
AssistError::PaymentRequired => self.render_payment_required_error(cx),
|
||||
AssistError::MaxMonthlySpendReached => {
|
||||
self.render_max_monthly_spend_reached_error(cx)
|
||||
}
|
||||
AssistError::Message(error_message) => {
|
||||
self.render_assist_error(error_message, cx)
|
||||
}
|
||||
})
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
|
||||
const ACCOUNT_URL: &str = "https://zed.dev/account";
|
||||
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_24()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(ERROR_MESSAGE)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.last_error = None;
|
||||
cx.open_url(ACCOUNT_URL);
|
||||
cx.notify();
|
||||
},
|
||||
)))
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.last_error = None;
|
||||
cx.notify();
|
||||
},
|
||||
))),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||
const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
|
||||
const ACCOUNT_URL: &str = "https://zed.dev/account";
|
||||
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_24()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(ERROR_MESSAGE)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.child(
|
||||
Button::new("subscribe", "Update Monthly Spend Limit").on_click(
|
||||
cx.listener(|this, _, cx| {
|
||||
this.last_error = None;
|
||||
cx.open_url(ACCOUNT_URL);
|
||||
cx.notify();
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.last_error = None;
|
||||
cx.notify();
|
||||
},
|
||||
))),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_assist_error(
|
||||
&self,
|
||||
error_message: &SharedString,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement {
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(
|
||||
Label::new("Error interacting with language model")
|
||||
.weight(FontWeight::MEDIUM),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_24()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(error_message.clone())),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.last_error = None;
|
||||
cx.notify();
|
||||
},
|
||||
))),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the contents of the *outermost* fenced code block that contains the given offset.
|
||||
@@ -4436,48 +4595,7 @@ impl Render for ContextEditor {
|
||||
.child(element),
|
||||
)
|
||||
})
|
||||
.when_some(self.error_message.clone(), |this, error_message| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.right_3()
|
||||
.bottom_12()
|
||||
.max_w_96()
|
||||
.py_2()
|
||||
.px_3()
|
||||
.elevation_2(cx)
|
||||
.occlude()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(
|
||||
Label::new("Error interacting with language model")
|
||||
.weight(FontWeight::MEDIUM),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_24()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(error_message)),
|
||||
)
|
||||
.child(h_flex().justify_end().mt_1().child(
|
||||
Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.error_message = None;
|
||||
cx.notify();
|
||||
},
|
||||
)),
|
||||
)),
|
||||
),
|
||||
)
|
||||
})
|
||||
.children(self.render_last_error(cx))
|
||||
.child(
|
||||
h_flex().w_full().relative().child(
|
||||
h_flex()
|
||||
|
||||
@@ -26,6 +26,7 @@ use gpui::{
|
||||
|
||||
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
|
||||
use language_model::{
|
||||
provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError},
|
||||
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
|
||||
LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role,
|
||||
@@ -46,7 +47,7 @@ use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use telemetry_events::{AssistantKind, AssistantPhase};
|
||||
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use text::BufferSnapshot;
|
||||
use util::{post_inc, ResultExt, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
@@ -294,6 +295,8 @@ impl ContextOperation {
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ContextEvent {
|
||||
ShowAssistError(SharedString),
|
||||
ShowPaymentRequiredError,
|
||||
ShowMaxMonthlySpendReachedError,
|
||||
MessagesEdited,
|
||||
SummaryChanged,
|
||||
StreamedCompletion,
|
||||
@@ -549,7 +552,7 @@ impl Context {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let buffer = cx.new_model(|_cx| {
|
||||
let mut buffer = Buffer::remote(
|
||||
let buffer = Buffer::remote(
|
||||
language::BufferId::new(1).unwrap(),
|
||||
replica_id,
|
||||
capability,
|
||||
@@ -2112,35 +2115,53 @@ impl Context {
|
||||
let result = stream_completion.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let error_message = result
|
||||
.as_ref()
|
||||
.err()
|
||||
.map(|error| error.to_string().trim().to_string());
|
||||
|
||||
if let Some(error_message) = error_message.as_ref() {
|
||||
cx.emit(ContextEvent::ShowAssistError(SharedString::from(
|
||||
error_message.clone(),
|
||||
)));
|
||||
}
|
||||
|
||||
this.update_metadata(assistant_message_id, cx, |metadata| {
|
||||
if let Some(error_message) = error_message.as_ref() {
|
||||
metadata.status =
|
||||
MessageStatus::Error(SharedString::from(error_message.clone()));
|
||||
let error_message = if let Some(error) = result.as_ref().err() {
|
||||
if error.is::<PaymentRequiredError>() {
|
||||
cx.emit(ContextEvent::ShowPaymentRequiredError);
|
||||
this.update_metadata(assistant_message_id, cx, |metadata| {
|
||||
metadata.status = MessageStatus::Canceled;
|
||||
});
|
||||
Some(error.to_string())
|
||||
} else if error.is::<MaxMonthlySpendReachedError>() {
|
||||
cx.emit(ContextEvent::ShowMaxMonthlySpendReachedError);
|
||||
this.update_metadata(assistant_message_id, cx, |metadata| {
|
||||
metadata.status = MessageStatus::Canceled;
|
||||
});
|
||||
Some(error.to_string())
|
||||
} else {
|
||||
metadata.status = MessageStatus::Done;
|
||||
let error_message = error.to_string().trim().to_string();
|
||||
cx.emit(ContextEvent::ShowAssistError(SharedString::from(
|
||||
error_message.clone(),
|
||||
)));
|
||||
this.update_metadata(assistant_message_id, cx, |metadata| {
|
||||
metadata.status =
|
||||
MessageStatus::Error(SharedString::from(error_message.clone()));
|
||||
});
|
||||
Some(error_message)
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.update_metadata(assistant_message_id, cx, |metadata| {
|
||||
metadata.status = MessageStatus::Done;
|
||||
});
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(telemetry) = this.telemetry.as_ref() {
|
||||
telemetry.report_assistant_event(
|
||||
Some(this.id.0.clone()),
|
||||
AssistantKind::Panel,
|
||||
AssistantPhase::Response,
|
||||
model.telemetry_id(),
|
||||
let language_name = this
|
||||
.buffer
|
||||
.read(cx)
|
||||
.language()
|
||||
.map(|language| language.name());
|
||||
telemetry.report_assistant_event(AssistantEvent {
|
||||
conversation_id: Some(this.id.0.clone()),
|
||||
kind: AssistantKind::Panel,
|
||||
phase: AssistantPhase::Response,
|
||||
model: model.telemetry_id(),
|
||||
model_provider: model.provider_id().to_string(),
|
||||
response_latency,
|
||||
error_message,
|
||||
);
|
||||
language_name,
|
||||
});
|
||||
}
|
||||
|
||||
if let Ok(stop_reason) = result {
|
||||
|
||||
@@ -50,6 +50,7 @@ use std::{
|
||||
task::{self, Poll},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use terminal_view::terminal_panel::TerminalPanel;
|
||||
use text::{OffsetRangeExt, ToPoint as _};
|
||||
use theme::ThemeSettings;
|
||||
@@ -209,18 +210,6 @@ impl InlineAssistant {
|
||||
initial_prompt: Option<String>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
if let Some(telemetry) = self.telemetry.as_ref() {
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||
telemetry.report_assistant_event(
|
||||
None,
|
||||
telemetry_events::AssistantKind::Inline,
|
||||
telemetry_events::AssistantPhase::Invoked,
|
||||
model.telemetry_id(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
|
||||
let mut selections = Vec::<Selection<Point>>::new();
|
||||
@@ -267,6 +256,21 @@ impl InlineAssistant {
|
||||
text_anchor: buffer.anchor_after(buffer_range.end),
|
||||
};
|
||||
codegen_ranges.push(start..end);
|
||||
|
||||
if let Some(telemetry) = self.telemetry.as_ref() {
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||
telemetry.report_assistant_event(AssistantEvent {
|
||||
conversation_id: None,
|
||||
kind: AssistantKind::Inline,
|
||||
phase: AssistantPhase::Invoked,
|
||||
model: model.telemetry_id(),
|
||||
model_provider: model.provider_id().to_string(),
|
||||
response_latency: None,
|
||||
error_message: None,
|
||||
language_name: buffer.language().map(|language| language.name()),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let assist_group_id = self.next_assist_group_id.post_inc();
|
||||
@@ -761,23 +765,34 @@ impl InlineAssistant {
|
||||
}
|
||||
|
||||
pub fn finish_assist(&mut self, assist_id: InlineAssistId, undo: bool, cx: &mut WindowContext) {
|
||||
if let Some(telemetry) = self.telemetry.as_ref() {
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||
telemetry.report_assistant_event(
|
||||
None,
|
||||
telemetry_events::AssistantKind::Inline,
|
||||
if undo {
|
||||
telemetry_events::AssistantPhase::Rejected
|
||||
} else {
|
||||
telemetry_events::AssistantPhase::Accepted
|
||||
},
|
||||
model.telemetry_id(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(assist) = self.assists.get(&assist_id) {
|
||||
if let Some(telemetry) = self.telemetry.as_ref() {
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||
let language_name = assist.editor.upgrade().and_then(|editor| {
|
||||
let multibuffer = editor.read(cx).buffer().read(cx);
|
||||
let ranges = multibuffer.range_to_buffer_ranges(assist.range.clone(), cx);
|
||||
ranges
|
||||
.first()
|
||||
.and_then(|(buffer, _, _)| buffer.read(cx).language())
|
||||
.map(|language| language.name())
|
||||
});
|
||||
telemetry.report_assistant_event(AssistantEvent {
|
||||
conversation_id: None,
|
||||
kind: AssistantKind::Inline,
|
||||
phase: if undo {
|
||||
AssistantPhase::Rejected
|
||||
} else {
|
||||
AssistantPhase::Accepted
|
||||
},
|
||||
model: model.telemetry_id(),
|
||||
model_provider: model.provider_id().to_string(),
|
||||
response_latency: None,
|
||||
error_message: None,
|
||||
language_name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let assist_group_id = assist.group_id;
|
||||
if self.assist_groups[&assist_group_id].linked {
|
||||
for assist_id in self.unlink_assist_group(assist_group_id, cx) {
|
||||
@@ -2706,6 +2721,7 @@ impl CodegenAlternative {
|
||||
self.edit_position = Some(self.range.start.bias_right(&self.snapshot));
|
||||
|
||||
let telemetry_id = model.telemetry_id();
|
||||
let provider_id = model.provider_id();
|
||||
let chunks: LocalBoxFuture<Result<BoxStream<Result<String>>>> =
|
||||
if user_prompt.trim().to_lowercase() == "delete" {
|
||||
async { Ok(stream::empty().boxed()) }.boxed_local()
|
||||
@@ -2716,7 +2732,7 @@ impl CodegenAlternative {
|
||||
.spawn(|_, cx| async move { model.stream_completion_text(request, &cx).await });
|
||||
async move { Ok(chunks.await?.boxed()) }.boxed_local()
|
||||
};
|
||||
self.handle_stream(telemetry_id, chunks, cx);
|
||||
self.handle_stream(telemetry_id, provider_id.to_string(), chunks, cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2780,6 +2796,7 @@ impl CodegenAlternative {
|
||||
pub fn handle_stream(
|
||||
&mut self,
|
||||
model_telemetry_id: String,
|
||||
model_provider_id: String,
|
||||
stream: impl 'static + Future<Output = Result<BoxStream<'static, Result<String>>>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
@@ -2810,6 +2827,15 @@ impl CodegenAlternative {
|
||||
}
|
||||
|
||||
let telemetry = self.telemetry.clone();
|
||||
let language_name = {
|
||||
let multibuffer = self.buffer.read(cx);
|
||||
let ranges = multibuffer.range_to_buffer_ranges(self.range.clone(), cx);
|
||||
ranges
|
||||
.first()
|
||||
.and_then(|(buffer, _, _)| buffer.read(cx).language())
|
||||
.map(|language| language.name())
|
||||
};
|
||||
|
||||
self.diff = Diff::default();
|
||||
self.status = CodegenStatus::Pending;
|
||||
let mut edit_start = self.range.start.to_offset(&snapshot);
|
||||
@@ -2920,14 +2946,16 @@ impl CodegenAlternative {
|
||||
let error_message =
|
||||
result.as_ref().err().map(|error| error.to_string());
|
||||
if let Some(telemetry) = telemetry {
|
||||
telemetry.report_assistant_event(
|
||||
None,
|
||||
telemetry_events::AssistantKind::Inline,
|
||||
telemetry_events::AssistantPhase::Response,
|
||||
model_telemetry_id,
|
||||
telemetry.report_assistant_event(AssistantEvent {
|
||||
conversation_id: None,
|
||||
kind: AssistantKind::Inline,
|
||||
phase: AssistantPhase::Response,
|
||||
model: model_telemetry_id,
|
||||
model_provider: model_provider_id.to_string(),
|
||||
response_latency,
|
||||
error_message,
|
||||
);
|
||||
language_name,
|
||||
});
|
||||
}
|
||||
|
||||
result?;
|
||||
@@ -3539,6 +3567,7 @@ mod tests {
|
||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.handle_stream(
|
||||
String::new(),
|
||||
String::new(),
|
||||
future::ready(Ok(chunks_rx.map(Ok).boxed())),
|
||||
cx,
|
||||
@@ -3610,6 +3639,7 @@ mod tests {
|
||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.handle_stream(
|
||||
String::new(),
|
||||
String::new(),
|
||||
future::ready(Ok(chunks_rx.map(Ok).boxed())),
|
||||
cx,
|
||||
@@ -3684,6 +3714,7 @@ mod tests {
|
||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.handle_stream(
|
||||
String::new(),
|
||||
String::new(),
|
||||
future::ready(Ok(chunks_rx.map(Ok).boxed())),
|
||||
cx,
|
||||
@@ -3757,6 +3788,7 @@ mod tests {
|
||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.handle_stream(
|
||||
String::new(),
|
||||
String::new(),
|
||||
future::ready(Ok(chunks_rx.map(Ok).boxed())),
|
||||
cx,
|
||||
@@ -3820,6 +3852,7 @@ mod tests {
|
||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.handle_stream(
|
||||
String::new(),
|
||||
String::new(),
|
||||
future::ready(Ok(chunks_rx.map(Ok).boxed())),
|
||||
cx,
|
||||
|
||||
@@ -910,7 +910,7 @@ impl PromptLibrary {
|
||||
.features
|
||||
.clone(),
|
||||
font_size: HeadlineSize::Large
|
||||
.size()
|
||||
.rems()
|
||||
.into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: relative(
|
||||
|
||||
@@ -31,11 +31,11 @@ impl SlashCommand for AutoCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Automatically infer what context to add, based on your prompt".into()
|
||||
"Automatically infer what context to add".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Automatically Infer Context".into()
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn label(&self, cx: &AppContext) -> CodeLabel {
|
||||
|
||||
@@ -19,11 +19,11 @@ impl SlashCommand for DeltaSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"re-insert changed files".into()
|
||||
"Re-insert changed files".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Re-insert Changed Files".into()
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -95,7 +95,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert Diagnostics".into()
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -104,11 +104,11 @@ impl SlashCommand for FetchSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert URL contents".into()
|
||||
"Insert fetched URL contents".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert fetched URL contents".into()
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -110,11 +110,11 @@ impl SlashCommand for FileSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert file".into()
|
||||
"Insert file".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert File".into()
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -19,11 +19,11 @@ impl SlashCommand for NowSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert the current date and time".into()
|
||||
"Insert current date and time".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert Current Date and Time".into()
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -47,11 +47,11 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Generate semantic searches based on the current context".into()
|
||||
"Generate a semantic search based on context".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Project Context".into()
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -16,11 +16,11 @@ impl SlashCommand for PromptSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert prompt from library".into()
|
||||
"Insert prompt from library".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert Prompt from Library".into()
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -34,11 +34,11 @@ impl SlashCommand for SearchSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"semantic search".into()
|
||||
"Search your project semantically".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Semantic Search".into()
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -17,11 +17,11 @@ impl SlashCommand for OutlineSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert symbols for active tab".into()
|
||||
"Insert symbols for active tab".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert Symbols for Active Tab".into()
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
|
||||
@@ -24,11 +24,11 @@ impl SlashCommand for TabSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert open tabs (active tab by default)".to_owned()
|
||||
"Insert open tabs (active tab by default)".to_owned()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert Open Tabs".to_owned()
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -29,11 +29,11 @@ impl SlashCommand for TerminalSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert terminal output".into()
|
||||
"Insert terminal output".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert Terminal Output".into()
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -29,11 +29,11 @@ impl SlashCommand for WorkflowSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert a prompt that opts into the edit workflow".into()
|
||||
"Insert prompt to opt into the edit workflow".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert Workflow Prompt".into()
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -184,7 +184,7 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
h_flex()
|
||||
.group(format!("command-entry-label-{ix}"))
|
||||
.w_full()
|
||||
.min_w(px(220.))
|
||||
.min_w(px(250.))
|
||||
.child(
|
||||
v_flex()
|
||||
.child(
|
||||
@@ -203,7 +203,9 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
div()
|
||||
.font_buffer(cx)
|
||||
.child(
|
||||
Label::new(args).size(LabelSize::Small),
|
||||
Label::new(args)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.visible_on_hover(format!(
|
||||
"command-entry-label-{ix}"
|
||||
|
||||
@@ -25,6 +25,7 @@ use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use terminal::Terminal;
|
||||
use terminal_view::TerminalView;
|
||||
use theme::ThemeSettings;
|
||||
@@ -1039,6 +1040,7 @@ impl Codegen {
|
||||
self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
|
||||
self.generation = cx.spawn(|this, mut cx| async move {
|
||||
let model_telemetry_id = model.telemetry_id();
|
||||
let model_provider_id = model.provider_id();
|
||||
let response = model.stream_completion_text(prompt, &cx).await;
|
||||
let generate = async {
|
||||
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
|
||||
@@ -1063,14 +1065,16 @@ impl Codegen {
|
||||
|
||||
let error_message = result.as_ref().err().map(|error| error.to_string());
|
||||
if let Some(telemetry) = telemetry {
|
||||
telemetry.report_assistant_event(
|
||||
None,
|
||||
telemetry_events::AssistantKind::Inline,
|
||||
telemetry_events::AssistantPhase::Response,
|
||||
model_telemetry_id,
|
||||
telemetry.report_assistant_event(AssistantEvent {
|
||||
conversation_id: None,
|
||||
kind: AssistantKind::Inline,
|
||||
phase: AssistantPhase::Response,
|
||||
model: model_telemetry_id,
|
||||
model_provider: model_provider_id.to_string(),
|
||||
response_latency,
|
||||
error_message,
|
||||
);
|
||||
language_name: None,
|
||||
});
|
||||
}
|
||||
|
||||
result?;
|
||||
|
||||
@@ -58,27 +58,32 @@ struct Args {
|
||||
dev_server_token: Option<String>,
|
||||
}
|
||||
|
||||
fn parse_path_with_position(argument_str: &str) -> Result<String, std::io::Error> {
|
||||
let path = PathWithPosition::parse_str(argument_str);
|
||||
let curdir = env::current_dir()?;
|
||||
|
||||
let canonicalized = path.map_path(|path| match fs::canonicalize(&path) {
|
||||
Ok(path) => Ok(path),
|
||||
Err(e) => {
|
||||
if let Some(mut parent) = path.parent() {
|
||||
if parent == Path::new("") {
|
||||
parent = &curdir
|
||||
fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
|
||||
let canonicalized = match Path::new(argument_str).canonicalize() {
|
||||
Ok(existing_path) => PathWithPosition::from_path(existing_path),
|
||||
Err(_) => {
|
||||
let path = PathWithPosition::parse_str(argument_str);
|
||||
let curdir = env::current_dir().context("reteiving current directory")?;
|
||||
path.map_path(|path| match fs::canonicalize(&path) {
|
||||
Ok(path) => Ok(path),
|
||||
Err(e) => {
|
||||
if let Some(mut parent) = path.parent() {
|
||||
if parent == Path::new("") {
|
||||
parent = &curdir
|
||||
}
|
||||
match fs::canonicalize(parent) {
|
||||
Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
|
||||
Err(_) => Err(e),
|
||||
}
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
match fs::canonicalize(parent) {
|
||||
Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
|
||||
Err(_) => Err(e),
|
||||
}
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
})?;
|
||||
Ok(canonicalized.to_string(|path| path.display().to_string()))
|
||||
.with_context(|| format!("parsing as path with position {argument_str}"))?,
|
||||
};
|
||||
Ok(canonicalized.to_string(|path| path.to_string_lossy().to_string()))
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
|
||||
@@ -394,7 +394,7 @@ pub struct PendingEntitySubscription<T: 'static> {
|
||||
}
|
||||
|
||||
impl<T: 'static> PendingEntitySubscription<T> {
|
||||
pub fn set_model(mut self, model: &Model<T>, cx: &mut AsyncAppContext) -> Subscription {
|
||||
pub fn set_model(mut self, model: &Model<T>, cx: &AsyncAppContext) -> Subscription {
|
||||
self.consumed = true;
|
||||
let mut handlers = self.client.handler_set.lock();
|
||||
let id = (TypeId::of::<T>(), self.remote_id);
|
||||
@@ -1752,7 +1752,7 @@ impl CredentialsProvider for KeychainCredentialsProvider {
|
||||
}
|
||||
|
||||
/// prefix for the zed:// url scheme
|
||||
pub static ZED_URL_SCHEME: &str = "zed";
|
||||
pub const ZED_URL_SCHEME: &str = "zed";
|
||||
|
||||
/// Parses the given link into a Zed link.
|
||||
///
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
mod event_coalescer;
|
||||
|
||||
use crate::{ChannelId, TelemetrySettings};
|
||||
use anyhow::Result;
|
||||
use chrono::{DateTime, Utc};
|
||||
use clock::SystemClock;
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::Future;
|
||||
use gpui::{AppContext, BackgroundExecutor, Task};
|
||||
use http_client::{self, HttpClient, HttpClientWithUrl, Method};
|
||||
use http_client::{self, AsyncBody, HttpClient, HttpClientWithUrl, Method, Request};
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::Mutex;
|
||||
use release_channel::ReleaseChannel;
|
||||
@@ -16,9 +17,9 @@ use std::io::Write;
|
||||
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
|
||||
use sysinfo::{CpuRefreshKind, Pid, ProcessRefreshKind, RefreshKind, System};
|
||||
use telemetry_events::{
|
||||
ActionEvent, AppEvent, AssistantEvent, AssistantKind, AssistantPhase, CallEvent, CpuEvent,
|
||||
EditEvent, EditorEvent, Event, EventRequestBody, EventWrapper, ExtensionEvent,
|
||||
InlineCompletionEvent, MemoryEvent, ReplEvent, SettingEvent,
|
||||
ActionEvent, AppEvent, AssistantEvent, CallEvent, CpuEvent, EditEvent, EditorEvent, Event,
|
||||
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, MemoryEvent, ReplEvent,
|
||||
SettingEvent,
|
||||
};
|
||||
use tempfile::NamedTempFile;
|
||||
#[cfg(not(debug_assertions))]
|
||||
@@ -288,7 +289,7 @@ impl Telemetry {
|
||||
system_id: Option<String>,
|
||||
installation_id: Option<String>,
|
||||
session_id: String,
|
||||
cx: &mut AppContext,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
let mut state = self.state.lock();
|
||||
state.system_id = system_id.map(|id| id.into());
|
||||
@@ -364,6 +365,7 @@ impl Telemetry {
|
||||
operation: &'static str,
|
||||
copilot_enabled: bool,
|
||||
copilot_enabled_for_language: bool,
|
||||
is_via_ssh: bool,
|
||||
) {
|
||||
let event = Event::Editor(EditorEvent {
|
||||
file_extension,
|
||||
@@ -371,6 +373,7 @@ impl Telemetry {
|
||||
operation: operation.into(),
|
||||
copilot_enabled,
|
||||
copilot_enabled_for_language,
|
||||
is_via_ssh,
|
||||
});
|
||||
|
||||
self.report_event(event)
|
||||
@@ -391,25 +394,8 @@ impl Telemetry {
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_assistant_event(
|
||||
self: &Arc<Self>,
|
||||
conversation_id: Option<String>,
|
||||
kind: AssistantKind,
|
||||
phase: AssistantPhase,
|
||||
model: String,
|
||||
response_latency: Option<Duration>,
|
||||
error_message: Option<String>,
|
||||
) {
|
||||
let event = Event::Assistant(AssistantEvent {
|
||||
conversation_id,
|
||||
kind,
|
||||
phase,
|
||||
model: model.to_string(),
|
||||
response_latency,
|
||||
error_message,
|
||||
});
|
||||
|
||||
self.report_event(event)
|
||||
pub fn report_assistant_event(self: &Arc<Self>, event: AssistantEvent) {
|
||||
self.report_event(Event::Assistant(event));
|
||||
}
|
||||
|
||||
pub fn report_call_event(
|
||||
@@ -473,7 +459,7 @@ impl Telemetry {
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn log_edit_event(self: &Arc<Self>, environment: &'static str) {
|
||||
pub fn log_edit_event(self: &Arc<Self>, environment: &'static str, is_via_ssh: bool) {
|
||||
let mut state = self.state.lock();
|
||||
let period_data = state.event_coalescer.log_event(environment);
|
||||
drop(state);
|
||||
@@ -482,6 +468,7 @@ impl Telemetry {
|
||||
let event = Event::Edit(EditEvent {
|
||||
duration: end.timestamp_millis() - start.timestamp_millis(),
|
||||
environment: environment.to_string(),
|
||||
is_via_ssh,
|
||||
});
|
||||
|
||||
self.report_event(event);
|
||||
@@ -502,7 +489,7 @@ impl Telemetry {
|
||||
worktree_id: WorktreeId,
|
||||
updated_entries_set: &UpdatedEntriesSet,
|
||||
) {
|
||||
let project_names: Vec<String> = {
|
||||
let project_type_names: Vec<String> = {
|
||||
let mut state = self.state.lock();
|
||||
state
|
||||
.worktree_id_map
|
||||
@@ -538,8 +525,8 @@ impl Telemetry {
|
||||
};
|
||||
|
||||
// Done on purpose to avoid calling `self.state.lock()` multiple times
|
||||
for project_name in project_names {
|
||||
self.report_app_event(format!("open {} project", project_name));
|
||||
for project_type_name in project_type_names {
|
||||
self.report_app_event(format!("open {} project", project_type_name));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -611,6 +598,29 @@ impl Telemetry {
|
||||
self.state.lock().is_staff
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
self: &Arc<Self>,
|
||||
// We take in the JSON bytes buffer so we can reuse the existing allocation.
|
||||
mut json_bytes: Vec<u8>,
|
||||
event_request: EventRequestBody,
|
||||
) -> Result<Request<AsyncBody>> {
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(&mut json_bytes, &event_request)?;
|
||||
|
||||
let checksum = calculate_json_checksum(&json_bytes).unwrap_or("".to_string());
|
||||
|
||||
Ok(Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri(
|
||||
self.http_client
|
||||
.build_zed_api_url("/telemetry/events", &[])?
|
||||
.as_ref(),
|
||||
)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("x-zed-checksum", checksum)
|
||||
.body(json_bytes.into())?)
|
||||
}
|
||||
|
||||
pub fn flush_events(self: &Arc<Self>) {
|
||||
let mut state = self.state.lock();
|
||||
state.first_event_date_time = None;
|
||||
@@ -637,10 +647,10 @@ impl Telemetry {
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let request_body = {
|
||||
let state = this.state.lock();
|
||||
|
||||
let request_body = EventRequestBody {
|
||||
EventRequestBody {
|
||||
system_id: state.system_id.as_deref().map(Into::into),
|
||||
installation_id: state.installation_id.as_deref().map(Into::into),
|
||||
session_id: state.session_id.clone(),
|
||||
@@ -653,25 +663,11 @@ impl Telemetry {
|
||||
|
||||
release_channel: state.release_channel.map(Into::into),
|
||||
events,
|
||||
};
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(&mut json_bytes, &request_body)?;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let checksum = calculate_json_checksum(&json_bytes).unwrap_or("".to_string());
|
||||
|
||||
let request = http_client::Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri(
|
||||
this.http_client
|
||||
.build_zed_api_url("/telemetry/events", &[])?
|
||||
.as_ref(),
|
||||
)
|
||||
.header("Content-Type", "text/plain")
|
||||
.header("x-zed-checksum", checksum)
|
||||
.body(json_bytes.into());
|
||||
|
||||
let response = this.http_client.send(request?).await?;
|
||||
let request = this.build_request(json_bytes, request_body)?;
|
||||
let response = this.http_client.send(request).await?;
|
||||
if response.status() != 200 {
|
||||
log::error!("Failed to send events: HTTP {:?}", response.status());
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ enum UpdateContacts {
|
||||
}
|
||||
|
||||
impl UserStore {
|
||||
pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
|
||||
pub fn new(client: Arc<Client>, cx: &ModelContext<Self>) -> Self {
|
||||
let (mut current_user_tx, current_user_rx) = watch::channel();
|
||||
let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded();
|
||||
let rpc_subscriptions = vec![
|
||||
@@ -310,7 +310,7 @@ impl UserStore {
|
||||
fn update_contacts(
|
||||
&mut self,
|
||||
message: UpdateContacts,
|
||||
cx: &mut ModelContext<Self>,
|
||||
cx: &ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
match message {
|
||||
UpdateContacts::Wait(barrier) => {
|
||||
@@ -525,9 +525,9 @@ impl UserStore {
|
||||
}
|
||||
|
||||
pub fn dismiss_contact_request(
|
||||
&mut self,
|
||||
&self,
|
||||
requester_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
cx: &ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.upgrade();
|
||||
cx.spawn(move |_, _| async move {
|
||||
@@ -573,7 +573,7 @@ impl UserStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clear_contacts(&mut self) -> impl Future<Output = ()> {
|
||||
pub fn clear_contacts(&self) -> impl Future<Output = ()> {
|
||||
let (tx, mut rx) = postage::barrier::channel();
|
||||
self.update_contacts_tx
|
||||
.unbounded_send(UpdateContacts::Clear(tx))
|
||||
@@ -583,7 +583,7 @@ impl UserStore {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contact_updates_done(&mut self) -> impl Future<Output = ()> {
|
||||
pub fn contact_updates_done(&self) -> impl Future<Output = ()> {
|
||||
let (tx, mut rx) = postage::barrier::channel();
|
||||
self.update_contacts_tx
|
||||
.unbounded_send(UpdateContacts::Wait(tx))
|
||||
@@ -594,9 +594,9 @@ impl UserStore {
|
||||
}
|
||||
|
||||
pub fn get_users(
|
||||
&mut self,
|
||||
&self,
|
||||
user_ids: Vec<u64>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
cx: &ModelContext<Self>,
|
||||
) -> Task<Result<Vec<Arc<User>>>> {
|
||||
let mut user_ids_to_fetch = user_ids.clone();
|
||||
user_ids_to_fetch.retain(|id| !self.users.contains_key(id));
|
||||
@@ -629,9 +629,9 @@ impl UserStore {
|
||||
}
|
||||
|
||||
pub fn fuzzy_search_users(
|
||||
&mut self,
|
||||
&self,
|
||||
query: String,
|
||||
cx: &mut ModelContext<Self>,
|
||||
cx: &ModelContext<Self>,
|
||||
) -> Task<Result<Vec<Arc<User>>>> {
|
||||
self.load_users(proto::FuzzySearchUsers { query }, cx)
|
||||
}
|
||||
@@ -640,11 +640,7 @@ impl UserStore {
|
||||
self.users.get(&user_id).cloned()
|
||||
}
|
||||
|
||||
pub fn get_user_optimistic(
|
||||
&mut self,
|
||||
user_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<Arc<User>> {
|
||||
pub fn get_user_optimistic(&self, user_id: u64, cx: &ModelContext<Self>) -> Option<Arc<User>> {
|
||||
if let Some(user) = self.users.get(&user_id).cloned() {
|
||||
return Some(user);
|
||||
}
|
||||
@@ -653,11 +649,7 @@ impl UserStore {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_user(
|
||||
&mut self,
|
||||
user_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Arc<User>>> {
|
||||
pub fn get_user(&self, user_id: u64, cx: &ModelContext<Self>) -> Task<Result<Arc<User>>> {
|
||||
if let Some(user) = self.users.get(&user_id).cloned() {
|
||||
return Task::ready(Ok(user));
|
||||
}
|
||||
@@ -697,7 +689,7 @@ impl UserStore {
|
||||
.map(|accepted_tos_at| accepted_tos_at.is_some())
|
||||
}
|
||||
|
||||
pub fn accept_terms_of_service(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
pub fn accept_terms_of_service(&self, cx: &ModelContext<Self>) -> Task<Result<()>> {
|
||||
if self.current_user().is_none() {
|
||||
return Task::ready(Err(anyhow!("no current user")));
|
||||
};
|
||||
@@ -726,9 +718,9 @@ impl UserStore {
|
||||
}
|
||||
|
||||
fn load_users(
|
||||
&mut self,
|
||||
&self,
|
||||
request: impl RequestMessage<Response = UsersResponse>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
cx: &ModelContext<Self>,
|
||||
) -> Task<Result<Vec<Arc<User>>>> {
|
||||
let client = self.client.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
|
||||
@@ -216,10 +216,11 @@ impl fmt::Debug for Global {
|
||||
if timestamp.replica_id > 0 {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
write!(f, "{}: {}", timestamp.replica_id, timestamp.value)?;
|
||||
}
|
||||
if self.local_branch_value > 0 {
|
||||
write!(f, "<branch>: {}", self.local_branch_value)?;
|
||||
if timestamp.replica_id == LOCAL_BRANCH_REPLICA_ID {
|
||||
write!(f, "<branch>: {}", timestamp.value)?;
|
||||
} else {
|
||||
write!(f, "{}: {}", timestamp.replica_id, timestamp.value)?;
|
||||
}
|
||||
}
|
||||
write!(f, "}}")
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ axum = { version = "0.6", features = ["json", "headers", "ws"] }
|
||||
axum-extra = { version = "0.4", features = ["erased-json"] }
|
||||
base64.workspace = true
|
||||
chrono.workspace = true
|
||||
clock.workspace = true
|
||||
clickhouse.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
dashmap.workspace = true
|
||||
envy = "0.4.2"
|
||||
@@ -43,13 +43,13 @@ live_kit_server.workspace = true
|
||||
log.workspace = true
|
||||
nanoid.workspace = true
|
||||
open_ai.workspace = true
|
||||
supermaven_api.workspace = true
|
||||
parking_lot.workspace = true
|
||||
prometheus = "0.13"
|
||||
prost.workspace = true
|
||||
rand.workspace = true
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
rpc.workspace = true
|
||||
rustc-demangle.workspace = true
|
||||
scrypt = "0.11"
|
||||
sea-orm = { version = "1.1.0-rc.1", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
|
||||
semantic_version.workspace = true
|
||||
@@ -61,7 +61,7 @@ sha2.workspace = true
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }
|
||||
strum.workspace = true
|
||||
subtle.workspace = true
|
||||
rustc-demangle.workspace = true
|
||||
supermaven_api.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
text.workspace = true
|
||||
thiserror.workspace = true
|
||||
@@ -85,6 +85,7 @@ client = { workspace = true, features = ["test-support"] }
|
||||
collab_ui = { workspace = true, features = ["test-support"] }
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
ctor.workspace = true
|
||||
dev_server_projects.workspace = true
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
file_finder.workspace = true
|
||||
@@ -92,6 +93,7 @@ fs = { workspace = true, features = ["test-support"] }
|
||||
git = { workspace = true, features = ["test-support"] }
|
||||
git_hosting_providers.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
headless.workspace = true
|
||||
hyper.workspace = true
|
||||
indoc.workspace = true
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
@@ -108,7 +110,6 @@ recent_projects = { workspace = true }
|
||||
release_channel.workspace = true
|
||||
remote = { workspace = true, features = ["test-support"] }
|
||||
remote_server.workspace = true
|
||||
dev_server_projects.workspace = true
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
sea-orm = { version = "1.1.0-rc.1", features = ["sqlx-sqlite"] }
|
||||
serde_json.workspace = true
|
||||
@@ -120,7 +121,6 @@ unindent.workspace = true
|
||||
util.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
worktree = { workspace = true, features = ["test-support"] }
|
||||
headless.workspace = true
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["async-stripe"]
|
||||
|
||||
@@ -112,6 +112,7 @@ CREATE TABLE "worktree_settings_files" (
|
||||
"worktree_id" INTEGER NOT NULL,
|
||||
"path" VARCHAR NOT NULL,
|
||||
"content" TEXT,
|
||||
"kind" VARCHAR,
|
||||
PRIMARY KEY(project_id, worktree_id, path),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "worktree_settings_files" ADD COLUMN "kind" VARCHAR;
|
||||
@@ -0,0 +1,11 @@
|
||||
alter table models
|
||||
add column price_per_million_cache_creation_input_tokens integer not null default 0,
|
||||
add column price_per_million_cache_read_input_tokens integer not null default 0;
|
||||
|
||||
alter table usages
|
||||
add column cache_creation_input_tokens_this_month bigint not null default 0,
|
||||
add column cache_read_input_tokens_this_month bigint not null default 0;
|
||||
|
||||
alter table lifetime_usages
|
||||
add column cache_creation_input_tokens bigint not null default 0,
|
||||
add column cache_read_input_tokens bigint not null default 0;
|
||||
@@ -0,0 +1,3 @@
|
||||
alter table usages
|
||||
drop column cache_creation_input_tokens_this_month,
|
||||
drop column cache_read_input_tokens_this_month;
|
||||
@@ -0,0 +1,13 @@
|
||||
create table monthly_usages (
|
||||
id serial primary key,
|
||||
user_id integer not null,
|
||||
model_id integer not null references models (id) on delete cascade,
|
||||
month integer not null,
|
||||
year integer not null,
|
||||
input_tokens bigint not null default 0,
|
||||
cache_creation_input_tokens bigint not null default 0,
|
||||
cache_read_input_tokens bigint not null default 0,
|
||||
output_tokens bigint not null default 0
|
||||
);
|
||||
|
||||
create unique index uix_monthly_usages_on_user_id_model_id_month_year on monthly_usages (user_id, model_id, month, year);
|
||||
@@ -22,12 +22,15 @@ use stripe::{
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::db::billing_subscription::StripeSubscriptionStatus;
|
||||
use crate::db::billing_subscription::{self, StripeSubscriptionStatus};
|
||||
use crate::db::{
|
||||
billing_customer, BillingSubscriptionId, CreateBillingCustomerParams,
|
||||
CreateBillingSubscriptionParams, CreateProcessedStripeEventParams, UpdateBillingCustomerParams,
|
||||
UpdateBillingSubscriptionParams,
|
||||
};
|
||||
use crate::llm::db::LlmDatabase;
|
||||
use crate::llm::MONTHLY_SPENDING_LIMIT_IN_CENTS;
|
||||
use crate::rpc::ResultExt as _;
|
||||
use crate::{AppState, Error, Result};
|
||||
|
||||
pub fn router() -> Router {
|
||||
@@ -79,7 +82,7 @@ async fn list_billing_subscriptions(
|
||||
.into_iter()
|
||||
.map(|subscription| BillingSubscriptionJson {
|
||||
id: subscription.id,
|
||||
name: "Zed Pro".to_string(),
|
||||
name: "Zed LLM Usage".to_string(),
|
||||
status: subscription.stripe_subscription_status,
|
||||
cancel_at: subscription.stripe_cancel_at.map(|cancel_at| {
|
||||
cancel_at
|
||||
@@ -117,7 +120,7 @@ async fn create_billing_subscription(
|
||||
let Some((stripe_client, stripe_price_id)) = app
|
||||
.stripe_client
|
||||
.clone()
|
||||
.zip(app.config.stripe_price_id.clone())
|
||||
.zip(app.config.stripe_llm_usage_price_id.clone())
|
||||
else {
|
||||
log::error!("failed to retrieve Stripe client or price ID");
|
||||
Err(Error::http(
|
||||
@@ -150,7 +153,7 @@ async fn create_billing_subscription(
|
||||
params.client_reference_id = Some(user.github_login.as_str());
|
||||
params.line_items = Some(vec![CreateCheckoutSessionLineItems {
|
||||
price: Some(stripe_price_id.to_string()),
|
||||
quantity: Some(1),
|
||||
quantity: Some(0),
|
||||
..Default::default()
|
||||
}]);
|
||||
let success_url = format!("{}/account", app.config.zed_dot_dev_url());
|
||||
@@ -631,3 +634,95 @@ async fn find_or_create_billing_customer(
|
||||
|
||||
Ok(Some(billing_customer))
|
||||
}
|
||||
|
||||
const SYNC_LLM_USAGE_WITH_STRIPE_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60);
|
||||
|
||||
pub fn sync_llm_usage_with_stripe_periodically(app: Arc<AppState>, llm_db: LlmDatabase) {
|
||||
let Some(stripe_client) = app.stripe_client.clone() else {
|
||||
log::warn!("failed to retrieve Stripe client");
|
||||
return;
|
||||
};
|
||||
let Some(stripe_llm_usage_price_id) = app.config.stripe_llm_usage_price_id.clone() else {
|
||||
log::warn!("failed to retrieve Stripe LLM usage price ID");
|
||||
return;
|
||||
};
|
||||
|
||||
let executor = app.executor.clone();
|
||||
executor.spawn_detached({
|
||||
let executor = executor.clone();
|
||||
async move {
|
||||
loop {
|
||||
sync_with_stripe(
|
||||
&app,
|
||||
&llm_db,
|
||||
&stripe_client,
|
||||
stripe_llm_usage_price_id.clone(),
|
||||
)
|
||||
.await
|
||||
.trace_err();
|
||||
|
||||
executor.sleep(SYNC_LLM_USAGE_WITH_STRIPE_INTERVAL).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn sync_with_stripe(
|
||||
app: &Arc<AppState>,
|
||||
llm_db: &LlmDatabase,
|
||||
stripe_client: &stripe::Client,
|
||||
stripe_llm_usage_price_id: Arc<str>,
|
||||
) -> anyhow::Result<()> {
|
||||
let subscriptions = app.db.get_active_billing_subscriptions().await?;
|
||||
|
||||
for (customer, subscription) in subscriptions {
|
||||
update_stripe_subscription(
|
||||
llm_db,
|
||||
stripe_client,
|
||||
&stripe_llm_usage_price_id,
|
||||
customer,
|
||||
subscription,
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_stripe_subscription(
|
||||
llm_db: &LlmDatabase,
|
||||
stripe_client: &stripe::Client,
|
||||
stripe_llm_usage_price_id: &Arc<str>,
|
||||
customer: billing_customer::Model,
|
||||
subscription: billing_subscription::Model,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let monthly_spending = llm_db
|
||||
.get_user_spending_for_month(customer.user_id, Utc::now())
|
||||
.await?;
|
||||
let subscription_id = SubscriptionId::from_str(&subscription.stripe_subscription_id)
|
||||
.context("failed to parse subscription ID")?;
|
||||
|
||||
let monthly_spending_over_free_tier =
|
||||
monthly_spending.saturating_sub(MONTHLY_SPENDING_LIMIT_IN_CENTS);
|
||||
|
||||
let new_quantity = (monthly_spending_over_free_tier as f32 / 100.).ceil();
|
||||
Subscription::update(
|
||||
stripe_client,
|
||||
&subscription_id,
|
||||
stripe::UpdateSubscription {
|
||||
items: Some(vec![stripe::UpdateSubscriptionItems {
|
||||
// TODO: Do we need to send up the `id` if a subscription item
|
||||
// with this price already exists, or will Stripe take care of
|
||||
// it?
|
||||
id: None,
|
||||
price: Some(stripe_llm_usage_price_id.to_string()),
|
||||
quantity: Some(new_quantity as u64),
|
||||
..Default::default()
|
||||
}]),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ use telemetry_events::{
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
static CRASH_REPORTS_BUCKET: &str = "zed-crash-reports";
|
||||
const CRASH_REPORTS_BUCKET: &str = "zed-crash-reports";
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
@@ -429,8 +429,6 @@ pub async fn post_events(
|
||||
country_code.clone(),
|
||||
checksum_matched,
|
||||
)),
|
||||
// Needed for clients sending old copilot_event types
|
||||
Event::Copilot(_) => {}
|
||||
Event::InlineCompletion(event) => {
|
||||
to_upload
|
||||
.inline_completion_events
|
||||
@@ -679,6 +677,7 @@ pub struct EditorEventRow {
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
checksum_matched: bool,
|
||||
is_via_ssh: bool,
|
||||
}
|
||||
|
||||
impl EditorEventRow {
|
||||
@@ -720,6 +719,7 @@ impl EditorEventRow {
|
||||
region_code: "".to_string(),
|
||||
city: "".to_string(),
|
||||
historical_event: false,
|
||||
is_via_ssh: event.is_via_ssh,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1263,6 +1263,7 @@ pub struct EditEventRow {
|
||||
period_start: i64,
|
||||
period_end: i64,
|
||||
environment: String,
|
||||
is_via_ssh: bool,
|
||||
}
|
||||
|
||||
impl EditEventRow {
|
||||
@@ -1296,6 +1297,7 @@ impl EditEventRow {
|
||||
period_start: period_start.timestamp_millis(),
|
||||
period_end: period_end.timestamp_millis(),
|
||||
environment: event.environment,
|
||||
is_via_ssh: event.is_via_ssh,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ use std::{
|
||||
};
|
||||
use time::PrimitiveDateTime;
|
||||
use tokio::sync::{Mutex, OwnedMutexGuard};
|
||||
use worktree_settings_file::LocalSettingsKind;
|
||||
|
||||
#[cfg(test)]
|
||||
pub use tests::TestDb;
|
||||
@@ -766,6 +767,7 @@ pub struct Worktree {
|
||||
pub struct WorktreeSettingsFile {
|
||||
pub path: String,
|
||||
pub content: String,
|
||||
pub kind: LocalSettingsKind,
|
||||
}
|
||||
|
||||
pub struct NewExtensionVersion {
|
||||
@@ -783,3 +785,21 @@ pub struct ExtensionVersionConstraints {
|
||||
pub schema_versions: RangeInclusive<i32>,
|
||||
pub wasm_api_versions: RangeInclusive<SemanticVersion>,
|
||||
}
|
||||
|
||||
impl LocalSettingsKind {
|
||||
pub fn from_proto(proto_kind: proto::LocalSettingsKind) -> Self {
|
||||
match proto_kind {
|
||||
proto::LocalSettingsKind::Settings => Self::Settings,
|
||||
proto::LocalSettingsKind::Tasks => Self::Tasks,
|
||||
proto::LocalSettingsKind::Editorconfig => Self::Editorconfig,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_proto(&self) -> proto::LocalSettingsKind {
|
||||
match self {
|
||||
Self::Settings => proto::LocalSettingsKind::Settings,
|
||||
Self::Tasks => proto::LocalSettingsKind::Tasks,
|
||||
Self::Editorconfig => proto::LocalSettingsKind::Editorconfig,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,29 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_active_billing_subscriptions(
|
||||
&self,
|
||||
) -> Result<Vec<(billing_customer::Model, billing_subscription::Model)>> {
|
||||
self.transaction(|tx| async move {
|
||||
let mut result = Vec::new();
|
||||
let mut rows = billing_subscription::Entity::find()
|
||||
.inner_join(billing_customer::Entity)
|
||||
.select_also(billing_customer::Entity)
|
||||
.order_by_asc(billing_subscription::Column::Id)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
while let Some(row) = rows.next().await {
|
||||
if let (subscription, Some(customer)) = row? {
|
||||
result.push((customer, subscription));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns whether the user has an active billing subscription.
|
||||
pub async fn has_active_billing_subscription(&self, user_id: UserId) -> Result<bool> {
|
||||
Ok(self.count_active_billing_subscriptions(user_id).await? > 0)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use anyhow::Context as _;
|
||||
use util::ResultExt;
|
||||
|
||||
use super::*;
|
||||
@@ -527,6 +528,12 @@ impl Database {
|
||||
connection: ConnectionId,
|
||||
) -> Result<TransactionGuard<Vec<ConnectionId>>> {
|
||||
let project_id = ProjectId::from_proto(update.project_id);
|
||||
let kind = match update.kind {
|
||||
Some(kind) => proto::LocalSettingsKind::from_i32(kind)
|
||||
.with_context(|| format!("unknown worktree settings kind: {kind}"))?,
|
||||
None => proto::LocalSettingsKind::Settings,
|
||||
};
|
||||
let kind = LocalSettingsKind::from_proto(kind);
|
||||
self.project_transaction(project_id, |tx| async move {
|
||||
// Ensure the update comes from the host.
|
||||
let project = project::Entity::find_by_id(project_id)
|
||||
@@ -543,6 +550,7 @@ impl Database {
|
||||
worktree_id: ActiveValue::Set(update.worktree_id as i64),
|
||||
path: ActiveValue::Set(update.path.clone()),
|
||||
content: ActiveValue::Set(content.clone()),
|
||||
kind: ActiveValue::Set(kind),
|
||||
})
|
||||
.on_conflict(
|
||||
OnConflict::columns([
|
||||
@@ -800,6 +808,7 @@ impl Database {
|
||||
worktree.settings_files.push(WorktreeSettingsFile {
|
||||
path: db_settings_file.path,
|
||||
content: db_settings_file.content,
|
||||
kind: db_settings_file.kind,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -735,6 +735,7 @@ impl Database {
|
||||
worktree.settings_files.push(WorktreeSettingsFile {
|
||||
path: db_settings_file.path,
|
||||
content: db_settings_file.content,
|
||||
kind: db_settings_file.kind,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,25 @@ pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub path: String,
|
||||
pub content: String,
|
||||
pub kind: LocalSettingsKind,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Default, Hash, serde::Serialize,
|
||||
)]
|
||||
#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum LocalSettingsKind {
|
||||
#[default]
|
||||
#[sea_orm(string_value = "settings")]
|
||||
Settings,
|
||||
#[sea_orm(string_value = "tasks")]
|
||||
Tasks,
|
||||
#[sea_orm(string_value = "editorconfig")]
|
||||
Editorconfig,
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ pub struct Config {
|
||||
pub slack_panics_webhook: Option<String>,
|
||||
pub auto_join_channel_id: Option<ChannelId>,
|
||||
pub stripe_api_key: Option<String>,
|
||||
pub stripe_price_id: Option<Arc<str>>,
|
||||
pub stripe_llm_usage_price_id: Option<Arc<str>>,
|
||||
pub supermaven_admin_api_key: Option<Arc<str>>,
|
||||
pub user_backfiller_github_access_token: Option<Arc<str>>,
|
||||
}
|
||||
@@ -193,6 +193,10 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_llm_billing_enabled(&self) -> bool {
|
||||
self.stripe_llm_usage_price_id.is_some()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn test() -> Self {
|
||||
Self {
|
||||
@@ -231,7 +235,7 @@ impl Config {
|
||||
migrations_path: None,
|
||||
seed_path: None,
|
||||
stripe_api_key: None,
|
||||
stripe_price_id: None,
|
||||
stripe_llm_usage_price_id: None,
|
||||
supermaven_admin_api_key: None,
|
||||
user_backfiller_github_access_token: None,
|
||||
}
|
||||
|
||||
@@ -320,22 +320,31 @@ async fn perform_completion(
|
||||
chunks
|
||||
.map(move |event| {
|
||||
let chunk = event?;
|
||||
let (input_tokens, output_tokens) = match &chunk {
|
||||
let (
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
cache_creation_input_tokens,
|
||||
cache_read_input_tokens,
|
||||
) = match &chunk {
|
||||
anthropic::Event::MessageStart {
|
||||
message: anthropic::Response { usage, .. },
|
||||
}
|
||||
| anthropic::Event::MessageDelta { usage, .. } => (
|
||||
usage.input_tokens.unwrap_or(0) as usize,
|
||||
usage.output_tokens.unwrap_or(0) as usize,
|
||||
usage.cache_creation_input_tokens.unwrap_or(0) as usize,
|
||||
usage.cache_read_input_tokens.unwrap_or(0) as usize,
|
||||
),
|
||||
_ => (0, 0),
|
||||
_ => (0, 0, 0, 0),
|
||||
};
|
||||
|
||||
anyhow::Ok((
|
||||
serde_json::to_vec(&chunk).unwrap(),
|
||||
anyhow::Ok(CompletionChunk {
|
||||
bytes: serde_json::to_vec(&chunk).unwrap(),
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
))
|
||||
cache_creation_input_tokens,
|
||||
cache_read_input_tokens,
|
||||
})
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
@@ -361,11 +370,13 @@ async fn perform_completion(
|
||||
chunk.usage.as_ref().map_or(0, |u| u.prompt_tokens) as usize;
|
||||
let output_tokens =
|
||||
chunk.usage.as_ref().map_or(0, |u| u.completion_tokens) as usize;
|
||||
(
|
||||
serde_json::to_vec(&chunk).unwrap(),
|
||||
CompletionChunk {
|
||||
bytes: serde_json::to_vec(&chunk).unwrap(),
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
)
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
}
|
||||
})
|
||||
})
|
||||
.boxed()
|
||||
@@ -389,13 +400,13 @@ async fn perform_completion(
|
||||
.map(|event| {
|
||||
event.map(|chunk| {
|
||||
// TODO - implement token counting for Google AI
|
||||
let input_tokens = 0;
|
||||
let output_tokens = 0;
|
||||
(
|
||||
serde_json::to_vec(&chunk).unwrap(),
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
)
|
||||
CompletionChunk {
|
||||
bytes: serde_json::to_vec(&chunk).unwrap(),
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
}
|
||||
})
|
||||
})
|
||||
.boxed()
|
||||
@@ -409,6 +420,8 @@ async fn perform_completion(
|
||||
model,
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
inner_stream: stream,
|
||||
})))
|
||||
}
|
||||
@@ -425,6 +438,9 @@ fn normalize_model_name(known_models: Vec<String>, name: String) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// The maximum monthly spending an individual user can reach before they have to pay.
|
||||
pub const MONTHLY_SPENDING_LIMIT_IN_CENTS: usize = 5 * 100;
|
||||
|
||||
/// The maximum lifetime spending an individual user can reach before being cut off.
|
||||
///
|
||||
/// Represented in cents.
|
||||
@@ -447,6 +463,18 @@ async fn check_usage_limit(
|
||||
)
|
||||
.await?;
|
||||
|
||||
if state.config.is_llm_billing_enabled() {
|
||||
if usage.spending_this_month >= MONTHLY_SPENDING_LIMIT_IN_CENTS {
|
||||
if !claims.has_llm_subscription.unwrap_or(false) {
|
||||
return Err(Error::http(
|
||||
StatusCode::PAYMENT_REQUIRED,
|
||||
"Maximum spending limit reached for this month.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove this once we've rolled out monthly spending limits.
|
||||
if usage.lifetime_spending >= LIFETIME_SPENDING_LIMIT_IN_CENTS {
|
||||
return Err(Error::http(
|
||||
StatusCode::FORBIDDEN,
|
||||
@@ -494,7 +522,6 @@ async fn check_usage_limit(
|
||||
UsageMeasure::RequestsPerMinute => "requests_per_minute",
|
||||
UsageMeasure::TokensPerMinute => "tokens_per_minute",
|
||||
UsageMeasure::TokensPerDay => "tokens_per_day",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
if let Some(client) = state.clickhouse_client.as_ref() {
|
||||
@@ -553,6 +580,14 @@ async fn check_usage_limit(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct CompletionChunk {
|
||||
bytes: Vec<u8>,
|
||||
input_tokens: usize,
|
||||
output_tokens: usize,
|
||||
cache_creation_input_tokens: usize,
|
||||
cache_read_input_tokens: usize,
|
||||
}
|
||||
|
||||
struct TokenCountingStream<S> {
|
||||
state: Arc<LlmState>,
|
||||
claims: LlmTokenClaims,
|
||||
@@ -560,22 +595,26 @@ struct TokenCountingStream<S> {
|
||||
model: String,
|
||||
input_tokens: usize,
|
||||
output_tokens: usize,
|
||||
cache_creation_input_tokens: usize,
|
||||
cache_read_input_tokens: usize,
|
||||
inner_stream: S,
|
||||
}
|
||||
|
||||
impl<S> Stream for TokenCountingStream<S>
|
||||
where
|
||||
S: Stream<Item = Result<(Vec<u8>, usize, usize), anyhow::Error>> + Unpin,
|
||||
S: Stream<Item = Result<CompletionChunk, anyhow::Error>> + Unpin,
|
||||
{
|
||||
type Item = Result<Vec<u8>, anyhow::Error>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
match Pin::new(&mut self.inner_stream).poll_next(cx) {
|
||||
Poll::Ready(Some(Ok((mut bytes, input_tokens, output_tokens)))) => {
|
||||
bytes.push(b'\n');
|
||||
self.input_tokens += input_tokens;
|
||||
self.output_tokens += output_tokens;
|
||||
Poll::Ready(Some(Ok(bytes)))
|
||||
Poll::Ready(Some(Ok(mut chunk))) => {
|
||||
chunk.bytes.push(b'\n');
|
||||
self.input_tokens += chunk.input_tokens;
|
||||
self.output_tokens += chunk.output_tokens;
|
||||
self.cache_creation_input_tokens += chunk.cache_creation_input_tokens;
|
||||
self.cache_read_input_tokens += chunk.cache_read_input_tokens;
|
||||
Poll::Ready(Some(Ok(chunk.bytes)))
|
||||
}
|
||||
Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
|
||||
Poll::Ready(None) => Poll::Ready(None),
|
||||
@@ -592,6 +631,8 @@ impl<S> Drop for TokenCountingStream<S> {
|
||||
let model = std::mem::take(&mut self.model);
|
||||
let input_token_count = self.input_tokens;
|
||||
let output_token_count = self.output_tokens;
|
||||
let cache_creation_input_token_count = self.cache_creation_input_tokens;
|
||||
let cache_read_input_token_count = self.cache_read_input_tokens;
|
||||
self.state.executor.spawn_detached(async move {
|
||||
let usage = state
|
||||
.db
|
||||
@@ -601,6 +642,8 @@ impl<S> Drop for TokenCountingStream<S> {
|
||||
provider,
|
||||
&model,
|
||||
input_token_count,
|
||||
cache_creation_input_token_count,
|
||||
cache_read_input_token_count,
|
||||
output_token_count,
|
||||
Utc::now(),
|
||||
)
|
||||
@@ -632,11 +675,20 @@ impl<S> Drop for TokenCountingStream<S> {
|
||||
model,
|
||||
provider: provider.to_string(),
|
||||
input_token_count: input_token_count as u64,
|
||||
cache_creation_input_token_count: cache_creation_input_token_count
|
||||
as u64,
|
||||
cache_read_input_token_count: cache_read_input_token_count as u64,
|
||||
output_token_count: output_token_count as u64,
|
||||
requests_this_minute: usage.requests_this_minute as u64,
|
||||
tokens_this_minute: usage.tokens_this_minute as u64,
|
||||
tokens_this_day: usage.tokens_this_day as u64,
|
||||
input_tokens_this_month: usage.input_tokens_this_month as u64,
|
||||
cache_creation_input_tokens_this_month: usage
|
||||
.cache_creation_input_tokens_this_month
|
||||
as u64,
|
||||
cache_read_input_tokens_this_month: usage
|
||||
.cache_read_input_tokens_this_month
|
||||
as u64,
|
||||
output_tokens_this_month: usage.output_tokens_this_month as u64,
|
||||
spending_this_month: usage.spending_this_month as u64,
|
||||
lifetime_spending: usage.lifetime_spending as u64,
|
||||
|
||||
@@ -97,6 +97,14 @@ impl LlmDatabase {
|
||||
.ok_or_else(|| anyhow!("unknown model {provider:?}:{name}"))?)
|
||||
}
|
||||
|
||||
pub fn model_by_id(&self, id: ModelId) -> Result<&model::Model> {
|
||||
Ok(self
|
||||
.models
|
||||
.values()
|
||||
.find(|model| model.id == id)
|
||||
.ok_or_else(|| anyhow!("no model for ID {id:?}"))?)
|
||||
}
|
||||
|
||||
pub fn options(&self) -> &ConnectOptions {
|
||||
&self.options
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::db::UserId;
|
||||
use chrono::Duration;
|
||||
use chrono::{Datelike, Duration};
|
||||
use futures::StreamExt as _;
|
||||
use rpc::LanguageModelProvider;
|
||||
use sea_orm::QuerySelect;
|
||||
@@ -14,6 +14,8 @@ pub struct Usage {
|
||||
pub tokens_this_minute: usize,
|
||||
pub tokens_this_day: usize,
|
||||
pub input_tokens_this_month: usize,
|
||||
pub cache_creation_input_tokens_this_month: usize,
|
||||
pub cache_read_input_tokens_this_month: usize,
|
||||
pub output_tokens_this_month: usize,
|
||||
pub spending_this_month: usize,
|
||||
pub lifetime_spending: usize,
|
||||
@@ -138,6 +140,46 @@ impl LlmDatabase {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_user_spending_for_month(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
now: DateTimeUtc,
|
||||
) -> Result<usize> {
|
||||
self.transaction(|tx| async move {
|
||||
let month = now.date_naive().month() as i32;
|
||||
let year = now.date_naive().year();
|
||||
|
||||
let mut monthly_usages = monthly_usage::Entity::find()
|
||||
.filter(
|
||||
monthly_usage::Column::UserId
|
||||
.eq(user_id)
|
||||
.and(monthly_usage::Column::Month.eq(month))
|
||||
.and(monthly_usage::Column::Year.eq(year)),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
let mut monthly_spending_in_cents = 0;
|
||||
|
||||
while let Some(usage) = monthly_usages.next().await {
|
||||
let usage = usage?;
|
||||
let Ok(model) = self.model_by_id(usage.model_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
monthly_spending_in_cents += calculate_spending(
|
||||
model,
|
||||
usage.input_tokens as usize,
|
||||
usage.cache_creation_input_tokens as usize,
|
||||
usage.cache_read_input_tokens as usize,
|
||||
usage.output_tokens as usize,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(monthly_spending_in_cents)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_usage(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
@@ -160,17 +202,26 @@ impl LlmDatabase {
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let (lifetime_input_tokens, lifetime_output_tokens) = lifetime_usage::Entity::find()
|
||||
let month = now.date_naive().month() as i32;
|
||||
let year = now.date_naive().year();
|
||||
let monthly_usage = monthly_usage::Entity::find()
|
||||
.filter(
|
||||
monthly_usage::Column::UserId
|
||||
.eq(user_id)
|
||||
.and(monthly_usage::Column::ModelId.eq(model.id))
|
||||
.and(monthly_usage::Column::Month.eq(month))
|
||||
.and(monthly_usage::Column::Year.eq(year)),
|
||||
)
|
||||
.one(&*tx)
|
||||
.await?;
|
||||
let lifetime_usage = lifetime_usage::Entity::find()
|
||||
.filter(
|
||||
lifetime_usage::Column::UserId
|
||||
.eq(user_id)
|
||||
.and(lifetime_usage::Column::ModelId.eq(model.id)),
|
||||
)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.map_or((0, 0), |usage| {
|
||||
(usage.input_tokens as usize, usage.output_tokens as usize)
|
||||
});
|
||||
.await?;
|
||||
|
||||
let requests_this_minute =
|
||||
self.get_usage_for_measure(&usages, now, UsageMeasure::RequestsPerMinute)?;
|
||||
@@ -178,21 +229,45 @@ impl LlmDatabase {
|
||||
self.get_usage_for_measure(&usages, now, UsageMeasure::TokensPerMinute)?;
|
||||
let tokens_this_day =
|
||||
self.get_usage_for_measure(&usages, now, UsageMeasure::TokensPerDay)?;
|
||||
let input_tokens_this_month =
|
||||
self.get_usage_for_measure(&usages, now, UsageMeasure::InputTokensPerMonth)?;
|
||||
let output_tokens_this_month =
|
||||
self.get_usage_for_measure(&usages, now, UsageMeasure::OutputTokensPerMonth)?;
|
||||
let spending_this_month =
|
||||
calculate_spending(model, input_tokens_this_month, output_tokens_this_month);
|
||||
let lifetime_spending =
|
||||
calculate_spending(model, lifetime_input_tokens, lifetime_output_tokens);
|
||||
let spending_this_month = if let Some(monthly_usage) = &monthly_usage {
|
||||
calculate_spending(
|
||||
model,
|
||||
monthly_usage.input_tokens as usize,
|
||||
monthly_usage.cache_creation_input_tokens as usize,
|
||||
monthly_usage.cache_read_input_tokens as usize,
|
||||
monthly_usage.output_tokens as usize,
|
||||
)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let lifetime_spending = if let Some(lifetime_usage) = &lifetime_usage {
|
||||
calculate_spending(
|
||||
model,
|
||||
lifetime_usage.input_tokens as usize,
|
||||
lifetime_usage.cache_creation_input_tokens as usize,
|
||||
lifetime_usage.cache_read_input_tokens as usize,
|
||||
lifetime_usage.output_tokens as usize,
|
||||
)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
Ok(Usage {
|
||||
requests_this_minute,
|
||||
tokens_this_minute,
|
||||
tokens_this_day,
|
||||
input_tokens_this_month,
|
||||
output_tokens_this_month,
|
||||
input_tokens_this_month: monthly_usage
|
||||
.as_ref()
|
||||
.map_or(0, |usage| usage.input_tokens as usize),
|
||||
cache_creation_input_tokens_this_month: monthly_usage
|
||||
.as_ref()
|
||||
.map_or(0, |usage| usage.cache_creation_input_tokens as usize),
|
||||
cache_read_input_tokens_this_month: monthly_usage
|
||||
.as_ref()
|
||||
.map_or(0, |usage| usage.cache_read_input_tokens as usize),
|
||||
output_tokens_this_month: monthly_usage
|
||||
.as_ref()
|
||||
.map_or(0, |usage| usage.output_tokens as usize),
|
||||
spending_this_month,
|
||||
lifetime_spending,
|
||||
})
|
||||
@@ -208,6 +283,8 @@ impl LlmDatabase {
|
||||
provider: LanguageModelProvider,
|
||||
model_name: &str,
|
||||
input_token_count: usize,
|
||||
cache_creation_input_tokens: usize,
|
||||
cache_read_input_tokens: usize,
|
||||
output_token_count: usize,
|
||||
now: DateTimeUtc,
|
||||
) -> Result<Usage> {
|
||||
@@ -235,6 +312,10 @@ impl LlmDatabase {
|
||||
&tx,
|
||||
)
|
||||
.await?;
|
||||
let total_token_count = input_token_count
|
||||
+ cache_read_input_tokens
|
||||
+ cache_creation_input_tokens
|
||||
+ output_token_count;
|
||||
let tokens_this_minute = self
|
||||
.update_usage_for_measure(
|
||||
user_id,
|
||||
@@ -243,7 +324,7 @@ impl LlmDatabase {
|
||||
&usages,
|
||||
UsageMeasure::TokensPerMinute,
|
||||
now,
|
||||
input_token_count + output_token_count,
|
||||
total_token_count,
|
||||
&tx,
|
||||
)
|
||||
.await?;
|
||||
@@ -255,36 +336,73 @@ impl LlmDatabase {
|
||||
&usages,
|
||||
UsageMeasure::TokensPerDay,
|
||||
now,
|
||||
input_token_count + output_token_count,
|
||||
total_token_count,
|
||||
&tx,
|
||||
)
|
||||
.await?;
|
||||
let input_tokens_this_month = self
|
||||
.update_usage_for_measure(
|
||||
user_id,
|
||||
is_staff,
|
||||
model.id,
|
||||
&usages,
|
||||
UsageMeasure::InputTokensPerMonth,
|
||||
now,
|
||||
input_token_count,
|
||||
&tx,
|
||||
|
||||
let month = now.date_naive().month() as i32;
|
||||
let year = now.date_naive().year();
|
||||
|
||||
// Update monthly usage
|
||||
let monthly_usage = monthly_usage::Entity::find()
|
||||
.filter(
|
||||
monthly_usage::Column::UserId
|
||||
.eq(user_id)
|
||||
.and(monthly_usage::Column::ModelId.eq(model.id))
|
||||
.and(monthly_usage::Column::Month.eq(month))
|
||||
.and(monthly_usage::Column::Year.eq(year)),
|
||||
)
|
||||
.one(&*tx)
|
||||
.await?;
|
||||
let output_tokens_this_month = self
|
||||
.update_usage_for_measure(
|
||||
user_id,
|
||||
is_staff,
|
||||
model.id,
|
||||
&usages,
|
||||
UsageMeasure::OutputTokensPerMonth,
|
||||
now,
|
||||
output_token_count,
|
||||
&tx,
|
||||
)
|
||||
.await?;
|
||||
let spending_this_month =
|
||||
calculate_spending(model, input_tokens_this_month, output_tokens_this_month);
|
||||
|
||||
let monthly_usage = match monthly_usage {
|
||||
Some(usage) => {
|
||||
monthly_usage::Entity::update(monthly_usage::ActiveModel {
|
||||
id: ActiveValue::unchanged(usage.id),
|
||||
input_tokens: ActiveValue::set(
|
||||
usage.input_tokens + input_token_count as i64,
|
||||
),
|
||||
cache_creation_input_tokens: ActiveValue::set(
|
||||
usage.cache_creation_input_tokens + cache_creation_input_tokens as i64,
|
||||
),
|
||||
cache_read_input_tokens: ActiveValue::set(
|
||||
usage.cache_read_input_tokens + cache_read_input_tokens as i64,
|
||||
),
|
||||
output_tokens: ActiveValue::set(
|
||||
usage.output_tokens + output_token_count as i64,
|
||||
),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?
|
||||
}
|
||||
None => {
|
||||
monthly_usage::ActiveModel {
|
||||
user_id: ActiveValue::set(user_id),
|
||||
model_id: ActiveValue::set(model.id),
|
||||
month: ActiveValue::set(month),
|
||||
year: ActiveValue::set(year),
|
||||
input_tokens: ActiveValue::set(input_token_count as i64),
|
||||
cache_creation_input_tokens: ActiveValue::set(
|
||||
cache_creation_input_tokens as i64,
|
||||
),
|
||||
cache_read_input_tokens: ActiveValue::set(cache_read_input_tokens as i64),
|
||||
output_tokens: ActiveValue::set(output_token_count as i64),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&*tx)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
let spending_this_month = calculate_spending(
|
||||
model,
|
||||
monthly_usage.input_tokens as usize,
|
||||
monthly_usage.cache_creation_input_tokens as usize,
|
||||
monthly_usage.cache_read_input_tokens as usize,
|
||||
monthly_usage.output_tokens as usize,
|
||||
);
|
||||
|
||||
// Update lifetime usage
|
||||
let lifetime_usage = lifetime_usage::Entity::find()
|
||||
@@ -303,6 +421,12 @@ impl LlmDatabase {
|
||||
input_tokens: ActiveValue::set(
|
||||
usage.input_tokens + input_token_count as i64,
|
||||
),
|
||||
cache_creation_input_tokens: ActiveValue::set(
|
||||
usage.cache_creation_input_tokens + cache_creation_input_tokens as i64,
|
||||
),
|
||||
cache_read_input_tokens: ActiveValue::set(
|
||||
usage.cache_read_input_tokens + cache_read_input_tokens as i64,
|
||||
),
|
||||
output_tokens: ActiveValue::set(
|
||||
usage.output_tokens + output_token_count as i64,
|
||||
),
|
||||
@@ -316,6 +440,10 @@ impl LlmDatabase {
|
||||
user_id: ActiveValue::set(user_id),
|
||||
model_id: ActiveValue::set(model.id),
|
||||
input_tokens: ActiveValue::set(input_token_count as i64),
|
||||
cache_creation_input_tokens: ActiveValue::set(
|
||||
cache_creation_input_tokens as i64,
|
||||
),
|
||||
cache_read_input_tokens: ActiveValue::set(cache_read_input_tokens as i64),
|
||||
output_tokens: ActiveValue::set(output_token_count as i64),
|
||||
..Default::default()
|
||||
}
|
||||
@@ -327,6 +455,8 @@ impl LlmDatabase {
|
||||
let lifetime_spending = calculate_spending(
|
||||
model,
|
||||
lifetime_usage.input_tokens as usize,
|
||||
lifetime_usage.cache_creation_input_tokens as usize,
|
||||
lifetime_usage.cache_read_input_tokens as usize,
|
||||
lifetime_usage.output_tokens as usize,
|
||||
);
|
||||
|
||||
@@ -334,8 +464,11 @@ impl LlmDatabase {
|
||||
requests_this_minute,
|
||||
tokens_this_minute,
|
||||
tokens_this_day,
|
||||
input_tokens_this_month,
|
||||
output_tokens_this_month,
|
||||
input_tokens_this_month: monthly_usage.input_tokens as usize,
|
||||
cache_creation_input_tokens_this_month: monthly_usage.cache_creation_input_tokens
|
||||
as usize,
|
||||
cache_read_input_tokens_this_month: monthly_usage.cache_read_input_tokens as usize,
|
||||
output_tokens_this_month: monthly_usage.output_tokens as usize,
|
||||
spending_this_month,
|
||||
lifetime_spending,
|
||||
})
|
||||
@@ -501,18 +634,28 @@ impl LlmDatabase {
|
||||
fn calculate_spending(
|
||||
model: &model::Model,
|
||||
input_tokens_this_month: usize,
|
||||
cache_creation_input_tokens_this_month: usize,
|
||||
cache_read_input_tokens_this_month: usize,
|
||||
output_tokens_this_month: usize,
|
||||
) -> usize {
|
||||
let input_token_cost =
|
||||
input_tokens_this_month * model.price_per_million_input_tokens as usize / 1_000_000;
|
||||
let cache_creation_input_token_cost = cache_creation_input_tokens_this_month
|
||||
* model.price_per_million_cache_creation_input_tokens as usize
|
||||
/ 1_000_000;
|
||||
let cache_read_input_token_cost = cache_read_input_tokens_this_month
|
||||
* model.price_per_million_cache_read_input_tokens as usize
|
||||
/ 1_000_000;
|
||||
let output_token_cost =
|
||||
output_tokens_this_month * model.price_per_million_output_tokens as usize / 1_000_000;
|
||||
input_token_cost + output_token_cost
|
||||
input_token_cost
|
||||
+ cache_creation_input_token_cost
|
||||
+ cache_read_input_token_cost
|
||||
+ output_token_cost
|
||||
}
|
||||
|
||||
const MINUTE_BUCKET_COUNT: usize = 12;
|
||||
const DAY_BUCKET_COUNT: usize = 48;
|
||||
const MONTH_BUCKET_COUNT: usize = 30;
|
||||
|
||||
impl UsageMeasure {
|
||||
fn bucket_count(&self) -> usize {
|
||||
@@ -520,8 +663,6 @@ impl UsageMeasure {
|
||||
UsageMeasure::RequestsPerMinute => MINUTE_BUCKET_COUNT,
|
||||
UsageMeasure::TokensPerMinute => MINUTE_BUCKET_COUNT,
|
||||
UsageMeasure::TokensPerDay => DAY_BUCKET_COUNT,
|
||||
UsageMeasure::InputTokensPerMonth => MONTH_BUCKET_COUNT,
|
||||
UsageMeasure::OutputTokensPerMonth => MONTH_BUCKET_COUNT,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,8 +671,6 @@ impl UsageMeasure {
|
||||
UsageMeasure::RequestsPerMinute => Duration::minutes(1),
|
||||
UsageMeasure::TokensPerMinute => Duration::minutes(1),
|
||||
UsageMeasure::TokensPerDay => Duration::hours(24),
|
||||
UsageMeasure::InputTokensPerMonth => Duration::days(30),
|
||||
UsageMeasure::OutputTokensPerMonth => Duration::days(30),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod lifetime_usage;
|
||||
pub mod model;
|
||||
pub mod monthly_usage;
|
||||
pub mod provider;
|
||||
pub mod revoked_access_token;
|
||||
pub mod usage;
|
||||
|
||||
@@ -9,6 +9,8 @@ pub struct Model {
|
||||
pub user_id: UserId,
|
||||
pub model_id: ModelId,
|
||||
pub input_tokens: i64,
|
||||
pub cache_creation_input_tokens: i64,
|
||||
pub cache_read_input_tokens: i64,
|
||||
pub output_tokens: i64,
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ pub struct Model {
|
||||
pub max_tokens_per_minute: i64,
|
||||
pub max_tokens_per_day: i64,
|
||||
pub price_per_million_input_tokens: i32,
|
||||
pub price_per_million_cache_creation_input_tokens: i32,
|
||||
pub price_per_million_cache_read_input_tokens: i32,
|
||||
pub price_per_million_output_tokens: i32,
|
||||
}
|
||||
|
||||
|
||||
22
crates/collab/src/llm/db/tables/monthly_usage.rs
Normal file
22
crates/collab/src/llm/db/tables/monthly_usage.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use crate::{db::UserId, llm::db::ModelId};
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "monthly_usages")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub user_id: UserId,
|
||||
pub model_id: ModelId,
|
||||
pub month: i32,
|
||||
pub year: i32,
|
||||
pub input_tokens: i64,
|
||||
pub cache_creation_input_tokens: i64,
|
||||
pub cache_read_input_tokens: i64,
|
||||
pub output_tokens: i64,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -9,8 +9,6 @@ pub enum UsageMeasure {
|
||||
RequestsPerMinute,
|
||||
TokensPerMinute,
|
||||
TokensPerDay,
|
||||
InputTokensPerMonth,
|
||||
OutputTokensPerMonth,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
},
|
||||
test_llm_db,
|
||||
};
|
||||
use chrono::{Duration, Utc};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use pretty_assertions::assert_eq;
|
||||
use rpc::LanguageModelProvider;
|
||||
|
||||
@@ -29,16 +29,19 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let t0 = Utc::now();
|
||||
// We're using a fixed datetime to prevent flakiness based on the clock.
|
||||
let t0 = DateTime::parse_from_rfc3339("2024-08-08T22:46:33Z")
|
||||
.unwrap()
|
||||
.with_timezone(&Utc);
|
||||
let user_id = UserId::from_proto(123);
|
||||
|
||||
let now = t0;
|
||||
db.record_usage(user_id, false, provider, model, 1000, 0, now)
|
||||
db.record_usage(user_id, false, provider, model, 1000, 0, 0, 0, now)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let now = t0 + Duration::seconds(10);
|
||||
db.record_usage(user_id, false, provider, model, 2000, 0, now)
|
||||
db.record_usage(user_id, false, provider, model, 2000, 0, 0, 0, now)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -50,6 +53,8 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
|
||||
tokens_this_minute: 3000,
|
||||
tokens_this_day: 3000,
|
||||
input_tokens_this_month: 3000,
|
||||
cache_creation_input_tokens_this_month: 0,
|
||||
cache_read_input_tokens_this_month: 0,
|
||||
output_tokens_this_month: 0,
|
||||
spending_this_month: 0,
|
||||
lifetime_spending: 0,
|
||||
@@ -65,6 +70,8 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
|
||||
tokens_this_minute: 2000,
|
||||
tokens_this_day: 3000,
|
||||
input_tokens_this_month: 3000,
|
||||
cache_creation_input_tokens_this_month: 0,
|
||||
cache_read_input_tokens_this_month: 0,
|
||||
output_tokens_this_month: 0,
|
||||
spending_this_month: 0,
|
||||
lifetime_spending: 0,
|
||||
@@ -72,7 +79,7 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
|
||||
);
|
||||
|
||||
let now = t0 + Duration::seconds(60);
|
||||
db.record_usage(user_id, false, provider, model, 3000, 0, now)
|
||||
db.record_usage(user_id, false, provider, model, 3000, 0, 0, 0, now)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -84,6 +91,8 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
|
||||
tokens_this_minute: 5000,
|
||||
tokens_this_day: 6000,
|
||||
input_tokens_this_month: 6000,
|
||||
cache_creation_input_tokens_this_month: 0,
|
||||
cache_read_input_tokens_this_month: 0,
|
||||
output_tokens_this_month: 0,
|
||||
spending_this_month: 0,
|
||||
lifetime_spending: 0,
|
||||
@@ -100,13 +109,15 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
|
||||
tokens_this_minute: 0,
|
||||
tokens_this_day: 5000,
|
||||
input_tokens_this_month: 6000,
|
||||
cache_creation_input_tokens_this_month: 0,
|
||||
cache_read_input_tokens_this_month: 0,
|
||||
output_tokens_this_month: 0,
|
||||
spending_this_month: 0,
|
||||
lifetime_spending: 0,
|
||||
}
|
||||
);
|
||||
|
||||
db.record_usage(user_id, false, provider, model, 4000, 0, now)
|
||||
db.record_usage(user_id, false, provider, model, 4000, 0, 0, 0, now)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -118,22 +129,55 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
|
||||
tokens_this_minute: 4000,
|
||||
tokens_this_day: 9000,
|
||||
input_tokens_this_month: 10000,
|
||||
cache_creation_input_tokens_this_month: 0,
|
||||
cache_read_input_tokens_this_month: 0,
|
||||
output_tokens_this_month: 0,
|
||||
spending_this_month: 0,
|
||||
lifetime_spending: 0,
|
||||
}
|
||||
);
|
||||
|
||||
let t2 = t0 + Duration::days(30);
|
||||
let now = t2;
|
||||
// We're using a fixed datetime to prevent flakiness based on the clock.
|
||||
let now = DateTime::parse_from_rfc3339("2024-10-08T22:15:58Z")
|
||||
.unwrap()
|
||||
.with_timezone(&Utc);
|
||||
|
||||
// Test cache creation input tokens
|
||||
db.record_usage(user_id, false, provider, model, 1000, 500, 0, 0, now)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let usage = db.get_usage(user_id, provider, model, now).await.unwrap();
|
||||
assert_eq!(
|
||||
usage,
|
||||
Usage {
|
||||
requests_this_minute: 0,
|
||||
tokens_this_minute: 0,
|
||||
tokens_this_day: 0,
|
||||
input_tokens_this_month: 9000,
|
||||
requests_this_minute: 1,
|
||||
tokens_this_minute: 1500,
|
||||
tokens_this_day: 1500,
|
||||
input_tokens_this_month: 1000,
|
||||
cache_creation_input_tokens_this_month: 500,
|
||||
cache_read_input_tokens_this_month: 0,
|
||||
output_tokens_this_month: 0,
|
||||
spending_this_month: 0,
|
||||
lifetime_spending: 0,
|
||||
}
|
||||
);
|
||||
|
||||
// Test cache read input tokens
|
||||
db.record_usage(user_id, false, provider, model, 1000, 0, 300, 0, now)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let usage = db.get_usage(user_id, provider, model, now).await.unwrap();
|
||||
assert_eq!(
|
||||
usage,
|
||||
Usage {
|
||||
requests_this_minute: 2,
|
||||
tokens_this_minute: 2800,
|
||||
tokens_this_day: 2800,
|
||||
input_tokens_this_month: 2000,
|
||||
cache_creation_input_tokens_this_month: 500,
|
||||
cache_read_input_tokens_this_month: 300,
|
||||
output_tokens_this_month: 0,
|
||||
spending_this_month: 0,
|
||||
lifetime_spending: 0,
|
||||
|
||||
@@ -12,11 +12,15 @@ pub struct LlmUsageEventRow {
|
||||
pub model: String,
|
||||
pub provider: String,
|
||||
pub input_token_count: u64,
|
||||
pub cache_creation_input_token_count: u64,
|
||||
pub cache_read_input_token_count: u64,
|
||||
pub output_token_count: u64,
|
||||
pub requests_this_minute: u64,
|
||||
pub tokens_this_minute: u64,
|
||||
pub tokens_this_day: u64,
|
||||
pub input_tokens_this_month: u64,
|
||||
pub cache_creation_input_tokens_this_month: u64,
|
||||
pub cache_read_input_tokens_this_month: u64,
|
||||
pub output_tokens_this_month: u64,
|
||||
pub spending_this_month: u64,
|
||||
pub lifetime_spending: u64,
|
||||
|
||||
@@ -13,15 +13,15 @@ pub struct LlmTokenClaims {
|
||||
pub exp: u64,
|
||||
pub jti: String,
|
||||
pub user_id: u64,
|
||||
pub github_user_login: String,
|
||||
pub is_staff: bool,
|
||||
pub has_llm_closed_beta_feature_flag: bool,
|
||||
// This field is temporarily optional so it can be added
|
||||
// in a backwards-compatible way. We can make it required
|
||||
// once all of the LLM tokens have cycled (~1 hour after
|
||||
// this change has been deployed).
|
||||
#[serde(default)]
|
||||
pub github_user_login: Option<String>,
|
||||
pub is_staff: bool,
|
||||
#[serde(default)]
|
||||
pub has_llm_closed_beta_feature_flag: bool,
|
||||
pub has_llm_subscription: Option<bool>,
|
||||
pub plan: rpc::proto::Plan,
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ impl LlmTokenClaims {
|
||||
github_user_login: String,
|
||||
is_staff: bool,
|
||||
has_llm_closed_beta_feature_flag: bool,
|
||||
has_llm_subscription: bool,
|
||||
plan: rpc::proto::Plan,
|
||||
config: &Config,
|
||||
) -> Result<String> {
|
||||
@@ -47,9 +48,10 @@ impl LlmTokenClaims {
|
||||
exp: (now + LLM_TOKEN_LIFETIME).timestamp() as u64,
|
||||
jti: uuid::Uuid::new_v4().to_string(),
|
||||
user_id: user_id.to_proto(),
|
||||
github_user_login: Some(github_user_login),
|
||||
github_user_login,
|
||||
is_staff,
|
||||
has_llm_closed_beta_feature_flag,
|
||||
has_llm_subscription: Some(has_llm_subscription),
|
||||
plan,
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use axum::{
|
||||
routing::get,
|
||||
Extension, Router,
|
||||
};
|
||||
use collab::api::billing::sync_llm_usage_with_stripe_periodically;
|
||||
use collab::api::CloudflareIpCountryHeader;
|
||||
use collab::llm::{db::LlmDatabase, log_usage_periodically};
|
||||
use collab::migrations::run_database_migrations;
|
||||
@@ -29,7 +30,7 @@ use tower_http::trace::TraceLayer;
|
||||
use tracing_subscriber::{
|
||||
filter::EnvFilter, fmt::format::JsonFields, util::SubscriberInitExt, Layer,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use util::{maybe, ResultExt as _};
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const REVISION: Option<&'static str> = option_env!("GITHUB_SHA");
|
||||
@@ -136,6 +137,28 @@ async fn main() -> Result<()> {
|
||||
fetch_extensions_from_blob_store_periodically(state.clone());
|
||||
spawn_user_backfiller(state.clone());
|
||||
|
||||
let llm_db = maybe!(async {
|
||||
let database_url = state
|
||||
.config
|
||||
.llm_database_url
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("missing LLM_DATABASE_URL"))?;
|
||||
let max_connections = state
|
||||
.config
|
||||
.llm_database_max_connections
|
||||
.ok_or_else(|| anyhow!("missing LLM_DATABASE_MAX_CONNECTIONS"))?;
|
||||
|
||||
let mut db_options = db::ConnectOptions::new(database_url);
|
||||
db_options.max_connections(max_connections);
|
||||
LlmDatabase::new(db_options, state.executor.clone()).await
|
||||
})
|
||||
.await
|
||||
.trace_err();
|
||||
|
||||
if let Some(llm_db) = llm_db {
|
||||
sync_llm_usage_with_stripe_periodically(state.clone(), llm_db);
|
||||
}
|
||||
|
||||
app = app
|
||||
.merge(collab::api::events::router())
|
||||
.merge(collab::api::extensions::router())
|
||||
|
||||
@@ -191,16 +191,26 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn current_plan(&self, db: MutexGuard<'_, DbHandle>) -> anyhow::Result<proto::Plan> {
|
||||
pub async fn has_llm_subscription(
|
||||
&self,
|
||||
db: &MutexGuard<'_, DbHandle>,
|
||||
) -> anyhow::Result<bool> {
|
||||
if self.is_staff() {
|
||||
return Ok(proto::Plan::ZedPro);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let Some(user_id) = self.user_id() else {
|
||||
return Ok(proto::Plan::Free);
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
if db.has_active_billing_subscription(user_id).await? {
|
||||
Ok(db.has_active_billing_subscription(user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn current_plan(
|
||||
&self,
|
||||
_db: &MutexGuard<'_, DbHandle>,
|
||||
) -> anyhow::Result<proto::Plan> {
|
||||
if self.is_staff() {
|
||||
Ok(proto::Plan::ZedPro)
|
||||
} else {
|
||||
Ok(proto::Plan::Free)
|
||||
@@ -1739,6 +1749,7 @@ fn notify_rejoined_projects(
|
||||
worktree_id: worktree.id,
|
||||
path: settings_file.path,
|
||||
content: Some(settings_file.content),
|
||||
kind: Some(settings_file.kind.to_proto().into()),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
@@ -2220,6 +2231,7 @@ fn join_project_internal(
|
||||
worktree_id: worktree.id,
|
||||
path: settings_file.path,
|
||||
content: Some(settings_file.content),
|
||||
kind: Some(proto::update_user_settings::Kind::Settings.into()),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
@@ -3469,7 +3481,7 @@ fn should_auto_subscribe_to_channels(version: ZedVersion) -> bool {
|
||||
}
|
||||
|
||||
async fn update_user_plan(_user_id: UserId, session: &Session) -> Result<()> {
|
||||
let plan = session.current_plan(session.db().await).await?;
|
||||
let plan = session.current_plan(&session.db().await).await?;
|
||||
|
||||
session
|
||||
.peer
|
||||
@@ -4469,7 +4481,7 @@ async fn count_language_model_tokens(
|
||||
};
|
||||
authorize_access_to_legacy_llm_endpoints(&session).await?;
|
||||
|
||||
let rate_limit: Box<dyn RateLimit> = match session.current_plan(session.db().await).await? {
|
||||
let rate_limit: Box<dyn RateLimit> = match session.current_plan(&session.db().await).await? {
|
||||
proto::Plan::ZedPro => Box::new(ZedProCountLanguageModelTokensRateLimit),
|
||||
proto::Plan::Free => Box::new(FreeCountLanguageModelTokensRateLimit),
|
||||
};
|
||||
@@ -4590,7 +4602,7 @@ async fn compute_embeddings(
|
||||
let api_key = api_key.context("no OpenAI API key configured on the server")?;
|
||||
authorize_access_to_legacy_llm_endpoints(&session).await?;
|
||||
|
||||
let rate_limit: Box<dyn RateLimit> = match session.current_plan(session.db().await).await? {
|
||||
let rate_limit: Box<dyn RateLimit> = match session.current_plan(&session.db().await).await? {
|
||||
proto::Plan::ZedPro => Box::new(ZedProComputeEmbeddingsRateLimit),
|
||||
proto::Plan::Free => Box::new(FreeComputeEmbeddingsRateLimit),
|
||||
};
|
||||
@@ -4913,7 +4925,8 @@ async fn get_llm_api_token(
|
||||
user.github_login.clone(),
|
||||
session.is_staff(),
|
||||
has_llm_closed_beta_feature_flag,
|
||||
session.current_plan(db).await?,
|
||||
session.has_llm_subscription(&db).await?,
|
||||
session.current_plan(&db).await?,
|
||||
&session.app_state.config,
|
||||
)?;
|
||||
response.send(proto::GetLlmTokenResponse { token })?;
|
||||
|
||||
@@ -33,7 +33,7 @@ use project::{
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use settings::{LocalSettingsKind, SettingsStore};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
env, future, mem,
|
||||
@@ -3327,8 +3327,16 @@ async fn test_local_settings(
|
||||
.local_settings(worktree_b.read(cx).id())
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
(Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
|
||||
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
|
||||
(
|
||||
Path::new("").into(),
|
||||
LocalSettingsKind::Settings,
|
||||
r#"{"tab_size":2}"#.to_string()
|
||||
),
|
||||
(
|
||||
Path::new("a").into(),
|
||||
LocalSettingsKind::Settings,
|
||||
r#"{"tab_size":8}"#.to_string()
|
||||
),
|
||||
]
|
||||
)
|
||||
});
|
||||
@@ -3346,8 +3354,16 @@ async fn test_local_settings(
|
||||
.local_settings(worktree_b.read(cx).id())
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
(Path::new("").into(), r#"{}"#.to_string()),
|
||||
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
|
||||
(
|
||||
Path::new("").into(),
|
||||
LocalSettingsKind::Settings,
|
||||
r#"{}"#.to_string()
|
||||
),
|
||||
(
|
||||
Path::new("a").into(),
|
||||
LocalSettingsKind::Settings,
|
||||
r#"{"tab_size":8}"#.to_string()
|
||||
),
|
||||
]
|
||||
)
|
||||
});
|
||||
@@ -3375,8 +3391,16 @@ async fn test_local_settings(
|
||||
.local_settings(worktree_b.read(cx).id())
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
|
||||
(Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
|
||||
(
|
||||
Path::new("a").into(),
|
||||
LocalSettingsKind::Settings,
|
||||
r#"{"tab_size":8}"#.to_string()
|
||||
),
|
||||
(
|
||||
Path::new("b").into(),
|
||||
LocalSettingsKind::Settings,
|
||||
r#"{"tab_size":4}"#.to_string()
|
||||
),
|
||||
]
|
||||
)
|
||||
});
|
||||
@@ -3406,7 +3430,11 @@ async fn test_local_settings(
|
||||
store
|
||||
.local_settings(worktree_b.read(cx).id())
|
||||
.collect::<Vec<_>>(),
|
||||
&[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
|
||||
&[(
|
||||
Path::new("a").into(),
|
||||
LocalSettingsKind::Settings,
|
||||
r#"{"hard_tabs":true}"#.to_string()
|
||||
),]
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use fs::{FakeFs, Fs as _};
|
||||
use gpui::{Context as _, TestAppContext};
|
||||
use language::language_settings::all_language_settings;
|
||||
use project::ProjectPath;
|
||||
use remote::SshSession;
|
||||
use remote::SshRemoteClient;
|
||||
use remote_server::HeadlessProject;
|
||||
use serde_json::json;
|
||||
use std::{path::Path, sync::Arc};
|
||||
@@ -24,7 +24,7 @@ async fn test_sharing_an_ssh_remote_project(
|
||||
.await;
|
||||
|
||||
// Set up project on remote FS
|
||||
let (client_ssh, server_ssh) = SshSession::fake(cx_a, server_cx);
|
||||
let (client_ssh, server_ssh) = SshRemoteClient::fake(cx_a, server_cx);
|
||||
let remote_fs = FakeFs::new(server_cx.executor());
|
||||
remote_fs
|
||||
.insert_tree(
|
||||
|
||||
@@ -25,7 +25,7 @@ use node_runtime::NodeRuntime;
|
||||
use notifications::NotificationStore;
|
||||
use parking_lot::Mutex;
|
||||
use project::{Project, WorktreeId};
|
||||
use remote::SshSession;
|
||||
use remote::SshRemoteClient;
|
||||
use rpc::{
|
||||
proto::{self, ChannelRole},
|
||||
RECEIVE_TIMEOUT,
|
||||
@@ -677,7 +677,7 @@ impl TestServer {
|
||||
migrations_path: None,
|
||||
seed_path: None,
|
||||
stripe_api_key: None,
|
||||
stripe_price_id: None,
|
||||
stripe_llm_usage_price_id: None,
|
||||
supermaven_admin_api_key: None,
|
||||
user_backfiller_github_access_token: None,
|
||||
},
|
||||
@@ -835,7 +835,7 @@ impl TestClient {
|
||||
pub async fn build_ssh_project(
|
||||
&self,
|
||||
root_path: impl AsRef<Path>,
|
||||
ssh: Arc<SshSession>,
|
||||
ssh: Model<SshRemoteClient>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Model<Project>, WorktreeId) {
|
||||
let project = cx.update(|cx| {
|
||||
|
||||
@@ -188,7 +188,7 @@ macro_rules! define_connection {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write_and_log<F>(cx: &mut AppContext, db_write: impl FnOnce() -> F + Send + 'static)
|
||||
pub fn write_and_log<F>(cx: &AppContext, db_write: impl FnOnce() -> F + Send + 'static)
|
||||
where
|
||||
F: Future<Output = anyhow::Result<()>> + Send,
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::ProjectDiagnosticsEditor;
|
||||
use gpui::{EventEmitter, ParentElement, Render, View, ViewContext, WeakView};
|
||||
use ui::prelude::*;
|
||||
use ui::{IconButton, IconName, Tooltip};
|
||||
use ui::{IconButton, IconButtonShape, IconName, Tooltip};
|
||||
use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
||||
|
||||
pub struct ToolbarControls {
|
||||
@@ -33,11 +33,19 @@ impl Render for ToolbarControls {
|
||||
"Include Warnings"
|
||||
};
|
||||
|
||||
let warning_color = if include_warnings {
|
||||
Color::Warning
|
||||
} else {
|
||||
Color::Muted
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when(has_stale_excerpts, |div| {
|
||||
div.child(
|
||||
IconButton::new("update-excerpts", IconName::Update)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.disabled(is_updating)
|
||||
.tooltip(move |cx| Tooltip::text("Update excerpts", cx))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
@@ -51,6 +59,8 @@ impl Render for ToolbarControls {
|
||||
})
|
||||
.child(
|
||||
IconButton::new("toggle-warnings", IconName::Warning)
|
||||
.icon_color(warning_color)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text(tooltip, cx))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
if let Some(editor) = this.editor() {
|
||||
|
||||
@@ -193,6 +193,7 @@ gpui::actions!(
|
||||
AcceptPartialInlineCompletion,
|
||||
AddSelectionAbove,
|
||||
AddSelectionBelow,
|
||||
ApplyDiffHunk,
|
||||
Backspace,
|
||||
Cancel,
|
||||
CancelLanguageServerWork,
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::lsp_ext::find_specific_language_server_in_selection;
|
||||
|
||||
use crate::{element::register_action, Editor, SwitchSourceHeader};
|
||||
|
||||
static CLANGD_SERVER_NAME: &str = "clangd";
|
||||
const CLANGD_SERVER_NAME: &str = "clangd";
|
||||
|
||||
fn is_c_language(language: &Language) -> bool {
|
||||
return language.name() == "C++".into() || language.name() == "C".into();
|
||||
|
||||
@@ -61,7 +61,7 @@ use debounced_delay::DebouncedDelay;
|
||||
use display_map::*;
|
||||
pub use display_map::{DisplayPoint, FoldPlaceholder};
|
||||
pub use editor_settings::{
|
||||
CurrentLineHighlight, EditorSettings, ScrollBeyondLastLine, SearchSettings,
|
||||
CurrentLineHighlight, EditorSettings, ScrollBeyondLastLine, SearchSettings, ShowScrollbar,
|
||||
};
|
||||
pub use editor_settings_controls::*;
|
||||
use element::LineWithInvisibles;
|
||||
@@ -1228,6 +1228,10 @@ impl CompletionsMenu {
|
||||
None
|
||||
};
|
||||
|
||||
let color_swatch = completion
|
||||
.color()
|
||||
.map(|color| div().size_4().bg(color).rounded_sm());
|
||||
|
||||
div().min_w(px(220.)).max_w(px(540.)).child(
|
||||
ListItem::new(mat.candidate_id)
|
||||
.inset(true)
|
||||
@@ -1243,6 +1247,7 @@ impl CompletionsMenu {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
}))
|
||||
.start_slot::<Div>(color_swatch)
|
||||
.child(h_flex().overflow_hidden().child(completion_label))
|
||||
.end_slot::<Label>(documentation_label),
|
||||
)
|
||||
@@ -3059,7 +3064,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
if self.clear_clicked_diff_hunks(cx) {
|
||||
if self.clear_expanded_diff_hunks(cx) {
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
@@ -6205,6 +6210,28 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_selected_diff_hunks(&mut self, _: &ApplyDiffHunk, cx: &mut ViewContext<Self>) {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let hunks = hunks_for_selections(&snapshot, &self.selections.disjoint_anchors());
|
||||
let mut ranges_by_buffer = HashMap::default();
|
||||
self.transact(cx, |editor, cx| {
|
||||
for hunk in hunks {
|
||||
if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) {
|
||||
ranges_by_buffer
|
||||
.entry(buffer.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(hunk.buffer_range.to_offset(buffer.read(cx)));
|
||||
}
|
||||
}
|
||||
|
||||
for (buffer, ranges) in ranges_by_buffer {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.merge_into_base(ranges, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn open_active_item_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
|
||||
if let Some(working_directory) = self.active_excerpt(cx).and_then(|(_, buffer, _)| {
|
||||
let project_path = buffer.read(cx).project_path(cx)?;
|
||||
@@ -10773,7 +10800,7 @@ impl Editor {
|
||||
.selections
|
||||
.all::<Point>(cx)
|
||||
.iter()
|
||||
.any(|selection| selection.range().overlaps(&intersection_range));
|
||||
.any(|selection| RangeExt::overlaps(&selection.range(), &intersection_range));
|
||||
|
||||
self.unfold_ranges(std::iter::once(intersection_range), true, autoscroll, cx)
|
||||
}
|
||||
@@ -11243,30 +11270,32 @@ impl Editor {
|
||||
None
|
||||
}
|
||||
|
||||
fn target_file<'a>(&self, cx: &'a AppContext) -> Option<&'a dyn language::LocalFile> {
|
||||
self.active_excerpt(cx)?
|
||||
.1
|
||||
.read(cx)
|
||||
.file()
|
||||
.and_then(|f| f.as_local())
|
||||
}
|
||||
|
||||
pub fn reveal_in_finder(&mut self, _: &RevealInFileManager, cx: &mut ViewContext<Self>) {
|
||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
|
||||
cx.reveal_path(&file.abs_path(cx));
|
||||
}
|
||||
if let Some(target) = self.target_file(cx) {
|
||||
cx.reveal_path(&target.abs_path(cx));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext<Self>) {
|
||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
|
||||
if let Some(path) = file.abs_path(cx).to_str() {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(path.to_string()));
|
||||
}
|
||||
if let Some(file) = self.target_file(cx) {
|
||||
if let Some(path) = file.abs_path(cx).to_str() {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(path.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext<Self>) {
|
||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
|
||||
if let Some(path) = file.path().to_str() {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(path.to_string()));
|
||||
}
|
||||
if let Some(file) = self.target_file(cx) {
|
||||
if let Some(path) = file.path().to_str() {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(path.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11477,12 +11506,10 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn copy_file_location(&mut self, _: &CopyFileLocation, cx: &mut ViewContext<Self>) {
|
||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
|
||||
if let Some(path) = file.path().to_str() {
|
||||
let selection = self.selections.newest::<Point>(cx).start.row + 1;
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(format!("{path}:{selection}")));
|
||||
}
|
||||
if let Some(file) = self.target_file(cx) {
|
||||
if let Some(path) = file.path().to_str() {
|
||||
let selection = self.selections.newest::<Point>(cx).start.row + 1;
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(format!("{path}:{selection}")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12119,9 +12146,14 @@ impl Editor {
|
||||
}
|
||||
|
||||
let Some(project) = &self.project else { return };
|
||||
let telemetry = project.read(cx).client().telemetry().clone();
|
||||
let (telemetry, is_via_ssh) = {
|
||||
let project = project.read(cx);
|
||||
let telemetry = project.client().telemetry().clone();
|
||||
let is_via_ssh = project.is_via_ssh();
|
||||
(telemetry, is_via_ssh)
|
||||
};
|
||||
refresh_linked_ranges(self, cx);
|
||||
telemetry.log_edit_event("editor");
|
||||
telemetry.log_edit_event("editor", is_via_ssh);
|
||||
}
|
||||
multi_buffer::Event::ExcerptsAdded {
|
||||
buffer,
|
||||
@@ -12243,12 +12275,9 @@ impl Editor {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let mut new_selections_by_buffer = HashMap::default();
|
||||
for selection in self.selections.all::<usize>(cx) {
|
||||
for (buffer, mut range, _) in
|
||||
for (buffer, range, _) in
|
||||
buffer.range_to_buffer_ranges(selection.start..selection.end, cx)
|
||||
{
|
||||
if selection.reversed {
|
||||
mem::swap(&mut range.start, &mut range.end);
|
||||
}
|
||||
let mut range = range.to_point(buffer.read(cx));
|
||||
range.start.column = 0;
|
||||
range.end.column = buffer.read(cx).line_len(range.end.row);
|
||||
@@ -12465,13 +12494,15 @@ impl Editor {
|
||||
.settings_at(0, cx)
|
||||
.show_inline_completions;
|
||||
|
||||
let telemetry = project.read(cx).client().telemetry().clone();
|
||||
let project = project.read(cx);
|
||||
let telemetry = project.client().telemetry().clone();
|
||||
telemetry.report_editor_event(
|
||||
file_extension,
|
||||
vim_mode,
|
||||
operation,
|
||||
copilot_enabled,
|
||||
copilot_enabled_for_language,
|
||||
project.is_via_ssh(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -11715,6 +11715,60 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext)
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
|
||||
let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\n";
|
||||
|
||||
let buffer = cx.new_model(|cx| {
|
||||
let mut buffer = Buffer::local(text.to_string(), cx);
|
||||
buffer.set_diff_base(Some(base.into()), cx);
|
||||
buffer
|
||||
});
|
||||
|
||||
let multi_buffer = cx.new_model(|cx| {
|
||||
let mut multibuffer = MultiBuffer::new(ReadWrite);
|
||||
multibuffer.push_excerpts(
|
||||
buffer.clone(),
|
||||
[
|
||||
ExcerptRange {
|
||||
context: Point::new(0, 0)..Point::new(2, 0),
|
||||
primary: None,
|
||||
},
|
||||
ExcerptRange {
|
||||
context: Point::new(5, 0)..Point::new(7, 0),
|
||||
primary: None,
|
||||
},
|
||||
],
|
||||
cx,
|
||||
);
|
||||
multibuffer
|
||||
});
|
||||
|
||||
let editor = cx.add_window(|cx| Editor::new(EditorMode::Full, multi_buffer, None, true, cx));
|
||||
let mut cx = EditorTestContext::for_editor(editor, cx).await;
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, cx| editor.expand_all_hunk_diffs(&Default::default(), cx));
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
cx.assert_diff_hunks(
|
||||
"
|
||||
aaa
|
||||
- bbb
|
||||
+ BBB
|
||||
|
||||
- ddd
|
||||
- eee
|
||||
+ EEE
|
||||
fff
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_edits_around_expanded_insertion_hunks(
|
||||
executor: BackgroundExecutor,
|
||||
|
||||
@@ -436,6 +436,7 @@ impl EditorElement {
|
||||
register_action(view, cx, Editor::accept_inline_completion);
|
||||
register_action(view, cx, Editor::revert_file);
|
||||
register_action(view, cx, Editor::revert_selected_hunks);
|
||||
register_action(view, cx, Editor::apply_selected_diff_hunks);
|
||||
register_action(view, cx, Editor::open_active_item_in_terminal)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ use ui::{
|
||||
use util::RangeExt;
|
||||
|
||||
use crate::{
|
||||
editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, BlockDisposition,
|
||||
BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, DisplayRow, DisplaySnapshot,
|
||||
Editor, EditorElement, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk, RevertFile,
|
||||
editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, ApplyDiffHunk,
|
||||
BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, DisplayRow,
|
||||
DisplaySnapshot, Editor, EditorElement, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk, RevertFile,
|
||||
RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff,
|
||||
};
|
||||
|
||||
@@ -32,6 +32,7 @@ pub(super) struct ExpandedHunks {
|
||||
pub(crate) hunks: Vec<ExpandedHunk>,
|
||||
diff_base: HashMap<BufferId, DiffBaseBuffer>,
|
||||
hunk_update_tasks: HashMap<Option<BufferId>, Task<()>>,
|
||||
expand_all: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -72,6 +73,10 @@ impl ExpandedHunks {
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn set_expand_all_diff_hunks(&mut self) {
|
||||
self.expanded_hunks.expand_all = true;
|
||||
}
|
||||
|
||||
pub(super) fn toggle_hovered_hunk(
|
||||
&mut self,
|
||||
hovered_hunk: &HoveredHunk,
|
||||
@@ -133,6 +138,10 @@ impl Editor {
|
||||
hunks_to_toggle: Vec<MultiBufferDiffHunk>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if self.expanded_hunks.expand_all {
|
||||
return;
|
||||
}
|
||||
|
||||
let previous_toggle_task = self.expanded_hunks.hunk_update_tasks.remove(&None);
|
||||
let new_toggle_task = cx.spawn(move |editor, mut cx| async move {
|
||||
if let Some(task) = previous_toggle_task {
|
||||
@@ -200,19 +209,20 @@ impl Editor {
|
||||
|
||||
retain
|
||||
});
|
||||
for remaining_hunk in hunks_to_toggle {
|
||||
let remaining_hunk_point_range =
|
||||
Point::new(remaining_hunk.row_range.start.0, 0)
|
||||
..Point::new(remaining_hunk.row_range.end.0, 0);
|
||||
for hunk in hunks_to_toggle {
|
||||
let remaining_hunk_point_range = Point::new(hunk.row_range.start.0, 0)
|
||||
..Point::new(hunk.row_range.end.0, 0);
|
||||
let hunk_start = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(remaining_hunk_point_range.start);
|
||||
let hunk_end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(hunk_start.excerpt_id, hunk.buffer_range.end)
|
||||
.unwrap();
|
||||
hunks_to_expand.push(HoveredHunk {
|
||||
status: hunk_status(&remaining_hunk),
|
||||
multi_buffer_range: snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(remaining_hunk_point_range.start)
|
||||
..snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_after(remaining_hunk_point_range.end),
|
||||
diff_base_byte_range: remaining_hunk.diff_base_byte_range.clone(),
|
||||
status: hunk_status(&hunk),
|
||||
multi_buffer_range: hunk_start..hunk_end,
|
||||
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -237,45 +247,29 @@ impl Editor {
|
||||
hunk: &HoveredHunk,
|
||||
cx: &mut ViewContext<'_, Editor>,
|
||||
) -> Option<()> {
|
||||
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
let multi_buffer_row_range = hunk
|
||||
.multi_buffer_range
|
||||
.start
|
||||
.to_point(&multi_buffer_snapshot)
|
||||
..hunk.multi_buffer_range.end.to_point(&multi_buffer_snapshot);
|
||||
let hunk_start = hunk.multi_buffer_range.start;
|
||||
let hunk_end = hunk.multi_buffer_range.end;
|
||||
|
||||
let buffer = self.buffer().clone();
|
||||
let snapshot = self.snapshot(cx);
|
||||
let buffer = self.buffer.clone();
|
||||
let multi_buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
let hunk_range = hunk.multi_buffer_range.clone();
|
||||
let (diff_base_buffer, deleted_text_lines) = buffer.update(cx, |buffer, cx| {
|
||||
let hunk = buffer_diff_hunk(&snapshot.buffer_snapshot, multi_buffer_row_range.clone())?;
|
||||
let mut buffer_ranges = buffer.range_to_buffer_ranges(multi_buffer_row_range, cx);
|
||||
if buffer_ranges.len() == 1 {
|
||||
let (buffer, _, _) = buffer_ranges.pop()?;
|
||||
let diff_base_buffer = diff_base_buffer
|
||||
.or_else(|| self.current_diff_base_buffer(&buffer, cx))
|
||||
.or_else(|| create_diff_base_buffer(&buffer, cx))?;
|
||||
let buffer = buffer.read(cx);
|
||||
let deleted_text_lines = buffer.diff_base().map(|diff_base| {
|
||||
let diff_start_row = diff_base
|
||||
.offset_to_point(hunk.diff_base_byte_range.start)
|
||||
.row;
|
||||
let diff_end_row = diff_base.offset_to_point(hunk.diff_base_byte_range.end).row;
|
||||
|
||||
diff_end_row - diff_start_row
|
||||
})?;
|
||||
Some((diff_base_buffer, deleted_text_lines))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
let buffer = buffer.buffer(hunk_range.start.buffer_id?)?;
|
||||
let diff_base_buffer = diff_base_buffer
|
||||
.or_else(|| self.current_diff_base_buffer(&buffer, cx))
|
||||
.or_else(|| create_diff_base_buffer(&buffer, cx))?;
|
||||
let deleted_text_lines = buffer.read(cx).diff_base().map(|diff_base| {
|
||||
let diff_start_row = diff_base
|
||||
.offset_to_point(hunk.diff_base_byte_range.start)
|
||||
.row;
|
||||
let diff_end_row = diff_base.offset_to_point(hunk.diff_base_byte_range.end).row;
|
||||
diff_end_row - diff_start_row
|
||||
})?;
|
||||
Some((diff_base_buffer, deleted_text_lines))
|
||||
})?;
|
||||
|
||||
let block_insert_index = match self.expanded_hunks.hunks.binary_search_by(|probe| {
|
||||
probe
|
||||
.hunk_range
|
||||
.start
|
||||
.cmp(&hunk_start, &multi_buffer_snapshot)
|
||||
.cmp(&hunk_range.start, &multi_buffer_snapshot)
|
||||
}) {
|
||||
Ok(_already_present) => return None,
|
||||
Err(ix) => ix,
|
||||
@@ -295,7 +289,7 @@ impl Editor {
|
||||
}
|
||||
DiffHunkStatus::Added => {
|
||||
self.highlight_rows::<DiffRowHighlight>(
|
||||
hunk_start..hunk_end,
|
||||
hunk_range.clone(),
|
||||
added_hunk_color(cx),
|
||||
false,
|
||||
cx,
|
||||
@@ -304,7 +298,7 @@ impl Editor {
|
||||
}
|
||||
DiffHunkStatus::Modified => {
|
||||
self.highlight_rows::<DiffRowHighlight>(
|
||||
hunk_start..hunk_end,
|
||||
hunk_range.clone(),
|
||||
added_hunk_color(cx),
|
||||
false,
|
||||
cx,
|
||||
@@ -323,7 +317,7 @@ impl Editor {
|
||||
block_insert_index,
|
||||
ExpandedHunk {
|
||||
blocks,
|
||||
hunk_range: hunk_start..hunk_end,
|
||||
hunk_range,
|
||||
status: hunk.status,
|
||||
folded: false,
|
||||
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
|
||||
@@ -333,12 +327,49 @@ impl Editor {
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn apply_changes_in_range(
|
||||
&mut self,
|
||||
range: Range<Anchor>,
|
||||
cx: &mut ViewContext<'_, Editor>,
|
||||
) -> Option<()> {
|
||||
let (buffer, range, _) = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.range_to_buffer_ranges(range, cx)
|
||||
.into_iter()
|
||||
.next()?;
|
||||
|
||||
buffer.update(cx, |branch_buffer, cx| {
|
||||
branch_buffer.merge_into_base(vec![range], cx);
|
||||
});
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn apply_all_changes(&self, cx: &mut ViewContext<Self>) {
|
||||
let buffers = self.buffer.read(cx).all_buffers();
|
||||
for branch_buffer in buffers {
|
||||
branch_buffer.update(cx, |branch_buffer, cx| {
|
||||
branch_buffer.merge_into_base(Vec::new(), cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn hunk_header_block(
|
||||
&self,
|
||||
hunk: &HoveredHunk,
|
||||
cx: &mut ViewContext<'_, Editor>,
|
||||
) -> BlockProperties<Anchor> {
|
||||
let is_branch_buffer = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.point_to_buffer_offset(hunk.multi_buffer_range.start, cx)
|
||||
.map_or(false, |(buffer, _, _)| {
|
||||
buffer.read(cx).diff_base_buffer().is_some()
|
||||
});
|
||||
|
||||
let border_color = cx.theme().colors().border_variant;
|
||||
let bg_color = cx.theme().colors().editor_background;
|
||||
let gutter_color = match hunk.status {
|
||||
DiffHunkStatus::Added => cx.theme().status().created,
|
||||
DiffHunkStatus::Modified => cx.theme().status().modified,
|
||||
@@ -354,6 +385,7 @@ impl Editor {
|
||||
render: Box::new({
|
||||
let editor = cx.view().clone();
|
||||
let hunk = hunk.clone();
|
||||
|
||||
move |cx| {
|
||||
let hunk_controls_menu_handle =
|
||||
editor.read(cx).hunk_controls_menu_handle.clone();
|
||||
@@ -364,7 +396,7 @@ impl Editor {
|
||||
.w_full()
|
||||
.border_t_1()
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.bg(bg_color)
|
||||
.child(
|
||||
div()
|
||||
.id("gutter-strip")
|
||||
@@ -384,135 +416,70 @@ impl Editor {
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.pl_2()
|
||||
.pr_6()
|
||||
.px_6()
|
||||
.size_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconButton::new("next-hunk", IconName::ArrowDown)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
move |cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Next Hunk",
|
||||
&GoToHunk,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
let hunk = hunk.clone();
|
||||
move |_event, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let position = hunk
|
||||
.multi_buffer_range
|
||||
.end
|
||||
.to_point(
|
||||
&snapshot.buffer_snapshot,
|
||||
);
|
||||
if let Some(hunk) = editor
|
||||
.go_to_hunk_after_position(
|
||||
&snapshot, position, cx,
|
||||
)
|
||||
{
|
||||
let multi_buffer_start = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(Point::new(
|
||||
hunk.row_range.start.0,
|
||||
0,
|
||||
));
|
||||
let multi_buffer_end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_after(Point::new(
|
||||
hunk.row_range.end.0,
|
||||
0,
|
||||
));
|
||||
editor.expand_diff_hunk(
|
||||
None,
|
||||
&HoveredHunk {
|
||||
multi_buffer_range:
|
||||
multi_buffer_start
|
||||
..multi_buffer_end,
|
||||
status: hunk_status(&hunk),
|
||||
diff_base_byte_range: hunk
|
||||
.diff_base_byte_range,
|
||||
},
|
||||
.when(!is_branch_buffer, |row| {
|
||||
row.child(
|
||||
IconButton::new("next-hunk", IconName::ArrowDown)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
move |cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Next Hunk",
|
||||
&GoToHunk,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
let hunk = hunk.clone();
|
||||
move |_event, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.go_to_subsequent_hunk(
|
||||
hunk.multi_buffer_range.end,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("prev-hunk", IconName::ArrowUp)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
move |cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Previous Hunk",
|
||||
&GoToPrevHunk,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
let hunk = hunk.clone();
|
||||
move |_event, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let position = hunk
|
||||
.multi_buffer_range
|
||||
.start
|
||||
.to_point(
|
||||
&snapshot.buffer_snapshot,
|
||||
);
|
||||
let hunk = editor
|
||||
.go_to_hunk_before_position(
|
||||
&snapshot, position, cx,
|
||||
);
|
||||
if let Some(hunk) = hunk {
|
||||
let multi_buffer_start = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(Point::new(
|
||||
hunk.row_range.start.0,
|
||||
0,
|
||||
));
|
||||
let multi_buffer_end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_after(Point::new(
|
||||
hunk.row_range.end.0,
|
||||
0,
|
||||
));
|
||||
editor.expand_diff_hunk(
|
||||
None,
|
||||
&HoveredHunk {
|
||||
multi_buffer_range:
|
||||
multi_buffer_start
|
||||
..multi_buffer_end,
|
||||
status: hunk_status(&hunk),
|
||||
diff_base_byte_range: hunk
|
||||
.diff_base_byte_range,
|
||||
},
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("prev-hunk", IconName::ArrowUp)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
move |cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Previous Hunk",
|
||||
&GoToPrevHunk,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
let hunk = hunk.clone();
|
||||
move |_event, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.go_to_preceding_hunk(
|
||||
hunk.multi_buffer_range.start,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
IconButton::new("discard", IconName::Undo)
|
||||
.shape(IconButtonShape::Square)
|
||||
@@ -558,46 +525,90 @@ impl Editor {
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child({
|
||||
let focus = editor.focus_handle(cx);
|
||||
PopoverMenu::new("hunk-controls-dropdown")
|
||||
.trigger(
|
||||
IconButton::new(
|
||||
"toggle_editor_selections_icon",
|
||||
IconName::EllipsisVertical,
|
||||
)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.selected(
|
||||
hunk_controls_menu_handle.is_deployed(),
|
||||
)
|
||||
.when(
|
||||
!hunk_controls_menu_handle.is_deployed(),
|
||||
|this| {
|
||||
this.tooltip(|cx| {
|
||||
Tooltip::text("Hunk Controls", cx)
|
||||
})
|
||||
},
|
||||
),
|
||||
.map(|this| {
|
||||
if is_branch_buffer {
|
||||
this.child(
|
||||
IconButton::new("apply", IconName::Check)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle =
|
||||
editor.focus_handle(cx);
|
||||
move |cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Apply Hunk",
|
||||
&ApplyDiffHunk,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
let hunk = hunk.clone();
|
||||
move |_event, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.apply_changes_in_range(
|
||||
hunk.multi_buffer_range
|
||||
.clone(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
.anchor(AnchorCorner::TopRight)
|
||||
.with_handle(hunk_controls_menu_handle)
|
||||
.menu(move |cx| {
|
||||
let focus = focus.clone();
|
||||
let menu =
|
||||
ContextMenu::build(cx, move |menu, _| {
|
||||
menu.context(focus.clone()).action(
|
||||
"Discard All",
|
||||
RevertFile.boxed_clone(),
|
||||
} else {
|
||||
this.child({
|
||||
let focus = editor.focus_handle(cx);
|
||||
PopoverMenu::new("hunk-controls-dropdown")
|
||||
.trigger(
|
||||
IconButton::new(
|
||||
"toggle_editor_selections_icon",
|
||||
IconName::EllipsisVertical,
|
||||
)
|
||||
});
|
||||
Some(menu)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.selected(
|
||||
hunk_controls_menu_handle
|
||||
.is_deployed(),
|
||||
)
|
||||
.when(
|
||||
!hunk_controls_menu_handle
|
||||
.is_deployed(),
|
||||
|this| {
|
||||
this.tooltip(|cx| {
|
||||
Tooltip::text(
|
||||
"Hunk Controls",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
.anchor(AnchorCorner::TopRight)
|
||||
.with_handle(hunk_controls_menu_handle)
|
||||
.menu(move |cx| {
|
||||
let focus = focus.clone();
|
||||
let menu = ContextMenu::build(
|
||||
cx,
|
||||
move |menu, _| {
|
||||
menu.context(focus.clone())
|
||||
.action(
|
||||
"Discard All Hunks",
|
||||
RevertFile
|
||||
.boxed_clone(),
|
||||
)
|
||||
},
|
||||
);
|
||||
Some(menu)
|
||||
})
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div().child(
|
||||
.when(!is_branch_buffer, |div| {
|
||||
div.child(
|
||||
IconButton::new("collapse", IconName::Close)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -621,8 +632,8 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -697,7 +708,10 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn clear_clicked_diff_hunks(&mut self, cx: &mut ViewContext<'_, Editor>) -> bool {
|
||||
pub(super) fn clear_expanded_diff_hunks(&mut self, cx: &mut ViewContext<'_, Editor>) -> bool {
|
||||
if self.expanded_hunks.expand_all {
|
||||
return false;
|
||||
}
|
||||
self.expanded_hunks.hunk_update_tasks.clear();
|
||||
self.clear_row_highlights::<DiffRowHighlight>();
|
||||
let to_remove = self
|
||||
@@ -801,33 +815,43 @@ impl Editor {
|
||||
status,
|
||||
} => {
|
||||
let hunk_display_range = display_row_range;
|
||||
|
||||
if expanded_hunk_display_range.start
|
||||
> hunk_display_range.end
|
||||
{
|
||||
recalculated_hunks.next();
|
||||
continue;
|
||||
} else if expanded_hunk_display_range.end
|
||||
< hunk_display_range.start
|
||||
{
|
||||
break;
|
||||
} else {
|
||||
if !expanded_hunk.folded
|
||||
&& expanded_hunk_display_range == hunk_display_range
|
||||
&& expanded_hunk.status == hunk_status(buffer_hunk)
|
||||
&& expanded_hunk.diff_base_byte_range
|
||||
== buffer_hunk.diff_base_byte_range
|
||||
{
|
||||
recalculated_hunks.next();
|
||||
retain = true;
|
||||
} else {
|
||||
if editor.expanded_hunks.expand_all {
|
||||
hunks_to_reexpand.push(HoveredHunk {
|
||||
status,
|
||||
multi_buffer_range,
|
||||
diff_base_byte_range,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if expanded_hunk_display_range.end
|
||||
< hunk_display_range.start
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if !expanded_hunk.folded
|
||||
&& expanded_hunk_display_range == hunk_display_range
|
||||
&& expanded_hunk.status == hunk_status(buffer_hunk)
|
||||
&& expanded_hunk.diff_base_byte_range
|
||||
== buffer_hunk.diff_base_byte_range
|
||||
{
|
||||
recalculated_hunks.next();
|
||||
retain = true;
|
||||
} else {
|
||||
hunks_to_reexpand.push(HoveredHunk {
|
||||
status,
|
||||
multi_buffer_range,
|
||||
diff_base_byte_range,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -839,6 +863,26 @@ impl Editor {
|
||||
retain
|
||||
});
|
||||
|
||||
if editor.expanded_hunks.expand_all {
|
||||
for hunk in recalculated_hunks {
|
||||
match diff_hunk_to_display(&hunk, &snapshot) {
|
||||
DisplayDiffHunk::Folded { .. } => {}
|
||||
DisplayDiffHunk::Unfolded {
|
||||
diff_base_byte_range,
|
||||
multi_buffer_range,
|
||||
status,
|
||||
..
|
||||
} => {
|
||||
hunks_to_reexpand.push(HoveredHunk {
|
||||
status,
|
||||
multi_buffer_range,
|
||||
diff_base_byte_range,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editor.remove_highlighted_rows::<DiffRowHighlight>(highlights_to_remove, cx);
|
||||
editor.remove_blocks(blocks_to_remove, None, cx);
|
||||
|
||||
@@ -876,6 +920,51 @@ impl Editor {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn go_to_subsequent_hunk(&mut self, position: Anchor, cx: &mut ViewContext<Self>) {
|
||||
let snapshot = self.snapshot(cx);
|
||||
let position = position.to_point(&snapshot.buffer_snapshot);
|
||||
if let Some(hunk) = self.go_to_hunk_after_position(&snapshot, position, cx) {
|
||||
let multi_buffer_start = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(Point::new(hunk.row_range.start.0, 0));
|
||||
let multi_buffer_end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_after(Point::new(hunk.row_range.end.0, 0));
|
||||
self.expand_diff_hunk(
|
||||
None,
|
||||
&HoveredHunk {
|
||||
multi_buffer_range: multi_buffer_start..multi_buffer_end,
|
||||
status: hunk_status(&hunk),
|
||||
diff_base_byte_range: hunk.diff_base_byte_range,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn go_to_preceding_hunk(&mut self, position: Anchor, cx: &mut ViewContext<Self>) {
|
||||
let snapshot = self.snapshot(cx);
|
||||
let position = position.to_point(&snapshot.buffer_snapshot);
|
||||
let hunk = self.go_to_hunk_before_position(&snapshot, position, cx);
|
||||
if let Some(hunk) = hunk {
|
||||
let multi_buffer_start = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(Point::new(hunk.row_range.start.0, 0));
|
||||
let multi_buffer_end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_after(Point::new(hunk.row_range.end.0, 0));
|
||||
self.expand_diff_hunk(
|
||||
None,
|
||||
&HoveredHunk {
|
||||
multi_buffer_range: multi_buffer_start..multi_buffer_end,
|
||||
status: hunk_status(&hunk),
|
||||
diff_base_byte_range: hunk.diff_base_byte_range,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_diff_hunk(
|
||||
@@ -958,13 +1047,15 @@ fn editor_with_deleted_text(
|
||||
editor.scroll_manager.set_forbid_vertical_scroll(true);
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_inline_completions(Some(false), cx);
|
||||
editor.highlight_rows::<DiffRowHighlight>(
|
||||
|
||||
enum DeletedBlockRowHighlight {}
|
||||
editor.highlight_rows::<DeletedBlockRowHighlight>(
|
||||
Anchor::min()..Anchor::max(),
|
||||
deleted_color,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
|
||||
editor.set_current_line_highlight(Some(CurrentLineHighlight::None)); //
|
||||
editor
|
||||
._subscriptions
|
||||
.extend([cx.on_blur(&editor.focus_handle, |editor, cx| {
|
||||
@@ -973,37 +1064,41 @@ fn editor_with_deleted_text(
|
||||
});
|
||||
})]);
|
||||
|
||||
let parent_editor_for_reverts = parent_editor.clone();
|
||||
let original_multi_buffer_range = hunk.multi_buffer_range.clone();
|
||||
let diff_base_range = hunk.diff_base_byte_range.clone();
|
||||
editor
|
||||
.register_action::<RevertSelectedHunks>(move |_, cx| {
|
||||
parent_editor_for_reverts
|
||||
.update(cx, |editor, cx| {
|
||||
let Some((buffer, original_text)) =
|
||||
editor.buffer().update(cx, |buffer, cx| {
|
||||
let (_, buffer, _) = buffer
|
||||
.excerpt_containing(original_multi_buffer_range.start, cx)?;
|
||||
let original_text =
|
||||
buffer.read(cx).diff_base()?.slice(diff_base_range.clone());
|
||||
Some((buffer, Arc::from(original_text.to_string())))
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
Some((
|
||||
original_multi_buffer_range.start.text_anchor
|
||||
..original_multi_buffer_range.end.text_anchor,
|
||||
original_text,
|
||||
)),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
.register_action::<RevertSelectedHunks>({
|
||||
let parent_editor = parent_editor.clone();
|
||||
move |_, cx| {
|
||||
parent_editor
|
||||
.update(cx, |editor, cx| {
|
||||
let Some((buffer, original_text)) =
|
||||
editor.buffer().update(cx, |buffer, cx| {
|
||||
let (_, buffer, _) = buffer.excerpt_containing(
|
||||
original_multi_buffer_range.start,
|
||||
cx,
|
||||
)?;
|
||||
let original_text =
|
||||
buffer.read(cx).diff_base()?.slice(diff_base_range.clone());
|
||||
Some((buffer, Arc::from(original_text.to_string())))
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
Some((
|
||||
original_multi_buffer_range.start.text_anchor
|
||||
..original_multi_buffer_range.end.text_anchor,
|
||||
original_text,
|
||||
)),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
let hunk = hunk.clone();
|
||||
@@ -1023,21 +1118,6 @@ fn editor_with_deleted_text(
|
||||
(editor_height, editor)
|
||||
}
|
||||
|
||||
fn buffer_diff_hunk(
|
||||
buffer_snapshot: &MultiBufferSnapshot,
|
||||
row_range: Range<Point>,
|
||||
) -> Option<MultiBufferDiffHunk> {
|
||||
let mut hunks = buffer_snapshot.git_diff_hunks_in_range(
|
||||
MultiBufferRow(row_range.start.row)..MultiBufferRow(row_range.end.row),
|
||||
);
|
||||
let hunk = hunks.next()?;
|
||||
let second_hunk = hunks.next();
|
||||
if second_hunk.is_none() {
|
||||
return Some(hunk);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
impl DisplayDiffHunk {
|
||||
pub fn start_display_row(&self) -> DisplayRow {
|
||||
match self {
|
||||
@@ -1104,7 +1184,10 @@ pub fn diff_hunk_to_display(
|
||||
let hunk_end_point = Point::new(hunk_end_row.0, 0);
|
||||
|
||||
let multi_buffer_start = snapshot.buffer_snapshot.anchor_before(hunk_start_point);
|
||||
let multi_buffer_end = snapshot.buffer_snapshot.anchor_after(hunk_end_point);
|
||||
let multi_buffer_end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(multi_buffer_start.excerpt_id, hunk.buffer_range.end)
|
||||
.unwrap();
|
||||
let end = hunk_end_point.to_display_point(snapshot).row();
|
||||
|
||||
DisplayDiffHunk::Unfolded {
|
||||
|
||||
@@ -158,6 +158,12 @@ pub fn deploy_context_menu(
|
||||
}
|
||||
|
||||
let focus = cx.focused();
|
||||
let has_reveal_target = editor.target_file(cx).is_some();
|
||||
let reveal_in_finder_label = if cfg!(target_os = "macos") {
|
||||
"Reveal in Finder"
|
||||
} else {
|
||||
"Reveal in File Manager"
|
||||
};
|
||||
ui::ContextMenu::build(cx, |menu, _cx| {
|
||||
let builder = menu
|
||||
.on_blur_subscription(Subscription::new(|| {}))
|
||||
@@ -180,11 +186,13 @@ pub fn deploy_context_menu(
|
||||
.action("Copy", Box::new(Copy))
|
||||
.action("Paste", Box::new(Paste))
|
||||
.separator()
|
||||
.when(cfg!(target_os = "macos"), |builder| {
|
||||
builder.action("Reveal in Finder", Box::new(RevealInFileManager))
|
||||
})
|
||||
.when(cfg!(not(target_os = "macos")), |builder| {
|
||||
builder.action("Reveal in File Manager", Box::new(RevealInFileManager))
|
||||
.map(|builder| {
|
||||
if has_reveal_target {
|
||||
builder.action(reveal_in_finder_label, Box::new(RevealInFileManager))
|
||||
} else {
|
||||
builder
|
||||
.disabled_action(reveal_in_finder_label, Box::new(RevealInFileManager))
|
||||
}
|
||||
})
|
||||
.action("Open in Terminal", Box::new(OpenInTerminal))
|
||||
.action("Copy Permalink", Box::new(CopyPermalinkToLine));
|
||||
|
||||
@@ -11,14 +11,14 @@ use text::ToOffset;
|
||||
use ui::prelude::*;
|
||||
use workspace::{
|
||||
searchable::SearchableItemHandle, Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation,
|
||||
ToolbarItemView,
|
||||
ToolbarItemView, Workspace,
|
||||
};
|
||||
|
||||
pub struct ProposedChangesEditor {
|
||||
editor: View<Editor>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
_recalculate_diffs_task: Task<Option<()>>,
|
||||
recalculate_diffs_tx: mpsc::UnboundedSender<Model<Buffer>>,
|
||||
recalculate_diffs_tx: mpsc::UnboundedSender<RecalculateDiff>,
|
||||
}
|
||||
|
||||
pub struct ProposedChangesBuffer<T> {
|
||||
@@ -30,6 +30,11 @@ pub struct ProposedChangesEditorToolbar {
|
||||
current_editor: Option<View<ProposedChangesEditor>>,
|
||||
}
|
||||
|
||||
struct RecalculateDiff {
|
||||
buffer: Model<Buffer>,
|
||||
debounce: bool,
|
||||
}
|
||||
|
||||
impl ProposedChangesEditor {
|
||||
pub fn new<T: ToOffset>(
|
||||
buffers: Vec<ProposedChangesBuffer<T>>,
|
||||
@@ -58,21 +63,26 @@ impl ProposedChangesEditor {
|
||||
let (recalculate_diffs_tx, mut recalculate_diffs_rx) = mpsc::unbounded();
|
||||
|
||||
Self {
|
||||
editor: cx
|
||||
.new_view(|cx| Editor::for_multibuffer(multibuffer.clone(), project, true, cx)),
|
||||
editor: cx.new_view(|cx| {
|
||||
let mut editor = Editor::for_multibuffer(multibuffer.clone(), project, true, cx);
|
||||
editor.set_expand_all_diff_hunks();
|
||||
editor
|
||||
}),
|
||||
recalculate_diffs_tx,
|
||||
_recalculate_diffs_task: cx.spawn(|_, mut cx| async move {
|
||||
let mut buffers_to_diff = HashSet::default();
|
||||
while let Some(buffer) = recalculate_diffs_rx.next().await {
|
||||
buffers_to_diff.insert(buffer);
|
||||
while let Some(mut recalculate_diff) = recalculate_diffs_rx.next().await {
|
||||
buffers_to_diff.insert(recalculate_diff.buffer);
|
||||
|
||||
loop {
|
||||
while recalculate_diff.debounce {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(250))
|
||||
.await;
|
||||
let mut had_further_changes = false;
|
||||
while let Ok(next_buffer) = recalculate_diffs_rx.try_next() {
|
||||
buffers_to_diff.insert(next_buffer?);
|
||||
while let Ok(next_recalculate_diff) = recalculate_diffs_rx.try_next() {
|
||||
let next_recalculate_diff = next_recalculate_diff?;
|
||||
recalculate_diff.debounce &= next_recalculate_diff.debounce;
|
||||
buffers_to_diff.insert(next_recalculate_diff.buffer);
|
||||
had_further_changes = true;
|
||||
}
|
||||
if !had_further_changes {
|
||||
@@ -99,19 +109,24 @@ impl ProposedChangesEditor {
|
||||
event: &BufferEvent,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let BufferEvent::Edited = event {
|
||||
self.recalculate_diffs_tx.unbounded_send(buffer).ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_all_changes(&self, cx: &mut ViewContext<Self>) {
|
||||
let buffers = self.editor.read(cx).buffer.read(cx).all_buffers();
|
||||
for branch_buffer in buffers {
|
||||
if let Some(base_buffer) = branch_buffer.read(cx).diff_base_buffer() {
|
||||
base_buffer.update(cx, |base_buffer, cx| {
|
||||
base_buffer.merge(&branch_buffer, None, cx)
|
||||
});
|
||||
match event {
|
||||
BufferEvent::Operation { .. } => {
|
||||
self.recalculate_diffs_tx
|
||||
.unbounded_send(RecalculateDiff {
|
||||
buffer,
|
||||
debounce: true,
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
BufferEvent::DiffBaseChanged => {
|
||||
self.recalculate_diffs_tx
|
||||
.unbounded_send(RecalculateDiff {
|
||||
buffer,
|
||||
debounce: false,
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,6 +174,31 @@ impl Item for ProposedChangesEditor {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
Item::added_to_workspace(editor, workspace, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, Item::deactivated);
|
||||
}
|
||||
|
||||
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| Item::navigate(editor, data, cx))
|
||||
}
|
||||
|
||||
fn set_nav_history(
|
||||
&mut self,
|
||||
nav_history: workspace::ItemNavHistory,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
Item::set_nav_history(editor, nav_history, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ProposedChangesEditorToolbar {
|
||||
@@ -183,7 +223,9 @@ impl Render for ProposedChangesEditorToolbar {
|
||||
Button::new("apply-changes", "Apply All").on_click(move |_, cx| {
|
||||
if let Some(editor) = &editor {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.apply_all_changes(cx);
|
||||
editor.editor.update(cx, |editor, cx| {
|
||||
editor.apply_all_changes(cx);
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
ExpandMacroRecursively,
|
||||
};
|
||||
|
||||
static RUST_ANALYZER_NAME: &str = "rust-analyzer";
|
||||
const RUST_ANALYZER_NAME: &str = "rust-analyzer";
|
||||
|
||||
fn is_rust_language(language: &Language) -> bool {
|
||||
language.name() == "Rust".into()
|
||||
|
||||
@@ -14,8 +14,8 @@ name = "eval"
|
||||
path = "src/eval.rs"
|
||||
|
||||
[dependencies]
|
||||
clap.workspace = true
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
@@ -24,15 +24,15 @@ feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
git.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
isahc_http_client.workspace = true
|
||||
language.workspace = true
|
||||
languages.workspace = true
|
||||
http_client.workspace = true
|
||||
node_runtime.workspace = true
|
||||
open_ai.workspace = true
|
||||
project.workspace = true
|
||||
settings.workspace = true
|
||||
semantic_index.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
semantic_index.workspace = true
|
||||
node_runtime.workspace = true
|
||||
|
||||
@@ -39,30 +39,30 @@ schemars.workspace = true
|
||||
semantic_version.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
settings.workspace = true
|
||||
snippet_provider.workspace = true
|
||||
task.workspace = true
|
||||
theme.workspace = true
|
||||
toml.workspace = true
|
||||
ui.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
wasm-encoder.workspace = true
|
||||
wasmtime.workspace = true
|
||||
wasmtime-wasi.workspace = true
|
||||
wasmparser.workspace = true
|
||||
wasmtime-wasi.workspace = true
|
||||
wasmtime.workspace = true
|
||||
wit-component.workspace = true
|
||||
workspace.workspace = true
|
||||
task.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
isahc_http_client.workspace = true
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
parking_lot.workspace = true
|
||||
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
parking_lot.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
tokio.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -54,6 +54,7 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[
|
||||
("ocaml", &["ml", "mli"]),
|
||||
("php", &["php"]),
|
||||
("prisma", &["prisma"]),
|
||||
("proto", &["proto"]),
|
||||
("purescript", &["purs"]),
|
||||
("r", &["r", "R"]),
|
||||
("racket", &["rkt"]),
|
||||
|
||||
@@ -587,38 +587,54 @@ impl Fs for RealFs {
|
||||
let pending_paths: Arc<Mutex<Vec<PathEvent>>> = Default::default();
|
||||
let root_path = path.to_path_buf();
|
||||
|
||||
watcher::global(|g| {
|
||||
let tx = tx.clone();
|
||||
let pending_paths = pending_paths.clone();
|
||||
g.add(move |event: ¬ify::Event| {
|
||||
let kind = match event.kind {
|
||||
EventKind::Create(_) => Some(PathEventKind::Created),
|
||||
EventKind::Modify(_) => Some(PathEventKind::Changed),
|
||||
EventKind::Remove(_) => Some(PathEventKind::Removed),
|
||||
_ => None,
|
||||
};
|
||||
let mut paths = event
|
||||
.paths
|
||||
.iter()
|
||||
.filter_map(|path| {
|
||||
path.starts_with(&root_path).then(|| PathEvent {
|
||||
path: path.clone(),
|
||||
kind,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
// Check if root path is a symlink
|
||||
let target_path = self.read_link(&path).await.ok();
|
||||
|
||||
if !paths.is_empty() {
|
||||
paths.sort();
|
||||
let mut pending_paths = pending_paths.lock();
|
||||
if pending_paths.is_empty() {
|
||||
tx.try_send(()).ok();
|
||||
watcher::global({
|
||||
let target_path = target_path.clone();
|
||||
|g| {
|
||||
let tx = tx.clone();
|
||||
let pending_paths = pending_paths.clone();
|
||||
g.add(move |event: ¬ify::Event| {
|
||||
let kind = match event.kind {
|
||||
EventKind::Create(_) => Some(PathEventKind::Created),
|
||||
EventKind::Modify(_) => Some(PathEventKind::Changed),
|
||||
EventKind::Remove(_) => Some(PathEventKind::Removed),
|
||||
_ => None,
|
||||
};
|
||||
let mut paths = event
|
||||
.paths
|
||||
.iter()
|
||||
.filter_map(|path| {
|
||||
if let Some(target) = target_path.clone() {
|
||||
if path.starts_with(target) {
|
||||
return Some(PathEvent {
|
||||
path: path.clone(),
|
||||
kind,
|
||||
});
|
||||
}
|
||||
} else if path.starts_with(&root_path) {
|
||||
return Some(PathEvent {
|
||||
path: path.clone(),
|
||||
kind,
|
||||
});
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !paths.is_empty() {
|
||||
paths.sort();
|
||||
let mut pending_paths = pending_paths.lock();
|
||||
if pending_paths.is_empty() {
|
||||
tx.try_send(()).ok();
|
||||
}
|
||||
util::extend_sorted(&mut *pending_paths, paths, usize::MAX, |a, b| {
|
||||
a.path.cmp(&b.path)
|
||||
});
|
||||
}
|
||||
util::extend_sorted(&mut *pending_paths, paths, usize::MAX, |a, b| {
|
||||
a.path.cmp(&b.path)
|
||||
});
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
|
||||
@@ -626,6 +642,14 @@ impl Fs for RealFs {
|
||||
|
||||
watcher.add(path).ok(); // Ignore "file doesn't exist error" and rely on parent watcher.
|
||||
|
||||
// Check if path is a symlink and follow the target parent
|
||||
if let Some(target) = target_path {
|
||||
watcher.add(&target).ok();
|
||||
if let Some(parent) = target.parent() {
|
||||
watcher.add(parent).log_err();
|
||||
}
|
||||
}
|
||||
|
||||
// watch the parent dir so we can tell when settings.json is created
|
||||
if let Some(parent) = path.parent() {
|
||||
watcher.add(parent).log_err();
|
||||
|
||||
@@ -8,7 +8,7 @@ use gpui::AppContext;
|
||||
pub use crate::providers::*;
|
||||
|
||||
/// Initializes the Git hosting providers.
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
pub fn init(cx: &AppContext) {
|
||||
let provider_registry = GitHostingProviderRegistry::global(cx);
|
||||
|
||||
// The providers are stored in a `BTreeMap`, so insertion order matters.
|
||||
|
||||
@@ -66,7 +66,7 @@ smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
strum.workspace = true
|
||||
sum_tree.workspace = true
|
||||
taffy = "0.4.3"
|
||||
taffy = "0.5"
|
||||
thiserror.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
@@ -150,6 +150,18 @@ impl Render for WindowDemo {
|
||||
)
|
||||
.unwrap();
|
||||
}))
|
||||
.child(button("Hide Application", |cx| {
|
||||
cx.hide();
|
||||
|
||||
// Restore the application after 3 seconds
|
||||
cx.spawn(|mut cx| async move {
|
||||
Timer::after(std::time::Duration::from_secs(3)).await;
|
||||
cx.update(|cx| {
|
||||
cx.activate(false);
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -348,7 +348,7 @@ impl AppContext {
|
||||
}
|
||||
|
||||
/// Gracefully quit the application via the platform's standard routine.
|
||||
pub fn quit(&mut self) {
|
||||
pub fn quit(&self) {
|
||||
self.platform.quit();
|
||||
}
|
||||
|
||||
@@ -1004,11 +1004,7 @@ impl AppContext {
|
||||
self.globals_by_type.insert(global_type, lease.global);
|
||||
}
|
||||
|
||||
pub(crate) fn new_view_observer(
|
||||
&mut self,
|
||||
key: TypeId,
|
||||
value: NewViewListener,
|
||||
) -> Subscription {
|
||||
pub(crate) fn new_view_observer(&self, key: TypeId, value: NewViewListener) -> Subscription {
|
||||
let (subscription, activate) = self.new_view_observers.insert(key, value);
|
||||
activate();
|
||||
subscription
|
||||
@@ -1016,7 +1012,7 @@ impl AppContext {
|
||||
/// Arrange for the given function to be invoked whenever a view of the specified type is created.
|
||||
/// The function will be passed a mutable reference to the view along with an appropriate context.
|
||||
pub fn observe_new_views<V: 'static>(
|
||||
&mut self,
|
||||
&self,
|
||||
on_new: impl 'static + Fn(&mut V, &mut ViewContext<V>),
|
||||
) -> Subscription {
|
||||
self.new_view_observer(
|
||||
@@ -1035,7 +1031,7 @@ impl AppContext {
|
||||
/// Observe the release of a model or view. The callback is invoked after the model or view
|
||||
/// has no more strong references but before it has been dropped.
|
||||
pub fn observe_release<E, T>(
|
||||
&mut self,
|
||||
&self,
|
||||
handle: &E,
|
||||
on_release: impl FnOnce(&mut T, &mut AppContext) + 'static,
|
||||
) -> Subscription
|
||||
@@ -1062,7 +1058,7 @@ impl AppContext {
|
||||
mut f: impl FnMut(&KeystrokeEvent, &mut WindowContext) + 'static,
|
||||
) -> Subscription {
|
||||
fn inner(
|
||||
keystroke_observers: &mut SubscriberSet<(), KeystrokeObserver>,
|
||||
keystroke_observers: &SubscriberSet<(), KeystrokeObserver>,
|
||||
handler: KeystrokeObserver,
|
||||
) -> Subscription {
|
||||
let (subscription, activate) = keystroke_observers.insert((), handler);
|
||||
@@ -1140,7 +1136,7 @@ impl AppContext {
|
||||
/// Register a callback to be invoked when the application is about to quit.
|
||||
/// It is not possible to cancel the quit event at this point.
|
||||
pub fn on_app_quit<Fut>(
|
||||
&mut self,
|
||||
&self,
|
||||
mut on_quit: impl FnMut(&mut AppContext) -> Fut + 'static,
|
||||
) -> Subscription
|
||||
where
|
||||
@@ -1186,7 +1182,7 @@ impl AppContext {
|
||||
}
|
||||
|
||||
/// Sets the menu bar for this application. This will replace any existing menu bar.
|
||||
pub fn set_menus(&mut self, menus: Vec<Menu>) {
|
||||
pub fn set_menus(&self, menus: Vec<Menu>) {
|
||||
self.platform.set_menus(menus, &self.keymap.borrow());
|
||||
}
|
||||
|
||||
@@ -1196,7 +1192,7 @@ impl AppContext {
|
||||
}
|
||||
|
||||
/// Sets the right click menu for the app icon in the dock
|
||||
pub fn set_dock_menu(&mut self, menus: Vec<MenuItem>) {
|
||||
pub fn set_dock_menu(&self, menus: Vec<MenuItem>) {
|
||||
self.platform.set_dock_menu(menus, &self.keymap.borrow());
|
||||
}
|
||||
|
||||
@@ -1204,7 +1200,7 @@ impl AppContext {
|
||||
/// The list is usually shown on the application icon's context menu in the dock,
|
||||
/// and allows to open the recent files via that context menu.
|
||||
/// If the path is already in the list, it will be moved to the bottom of the list.
|
||||
pub fn add_recent_document(&mut self, path: &Path) {
|
||||
pub fn add_recent_document(&self, path: &Path) {
|
||||
self.platform.add_recent_document(path);
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ impl Context for AsyncAppContext {
|
||||
|
||||
impl AsyncAppContext {
|
||||
/// Schedules all windows in the application to be redrawn.
|
||||
pub fn refresh(&mut self) -> Result<()> {
|
||||
pub fn refresh(&self) -> Result<()> {
|
||||
let app = self
|
||||
.app
|
||||
.upgrade()
|
||||
@@ -205,7 +205,7 @@ impl AsyncAppContext {
|
||||
/// A convenience method for [AppContext::update_global]
|
||||
/// for updating the global state of the specified type.
|
||||
pub fn update_global<G: Global, R>(
|
||||
&mut self,
|
||||
&self,
|
||||
update: impl FnOnce(&mut G, &mut AppContext) -> R,
|
||||
) -> Result<R> {
|
||||
let app = self
|
||||
|
||||
@@ -91,7 +91,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
|
||||
|
||||
/// Register a callback to be invoked when GPUI releases this model.
|
||||
pub fn on_release(
|
||||
&mut self,
|
||||
&self,
|
||||
on_release: impl FnOnce(&mut T, &mut AppContext) + 'static,
|
||||
) -> Subscription
|
||||
where
|
||||
@@ -110,7 +110,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
|
||||
|
||||
/// Register a callback to be run on the release of another model or view
|
||||
pub fn observe_release<T2, E>(
|
||||
&mut self,
|
||||
&self,
|
||||
entity: &E,
|
||||
on_release: impl FnOnce(&mut T, &mut T2, &mut ModelContext<'_, T>) + 'static,
|
||||
) -> Subscription
|
||||
@@ -154,7 +154,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
|
||||
/// Arrange for the given function to be invoked whenever the application is quit.
|
||||
/// The future returned from this callback will be polled for up to [crate::SHUTDOWN_TIMEOUT] until the app fully quits.
|
||||
pub fn on_app_quit<Fut>(
|
||||
&mut self,
|
||||
&self,
|
||||
mut on_quit: impl FnMut(&mut T, &mut ModelContext<T>) -> Fut + 'static,
|
||||
) -> Subscription
|
||||
where
|
||||
|
||||
@@ -1418,7 +1418,7 @@ impl Interactivity {
|
||||
}
|
||||
|
||||
fn clamp_scroll_position(
|
||||
&mut self,
|
||||
&self,
|
||||
bounds: Bounds<Pixels>,
|
||||
style: &Style,
|
||||
cx: &mut WindowContext,
|
||||
@@ -1547,7 +1547,7 @@ impl Interactivity {
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn paint_debug_info(
|
||||
&mut self,
|
||||
&self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
hitbox: &Hitbox,
|
||||
style: &Style,
|
||||
@@ -2057,6 +2057,7 @@ impl Interactivity {
|
||||
fn paint_scroll_listener(&self, hitbox: &Hitbox, style: &Style, cx: &mut WindowContext) {
|
||||
if let Some(scroll_offset) = self.scroll_offset.clone() {
|
||||
let overflow = style.overflow;
|
||||
let allow_concurrent_scroll = style.allow_concurrent_scroll;
|
||||
let line_height = cx.line_height();
|
||||
let hitbox = hitbox.clone();
|
||||
cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
|
||||
@@ -2065,27 +2066,31 @@ impl Interactivity {
|
||||
let old_scroll_offset = *scroll_offset;
|
||||
let delta = event.delta.pixel_delta(line_height);
|
||||
|
||||
let mut delta_x = Pixels::ZERO;
|
||||
if overflow.x == Overflow::Scroll {
|
||||
let mut delta_x = Pixels::ZERO;
|
||||
if !delta.x.is_zero() {
|
||||
delta_x = delta.x;
|
||||
} else if overflow.y != Overflow::Scroll {
|
||||
delta_x = delta.y;
|
||||
}
|
||||
|
||||
scroll_offset.x += delta_x;
|
||||
}
|
||||
|
||||
let mut delta_y = Pixels::ZERO;
|
||||
if overflow.y == Overflow::Scroll {
|
||||
let mut delta_y = Pixels::ZERO;
|
||||
if !delta.y.is_zero() {
|
||||
delta_y = delta.y;
|
||||
} else if overflow.x != Overflow::Scroll {
|
||||
delta_y = delta.x;
|
||||
}
|
||||
|
||||
scroll_offset.y += delta_y;
|
||||
}
|
||||
if !allow_concurrent_scroll && !delta_x.is_zero() && !delta_y.is_zero() {
|
||||
if delta_x.abs() > delta_y.abs() {
|
||||
delta_y = Pixels::ZERO;
|
||||
} else {
|
||||
delta_x = Pixels::ZERO;
|
||||
}
|
||||
}
|
||||
scroll_offset.y += delta_y;
|
||||
scroll_offset.x += delta_x;
|
||||
|
||||
cx.stop_propagation();
|
||||
if *scroll_offset != old_scroll_offset {
|
||||
|
||||
@@ -89,6 +89,16 @@ pub enum ListSizingBehavior {
|
||||
Auto,
|
||||
}
|
||||
|
||||
/// The horizontal sizing behavior to apply during layout.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ListHorizontalSizingBehavior {
|
||||
/// List items' width can never exceed the width of the list.
|
||||
#[default]
|
||||
FitList,
|
||||
/// List items' width may go over the width of the list, if any item is wider.
|
||||
Unconstrained,
|
||||
}
|
||||
|
||||
struct LayoutItemsResponse {
|
||||
max_item_width: Pixels,
|
||||
scroll_top: ListOffset,
|
||||
|
||||
@@ -252,7 +252,7 @@ impl TextLayout {
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
&self,
|
||||
text: SharedString,
|
||||
runs: Option<Vec<TextRun>>,
|
||||
cx: &mut WindowContext,
|
||||
@@ -350,7 +350,7 @@ impl TextLayout {
|
||||
layout_id
|
||||
}
|
||||
|
||||
fn prepaint(&mut self, bounds: Bounds<Pixels>, text: &str) {
|
||||
fn prepaint(&self, bounds: Bounds<Pixels>, text: &str) {
|
||||
let mut element_state = self.lock();
|
||||
let element_state = element_state
|
||||
.as_mut()
|
||||
@@ -359,7 +359,7 @@ impl TextLayout {
|
||||
element_state.bounds = Some(bounds);
|
||||
}
|
||||
|
||||
fn paint(&mut self, text: &str, cx: &mut WindowContext) {
|
||||
fn paint(&self, text: &str, cx: &mut WindowContext) {
|
||||
let element_state = self.lock();
|
||||
let element_state = element_state
|
||||
.as_ref()
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
//! elements with uniform height.
|
||||
|
||||
use crate::{
|
||||
point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, Element, ElementId,
|
||||
GlobalElementId, Hitbox, InteractiveElement, Interactivity, IntoElement, LayoutId,
|
||||
point, size, AnyElement, AvailableSpace, Bounds, ContentMask, Element, ElementId,
|
||||
GlobalElementId, Hitbox, InteractiveElement, Interactivity, IntoElement, IsZero, LayoutId,
|
||||
ListSizingBehavior, Pixels, Render, ScrollHandle, Size, StyleRefinement, Styled, View,
|
||||
ViewContext, WindowContext,
|
||||
};
|
||||
@@ -14,6 +14,8 @@ use smallvec::SmallVec;
|
||||
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
||||
use taffy::style::Overflow;
|
||||
|
||||
use super::ListHorizontalSizingBehavior;
|
||||
|
||||
/// uniform_list provides lazy rendering for a set of items that are of uniform height.
|
||||
/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
|
||||
/// uniform_list will only render the visible subset of items.
|
||||
@@ -57,6 +59,7 @@ where
|
||||
},
|
||||
scroll_handle: None,
|
||||
sizing_behavior: ListSizingBehavior::default(),
|
||||
horizontal_sizing_behavior: ListHorizontalSizingBehavior::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,11 +72,11 @@ pub struct UniformList {
|
||||
interactivity: Interactivity,
|
||||
scroll_handle: Option<UniformListScrollHandle>,
|
||||
sizing_behavior: ListSizingBehavior,
|
||||
horizontal_sizing_behavior: ListHorizontalSizingBehavior,
|
||||
}
|
||||
|
||||
/// Frame state used by the [UniformList].
|
||||
pub struct UniformListFrameState {
|
||||
item_size: Size<Pixels>,
|
||||
items: SmallVec<[AnyElement; 32]>,
|
||||
}
|
||||
|
||||
@@ -87,7 +90,18 @@ pub struct UniformListScrollHandle(pub Rc<RefCell<UniformListScrollState>>);
|
||||
pub struct UniformListScrollState {
|
||||
pub base_handle: ScrollHandle,
|
||||
pub deferred_scroll_to_item: Option<usize>,
|
||||
pub last_item_height: Option<Pixels>,
|
||||
/// Size of the item, captured during last layout.
|
||||
pub last_item_size: Option<ItemSize>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
/// The size of the item and its contents.
|
||||
pub struct ItemSize {
|
||||
/// The size of the item.
|
||||
pub item: Size<Pixels>,
|
||||
/// The size of the item's contents, which may be larger than the item itself,
|
||||
/// if the item was bounded by a parent element.
|
||||
pub contents: Size<Pixels>,
|
||||
}
|
||||
|
||||
impl UniformListScrollHandle {
|
||||
@@ -96,12 +110,12 @@ impl UniformListScrollHandle {
|
||||
Self(Rc::new(RefCell::new(UniformListScrollState {
|
||||
base_handle: ScrollHandle::new(),
|
||||
deferred_scroll_to_item: None,
|
||||
last_item_height: None,
|
||||
last_item_size: None,
|
||||
})))
|
||||
}
|
||||
|
||||
/// Scroll the list to the given item index.
|
||||
pub fn scroll_to_item(&mut self, ix: usize) {
|
||||
pub fn scroll_to_item(&self, ix: usize) {
|
||||
self.0.borrow_mut().deferred_scroll_to_item = Some(ix);
|
||||
}
|
||||
|
||||
@@ -170,7 +184,6 @@ impl Element for UniformList {
|
||||
(
|
||||
layout_id,
|
||||
UniformListFrameState {
|
||||
item_size,
|
||||
items: SmallVec::new(),
|
||||
},
|
||||
)
|
||||
@@ -193,17 +206,30 @@ impl Element for UniformList {
|
||||
- point(border.right + padding.right, border.bottom + padding.bottom),
|
||||
);
|
||||
|
||||
let can_scroll_horizontally = matches!(
|
||||
self.horizontal_sizing_behavior,
|
||||
ListHorizontalSizingBehavior::Unconstrained
|
||||
);
|
||||
|
||||
let longest_item_size = self.measure_item(None, cx);
|
||||
let content_width = if can_scroll_horizontally {
|
||||
padded_bounds.size.width.max(longest_item_size.width)
|
||||
} else {
|
||||
padded_bounds.size.width
|
||||
};
|
||||
let content_size = Size {
|
||||
width: padded_bounds.size.width,
|
||||
height: frame_state.item_size.height * self.item_count + padding.top + padding.bottom,
|
||||
width: content_width,
|
||||
height: longest_item_size.height * self.item_count + padding.top + padding.bottom,
|
||||
};
|
||||
|
||||
let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap();
|
||||
|
||||
let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height;
|
||||
let item_height = longest_item_size.height;
|
||||
let shared_scroll_to_item = self.scroll_handle.as_mut().and_then(|handle| {
|
||||
let mut handle = handle.0.borrow_mut();
|
||||
handle.last_item_height = Some(item_height);
|
||||
handle.last_item_size = Some(ItemSize {
|
||||
item: padded_bounds.size,
|
||||
contents: content_size,
|
||||
});
|
||||
handle.deferred_scroll_to_item.take()
|
||||
});
|
||||
|
||||
@@ -228,12 +254,19 @@ impl Element for UniformList {
|
||||
if self.item_count > 0 {
|
||||
let content_height =
|
||||
item_height * self.item_count + padding.top + padding.bottom;
|
||||
let min_scroll_offset = padded_bounds.size.height - content_height;
|
||||
let is_scrolled = scroll_offset.y != px(0.);
|
||||
let is_scrolled_vertically = !scroll_offset.y.is_zero();
|
||||
let min_vertical_scroll_offset = padded_bounds.size.height - content_height;
|
||||
if is_scrolled_vertically && scroll_offset.y < min_vertical_scroll_offset {
|
||||
shared_scroll_offset.borrow_mut().y = min_vertical_scroll_offset;
|
||||
scroll_offset.y = min_vertical_scroll_offset;
|
||||
}
|
||||
|
||||
if is_scrolled && scroll_offset.y < min_scroll_offset {
|
||||
shared_scroll_offset.borrow_mut().y = min_scroll_offset;
|
||||
scroll_offset.y = min_scroll_offset;
|
||||
let content_width = content_size.width + padding.left + padding.right;
|
||||
let is_scrolled_horizontally =
|
||||
can_scroll_horizontally && !scroll_offset.x.is_zero();
|
||||
if is_scrolled_horizontally && content_width <= padded_bounds.size.width {
|
||||
shared_scroll_offset.borrow_mut().x = Pixels::ZERO;
|
||||
scroll_offset.x = Pixels::ZERO;
|
||||
}
|
||||
|
||||
if let Some(ix) = shared_scroll_to_item {
|
||||
@@ -263,9 +296,21 @@ impl Element for UniformList {
|
||||
cx.with_content_mask(Some(content_mask), |cx| {
|
||||
for (mut item, ix) in items.into_iter().zip(visible_range) {
|
||||
let item_origin = padded_bounds.origin
|
||||
+ point(px(0.), item_height * ix + scroll_offset.y + padding.top);
|
||||
+ point(
|
||||
if can_scroll_horizontally {
|
||||
scroll_offset.x + padding.left
|
||||
} else {
|
||||
scroll_offset.x
|
||||
},
|
||||
item_height * ix + scroll_offset.y + padding.top,
|
||||
);
|
||||
let available_width = if can_scroll_horizontally {
|
||||
padded_bounds.size.width + scroll_offset.x.abs()
|
||||
} else {
|
||||
padded_bounds.size.width
|
||||
};
|
||||
let available_space = size(
|
||||
AvailableSpace::Definite(padded_bounds.size.width),
|
||||
AvailableSpace::Definite(available_width),
|
||||
AvailableSpace::Definite(item_height),
|
||||
);
|
||||
item.layout_as_root(available_space, cx);
|
||||
@@ -318,6 +363,25 @@ impl UniformList {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the horizontal sizing behavior, controlling the way list items laid out horizontally.
|
||||
/// With [`ListHorizontalSizingBehavior::Unconstrained`] behavior, every item and the list itself will
|
||||
/// have the size of the widest item and lay out pushing the `end_slot` to the right end.
|
||||
pub fn with_horizontal_sizing_behavior(
|
||||
mut self,
|
||||
behavior: ListHorizontalSizingBehavior,
|
||||
) -> Self {
|
||||
self.horizontal_sizing_behavior = behavior;
|
||||
match behavior {
|
||||
ListHorizontalSizingBehavior::FitList => {
|
||||
self.interactivity.base_style.overflow.x = None;
|
||||
}
|
||||
ListHorizontalSizingBehavior::Unconstrained => {
|
||||
self.interactivity.base_style.overflow.x = Some(Overflow::Scroll);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn measure_item(&self, list_width: Option<Pixels>, cx: &mut WindowContext) -> Size<Pixels> {
|
||||
if self.item_count == 0 {
|
||||
return Size::default();
|
||||
|
||||
@@ -706,11 +706,7 @@ pub struct Bounds<T: Clone + Default + Debug> {
|
||||
|
||||
impl Bounds<Pixels> {
|
||||
/// Generate a centered bounds for the given display or primary display if none is provided
|
||||
pub fn centered(
|
||||
display_id: Option<DisplayId>,
|
||||
size: Size<Pixels>,
|
||||
cx: &mut AppContext,
|
||||
) -> Self {
|
||||
pub fn centered(display_id: Option<DisplayId>, size: Size<Pixels>, cx: &AppContext) -> Self {
|
||||
let display = display_id
|
||||
.and_then(|id| cx.find_display(id))
|
||||
.or_else(|| cx.primary_display());
|
||||
@@ -730,7 +726,7 @@ impl Bounds<Pixels> {
|
||||
}
|
||||
|
||||
/// Generate maximized bounds for the given display or primary display if none is provided
|
||||
pub fn maximized(display_id: Option<DisplayId>, cx: &mut AppContext) -> Self {
|
||||
pub fn maximized(display_id: Option<DisplayId>, cx: &AppContext) -> Self {
|
||||
let display = display_id
|
||||
.and_then(|id| cx.find_display(id))
|
||||
.or_else(|| cx.primary_display());
|
||||
|
||||
@@ -219,7 +219,7 @@ impl DispatchTree {
|
||||
self.focusable_node_ids.insert(focus_id, node_id);
|
||||
}
|
||||
|
||||
pub fn parent_view_id(&mut self) -> Option<EntityId> {
|
||||
pub fn parent_view_id(&self) -> Option<EntityId> {
|
||||
self.view_stack.last().copied()
|
||||
}
|
||||
|
||||
@@ -484,7 +484,7 @@ impl DispatchTree {
|
||||
|
||||
/// Converts the longest prefix of input to a replay event and returns the rest.
|
||||
fn replay_prefix(
|
||||
&mut self,
|
||||
&self,
|
||||
mut input: SmallVec<[Keystroke; 1]>,
|
||||
dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
|
||||
) -> (SmallVec<[Keystroke; 1]>, SmallVec<[Replay; 1]>) {
|
||||
|
||||
@@ -171,7 +171,7 @@ pub enum OsAction {
|
||||
Redo,
|
||||
}
|
||||
|
||||
pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &mut AppContext) {
|
||||
pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &AppContext) {
|
||||
platform.on_will_open_app_menu(Box::new({
|
||||
let cx = cx.to_async();
|
||||
move || {
|
||||
|
||||
@@ -45,7 +45,7 @@ use crate::{
|
||||
|
||||
use super::x11::X11Client;
|
||||
|
||||
pub(crate) const SCROLL_LINES: f64 = 3.0;
|
||||
pub(crate) const SCROLL_LINES: f32 = 3.0;
|
||||
|
||||
// Values match the defaults on GTK.
|
||||
// Taken from https://github.com/GNOME/gtk/blob/main/gtk/gtksettings.c#L320
|
||||
|
||||
@@ -477,8 +477,7 @@ impl WaylandClient {
|
||||
.as_ref()
|
||||
.map(|primary_selection_manager| primary_selection_manager.get_device(&seat, &qh, ()));
|
||||
|
||||
// FIXME: Determine the scaling factor dynamically by the compositor
|
||||
let mut cursor = Cursor::new(&conn, &globals, 24, 2);
|
||||
let mut cursor = Cursor::new(&conn, &globals, 24);
|
||||
|
||||
handle
|
||||
.insert_source(XDPEventSource::new(&common.background_executor), {
|
||||
@@ -1634,10 +1633,10 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
let scroll_delta = state.discrete_scroll_delta.get_or_insert(point(0.0, 0.0));
|
||||
match axis {
|
||||
wl_pointer::Axis::VerticalScroll => {
|
||||
scroll_delta.y += discrete as f32 * axis_modifier * SCROLL_LINES as f32;
|
||||
scroll_delta.y += discrete as f32 * axis_modifier * SCROLL_LINES;
|
||||
}
|
||||
wl_pointer::Axis::HorizontalScroll => {
|
||||
scroll_delta.x += discrete as f32 * axis_modifier * SCROLL_LINES as f32;
|
||||
scroll_delta.x += discrete as f32 * axis_modifier * SCROLL_LINES;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
@@ -1662,10 +1661,10 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
let wheel_percent = value120 as f32 / 120.0;
|
||||
match axis {
|
||||
wl_pointer::Axis::VerticalScroll => {
|
||||
scroll_delta.y += wheel_percent * axis_modifier * SCROLL_LINES as f32;
|
||||
scroll_delta.y += wheel_percent * axis_modifier * SCROLL_LINES;
|
||||
}
|
||||
wl_pointer::Axis::HorizontalScroll => {
|
||||
scroll_delta.x += wheel_percent * axis_modifier * SCROLL_LINES as f32;
|
||||
scroll_delta.x += wheel_percent * axis_modifier * SCROLL_LINES;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ pub(crate) struct Cursor {
|
||||
theme_name: Option<String>,
|
||||
surface: WlSurface,
|
||||
size: u32,
|
||||
scale: u32,
|
||||
shm: WlShm,
|
||||
connection: Connection,
|
||||
}
|
||||
@@ -24,7 +23,7 @@ impl Drop for Cursor {
|
||||
}
|
||||
|
||||
impl Cursor {
|
||||
pub fn new(connection: &Connection, globals: &Globals, size: u32, scale: u32) -> Self {
|
||||
pub fn new(connection: &Connection, globals: &Globals, size: u32) -> Self {
|
||||
Self {
|
||||
theme: CursorTheme::load(&connection, globals.shm.clone(), size).log_err(),
|
||||
theme_name: None,
|
||||
@@ -32,7 +31,6 @@ impl Cursor {
|
||||
shm: globals.shm.clone(),
|
||||
connection: connection.clone(),
|
||||
size,
|
||||
scale,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,18 +38,14 @@ impl Cursor {
|
||||
if let Some(size) = size {
|
||||
self.size = size;
|
||||
}
|
||||
if let Some(theme) = CursorTheme::load_from_name(
|
||||
&self.connection,
|
||||
self.shm.clone(),
|
||||
theme_name,
|
||||
self.size * self.scale,
|
||||
)
|
||||
.log_err()
|
||||
if let Some(theme) =
|
||||
CursorTheme::load_from_name(&self.connection, self.shm.clone(), theme_name, self.size)
|
||||
.log_err()
|
||||
{
|
||||
self.theme = Some(theme);
|
||||
self.theme_name = Some(theme_name.to_string());
|
||||
} else if let Some(theme) =
|
||||
CursorTheme::load(&self.connection, self.shm.clone(), self.size * self.scale).log_err()
|
||||
CursorTheme::load(&self.connection, self.shm.clone(), self.size).log_err()
|
||||
{
|
||||
self.theme = Some(theme);
|
||||
self.theme_name = None;
|
||||
@@ -97,22 +91,9 @@ impl Cursor {
|
||||
let (width, height) = buffer.dimensions();
|
||||
let (hot_x, hot_y) = buffer.hotspot();
|
||||
|
||||
let scaled_width = width / self.scale;
|
||||
let scaled_height = height / self.scale;
|
||||
let scaled_hot_x = hot_x / self.scale;
|
||||
let scaled_hot_y = hot_y / self.scale;
|
||||
|
||||
self.surface.set_buffer_scale(self.scale as i32);
|
||||
|
||||
wl_pointer.set_cursor(
|
||||
serial_id,
|
||||
Some(&self.surface),
|
||||
scaled_hot_x as i32,
|
||||
scaled_hot_y as i32,
|
||||
);
|
||||
wl_pointer.set_cursor(serial_id, Some(&self.surface), hot_x as i32, hot_y as i32);
|
||||
self.surface.attach(Some(&buffer), 0, 0);
|
||||
self.surface
|
||||
.damage(0, 0, scaled_width as i32, scaled_height as i32);
|
||||
self.surface.damage(0, 0, width as i32, height as i32);
|
||||
self.surface.commit();
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use core::str;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::{Rc, Weak};
|
||||
@@ -42,7 +42,10 @@ use crate::{
|
||||
WindowParams, X11Window,
|
||||
};
|
||||
|
||||
use super::{button_of_key, modifiers_from_state, pressed_button_from_mask};
|
||||
use super::{
|
||||
button_or_scroll_from_event_detail, get_valuator_axis_index, modifiers_from_state,
|
||||
pressed_button_from_mask, ButtonOrScroll, ScrollDirection,
|
||||
};
|
||||
use super::{X11Display, X11WindowStatePtr, XcbAtoms};
|
||||
use super::{XimCallbackEvent, XimHandler};
|
||||
use crate::platform::linux::platform::{DOUBLE_CLICK_INTERVAL, SCROLL_LINES};
|
||||
@@ -51,7 +54,15 @@ use crate::platform::linux::{
|
||||
get_xkb_compose_state, is_within_click_distance, open_uri_internal, reveal_path_internal,
|
||||
};
|
||||
|
||||
pub(super) const XINPUT_MASTER_DEVICE: u16 = 1;
|
||||
/// Value for DeviceId parameters which selects all devices.
|
||||
pub(crate) const XINPUT_ALL_DEVICES: xinput::DeviceId = 0;
|
||||
|
||||
/// Value for DeviceId parameters which selects all device groups. Events that
|
||||
/// occur within the group are emitted by the group itself.
|
||||
///
|
||||
/// In XInput 2's interface, these are referred to as "master devices", but that
|
||||
/// terminology is both archaic and unclear.
|
||||
pub(crate) const XINPUT_ALL_DEVICE_GROUPS: xinput::DeviceId = 1;
|
||||
|
||||
pub(crate) struct WindowRef {
|
||||
window: X11WindowStatePtr,
|
||||
@@ -117,6 +128,26 @@ pub struct Xdnd {
|
||||
position: Point<Pixels>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PointerDeviceState {
|
||||
horizontal: ScrollAxisState,
|
||||
vertical: ScrollAxisState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ScrollAxisState {
|
||||
/// Valuator number for looking up this axis's scroll value.
|
||||
valuator_number: Option<u16>,
|
||||
/// Conversion factor from scroll units to lines.
|
||||
multiplier: f32,
|
||||
/// Last scroll value for calculating scroll delta.
|
||||
///
|
||||
/// This gets set to `None` whenever it might be invalid - when devices change or when window focus changes.
|
||||
/// The logic errs on the side of invalidating this, since the consequence is just skipping the delta of one scroll event.
|
||||
/// The consequence of not invalidating it can be large invalid deltas, which are much more user visible.
|
||||
scroll_value: Option<f32>,
|
||||
}
|
||||
|
||||
pub struct X11ClientState {
|
||||
pub(crate) loop_handle: LoopHandle<'static, X11Client>,
|
||||
pub(crate) event_loop: Option<calloop::EventLoop<'static, X11Client>>,
|
||||
@@ -152,9 +183,7 @@ pub struct X11ClientState {
|
||||
pub(crate) cursor_styles: HashMap<xproto::Window, CursorStyle>,
|
||||
pub(crate) cursor_cache: HashMap<CursorStyle, xproto::Cursor>,
|
||||
|
||||
pub(crate) scroll_class_data: Vec<xinput::DeviceClassDataScroll>,
|
||||
pub(crate) scroll_x: Option<f32>,
|
||||
pub(crate) scroll_y: Option<f32>,
|
||||
pointer_device_states: BTreeMap<xinput::DeviceId, PointerDeviceState>,
|
||||
|
||||
pub(crate) common: LinuxCommon,
|
||||
pub(crate) clipboard: x11_clipboard::Clipboard,
|
||||
@@ -266,31 +295,21 @@ impl X11Client {
|
||||
.prefetch_extension_information(xinput::X11_EXTENSION_NAME)
|
||||
.unwrap();
|
||||
|
||||
// Announce to X server that XInput up to 2.1 is supported. To increase this to 2.2 and
|
||||
// beyond, support for touch events would need to be added.
|
||||
let xinput_version = xcb_connection
|
||||
.xinput_xi_query_version(2, 0)
|
||||
.xinput_xi_query_version(2, 1)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap();
|
||||
// XInput 1.x is not supported.
|
||||
assert!(
|
||||
xinput_version.major_version >= 2,
|
||||
"XInput Extension v2 not supported."
|
||||
"XInput version >= 2 required."
|
||||
);
|
||||
|
||||
let master_device_query = xcb_connection
|
||||
.xinput_xi_query_device(XINPUT_MASTER_DEVICE)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap();
|
||||
let scroll_class_data = master_device_query
|
||||
.infos
|
||||
.iter()
|
||||
.find(|info| info.type_ == xinput::DeviceType::MASTER_POINTER)
|
||||
.unwrap()
|
||||
.classes
|
||||
.iter()
|
||||
.filter_map(|class| class.data.as_scroll())
|
||||
.map(|class| *class)
|
||||
.collect::<Vec<_>>();
|
||||
let pointer_device_states =
|
||||
get_new_pointer_device_states(&xcb_connection, &BTreeMap::new());
|
||||
|
||||
let atoms = XcbAtoms::new(&xcb_connection).unwrap().reply().unwrap();
|
||||
|
||||
@@ -434,9 +453,7 @@ impl X11Client {
|
||||
cursor_styles: HashMap::default(),
|
||||
cursor_cache: HashMap::default(),
|
||||
|
||||
scroll_class_data,
|
||||
scroll_x: None,
|
||||
scroll_y: None,
|
||||
pointer_device_states,
|
||||
|
||||
clipboard,
|
||||
clipboard_item: None,
|
||||
@@ -950,35 +967,56 @@ impl X11Client {
|
||||
window.handle_ime_commit(text);
|
||||
state = self.0.borrow_mut();
|
||||
}
|
||||
if let Some(button) = button_of_key(event.detail.try_into().unwrap()) {
|
||||
let click_elapsed = state.last_click.elapsed();
|
||||
match button_or_scroll_from_event_detail(event.detail) {
|
||||
Some(ButtonOrScroll::Button(button)) => {
|
||||
let click_elapsed = state.last_click.elapsed();
|
||||
if click_elapsed < DOUBLE_CLICK_INTERVAL
|
||||
&& state
|
||||
.last_mouse_button
|
||||
.is_some_and(|prev_button| prev_button == button)
|
||||
&& is_within_click_distance(state.last_location, position)
|
||||
{
|
||||
state.current_count += 1;
|
||||
} else {
|
||||
state.current_count = 1;
|
||||
}
|
||||
|
||||
if click_elapsed < DOUBLE_CLICK_INTERVAL
|
||||
&& state
|
||||
.last_mouse_button
|
||||
.is_some_and(|prev_button| prev_button == button)
|
||||
&& is_within_click_distance(state.last_location, position)
|
||||
{
|
||||
state.current_count += 1;
|
||||
} else {
|
||||
state.current_count = 1;
|
||||
state.last_click = Instant::now();
|
||||
state.last_mouse_button = Some(button);
|
||||
state.last_location = position;
|
||||
let current_count = state.current_count;
|
||||
|
||||
drop(state);
|
||||
window.handle_input(PlatformInput::MouseDown(crate::MouseDownEvent {
|
||||
button,
|
||||
position,
|
||||
modifiers,
|
||||
click_count: current_count,
|
||||
first_mouse: false,
|
||||
}));
|
||||
}
|
||||
Some(ButtonOrScroll::Scroll(direction)) => {
|
||||
drop(state);
|
||||
// Emulated scroll button presses are sent simultaneously with smooth scrolling XinputMotion events.
|
||||
// Since handling those events does the scrolling, they are skipped here.
|
||||
if !event
|
||||
.flags
|
||||
.contains(xinput::PointerEventFlags::POINTER_EMULATED)
|
||||
{
|
||||
let scroll_delta = match direction {
|
||||
ScrollDirection::Up => Point::new(0.0, SCROLL_LINES),
|
||||
ScrollDirection::Down => Point::new(0.0, -SCROLL_LINES),
|
||||
ScrollDirection::Left => Point::new(SCROLL_LINES, 0.0),
|
||||
ScrollDirection::Right => Point::new(-SCROLL_LINES, 0.0),
|
||||
};
|
||||
window.handle_input(PlatformInput::ScrollWheel(
|
||||
make_scroll_wheel_event(position, scroll_delta, modifiers),
|
||||
));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
log::error!("Unknown x11 button: {}", event.detail);
|
||||
}
|
||||
|
||||
state.last_click = Instant::now();
|
||||
state.last_mouse_button = Some(button);
|
||||
state.last_location = position;
|
||||
let current_count = state.current_count;
|
||||
|
||||
drop(state);
|
||||
window.handle_input(PlatformInput::MouseDown(crate::MouseDownEvent {
|
||||
button,
|
||||
position,
|
||||
modifiers,
|
||||
click_count: current_count,
|
||||
first_mouse: false,
|
||||
}));
|
||||
} else {
|
||||
log::warn!("Unknown button press: {event:?}");
|
||||
}
|
||||
}
|
||||
Event::XinputButtonRelease(event) => {
|
||||
@@ -991,15 +1029,19 @@ impl X11Client {
|
||||
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
);
|
||||
if let Some(button) = button_of_key(event.detail.try_into().unwrap()) {
|
||||
let click_count = state.current_count;
|
||||
drop(state);
|
||||
window.handle_input(PlatformInput::MouseUp(crate::MouseUpEvent {
|
||||
button,
|
||||
position,
|
||||
modifiers,
|
||||
click_count,
|
||||
}));
|
||||
match button_or_scroll_from_event_detail(event.detail) {
|
||||
Some(ButtonOrScroll::Button(button)) => {
|
||||
let click_count = state.current_count;
|
||||
drop(state);
|
||||
window.handle_input(PlatformInput::MouseUp(crate::MouseUpEvent {
|
||||
button,
|
||||
position,
|
||||
modifiers,
|
||||
click_count,
|
||||
}));
|
||||
}
|
||||
Some(ButtonOrScroll::Scroll(_)) => {}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
Event::XinputMotion(event) => {
|
||||
@@ -1014,12 +1056,6 @@ impl X11Client {
|
||||
state.modifiers = modifiers;
|
||||
drop(state);
|
||||
|
||||
let axisvalues = event
|
||||
.axisvalues
|
||||
.iter()
|
||||
.map(|axisvalue| fp3232_to_f32(*axisvalue))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if event.valuator_mask[0] & 3 != 0 {
|
||||
window.handle_input(PlatformInput::MouseMove(crate::MouseMoveEvent {
|
||||
position,
|
||||
@@ -1028,64 +1064,17 @@ impl X11Client {
|
||||
}));
|
||||
}
|
||||
|
||||
let mut valuator_idx = 0;
|
||||
let scroll_class_data = self.0.borrow().scroll_class_data.clone();
|
||||
for shift in 0..32 {
|
||||
if (event.valuator_mask[0] >> shift) & 1 == 0 {
|
||||
continue;
|
||||
state = self.0.borrow_mut();
|
||||
if let Some(mut pointer) = state.pointer_device_states.get_mut(&event.sourceid) {
|
||||
let scroll_delta = get_scroll_delta_and_update_state(&mut pointer, &event);
|
||||
drop(state);
|
||||
if let Some(scroll_delta) = scroll_delta {
|
||||
window.handle_input(PlatformInput::ScrollWheel(make_scroll_wheel_event(
|
||||
position,
|
||||
scroll_delta,
|
||||
modifiers,
|
||||
)));
|
||||
}
|
||||
|
||||
for scroll_class in &scroll_class_data {
|
||||
if scroll_class.scroll_type == xinput::ScrollType::HORIZONTAL
|
||||
&& scroll_class.number == shift
|
||||
{
|
||||
let new_scroll = axisvalues[valuator_idx]
|
||||
/ fp3232_to_f32(scroll_class.increment)
|
||||
* SCROLL_LINES as f32;
|
||||
let old_scroll = self.0.borrow().scroll_x;
|
||||
self.0.borrow_mut().scroll_x = Some(new_scroll);
|
||||
|
||||
if let Some(old_scroll) = old_scroll {
|
||||
let delta_scroll = old_scroll - new_scroll;
|
||||
window.handle_input(PlatformInput::ScrollWheel(
|
||||
crate::ScrollWheelEvent {
|
||||
position,
|
||||
delta: ScrollDelta::Lines(Point::new(delta_scroll, 0.0)),
|
||||
modifiers,
|
||||
touch_phase: TouchPhase::default(),
|
||||
},
|
||||
));
|
||||
}
|
||||
} else if scroll_class.scroll_type == xinput::ScrollType::VERTICAL
|
||||
&& scroll_class.number == shift
|
||||
{
|
||||
// the `increment` is the valuator delta equivalent to one positive unit of scrolling. Here that means SCROLL_LINES lines.
|
||||
let new_scroll = axisvalues[valuator_idx]
|
||||
/ fp3232_to_f32(scroll_class.increment)
|
||||
* SCROLL_LINES as f32;
|
||||
let old_scroll = self.0.borrow().scroll_y;
|
||||
self.0.borrow_mut().scroll_y = Some(new_scroll);
|
||||
|
||||
if let Some(old_scroll) = old_scroll {
|
||||
let delta_scroll = old_scroll - new_scroll;
|
||||
let (x, y) = if !modifiers.shift {
|
||||
(0.0, delta_scroll)
|
||||
} else {
|
||||
(delta_scroll, 0.0)
|
||||
};
|
||||
window.handle_input(PlatformInput::ScrollWheel(
|
||||
crate::ScrollWheelEvent {
|
||||
position,
|
||||
delta: ScrollDelta::Lines(Point::new(x, y)),
|
||||
modifiers,
|
||||
touch_phase: TouchPhase::default(),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
valuator_idx += 1;
|
||||
}
|
||||
}
|
||||
Event::XinputEnter(event) if event.mode == xinput::NotifyMode::NORMAL => {
|
||||
@@ -1095,10 +1084,10 @@ impl X11Client {
|
||||
state.mouse_focused_window = Some(event.event);
|
||||
}
|
||||
Event::XinputLeave(event) if event.mode == xinput::NotifyMode::NORMAL => {
|
||||
self.0.borrow_mut().scroll_x = None; // Set last scroll to `None` so that a large delta isn't created if scrolling is done outside the window (the valuator is global)
|
||||
self.0.borrow_mut().scroll_y = None;
|
||||
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
// Set last scroll values to `None` so that a large delta isn't created if scrolling is done outside the window (the valuator is global)
|
||||
reset_all_pointer_device_scroll_positions(&mut state.pointer_device_states);
|
||||
state.mouse_focused_window = None;
|
||||
let pressed_button = pressed_button_from_mask(event.buttons[0]);
|
||||
let position = point(
|
||||
@@ -1117,6 +1106,26 @@ impl X11Client {
|
||||
}));
|
||||
window.set_hovered(false);
|
||||
}
|
||||
Event::XinputHierarchy(event) => {
|
||||
let mut state = self.0.borrow_mut();
|
||||
// Temporarily use `state.pointer_device_states` to only store pointers that still have valid scroll values.
|
||||
// Any change to a device invalidates its scroll values.
|
||||
for info in event.infos {
|
||||
if is_pointer_device(info.type_) {
|
||||
state.pointer_device_states.remove(&info.deviceid);
|
||||
}
|
||||
}
|
||||
state.pointer_device_states = get_new_pointer_device_states(
|
||||
&state.xcb_connection,
|
||||
&state.pointer_device_states,
|
||||
);
|
||||
}
|
||||
Event::XinputDeviceChanged(event) => {
|
||||
let mut state = self.0.borrow_mut();
|
||||
if let Some(mut pointer) = state.pointer_device_states.get_mut(&event.sourceid) {
|
||||
reset_pointer_device_scroll_positions(&mut pointer);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
@@ -1742,3 +1751,142 @@ fn xdnd_send_status(
|
||||
.send_event(false, target, EventMask::default(), message)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Recomputes `pointer_device_states` by querying all pointer devices.
|
||||
/// When a device is present in `scroll_values_to_preserve`, its value for `ScrollAxisState.scroll_value` is used.
|
||||
fn get_new_pointer_device_states(
|
||||
xcb_connection: &XCBConnection,
|
||||
scroll_values_to_preserve: &BTreeMap<xinput::DeviceId, PointerDeviceState>,
|
||||
) -> BTreeMap<xinput::DeviceId, PointerDeviceState> {
|
||||
let devices_query_result = xcb_connection
|
||||
.xinput_xi_query_device(XINPUT_ALL_DEVICES)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap();
|
||||
|
||||
let mut pointer_device_states = BTreeMap::new();
|
||||
pointer_device_states.extend(
|
||||
devices_query_result
|
||||
.infos
|
||||
.iter()
|
||||
.filter(|info| is_pointer_device(info.type_))
|
||||
.filter_map(|info| {
|
||||
let scroll_data = info
|
||||
.classes
|
||||
.iter()
|
||||
.filter_map(|class| class.data.as_scroll())
|
||||
.map(|class| *class)
|
||||
.rev()
|
||||
.collect::<Vec<_>>();
|
||||
let old_state = scroll_values_to_preserve.get(&info.deviceid);
|
||||
let old_horizontal = old_state.map(|state| &state.horizontal);
|
||||
let old_vertical = old_state.map(|state| &state.vertical);
|
||||
let horizontal = scroll_data
|
||||
.iter()
|
||||
.find(|data| data.scroll_type == xinput::ScrollType::HORIZONTAL)
|
||||
.map(|data| scroll_data_to_axis_state(data, old_horizontal));
|
||||
let vertical = scroll_data
|
||||
.iter()
|
||||
.find(|data| data.scroll_type == xinput::ScrollType::VERTICAL)
|
||||
.map(|data| scroll_data_to_axis_state(data, old_vertical));
|
||||
if horizontal.is_none() && vertical.is_none() {
|
||||
None
|
||||
} else {
|
||||
Some((
|
||||
info.deviceid,
|
||||
PointerDeviceState {
|
||||
horizontal: horizontal.unwrap_or_else(Default::default),
|
||||
vertical: vertical.unwrap_or_else(Default::default),
|
||||
},
|
||||
))
|
||||
}
|
||||
}),
|
||||
);
|
||||
if pointer_device_states.is_empty() {
|
||||
log::error!("Found no xinput mouse pointers.");
|
||||
}
|
||||
return pointer_device_states;
|
||||
}
|
||||
|
||||
/// Returns true if the device is a pointer device. Does not include pointer device groups.
|
||||
fn is_pointer_device(type_: xinput::DeviceType) -> bool {
|
||||
type_ == xinput::DeviceType::SLAVE_POINTER
|
||||
}
|
||||
|
||||
fn scroll_data_to_axis_state(
|
||||
data: &xinput::DeviceClassDataScroll,
|
||||
old_axis_state_with_valid_scroll_value: Option<&ScrollAxisState>,
|
||||
) -> ScrollAxisState {
|
||||
ScrollAxisState {
|
||||
valuator_number: Some(data.number),
|
||||
multiplier: SCROLL_LINES / fp3232_to_f32(data.increment),
|
||||
scroll_value: old_axis_state_with_valid_scroll_value.and_then(|state| state.scroll_value),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_all_pointer_device_scroll_positions(
|
||||
pointer_device_states: &mut BTreeMap<xinput::DeviceId, PointerDeviceState>,
|
||||
) {
|
||||
pointer_device_states
|
||||
.iter_mut()
|
||||
.for_each(|(_, device_state)| reset_pointer_device_scroll_positions(device_state));
|
||||
}
|
||||
|
||||
fn reset_pointer_device_scroll_positions(pointer: &mut PointerDeviceState) {
|
||||
pointer.horizontal.scroll_value = None;
|
||||
pointer.vertical.scroll_value = None;
|
||||
}
|
||||
|
||||
/// Returns the scroll delta for a smooth scrolling motion event, or `None` if no scroll data is present.
|
||||
fn get_scroll_delta_and_update_state(
|
||||
pointer: &mut PointerDeviceState,
|
||||
event: &xinput::MotionEvent,
|
||||
) -> Option<Point<f32>> {
|
||||
let delta_x = get_axis_scroll_delta_and_update_state(event, &mut pointer.horizontal);
|
||||
let delta_y = get_axis_scroll_delta_and_update_state(event, &mut pointer.vertical);
|
||||
if delta_x.is_some() || delta_y.is_some() {
|
||||
Some(Point::new(delta_x.unwrap_or(0.0), delta_y.unwrap_or(0.0)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_axis_scroll_delta_and_update_state(
|
||||
event: &xinput::MotionEvent,
|
||||
axis: &mut ScrollAxisState,
|
||||
) -> Option<f32> {
|
||||
let axis_index = get_valuator_axis_index(&event.valuator_mask, axis.valuator_number?)?;
|
||||
if let Some(axis_value) = event.axisvalues.get(axis_index) {
|
||||
let new_scroll = fp3232_to_f32(*axis_value);
|
||||
let delta_scroll = axis
|
||||
.scroll_value
|
||||
.map(|old_scroll| (old_scroll - new_scroll) * axis.multiplier);
|
||||
axis.scroll_value = Some(new_scroll);
|
||||
delta_scroll
|
||||
} else {
|
||||
log::error!("Encountered invalid XInput valuator_mask, scrolling may not work properly.");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn make_scroll_wheel_event(
|
||||
position: Point<Pixels>,
|
||||
scroll_delta: Point<f32>,
|
||||
modifiers: Modifiers,
|
||||
) -> crate::ScrollWheelEvent {
|
||||
// When shift is held down, vertical scrolling turns into horizontal scrolling.
|
||||
let delta = if modifiers.shift {
|
||||
Point {
|
||||
x: scroll_delta.y,
|
||||
y: 0.0,
|
||||
}
|
||||
} else {
|
||||
scroll_delta
|
||||
};
|
||||
crate::ScrollWheelEvent {
|
||||
position,
|
||||
delta: ScrollDelta::Lines(delta),
|
||||
modifiers,
|
||||
touch_phase: TouchPhase::default(),
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user