Compare commits
44 Commits
sublime_20
...
fix-keep-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
585110b2cb | ||
|
|
4059a21422 | ||
|
|
3a651c546b | ||
|
|
87014cec71 | ||
|
|
2b671a46f2 | ||
|
|
eaccd542fd | ||
|
|
5a530ecd39 | ||
|
|
233e66d35f | ||
|
|
15353630e4 | ||
|
|
5289b815fe | ||
|
|
8515487bbc | ||
|
|
19ab1eb792 | ||
|
|
722a05bc21 | ||
|
|
5b3e371812 | ||
|
|
8eca7f32e2 | ||
|
|
1a1715766f | ||
|
|
241acbe4be | ||
|
|
6ea09beea8 | ||
|
|
3e50d997dd | ||
|
|
da8bf9ad79 | ||
|
|
589af59dfe | ||
|
|
254c7a330a | ||
|
|
b6cf398eab | ||
|
|
bc5c5cf5d6 | ||
|
|
bf8aba566c | ||
|
|
e14c9479e4 | ||
|
|
97af7e1bd9 | ||
|
|
c251f2a2d4 | ||
|
|
cc56196152 | ||
|
|
405244d422 | ||
|
|
35b4a918c9 | ||
|
|
56fd950d94 | ||
|
|
88af35fe47 | ||
|
|
57ab09c2da | ||
|
|
caa4b529e4 | ||
|
|
137081f050 | ||
|
|
7c1040bc93 | ||
|
|
ff79b29f38 | ||
|
|
2e41e312ad | ||
|
|
5f92ac25a7 | ||
|
|
fb88de9223 | ||
|
|
29111304dd | ||
|
|
0ffd93774c | ||
|
|
2da2ae65a0 |
@@ -24,7 +24,7 @@ workspace-members = [
|
||||
third-party = [
|
||||
{ name = "reqwest", version = "0.11.27" },
|
||||
# build of remote_server should not include scap / its x11 dependency
|
||||
{ name = "scap", git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318" },
|
||||
{ name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" },
|
||||
]
|
||||
|
||||
[final-excludes]
|
||||
|
||||
42
Cargo.lock
generated
@@ -150,7 +150,9 @@ dependencies = [
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"libc",
|
||||
"log",
|
||||
"nix 0.29.0",
|
||||
"paths",
|
||||
"project",
|
||||
"schemars",
|
||||
@@ -162,6 +164,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"ui",
|
||||
"util",
|
||||
"uuid",
|
||||
"watch",
|
||||
"which 6.0.3",
|
||||
"workspace-hack",
|
||||
@@ -279,9 +282,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agentic-coding-protocol"
|
||||
version = "0.0.9"
|
||||
version = "0.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e276b798eddd02562a339340a96919d90bbfcf78de118fdddc932524646fac7"
|
||||
checksum = "a3e6ae951b36fa2f8d9dd6e1af6da2fcaba13d7c866cf6a9e65deda9dc6c5fe4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -7396,9 +7399,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"
|
||||
@@ -9165,7 +9168,6 @@ dependencies = [
|
||||
"collections",
|
||||
"copilot",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"itertools 0.14.0",
|
||||
@@ -10983,6 +10985,23 @@ dependencies = [
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "onboarding"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"command_palette_hooks",
|
||||
"db",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"gpui",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
@@ -14185,7 +14204,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "scap"
|
||||
version = "0.0.8"
|
||||
source = "git+https://github.com/zed-industries/scap?rev=270538dc780f5240723233ff901e1054641ed318#270538dc780f5240723233ff901e1054641ed318"
|
||||
source = "git+https://github.com/zed-industries/scap?rev=808aa5c45b41e8f44729d02e38fd00a2fe2722e7#808aa5c45b41e8f44729d02e38fd00a2fe2722e7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cocoa 0.25.0",
|
||||
@@ -14768,6 +14787,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"telemetry",
|
||||
"tempfile",
|
||||
"theme",
|
||||
"tree-sitter-json",
|
||||
"tree-sitter-rust",
|
||||
@@ -15935,9 +15955,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "taffy"
|
||||
version = "0.4.4"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ec17858c2d465b2f734b798b920818a974faf0babb15d7fef81818a4b2d16f1"
|
||||
checksum = "e8b61630cba2afd2c851821add2e1bb1b7851a2436e839ab73b56558b009035e"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"grid",
|
||||
@@ -16483,6 +16503,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
name = "title_bar"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"auto_update",
|
||||
"call",
|
||||
"chrono",
|
||||
@@ -17667,6 +17688,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
name = "vim"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"agent_ui",
|
||||
"anyhow",
|
||||
"assets",
|
||||
"async-compat",
|
||||
@@ -18728,8 +18750,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "windows-capture"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59d10b4be8b907c7055bc7270dd68d2b920978ffacc1599dcb563a79f0e68d16"
|
||||
source = "git+https://github.com/zed-industries/windows-capture.git?rev=f0d6c1b6691db75461b732f6d5ff56eed002eeb9#f0d6c1b6691db75461b732f6d5ff56eed002eeb9"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"ctrlc",
|
||||
@@ -20222,6 +20243,7 @@ dependencies = [
|
||||
"nix 0.29.0",
|
||||
"node_runtime",
|
||||
"notifications",
|
||||
"onboarding",
|
||||
"outline",
|
||||
"outline_panel",
|
||||
"parking_lot",
|
||||
|
||||
@@ -108,6 +108,7 @@ members = [
|
||||
"crates/node_runtime",
|
||||
"crates/notifications",
|
||||
"crates/ollama",
|
||||
"crates/onboarding",
|
||||
"crates/open_ai",
|
||||
"crates/open_router",
|
||||
"crates/outline",
|
||||
@@ -325,6 +326,7 @@ net = { path = "crates/net" }
|
||||
node_runtime = { path = "crates/node_runtime" }
|
||||
notifications = { path = "crates/notifications" }
|
||||
ollama = { path = "crates/ollama" }
|
||||
onboarding = { path = "crates/onboarding" }
|
||||
open_ai = { path = "crates/open_ai" }
|
||||
open_router = { path = "crates/open_router", features = ["schemars"] }
|
||||
outline = { path = "crates/outline" }
|
||||
@@ -410,7 +412,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||
# External crates
|
||||
#
|
||||
|
||||
agentic-coding-protocol = "0.0.9"
|
||||
agentic-coding-protocol = "0.0.10"
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
||||
any_vec = "0.14"
|
||||
@@ -553,8 +555,7 @@ rustc-demangle = "0.1.23"
|
||||
rustc-hash = "2.1.0"
|
||||
rustls = { version = "0.23.26" }
|
||||
rustls-platform-verifier = "0.5.0"
|
||||
# When updating scap rev, also update it in .config/hakari.toml
|
||||
scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false }
|
||||
scap = { git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7", default-features = false }
|
||||
schemars = { version = "1.0", features = ["indexmap2"] }
|
||||
semver = "1.0"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
@@ -708,6 +709,7 @@ features = [
|
||||
[patch.crates-io]
|
||||
notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
|
||||
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
|
||||
windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" }
|
||||
|
||||
# Makes the workspace hack crate refer to the local one, but only when you're building locally
|
||||
workspace-hack = { path = "tooling/workspace-hack" }
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.8481 26.5925L15.7165 22.1806L15.8481 21.7961L15.7165 21.5836H15.3316L14.0152 21.5027L9.51899 21.3812L5.62025 21.2193L1.84304 21.0169L0.891139 20.8146L0 19.6408L0.0911392 19.0539L0.891139 18.5176L2.03544 18.6188L4.56709 18.7908L8.36456 19.0539L11.119 19.2158L15.2 19.6408H15.8481L15.9392 19.3777L15.7165 19.2158L15.5443 19.0539L11.6152 16.3926L7.36203 13.5796L5.13418 11.9605L3.92911 11.1409L3.32152 10.3719L3.05823 8.69213L4.1519 7.48798L5.62025 7.58917L5.99494 7.69036L7.48354 8.8338L10.6633 11.2927L14.8152 14.3486L15.4228 14.8545L15.6658 14.6825L15.6962 14.5611L15.4228 14.1057L13.1646 10.0278L10.7544 5.87908L9.68101 4.15887L9.39747 3.12674C9.2962 2.70175 9.22532 2.34758 9.22532 1.91247L10.4709 0.222616L11.1595 0L12.8203 0.222616L13.519 0.82975L14.5519 3.18745L16.2228 6.90109L18.8152 11.9504L19.5747 13.448L19.9797 14.8343L20.1316 15.2593H20.3949V15.0164L20.6076 12.173L21.0025 8.68201L21.3873 4.18922L21.519 2.92436L22.1468 1.40653L23.3924 0.586896L24.3646 1.05237L25.1646 2.1958L25.0532 2.93448L24.5772 6.02074L23.6456 10.8576L23.038 14.0956H23.3924L23.7975 13.6909L25.438 11.5153L28.1924 8.07488L29.4076 6.70883L30.8253 5.20111L31.7367 4.48267H33.4582L34.7241 6.36479L34.157 8.30761L32.3848 10.554L30.9165 12.4564L28.8101 15.2897L27.4937 17.5563L27.6152 17.7384L27.9291 17.7081L32.6886 16.6962L35.2608 16.2307L38.3291 15.7045L39.7165 16.3521L39.8684 17.0099L39.3215 18.3557L36.0405 19.1652L32.1924 19.9342L26.4608 21.2902L26.3899 21.3408L26.4709 21.4419L29.0532 21.6848L30.157 21.7455H32.8608L37.8937 22.1199L39.2101 22.9901L40 24.0526L39.8684 24.8621L37.843 25.8943L35.1089 25.2466L28.7291 23.7288L26.5418 23.1824H26.238V23.3645L28.0608 25.1455L31.4025 28.1609L35.5848 32.0465L35.7975 33.0078L35.2608 33.7668L34.6937 33.6858L31.0177 30.9233L29.6 29.6787L26.3899 26.977H26.1772V27.2603L26.9165 28.343L30.8253 34.212L31.0278 36.0132L30.7443 36.6L29.7316 36.9542L28.6177 36.7518L26.3291 33.5441L23.9696 29.9317L22.0658 26.6937L21.8329 26.8252L20.7089 38.9173L20.1823 39.5345L18.9671 40L17.9544 39.231L17.4177 37.9863L17.9544 35.5274L18.6025 32.3198L19.1291 29.7698L19.6051 26.6026L19.8886 25.5502L19.8684 25.4794L19.6354 25.5097L17.2456 28.7883L13.6101 33.6959L10.7342 36.7721L10.0456 37.0453L8.85063 36.428L8.96203 35.3251L9.63038 34.3435L13.6101 29.2841L16.0101 26.1472L17.5595 24.3359L17.5494 24.0729H17.4582L6.88608 30.9335L5.00253 31.1763L4.1924 30.4174L4.29367 29.1728L4.67848 28.768L7.85823 26.5823L7.8481 26.5925Z" fill="black"/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.35443 9.97775L6.71495 8.65418L6.75443 8.53883L6.71495 8.47508H6.59948L6.20456 8.45081L4.8557 8.41436L3.68608 8.36579L2.55291 8.30507L2.26734 8.24438L2 7.89224L2.02734 7.71617L2.26734 7.55528L2.61063 7.58564L3.37013 7.63724L4.50937 7.71617L5.3357 7.76474L6.56 7.89224H6.75443L6.78176 7.81331L6.71495 7.76474L6.66329 7.71617L5.48456 6.91778L4.20861 6.07388L3.54025 5.58815L3.17873 5.34227L2.99646 5.11157L2.91747 4.60764L3.24557 4.24639L3.68608 4.27675L3.79848 4.30711L4.24506 4.65014L5.19899 5.38781L6.44456 6.30458L6.62684 6.45635L6.69974 6.40475L6.70886 6.36833L6.62684 6.23171L5.94938 5.00834L5.22632 3.76372L4.9043 3.24766L4.81924 2.93802C4.78886 2.81053 4.7676 2.70427 4.7676 2.57374L5.14127 2.06678L5.34785 2L5.84609 2.06678L6.0557 2.24893L6.36557 2.95624L6.86684 4.07033L7.64456 5.58512L7.87241 6.0344L7.99391 6.45029L8.03948 6.57779H8.11847V6.50492L8.18228 5.6519L8.30075 4.6046L8.41619 3.25677L8.4557 2.87731L8.64404 2.42196L9.01772 2.17607L9.30938 2.31571L9.54938 2.65874L9.51596 2.88034L9.37316 3.80622L9.09368 5.25728L8.9114 6.22868H9.01772L9.13925 6.10727L9.6314 5.45459L10.4577 4.42246L10.8223 4.01265L11.2476 3.56033L11.521 3.3448H12.0375L12.4172 3.90944L12.2471 4.49228L11.7154 5.1662L11.275 5.73692L10.643 6.58691L10.2481 7.26689L10.2846 7.32152L10.3787 7.31243L11.8066 7.00886L12.5782 6.86921L13.4987 6.71135L13.915 6.90563L13.9605 7.10297L13.7965 7.50671L12.8122 7.74956L11.6577 7.98026L9.93824 8.38706L9.91697 8.40224L9.94127 8.43257L10.716 8.50544L11.0471 8.52365H11.8582L13.3681 8.63597L13.763 8.89703L14 9.21578L13.9605 9.45863L13.3529 9.76829L12.5327 9.57398L10.6187 9.11864L9.96254 8.95472H9.8714V9.00935L10.4182 9.54365L11.4208 10.4483L12.6754 11.614L12.7393 11.9023L12.5782 12.13L12.4081 12.1057L11.3053 11.277L10.88 10.9036L9.91697 10.0931H9.85316V10.1781L10.075 10.5029L11.2476 12.2636L11.3083 12.804L11.2233 12.98L10.9195 13.0863L10.5853 13.0255L9.89873 12.0632L9.19088 10.9795L8.61974 10.0081L8.54987 10.0476L8.21267 13.6752L8.05469 13.8604L7.69013 14L7.38632 13.7693L7.22531 13.3959L7.38632 12.6582L7.58075 11.6959L7.73873 10.9309L7.88153 9.98078L7.96658 9.66506L7.96052 9.64382L7.89062 9.65291L7.17368 10.6365L6.08303 12.1088L5.22026 13.0316L5.01368 13.1136L4.65519 12.9284L4.68861 12.5975L4.88911 12.303L6.08303 10.7852L6.80303 9.84416L7.26785 9.30077L7.26482 9.22187H7.23746L4.06582 11.2801L3.50076 11.3529L3.25772 11.1252L3.2881 10.7518L3.40354 10.6304L4.35747 9.97469L4.35443 9.97775Z" fill="black"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
@@ -1 +1,3 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google Gemini</title><path d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81"/></svg>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.44 12.27C7.81333 13.1217 8 14.0317 8 15C8 14.0317 8.18083 13.1217 8.5425 12.27C8.91583 11.4183 9.4175 10.6775 10.0475 10.0475C10.6775 9.4175 11.4183 8.92167 12.27 8.56C13.1217 8.18667 14.0317 8 15 8C14.0317 8 13.1217 7.81917 12.27 7.4575C11.4411 7.1001 10.6871 6.5895 10.0475 5.9525C9.4105 5.31293 8.8999 4.55891 8.5425 3.73C8.18083 2.87833 8 1.96833 8 1C8 1.96833 7.81333 2.87833 7.44 3.73C7.07833 4.58167 6.5825 5.3225 5.9525 5.9525C5.31293 6.5895 4.55891 7.1001 3.73 7.4575C2.87833 7.81917 1.96833 8 1 8C1.96833 8 2.87833 8.18667 3.73 8.56C4.58167 8.92167 5.3225 9.4175 5.9525 10.0475C6.5825 10.6775 7.07833 11.4183 7.44 12.27Z" fill="black"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 402 B After Width: | Height: | Size: 762 B |
@@ -1,5 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor" stroke="currentColor">
|
||||
<g clip-path="url(#clip0_205_3)">
|
||||
<g>
|
||||
<path d="M0.094 7.78c0.469 0 2.281 -0.405 3.219 -0.936s0.938 -0.531 2.875 -1.906c2.453 -1.741 4.188 -1.158 7.031 -1.158" stroke-width="2.8125" />
|
||||
<path d="m15.969 3.797 -4.805 2.774V1.023z" />
|
||||
<path d="M0 7.781c0.469 0 2.281 0.405 3.219 0.936s0.938 0.531 2.875 1.906C8.547 12.364 10.281 11.781 13.125 11.781" stroke-width="2.8125" />
|
||||
|
||||
|
Before Width: | Height: | Size: 575 B After Width: | Height: | Size: 545 B |
7
assets/icons/new_from_summary.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 4H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.66667 8H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.66667 12H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.00016 12C8.41993 12.5597 9.00515 12.9731 9.67294 13.1817C10.3407 13.3903 11.0572 13.3835 11.7209 13.1623C12.3846 12.941 12.9619 12.5166 13.371 11.949C13.78 11.3815 14.0002 10.6996 14.0002 10C14.0002 9.20435 13.6841 8.44129 13.1215 7.87868C12.5589 7.31607 11.7958 7 11.0002 7C10.1135 7 9.30683 7.36 8.72683 7.94L7.3335 9.33333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.3335 6.66669V9.33335H10.0002" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 964 B |
7
assets/icons/new_text_thread.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.33333 8H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.6667 5H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 11H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 7V11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 9H10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 620 B |
3
assets/icons/new_thread.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.31254 12.549C7.3841 13.0987 8.61676 13.2476 9.78839 12.9688C10.96 12.6901 11.9936 12.0021 12.7028 11.0287C13.412 10.0554 13.7503 8.8607 13.6566 7.66002C13.5629 6.45934 13.0435 5.33159 12.1919 4.48C11.3403 3.62841 10.2126 3.10898 9.01188 3.01531C7.8112 2.92164 6.61655 3.2599 5.64319 3.96912C4.66984 4.67834 3.9818 5.71188 3.70306 6.88351C3.42432 8.05514 3.5732 9.2878 4.12289 10.3594L3 13.6719L6.31254 12.549Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 609 B |
4
assets/icons/todo_complete.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 8L7.33333 9L10 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 418 B |
10
assets/icons/todo_pending.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 3C7.66045 3 8.33955 3 9 3" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11 4C11.3949 4.26602 11.7345 4.60558 12 5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13 7C13 7.66045 13 8.33955 13 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 11C11.734 11.3949 11.3944 11.7345 11 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 13C8.33954 13 7.66046 13 7 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 12C4.6051 11.734 4.26554 11.3944 4 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 9C3 8.33955 3 7.66045 3 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 5C4.26602 4.6051 4.60558 4.26554 5 4" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
11
assets/icons/todo_progress.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 3C7.66045 3 8.33955 3 9 3" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11 4C11.3949 4.26602 11.7345 4.60558 12 5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13 7C13 7.66045 13 8.33955 13 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 11C11.734 11.3949 11.3944 11.7345 11 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 13C8.33954 13 7.66046 13 7 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 12C4.6051 11.734 4.26554 11.3944 4 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 9C3 8.33955 3 7.66045 3 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 5C4.26602 4.6051 4.60558 4.26554 5 4" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.00016 8.66665C8.36835 8.66665 8.66683 8.36817 8.66683 7.99998C8.66683 7.63179 8.36835 7.33331 8.00016 7.33331C7.63197 7.33331 7.3335 7.63179 7.3335 7.99998C7.3335 8.36817 7.63197 8.66665 8.00016 8.66665Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -277,7 +277,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor > Editor && !use_modifier_to_send",
|
||||
"context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"ctrl-enter": "agent::ChatWithFollow",
|
||||
@@ -288,7 +288,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor > Editor && use_modifier_to_send",
|
||||
"context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
|
||||
"bindings": {
|
||||
"ctrl-enter": "agent::Chat",
|
||||
"enter": "editor::Newline",
|
||||
@@ -483,9 +483,8 @@
|
||||
"ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip
|
||||
"ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
|
||||
"ctrl-k ctrl-i": "editor::Hover",
|
||||
"ctrl-k ctrl-b": "editor::BlameHover",
|
||||
"ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"ctrl-u": "editor::UndoSelection",
|
||||
"ctrl-shift-u": "editor::RedoSelection",
|
||||
"f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
|
||||
"shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
|
||||
"f2": "editor::Rename",
|
||||
@@ -663,6 +662,8 @@
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-u": "editor::UndoSelection",
|
||||
"ctrl-shift-u": "editor::RedoSelection",
|
||||
"ctrl-shift-j": "editor::JoinLines",
|
||||
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
|
||||
|
||||
@@ -318,7 +318,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor > Editor && !use_modifier_to_send",
|
||||
"context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
@@ -330,7 +330,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor > Editor && use_modifier_to_send",
|
||||
"context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "agent::Chat",
|
||||
@@ -537,9 +537,8 @@
|
||||
"ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch
|
||||
"cmd-k ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
|
||||
"cmd-k cmd-i": "editor::Hover",
|
||||
"cmd-k cmd-b": "editor::BlameHover",
|
||||
"cmd-/": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"cmd-u": "editor::UndoSelection",
|
||||
"cmd-shift-u": "editor::RedoSelection",
|
||||
"f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
|
||||
"shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
|
||||
"f2": "editor::Rename",
|
||||
@@ -726,6 +725,8 @@
|
||||
"context": "Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-u": "editor::UndoSelection",
|
||||
"cmd-shift-u": "editor::RedoSelection",
|
||||
"ctrl-j": "editor::JoinLines",
|
||||
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
|
||||
@@ -1228,6 +1229,7 @@
|
||||
"context": "KeymapEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-f": "search::FocusSearch",
|
||||
"cmd-alt-f": "keymap_editor::ToggleKeystrokeSearch",
|
||||
"cmd-alt-c": "keymap_editor::ToggleConflictFilter",
|
||||
"enter": "keymap_editor::EditBinding",
|
||||
|
||||
@@ -41,9 +41,9 @@
|
||||
// "ctrl-alt-shift-g": "" // find_under_prev (cancels any selections)
|
||||
"f9": "editor::SortLinesCaseSensitive",
|
||||
"ctrl-f9": "editor::SortLinesCaseInsensitive",
|
||||
"f12": "editor::GoToDefinition", // lsp_symbol_definition
|
||||
"f12": "editor::GoToDefinition",
|
||||
"ctrl-f12": "editor::GoToDefinitionSplit",
|
||||
"shift-f12": "editor::FindAllReferences", // lsp_symbol_references
|
||||
"shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-.": "editor::GoToHunk",
|
||||
"ctrl-,": "editor::GoToPreviousHunk",
|
||||
@@ -61,9 +61,7 @@
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"ctrl-alt-space": "editor::ShowSignatureHelp", // lsp_signature_help_show
|
||||
"ctrl-shift-r": "project_symbols::Toggle", // lsp_workspace_symbols
|
||||
"ctrl-r": "outline::Toggle" // lsp_document_symbols
|
||||
"ctrl-r": "outline::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -92,7 +90,6 @@
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"ctrl-k ctrl-b": "workspace::ToggleLeftDock",
|
||||
"ctrl-alt-m": "diagnostics::Deploy", // lsp_show_diagnostics_panel
|
||||
// "ctrl-0": "project_panel::ToggleFocus", // normally resets zoom
|
||||
"shift-ctrl-r": "project_symbols::Toggle"
|
||||
}
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"g r a": "editor::ToggleCodeActions",
|
||||
"g g": "vim::StartOfDocument",
|
||||
"g h": "editor::Hover",
|
||||
"g B": "editor::BlameHover",
|
||||
"g t": "pane::ActivateNextItem",
|
||||
"g shift-t": "pane::ActivatePreviousItem",
|
||||
"g d": "editor::GoToDefinition",
|
||||
@@ -578,6 +579,13 @@
|
||||
"shift-u": "git::UnstageAndNext" // "d shift-u"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "VimControl && (AgentDiff || editor_agent_diff)",
|
||||
"bindings": {
|
||||
"d p": "agent::Reject",
|
||||
"d u": "agent::Keep"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gu",
|
||||
"bindings": {
|
||||
@@ -858,6 +866,14 @@
|
||||
"shift-n": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Picker > Editor",
|
||||
"bindings": {
|
||||
"ctrl-h": "editor::Backspace",
|
||||
"ctrl-u": "editor::DeleteToBeginningOfLine",
|
||||
"ctrl-w": "editor::DeleteToPreviousWordStart"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitCommit > Editor && VimControl && vim_mode == normal",
|
||||
"bindings": {
|
||||
|
||||
@@ -60,7 +60,7 @@ services:
|
||||
- postgres
|
||||
|
||||
stripe-mock:
|
||||
image: stripe/stripe-mock:v0.184.0
|
||||
image: stripe/stripe-mock:v0.178.0
|
||||
ports:
|
||||
- 12111:12111
|
||||
- 12112:12112
|
||||
|
||||
@@ -453,9 +453,69 @@ impl Diff {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Plan {
|
||||
pub entries: Vec<PlanEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PlanStats<'a> {
|
||||
pub in_progress_entry: Option<&'a PlanEntry>,
|
||||
pub pending: u32,
|
||||
pub completed: u32,
|
||||
}
|
||||
|
||||
impl Plan {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.entries.is_empty()
|
||||
}
|
||||
|
||||
pub fn stats(&self) -> PlanStats<'_> {
|
||||
let mut stats = PlanStats {
|
||||
in_progress_entry: None,
|
||||
pending: 0,
|
||||
completed: 0,
|
||||
};
|
||||
|
||||
for entry in &self.entries {
|
||||
match &entry.status {
|
||||
acp::PlanEntryStatus::Pending => {
|
||||
stats.pending += 1;
|
||||
}
|
||||
acp::PlanEntryStatus::InProgress => {
|
||||
stats.in_progress_entry = stats.in_progress_entry.or(Some(entry));
|
||||
}
|
||||
acp::PlanEntryStatus::Completed => {
|
||||
stats.completed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stats
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PlanEntry {
|
||||
pub content: Entity<Markdown>,
|
||||
pub priority: acp::PlanEntryPriority,
|
||||
pub status: acp::PlanEntryStatus,
|
||||
}
|
||||
|
||||
impl PlanEntry {
|
||||
pub fn from_acp(entry: acp::PlanEntry, cx: &mut App) -> Self {
|
||||
Self {
|
||||
content: cx.new(|cx| Markdown::new_text(entry.content.into(), cx)),
|
||||
priority: entry.priority,
|
||||
status: entry.status,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AcpThread {
|
||||
entries: Vec<AgentThreadEntry>,
|
||||
title: SharedString,
|
||||
entries: Vec<AgentThreadEntry>,
|
||||
plan: Plan,
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
shared_buffers: HashMap<Entity<Buffer>, BufferSnapshot>,
|
||||
@@ -515,6 +575,7 @@ impl AcpThread {
|
||||
action_log,
|
||||
shared_buffers: Default::default(),
|
||||
entries: Default::default(),
|
||||
plan: Default::default(),
|
||||
title,
|
||||
project,
|
||||
send_task: None,
|
||||
@@ -819,6 +880,29 @@ impl AcpThread {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn plan(&self) -> &Plan {
|
||||
&self.plan
|
||||
}
|
||||
|
||||
pub fn update_plan(&mut self, request: acp::UpdatePlanParams, cx: &mut Context<Self>) {
|
||||
self.plan = Plan {
|
||||
entries: request
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(|entry| PlanEntry::from_acp(entry, cx))
|
||||
.collect(),
|
||||
};
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn clear_completed_plan_entries(&mut self, cx: &mut Context<Self>) {
|
||||
self.plan
|
||||
.entries
|
||||
.retain(|entry| !matches!(entry.status, acp::PlanEntryStatus::Completed));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_project_location(&self, location: ToolCallLocation, cx: &mut Context<Self>) {
|
||||
self.project.update(cx, |project, cx| {
|
||||
let Some(path) = project.project_path_for_absolute_path(&location.path, cx) else {
|
||||
@@ -1136,6 +1220,17 @@ impl AcpClientDelegate {
|
||||
Self { thread, cx }
|
||||
}
|
||||
|
||||
pub async fn clear_completed_plan_entries(&self) -> Result<()> {
|
||||
let cx = &mut self.cx.clone();
|
||||
cx.update(|cx| {
|
||||
self.thread
|
||||
.update(cx, |thread, cx| thread.clear_completed_plan_entries(cx))
|
||||
})?
|
||||
.context("Failed to update thread")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn request_existing_tool_call_confirmation(
|
||||
&self,
|
||||
tool_call_id: ToolCallId,
|
||||
@@ -1233,6 +1328,18 @@ impl acp::Client for AcpClientDelegate {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_plan(&self, request: acp::UpdatePlanParams) -> Result<(), acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
self.thread
|
||||
.update(cx, |thread, cx| thread.update_plan(request, cx))
|
||||
})?
|
||||
.context("Failed to update thread")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_text_file(
|
||||
&self,
|
||||
request: acp::ReadTextFileParams,
|
||||
|
||||
@@ -231,7 +231,6 @@ impl ActivityIndicator {
|
||||
status,
|
||||
} => {
|
||||
let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
|
||||
let project = project.clone();
|
||||
let status = status.clone();
|
||||
let server_name = server_name.clone();
|
||||
cx.spawn_in(window, async move |workspace, cx| {
|
||||
@@ -247,8 +246,7 @@ impl ActivityIndicator {
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(cx.new(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_buffer(buffer, Some(project.clone()), window, cx);
|
||||
let mut editor = Editor::for_buffer(buffer, None, window, cx);
|
||||
editor.set_read_only(true);
|
||||
editor
|
||||
})),
|
||||
|
||||
@@ -51,7 +51,7 @@ use util::{ResultExt as _, debug_panic, post_inc};
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
|
||||
|
||||
const MAX_RETRY_ATTEMPTS: u8 = 3;
|
||||
const MAX_RETRY_ATTEMPTS: u8 = 4;
|
||||
const BASE_RETRY_DELAY: Duration = Duration::from_secs(5);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -2182,8 +2182,8 @@ impl Thread {
|
||||
|
||||
// General strategy here:
|
||||
// - If retrying won't help (e.g. invalid API key or payload too large), return None so we don't retry at all.
|
||||
// - If it's a time-based issue (e.g. server overloaded, rate limit exceeded), try multiple times with exponential backoff.
|
||||
// - If it's an issue that *might* be fixed by retrying (e.g. internal server error), just retry once.
|
||||
// - If it's a time-based issue (e.g. server overloaded, rate limit exceeded), retry up to 4 times with exponential backoff.
|
||||
// - If it's an issue that *might* be fixed by retrying (e.g. internal server error), retry up to 3 times.
|
||||
match error {
|
||||
HttpResponseError {
|
||||
status_code: StatusCode::TOO_MANY_REQUESTS,
|
||||
@@ -2211,8 +2211,8 @@ impl Thread {
|
||||
}
|
||||
StatusCode::INTERNAL_SERVER_ERROR => Some(RetryStrategy::Fixed {
|
||||
delay: retry_after.unwrap_or(BASE_RETRY_DELAY),
|
||||
// Internal Server Error could be anything, so only retry once.
|
||||
max_attempts: 1,
|
||||
// Internal Server Error could be anything, retry up to 3 times.
|
||||
max_attempts: 3,
|
||||
}),
|
||||
status => {
|
||||
// There is no StatusCode variant for the unofficial HTTP 529 ("The service is overloaded"),
|
||||
@@ -2223,20 +2223,23 @@ impl Thread {
|
||||
max_attempts: MAX_RETRY_ATTEMPTS,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
Some(RetryStrategy::Fixed {
|
||||
delay: retry_after.unwrap_or(BASE_RETRY_DELAY),
|
||||
max_attempts: 2,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
ApiInternalServerError { .. } => Some(RetryStrategy::Fixed {
|
||||
delay: BASE_RETRY_DELAY,
|
||||
max_attempts: 1,
|
||||
max_attempts: 3,
|
||||
}),
|
||||
ApiReadResponseError { .. }
|
||||
| HttpSend { .. }
|
||||
| DeserializeResponse { .. }
|
||||
| BadRequestFormat { .. } => Some(RetryStrategy::Fixed {
|
||||
delay: BASE_RETRY_DELAY,
|
||||
max_attempts: 1,
|
||||
max_attempts: 3,
|
||||
}),
|
||||
// Retrying these errors definitely shouldn't help.
|
||||
HttpResponseError {
|
||||
@@ -2244,24 +2247,31 @@ impl Thread {
|
||||
StatusCode::PAYLOAD_TOO_LARGE | StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED,
|
||||
..
|
||||
}
|
||||
| SerializeRequest { .. }
|
||||
| AuthenticationError { .. }
|
||||
| PermissionError { .. } => None,
|
||||
// These errors might be transient, so retry them
|
||||
SerializeRequest { .. }
|
||||
| BuildRequestBody { .. }
|
||||
| PromptTooLarge { .. }
|
||||
| AuthenticationError { .. }
|
||||
| PermissionError { .. }
|
||||
| ApiEndpointNotFound { .. }
|
||||
| NoApiKey { .. } => None,
|
||||
| NoApiKey { .. } => Some(RetryStrategy::Fixed {
|
||||
delay: BASE_RETRY_DELAY,
|
||||
max_attempts: 2,
|
||||
}),
|
||||
// Retry all other 4xx and 5xx errors once.
|
||||
HttpResponseError { status_code, .. }
|
||||
if status_code.is_client_error() || status_code.is_server_error() =>
|
||||
{
|
||||
Some(RetryStrategy::Fixed {
|
||||
delay: BASE_RETRY_DELAY,
|
||||
max_attempts: 1,
|
||||
max_attempts: 3,
|
||||
})
|
||||
}
|
||||
// Conservatively assume that any other errors are non-retryable
|
||||
HttpResponseError { .. } | Other(..) => None,
|
||||
HttpResponseError { .. } | Other(..) => Some(RetryStrategy::Fixed {
|
||||
delay: BASE_RETRY_DELAY,
|
||||
max_attempts: 2,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4352,7 +4362,7 @@ fn main() {{
|
||||
let retry_state = thread.retry_state.as_ref().unwrap();
|
||||
assert_eq!(retry_state.attempt, 1, "Should be first retry attempt");
|
||||
assert_eq!(
|
||||
retry_state.max_attempts, 1,
|
||||
retry_state.max_attempts, 3,
|
||||
"Should have correct max attempts"
|
||||
);
|
||||
});
|
||||
@@ -4368,8 +4378,9 @@ fn main() {{
|
||||
if let MessageSegment::Text(text) = seg {
|
||||
text.contains("internal")
|
||||
&& text.contains("Fake")
|
||||
&& text.contains("Retrying in")
|
||||
&& !text.contains("attempt")
|
||||
&& text.contains("Retrying")
|
||||
&& text.contains("attempt 1 of 3")
|
||||
&& text.contains("seconds")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -4464,8 +4475,8 @@ fn main() {{
|
||||
let retry_state = thread.retry_state.as_ref().unwrap();
|
||||
assert_eq!(retry_state.attempt, 1, "Should be first retry attempt");
|
||||
assert_eq!(
|
||||
retry_state.max_attempts, 1,
|
||||
"Internal server errors should only retry once"
|
||||
retry_state.max_attempts, 3,
|
||||
"Internal server errors should retry up to 3 times"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4473,7 +4484,15 @@ fn main() {{
|
||||
cx.executor().advance_clock(BASE_RETRY_DELAY);
|
||||
cx.run_until_parked();
|
||||
|
||||
// Should have scheduled second retry - count retry messages
|
||||
// Advance clock for second retry
|
||||
cx.executor().advance_clock(BASE_RETRY_DELAY);
|
||||
cx.run_until_parked();
|
||||
|
||||
// Advance clock for third retry
|
||||
cx.executor().advance_clock(BASE_RETRY_DELAY);
|
||||
cx.run_until_parked();
|
||||
|
||||
// Should have completed all retries - count retry messages
|
||||
let retry_count = thread.update(cx, |thread, _| {
|
||||
thread
|
||||
.messages
|
||||
@@ -4491,24 +4510,24 @@ fn main() {{
|
||||
.count()
|
||||
});
|
||||
assert_eq!(
|
||||
retry_count, 1,
|
||||
"Should have only one retry for internal server errors"
|
||||
retry_count, 3,
|
||||
"Should have 3 retries for internal server errors"
|
||||
);
|
||||
|
||||
// For internal server errors, we only retry once and then give up
|
||||
// Check that retry_state is cleared after the single retry
|
||||
// For internal server errors, we retry 3 times and then give up
|
||||
// Check that retry_state is cleared after all retries
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(
|
||||
thread.retry_state.is_none(),
|
||||
"Retry state should be cleared after single retry"
|
||||
"Retry state should be cleared after all retries"
|
||||
);
|
||||
});
|
||||
|
||||
// Verify total attempts (1 initial + 1 retry)
|
||||
// Verify total attempts (1 initial + 3 retries)
|
||||
assert_eq!(
|
||||
*completion_count.lock(),
|
||||
2,
|
||||
"Should have attempted once plus 1 retry"
|
||||
4,
|
||||
"Should have attempted once plus 3 retries"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,10 +37,15 @@ strum.workspace = true
|
||||
tempfile.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
watch.workspace = true
|
||||
which.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc.workspace = true
|
||||
nix.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger.workspace = true
|
||||
language.workspace = true
|
||||
|
||||
@@ -4,10 +4,13 @@ mod tools;
|
||||
use collections::HashMap;
|
||||
use project::Project;
|
||||
use settings::SettingsStore;
|
||||
use smol::process::Child;
|
||||
use std::cell::RefCell;
|
||||
use std::fmt::Display;
|
||||
use std::path::Path;
|
||||
use std::pin::pin;
|
||||
use std::rc::Rc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use agentic_coding_protocol::{
|
||||
self as acp, AnyAgentRequest, AnyAgentResult, Client, ProtocolVersion,
|
||||
@@ -16,7 +19,7 @@ use agentic_coding_protocol::{
|
||||
use anyhow::{Result, anyhow};
|
||||
use futures::channel::oneshot;
|
||||
use futures::future::LocalBoxFuture;
|
||||
use futures::{AsyncBufReadExt, AsyncWriteExt};
|
||||
use futures::{AsyncBufReadExt, AsyncWriteExt, SinkExt};
|
||||
use futures::{
|
||||
AsyncRead, AsyncWrite, FutureExt, StreamExt,
|
||||
channel::mpsc::{self, UnboundedReceiver, UnboundedSender},
|
||||
@@ -69,13 +72,12 @@ impl AgentServer for ClaudeCode {
|
||||
let (mut delegate_tx, delegate_rx) = watch::channel(None);
|
||||
let tool_id_map = Rc::new(RefCell::new(HashMap::default()));
|
||||
|
||||
let permission_mcp_server =
|
||||
ClaudeMcpServer::new(delegate_rx, tool_id_map.clone(), cx).await?;
|
||||
let mcp_server = ClaudeMcpServer::new(delegate_rx, tool_id_map.clone(), cx).await?;
|
||||
|
||||
let mut mcp_servers = HashMap::default();
|
||||
mcp_servers.insert(
|
||||
mcp_server::SERVER_NAME.to_string(),
|
||||
permission_mcp_server.server_config()?,
|
||||
mcp_server.server_config()?,
|
||||
);
|
||||
let mcp_config = McpConfig { mcp_servers };
|
||||
|
||||
@@ -98,50 +100,58 @@ impl AgentServer for ClaudeCode {
|
||||
anyhow::bail!("Failed to find claude binary");
|
||||
};
|
||||
|
||||
let mut child = util::command::new_smol_command(&command.path)
|
||||
.args(
|
||||
[
|
||||
"--input-format",
|
||||
"stream-json",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"--print",
|
||||
"--verbose",
|
||||
"--mcp-config",
|
||||
mcp_config_path.to_string_lossy().as_ref(),
|
||||
"--permission-prompt-tool",
|
||||
&format!(
|
||||
"mcp__{}__{}",
|
||||
mcp_server::SERVER_NAME,
|
||||
mcp_server::PERMISSION_TOOL
|
||||
),
|
||||
"--allowedTools",
|
||||
"mcp__zed__Read,mcp__zed__Edit",
|
||||
"--disallowedTools",
|
||||
"Read,Edit",
|
||||
]
|
||||
.into_iter()
|
||||
.chain(command.args.iter().map(|arg| arg.as_str())),
|
||||
)
|
||||
.current_dir(root_dir)
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::inherit())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
let stdin = child.stdin.take().unwrap();
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
|
||||
let (incoming_message_tx, mut incoming_message_rx) = mpsc::unbounded();
|
||||
let (outgoing_tx, outgoing_rx) = mpsc::unbounded();
|
||||
let (cancel_tx, mut cancel_rx) = mpsc::unbounded::<oneshot::Sender<Result<()>>>();
|
||||
|
||||
let session_id = Uuid::new_v4();
|
||||
|
||||
log::trace!("Starting session with id: {}", session_id);
|
||||
|
||||
let io_task =
|
||||
ClaudeAgentConnection::handle_io(outgoing_rx, incoming_message_tx, stdin, stdout);
|
||||
cx.background_spawn(async move {
|
||||
io_task.await.log_err();
|
||||
let mut outgoing_rx = Some(outgoing_rx);
|
||||
let mut mode = ClaudeSessionMode::Start;
|
||||
|
||||
loop {
|
||||
let mut child =
|
||||
spawn_claude(&command, mode, session_id, &mcp_config_path, &root_dir)
|
||||
.await?;
|
||||
mode = ClaudeSessionMode::Resume;
|
||||
|
||||
let pid = child.id();
|
||||
log::trace!("Spawned (pid: {})", pid);
|
||||
|
||||
let mut io_fut = pin!(
|
||||
ClaudeAgentConnection::handle_io(
|
||||
outgoing_rx.take().unwrap(),
|
||||
incoming_message_tx.clone(),
|
||||
child.stdin.take().unwrap(),
|
||||
child.stdout.take().unwrap(),
|
||||
)
|
||||
.fuse()
|
||||
);
|
||||
|
||||
select_biased! {
|
||||
done_tx = cancel_rx.next() => {
|
||||
if let Some(done_tx) = done_tx {
|
||||
log::trace!("Interrupted (pid: {})", pid);
|
||||
let result = send_interrupt(pid as i32);
|
||||
outgoing_rx.replace(io_fut.await?);
|
||||
done_tx.send(result).log_err();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result = io_fut => {
|
||||
result?;
|
||||
}
|
||||
}
|
||||
|
||||
log::trace!("Stopped (pid: {})", pid);
|
||||
break;
|
||||
}
|
||||
|
||||
drop(mcp_config_path);
|
||||
drop(child);
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -153,6 +163,7 @@ impl AgentServer for ClaudeCode {
|
||||
let handler_task = cx.foreground_executor().spawn({
|
||||
let end_turn_tx = end_turn_tx.clone();
|
||||
let tool_id_map = tool_id_map.clone();
|
||||
let delegate = delegate.clone();
|
||||
async move {
|
||||
while let Some(message) = incoming_message_rx.next().await {
|
||||
ClaudeAgentConnection::handle_message(
|
||||
@@ -167,27 +178,46 @@ impl AgentServer for ClaudeCode {
|
||||
});
|
||||
|
||||
let mut connection = ClaudeAgentConnection {
|
||||
delegate,
|
||||
outgoing_tx,
|
||||
end_turn_tx,
|
||||
cancel_tx,
|
||||
session_id,
|
||||
_handler_task: handler_task,
|
||||
_mcp_server: None,
|
||||
};
|
||||
|
||||
connection._mcp_server = Some(permission_mcp_server);
|
||||
connection._mcp_server = Some(mcp_server);
|
||||
acp_thread::AcpThread::new(connection, title, None, project.clone(), cx)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn send_interrupt(pid: libc::pid_t) -> anyhow::Result<()> {
|
||||
let pid = nix::unistd::Pid::from_raw(pid);
|
||||
|
||||
nix::sys::signal::kill(pid, nix::sys::signal::SIGINT)
|
||||
.map_err(|e| anyhow!("Failed to interrupt process: {}", e))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn send_interrupt(_pid: i32) -> anyhow::Result<()> {
|
||||
panic!("Cancel not implemented on Windows")
|
||||
}
|
||||
|
||||
impl AgentConnection for ClaudeAgentConnection {
|
||||
/// Send a request to the agent and wait for a response.
|
||||
fn request_any(
|
||||
&self,
|
||||
params: AnyAgentRequest,
|
||||
) -> LocalBoxFuture<'static, Result<acp::AnyAgentResult>> {
|
||||
let delegate = self.delegate.clone();
|
||||
let end_turn_tx = self.end_turn_tx.clone();
|
||||
let outgoing_tx = self.outgoing_tx.clone();
|
||||
let mut cancel_tx = self.cancel_tx.clone();
|
||||
let session_id = self.session_id;
|
||||
async move {
|
||||
match params {
|
||||
// todo: consider sending an empty request so we get the init response?
|
||||
@@ -201,6 +231,8 @@ impl AgentConnection for ClaudeAgentConnection {
|
||||
Err(anyhow!("Authentication not supported"))
|
||||
}
|
||||
AnyAgentRequest::SendUserMessageParams(message) => {
|
||||
delegate.clear_completed_plan_entries().await?;
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
end_turn_tx.borrow_mut().replace(tx);
|
||||
let mut content = String::new();
|
||||
@@ -224,25 +256,83 @@ impl AgentConnection for ClaudeAgentConnection {
|
||||
stop_sequence: None,
|
||||
usage: None,
|
||||
},
|
||||
session_id: None,
|
||||
session_id: Some(session_id),
|
||||
})?;
|
||||
rx.await??;
|
||||
Ok(AnyAgentResult::SendUserMessageResponse(
|
||||
acp::SendUserMessageResponse,
|
||||
))
|
||||
}
|
||||
AnyAgentRequest::CancelSendMessageParams(_) => Ok(
|
||||
AnyAgentResult::CancelSendMessageResponse(acp::CancelSendMessageResponse),
|
||||
),
|
||||
AnyAgentRequest::CancelSendMessageParams(_) => {
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
cancel_tx.send(done_tx).await?;
|
||||
done_rx.await??;
|
||||
|
||||
Ok(AnyAgentResult::CancelSendMessageResponse(
|
||||
acp::CancelSendMessageResponse,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum ClaudeSessionMode {
|
||||
Start,
|
||||
Resume,
|
||||
}
|
||||
|
||||
async fn spawn_claude(
|
||||
command: &AgentServerCommand,
|
||||
mode: ClaudeSessionMode,
|
||||
session_id: Uuid,
|
||||
mcp_config_path: &Path,
|
||||
root_dir: &Path,
|
||||
) -> Result<Child> {
|
||||
let child = util::command::new_smol_command(&command.path)
|
||||
.args([
|
||||
"--input-format",
|
||||
"stream-json",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"--print",
|
||||
"--verbose",
|
||||
"--mcp-config",
|
||||
mcp_config_path.to_string_lossy().as_ref(),
|
||||
"--permission-prompt-tool",
|
||||
&format!(
|
||||
"mcp__{}__{}",
|
||||
mcp_server::SERVER_NAME,
|
||||
mcp_server::PERMISSION_TOOL
|
||||
),
|
||||
"--allowedTools",
|
||||
"mcp__zed__Read,mcp__zed__Edit",
|
||||
"--disallowedTools",
|
||||
"Read,Edit",
|
||||
])
|
||||
.args(match mode {
|
||||
ClaudeSessionMode::Start => ["--session-id".to_string(), session_id.to_string()],
|
||||
ClaudeSessionMode::Resume => ["--resume".to_string(), session_id.to_string()],
|
||||
})
|
||||
.args(command.args.iter().map(|arg| arg.as_str()))
|
||||
.current_dir(root_dir)
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::inherit())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
Ok(child)
|
||||
}
|
||||
|
||||
struct ClaudeAgentConnection {
|
||||
delegate: AcpClientDelegate,
|
||||
session_id: Uuid,
|
||||
outgoing_tx: UnboundedSender<SdkMessage>,
|
||||
end_turn_tx: Rc<RefCell<Option<oneshot::Sender<Result<()>>>>>,
|
||||
cancel_tx: UnboundedSender<oneshot::Sender<Result<()>>>,
|
||||
_mcp_server: Option<ClaudeMcpServer>,
|
||||
_handler_task: Task<()>,
|
||||
}
|
||||
@@ -267,8 +357,17 @@ impl ClaudeAgentConnection {
|
||||
.log_err();
|
||||
}
|
||||
ContentChunk::ToolUse { id, name, input } => {
|
||||
if let Some(resp) = delegate
|
||||
.push_tool_call(ClaudeTool::infer(&name, input).as_acp())
|
||||
let claude_tool = ClaudeTool::infer(&name, input);
|
||||
|
||||
if let ClaudeTool::TodoWrite(Some(params)) = claude_tool {
|
||||
delegate
|
||||
.update_plan(acp::UpdatePlanParams {
|
||||
entries: params.todos.into_iter().map(Into::into).collect(),
|
||||
})
|
||||
.await
|
||||
.log_err();
|
||||
} else if let Some(resp) = delegate
|
||||
.push_tool_call(claude_tool.as_acp())
|
||||
.await
|
||||
.log_err()
|
||||
{
|
||||
@@ -335,7 +434,7 @@ impl ClaudeAgentConnection {
|
||||
incoming_tx: UnboundedSender<SdkMessage>,
|
||||
mut outgoing_bytes: impl Unpin + AsyncWrite,
|
||||
incoming_bytes: impl Unpin + AsyncRead,
|
||||
) -> Result<()> {
|
||||
) -> Result<UnboundedReceiver<SdkMessage>> {
|
||||
let mut output_reader = BufReader::new(incoming_bytes);
|
||||
let mut outgoing_line = Vec::new();
|
||||
let mut incoming_line = String::new();
|
||||
@@ -369,7 +468,8 @@ impl ClaudeAgentConnection {
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
Ok(outgoing_rx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,14 +592,14 @@ enum SdkMessage {
|
||||
Assistant {
|
||||
message: Message, // from Anthropic SDK
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
session_id: Option<String>,
|
||||
session_id: Option<Uuid>,
|
||||
},
|
||||
|
||||
// A user message
|
||||
User {
|
||||
message: Message, // from Anthropic SDK
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
session_id: Option<String>,
|
||||
session_id: Option<Uuid>,
|
||||
},
|
||||
|
||||
// Emitted as the last message in a conversation
|
||||
|
||||
@@ -69,9 +69,6 @@ impl ClaudeMcpServer {
|
||||
}
|
||||
|
||||
pub fn server_config(&self) -> Result<McpServerConfig> {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let zed_path = util::get_shell_safe_zed_path()?;
|
||||
#[cfg(target_os = "windows")]
|
||||
let zed_path = std::env::current_exe()
|
||||
.context("finding current executable path for use in mcp_server")?
|
||||
.to_string_lossy()
|
||||
|
||||
@@ -614,6 +614,16 @@ pub enum TodoPriority {
|
||||
Low,
|
||||
}
|
||||
|
||||
impl Into<acp::PlanEntryPriority> for TodoPriority {
|
||||
fn into(self) -> acp::PlanEntryPriority {
|
||||
match self {
|
||||
TodoPriority::High => acp::PlanEntryPriority::High,
|
||||
TodoPriority::Medium => acp::PlanEntryPriority::Medium,
|
||||
TodoPriority::Low => acp::PlanEntryPriority::Low,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TodoStatus {
|
||||
@@ -622,6 +632,16 @@ pub enum TodoStatus {
|
||||
Completed,
|
||||
}
|
||||
|
||||
impl Into<acp::PlanEntryStatus> for TodoStatus {
|
||||
fn into(self) -> acp::PlanEntryStatus {
|
||||
match self {
|
||||
TodoStatus::Pending => acp::PlanEntryStatus::Pending,
|
||||
TodoStatus::InProgress => acp::PlanEntryStatus::InProgress,
|
||||
TodoStatus::Completed => acp::PlanEntryStatus::Completed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
|
||||
pub struct Todo {
|
||||
/// Unique identifier
|
||||
@@ -634,6 +654,16 @@ pub struct Todo {
|
||||
pub status: TodoStatus,
|
||||
}
|
||||
|
||||
impl Into<acp::PlanEntry> for Todo {
|
||||
fn into(self) -> acp::PlanEntry {
|
||||
acp::PlanEntry {
|
||||
content: self.content,
|
||||
priority: self.priority.into(),
|
||||
status: self.status.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema, Debug)]
|
||||
pub struct TodoWriteToolParams {
|
||||
pub todos: Vec<Todo>,
|
||||
|
||||
@@ -29,9 +29,12 @@ impl settings::Settings for AllAgentServersSettings {
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
let mut settings = AllAgentServersSettings::default();
|
||||
|
||||
for value in sources.defaults_and_customizations() {
|
||||
if value.gemini.is_some() {
|
||||
settings.gemini = value.gemini.clone();
|
||||
for AllAgentServersSettings { gemini, claude } in sources.defaults_and_customizations() {
|
||||
if gemini.is_some() {
|
||||
settings.gemini = gemini.clone();
|
||||
}
|
||||
if claude.is_some() {
|
||||
settings.claude = claude.clone();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use acp_thread::Plan;
|
||||
use agent_servers::AgentServer;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
@@ -45,7 +46,8 @@ use ::acp_thread::{
|
||||
use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
|
||||
use crate::acp::message_history::MessageHistory;
|
||||
use crate::agent_diff::AgentDiff;
|
||||
use crate::{AgentDiffPane, Follow, KeepAll, OpenAgentDiff, RejectAll};
|
||||
use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
|
||||
use crate::{AgentDiffPane, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll};
|
||||
|
||||
const RESPONSE_PADDING_X: Pixels = px(19.);
|
||||
|
||||
@@ -65,6 +67,8 @@ pub struct AcpThreadView {
|
||||
expanded_tool_calls: HashSet<ToolCallId>,
|
||||
expanded_thinking_blocks: HashSet<(usize, usize)>,
|
||||
edits_expanded: bool,
|
||||
plan_expanded: bool,
|
||||
editor_expanded: bool,
|
||||
message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
|
||||
}
|
||||
|
||||
@@ -94,6 +98,8 @@ impl AcpThreadView {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
|
||||
min_lines: usize,
|
||||
max_lines: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -113,8 +119,8 @@ impl AcpThreadView {
|
||||
|
||||
let mut editor = Editor::new(
|
||||
editor::EditorMode::AutoHeight {
|
||||
min_lines: 4,
|
||||
max_lines: None,
|
||||
min_lines,
|
||||
max_lines: max_lines,
|
||||
},
|
||||
buffer,
|
||||
None,
|
||||
@@ -182,6 +188,8 @@ impl AcpThreadView {
|
||||
expanded_tool_calls: HashSet::default(),
|
||||
expanded_thinking_blocks: HashSet::default(),
|
||||
edits_expanded: false,
|
||||
plan_expanded: false,
|
||||
editor_expanded: false,
|
||||
message_history,
|
||||
}
|
||||
}
|
||||
@@ -321,6 +329,35 @@ impl AcpThreadView {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand_message_editor(
|
||||
&mut self,
|
||||
_: &ExpandMessageEditor,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.set_editor_is_expanded(!self.editor_expanded, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
|
||||
self.editor_expanded = is_expanded;
|
||||
self.message_editor.update(cx, |editor, _| {
|
||||
if self.editor_expanded {
|
||||
editor.set_mode(EditorMode::Full {
|
||||
scale_ui_elements_with_buffer_font_size: false,
|
||||
show_active_line_background: false,
|
||||
sized_by_content: false,
|
||||
})
|
||||
} else {
|
||||
editor.set_mode(EditorMode::AutoHeight {
|
||||
min_lines: MIN_EDITOR_LINES,
|
||||
max_lines: Some(MAX_EDITOR_LINES),
|
||||
})
|
||||
}
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.last_error.take();
|
||||
|
||||
@@ -381,6 +418,7 @@ impl AcpThreadView {
|
||||
|
||||
let mention_set = self.mention_set.clone();
|
||||
|
||||
self.set_editor_is_expanded(false, cx);
|
||||
self.message_editor.update(cx, |editor, cx| {
|
||||
editor.clear(window, cx);
|
||||
editor.remove_creases(mention_set.lock().drain(), cx)
|
||||
@@ -1442,7 +1480,7 @@ impl AcpThreadView {
|
||||
container.into_any()
|
||||
}
|
||||
|
||||
fn render_edits_bar(
|
||||
fn render_activity_bar(
|
||||
&self,
|
||||
thread_entity: &Entity<AcpThread>,
|
||||
window: &mut Window,
|
||||
@@ -1451,8 +1489,9 @@ impl AcpThreadView {
|
||||
let thread = thread_entity.read(cx);
|
||||
let action_log = thread.action_log();
|
||||
let changed_buffers = action_log.read(cx).changed_buffers(cx);
|
||||
let plan = thread.plan();
|
||||
|
||||
if changed_buffers.is_empty() {
|
||||
if changed_buffers.is_empty() && plan.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -1461,7 +1500,6 @@ impl AcpThreadView {
|
||||
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
|
||||
|
||||
let pending_edits = thread.has_pending_edit_tool_calls();
|
||||
let expanded = self.edits_expanded;
|
||||
|
||||
v_flex()
|
||||
.mt_1()
|
||||
@@ -1477,27 +1515,165 @@ impl AcpThreadView {
|
||||
blur_radius: px(3.),
|
||||
spread_radius: px(0.),
|
||||
}])
|
||||
.child(self.render_edits_bar_summary(
|
||||
action_log,
|
||||
&changed_buffers,
|
||||
expanded,
|
||||
pending_edits,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.when(expanded, |parent| {
|
||||
parent.child(self.render_edits_bar_files(
|
||||
action_log,
|
||||
&changed_buffers,
|
||||
pending_edits,
|
||||
cx,
|
||||
))
|
||||
.when(!plan.is_empty(), |this| {
|
||||
this.child(self.render_plan_summary(plan, window, cx))
|
||||
.when(self.plan_expanded, |parent| {
|
||||
parent.child(self.render_plan_entries(plan, window, cx))
|
||||
})
|
||||
})
|
||||
.when(!changed_buffers.is_empty(), |this| {
|
||||
this.child(Divider::horizontal())
|
||||
.child(self.render_edits_summary(
|
||||
action_log,
|
||||
&changed_buffers,
|
||||
self.edits_expanded,
|
||||
pending_edits,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.when(self.edits_expanded, |parent| {
|
||||
parent.child(self.render_edited_files(
|
||||
action_log,
|
||||
&changed_buffers,
|
||||
pending_edits,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
})
|
||||
.into_any()
|
||||
.into()
|
||||
}
|
||||
|
||||
fn render_edits_bar_summary(
|
||||
fn render_plan_summary(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
|
||||
let stats = plan.stats();
|
||||
|
||||
let title = if let Some(entry) = stats.in_progress_entry
|
||||
&& !self.plan_expanded
|
||||
{
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new("Current:")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(MarkdownElement::new(
|
||||
entry.content.clone(),
|
||||
plan_label_markdown_style(&entry.status, window, cx),
|
||||
)),
|
||||
)
|
||||
.when(stats.pending > 0, |this| {
|
||||
this.child(
|
||||
Label::new(format!("{} left", stats.pending))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.mr_1(),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
let status_label = if stats.pending == 0 {
|
||||
"All Done".to_string()
|
||||
} else if stats.completed == 0 {
|
||||
format!("{}", plan.entries.len())
|
||||
} else {
|
||||
format!("{}/{}", stats.completed, plan.entries.len())
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.justify_between()
|
||||
.child(
|
||||
Label::new("Plan")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new(status_label)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.mr_1(),
|
||||
)
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.p_1()
|
||||
.justify_between()
|
||||
.when(self.plan_expanded, |this| {
|
||||
this.border_b_1().border_color(cx.theme().colors().border)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.id("plan_summary")
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(Disclosure::new("plan_disclosure", self.plan_expanded))
|
||||
.child(title)
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.plan_expanded = !this.plan_expanded;
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_plan_entries(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
|
||||
v_flex().children(plan.entries.iter().enumerate().flat_map(|(index, entry)| {
|
||||
let element = h_flex()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.when(index < plan.entries.len() - 1, |parent| {
|
||||
parent.border_color(cx.theme().colors().border).border_b_1()
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.id(("plan_entry", index))
|
||||
.gap_1p5()
|
||||
.max_w_full()
|
||||
.overflow_x_scroll()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(match entry.status {
|
||||
acp::PlanEntryStatus::Pending => Icon::new(IconName::TodoPending)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
acp::PlanEntryStatus::InProgress => Icon::new(IconName::TodoProgress)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Accent)
|
||||
.with_animation(
|
||||
"running",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(percentage(delta)))
|
||||
},
|
||||
)
|
||||
.into_any_element(),
|
||||
acp::PlanEntryStatus::Completed => Icon::new(IconName::TodoComplete)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Success)
|
||||
.into_any_element(),
|
||||
})
|
||||
.child(MarkdownElement::new(
|
||||
entry.content.clone(),
|
||||
plan_label_markdown_style(&entry.status, window, cx),
|
||||
)),
|
||||
);
|
||||
|
||||
Some(element)
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_edits_summary(
|
||||
&self,
|
||||
action_log: &Entity<ActionLog>,
|
||||
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
|
||||
@@ -1643,7 +1819,7 @@ impl AcpThreadView {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_edits_bar_files(
|
||||
fn render_edited_files(
|
||||
&self,
|
||||
action_log: &Entity<ActionLog>,
|
||||
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
|
||||
@@ -1793,34 +1969,96 @@ impl AcpThreadView {
|
||||
))
|
||||
}
|
||||
|
||||
fn render_message_editor(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let font_size = TextSize::Small
|
||||
.rems(cx)
|
||||
.to_pixels(settings.agent_font_size(cx));
|
||||
let line_height = settings.buffer_line_height.value() * font_size;
|
||||
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_fallbacks: settings.buffer_font.fallbacks.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: font_size.into(),
|
||||
line_height: line_height.into(),
|
||||
..Default::default()
|
||||
fn render_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
let focus_handle = self.message_editor.focus_handle(cx);
|
||||
let editor_bg_color = cx.theme().colors().editor_background;
|
||||
let (expand_icon, expand_tooltip) = if self.editor_expanded {
|
||||
(IconName::Minimize, "Minimize Message Editor")
|
||||
} else {
|
||||
(IconName::Maximize, "Expand Message Editor")
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
&self.message_editor,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.into_any()
|
||||
v_flex()
|
||||
.on_action(cx.listener(Self::expand_message_editor))
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(editor_bg_color)
|
||||
.when(self.editor_expanded, |this| {
|
||||
this.h(vh(0.8, window)).size_full().justify_between()
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.relative()
|
||||
.size_full()
|
||||
.pt_1()
|
||||
.pr_2p5()
|
||||
.child(div().flex_1().child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let font_size = TextSize::Small
|
||||
.rems(cx)
|
||||
.to_pixels(settings.agent_font_size(cx));
|
||||
let line_height = settings.buffer_line_height.value() * font_size;
|
||||
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_fallbacks: settings.buffer_font.fallbacks.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: font_size.into(),
|
||||
line_height: line_height.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
&self.message_editor,
|
||||
EditorStyle {
|
||||
background: editor_bg_color,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.right_0()
|
||||
.opacity(0.5)
|
||||
.hover(|this| this.opacity(1.0))
|
||||
.child(
|
||||
IconButton::new("toggle-height", expand_icon)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
expand_tooltip,
|
||||
&ExpandMessageEditor,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(Box::new(ExpandMessageEditor), cx);
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.justify_between()
|
||||
.child(self.render_follow_toggle(cx))
|
||||
.child(self.render_send_button(cx)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
@@ -2132,7 +2370,6 @@ impl Render for AcpThreadView {
|
||||
.px(RESPONSE_PADDING_X)
|
||||
.opacity(0.4)
|
||||
.hover(|style| style.opacity(1.))
|
||||
.gap_1()
|
||||
.flex_wrap()
|
||||
.justify_end()
|
||||
.child(open_as_markdown)
|
||||
@@ -2147,7 +2384,7 @@ impl Render for AcpThreadView {
|
||||
.child(LoadingLabel::new("").size(LabelSize::Small))
|
||||
.into(),
|
||||
})
|
||||
.children(self.render_edits_bar(&thread, window, cx))
|
||||
.children(self.render_activity_bar(&thread, window, cx))
|
||||
} else {
|
||||
this.child(self.render_empty_state(cx))
|
||||
}
|
||||
@@ -2166,22 +2403,7 @@ impl Render for AcpThreadView {
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.p_2()
|
||||
.pt_3()
|
||||
.gap_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(self.render_message_editor(cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(self.render_follow_toggle(cx))
|
||||
.child(self.render_send_button(cx)),
|
||||
),
|
||||
)
|
||||
.child(self.render_message_editor(window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2328,3 +2550,27 @@ fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> Markd
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_label_markdown_style(
|
||||
status: &acp::PlanEntryStatus,
|
||||
window: &Window,
|
||||
cx: &App,
|
||||
) -> MarkdownStyle {
|
||||
let default_md_style = default_markdown_style(false, window, cx);
|
||||
|
||||
MarkdownStyle {
|
||||
base_text_style: TextStyle {
|
||||
color: cx.theme().colors().text_muted,
|
||||
strikethrough: if matches!(status, acp::PlanEntryStatus::Completed) {
|
||||
Some(gpui::StrikethroughStyle {
|
||||
thickness: px(1.),
|
||||
color: Some(cx.theme().colors().text_muted.opacity(0.8)),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
},
|
||||
..default_md_style.base_text_style
|
||||
},
|
||||
..default_md_style
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3724,8 +3724,11 @@ pub(crate) fn open_context(
|
||||
|
||||
AgentContextHandle::Thread(thread_context) => workspace.update(cx, |workspace, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.open_thread(thread_context.thread.clone(), window, cx);
|
||||
let thread = thread_context.thread.clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.open_thread(thread, window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
}),
|
||||
@@ -3733,8 +3736,11 @@ pub(crate) fn open_context(
|
||||
AgentContextHandle::TextThread(text_thread_context) => {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.open_prompt_editor(text_thread_context.context.clone(), window, cx)
|
||||
let context = text_thread_context.context.clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.open_prompt_editor(context, window, cx)
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
@@ -188,7 +189,7 @@ fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)
|
||||
}
|
||||
None => (
|
||||
"some-mcp-server".to_string(),
|
||||
"".to_string(),
|
||||
PathBuf::new(),
|
||||
"[]".to_string(),
|
||||
"{}".to_string(),
|
||||
),
|
||||
@@ -199,13 +200,14 @@ fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)
|
||||
/// The name of your MCP server
|
||||
"{name}": {{
|
||||
/// The command which runs the MCP server
|
||||
"command": "{command}",
|
||||
"command": "{}",
|
||||
/// The arguments to pass to the MCP server
|
||||
"args": {args},
|
||||
/// The environment variables to set
|
||||
"env": {env}
|
||||
}}
|
||||
}}"#
|
||||
}}"#,
|
||||
command.display()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::NewExternalAgentThread;
|
||||
use crate::agent_diff::AgentDiffThread;
|
||||
use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
|
||||
use crate::ui::NewThreadButton;
|
||||
use crate::{
|
||||
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
|
||||
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
|
||||
@@ -65,8 +67,8 @@ use theme::ThemeSettings;
|
||||
use time::UtcOffset;
|
||||
use ui::utils::WithRemSize;
|
||||
use ui::{
|
||||
Banner, Callout, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu, PopoverMenuHandle,
|
||||
ProgressBar, Tab, Tooltip, prelude::*,
|
||||
Banner, Callout, ContextMenu, ContextMenuEntry, ElevationIndex, KeyBinding, PopoverMenu,
|
||||
PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{
|
||||
@@ -960,6 +962,8 @@ impl AgentPanel {
|
||||
workspace.clone(),
|
||||
project,
|
||||
message_history,
|
||||
MIN_EDITOR_LINES,
|
||||
Some(MAX_EDITOR_LINES),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -1903,16 +1907,39 @@ impl AgentPanel {
|
||||
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
|
||||
this.header("Zed Agent")
|
||||
})
|
||||
.action("New Thread", NewThread::default().boxed_clone())
|
||||
.action("New Text Thread", NewTextThread.boxed_clone())
|
||||
.item(
|
||||
ContextMenuEntry::new("New Thread")
|
||||
.icon(IconName::NewThread)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(NewThread::default().boxed_clone(), cx);
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("New Text Thread")
|
||||
.icon(IconName::NewTextThread)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(NewTextThread.boxed_clone(), cx);
|
||||
}),
|
||||
)
|
||||
.when_some(active_thread, |this, active_thread| {
|
||||
let thread = active_thread.read(cx);
|
||||
|
||||
if !thread.is_empty() {
|
||||
this.action(
|
||||
"New From Summary",
|
||||
Box::new(NewThread {
|
||||
from_thread_id: Some(thread.id().clone()),
|
||||
}),
|
||||
let thread_id = thread.id().clone();
|
||||
this.item(
|
||||
ContextMenuEntry::new("New From Summary")
|
||||
.icon(IconName::NewFromSummary)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(NewThread {
|
||||
from_thread_id: Some(thread_id.clone()),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
@@ -1921,19 +1948,33 @@ impl AgentPanel {
|
||||
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
|
||||
this.separator()
|
||||
.header("External Agents")
|
||||
.action(
|
||||
"New Gemini Thread",
|
||||
NewExternalAgentThread {
|
||||
agent: Some(crate::ExternalAgent::Gemini),
|
||||
}
|
||||
.boxed_clone(),
|
||||
.item(
|
||||
ContextMenuEntry::new("New Gemini Thread")
|
||||
.icon(IconName::AiGemini)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(
|
||||
NewExternalAgentThread {
|
||||
agent: Some(crate::ExternalAgent::Gemini),
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
.action(
|
||||
"New Claude Code Thread",
|
||||
NewExternalAgentThread {
|
||||
agent: Some(crate::ExternalAgent::ClaudeCode),
|
||||
}
|
||||
.boxed_clone(),
|
||||
.item(
|
||||
ContextMenuEntry::new("New Claude Code Thread")
|
||||
.icon(IconName::AiClaude)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(
|
||||
NewExternalAgentThread {
|
||||
agent: Some(crate::ExternalAgent::ClaudeCode),
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
});
|
||||
menu
|
||||
@@ -2259,7 +2300,20 @@ impl AgentPanel {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(div().size_full().child(self.onboarding.clone()))
|
||||
let thread_view = matches!(&self.active_view, ActiveView::Thread { .. });
|
||||
let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. });
|
||||
|
||||
Some(
|
||||
div()
|
||||
.size_full()
|
||||
.when(thread_view, |this| {
|
||||
this.bg(cx.theme().colors().panel_background)
|
||||
})
|
||||
.when(text_thread_view, |this| {
|
||||
this.bg(cx.theme().colors().editor_background)
|
||||
})
|
||||
.child(self.onboarding.clone()),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_trial_end_upsell(
|
||||
@@ -2282,6 +2336,28 @@ impl AgentPanel {
|
||||
})))
|
||||
}
|
||||
|
||||
fn render_empty_state_section_header(
|
||||
&self,
|
||||
label: impl Into<SharedString>,
|
||||
action_slot: Option<AnyElement>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
h_flex()
|
||||
.mt_2()
|
||||
.pl_1p5()
|
||||
.pb_1()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
Label::new(label.into())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.children(action_slot)
|
||||
}
|
||||
|
||||
fn render_thread_empty_state(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
@@ -2404,19 +2480,9 @@ impl AgentPanel {
|
||||
.justify_end()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.pl_1p5()
|
||||
.pb_1()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
Label::new("Recent")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
self.render_empty_state_section_header(
|
||||
"Recent",
|
||||
Some(
|
||||
Button::new("view-history", "View All")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.label_size(LabelSize::Small)
|
||||
@@ -2431,8 +2497,11 @@ impl AgentPanel {
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
window.dispatch_action(OpenHistory.boxed_clone(), cx);
|
||||
}),
|
||||
})
|
||||
.into_any_element(),
|
||||
),
|
||||
cx,
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
@@ -2460,6 +2529,113 @@ impl AgentPanel {
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(self.render_empty_state_section_header("Start", None, cx))
|
||||
.child(
|
||||
v_flex()
|
||||
.p_1()
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child(
|
||||
NewThreadButton::new(
|
||||
"new-thread-btn",
|
||||
"New Thread",
|
||||
IconName::NewThread,
|
||||
)
|
||||
.keybinding(KeyBinding::for_action_in(
|
||||
&NewThread::default(),
|
||||
&self.focus_handle(cx),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.on_click(
|
||||
|window, cx| {
|
||||
window.dispatch_action(
|
||||
NewThread::default().boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
NewThreadButton::new(
|
||||
"new-text-thread-btn",
|
||||
"New Text Thread",
|
||||
IconName::NewTextThread,
|
||||
)
|
||||
.keybinding(KeyBinding::for_action_in(
|
||||
&NewTextThread,
|
||||
&self.focus_handle(cx),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.on_click(
|
||||
|window, cx| {
|
||||
window.dispatch_action(Box::new(NewTextThread), cx)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child(
|
||||
NewThreadButton::new(
|
||||
"new-gemini-thread-btn",
|
||||
"New Gemini Thread",
|
||||
IconName::AiGemini,
|
||||
)
|
||||
// .keybinding(KeyBinding::for_action_in(
|
||||
// &OpenHistory,
|
||||
// &self.focus_handle(cx),
|
||||
// window,
|
||||
// cx,
|
||||
// ))
|
||||
.on_click(
|
||||
|window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(NewExternalAgentThread {
|
||||
agent: Some(
|
||||
crate::ExternalAgent::Gemini,
|
||||
),
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
NewThreadButton::new(
|
||||
"new-claude-thread-btn",
|
||||
"New Claude Code Thread",
|
||||
IconName::AiClaude,
|
||||
)
|
||||
// .keybinding(KeyBinding::for_action_in(
|
||||
// &OpenHistory,
|
||||
// &self.focus_handle(cx),
|
||||
// window,
|
||||
// cx,
|
||||
// ))
|
||||
.on_click(
|
||||
|window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(NewExternalAgentThread {
|
||||
agent: Some(
|
||||
crate::ExternalAgent::ClaudeCode,
|
||||
),
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.when_some(configuration_error.as_ref(), |this, err| {
|
||||
this.child(self.render_configuration_error(err, &focus_handle, window, cx))
|
||||
})
|
||||
@@ -3074,7 +3250,20 @@ impl Render for AgentPanel {
|
||||
.into_any(),
|
||||
)
|
||||
})
|
||||
.child(h_flex().child(message_editor.clone()))
|
||||
.child(h_flex().relative().child(message_editor.clone()).when(
|
||||
!LanguageModelRegistry::read_global(cx).has_authenticated_provider(cx),
|
||||
|this| {
|
||||
this.child(
|
||||
div()
|
||||
.size_full()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.opacity(0.8)
|
||||
.block_mouse_except_scroll(),
|
||||
)
|
||||
},
|
||||
))
|
||||
.child(self.render_drag_target(cx)),
|
||||
ActiveView::ExternalAgentThread { thread_view, .. } => parent
|
||||
.relative()
|
||||
|
||||
@@ -197,6 +197,11 @@ impl ModelUsageContext {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_settings(cx: &mut App) {
|
||||
AgentSettings::register(cx);
|
||||
SlashCommandSettings::register(cx);
|
||||
}
|
||||
|
||||
/// Initializes the `agent` crate.
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -206,8 +211,7 @@ pub fn init(
|
||||
is_eval: bool,
|
||||
cx: &mut App,
|
||||
) {
|
||||
AgentSettings::register(cx);
|
||||
SlashCommandSettings::register(cx);
|
||||
init_settings(cx);
|
||||
|
||||
assistant_context::init(client.clone(), cx);
|
||||
rules_library::init(cx);
|
||||
|
||||
@@ -14,6 +14,7 @@ use agent::{
|
||||
context_store::ContextStoreEvent,
|
||||
};
|
||||
use agent_settings::{AgentSettings, CompletionMode};
|
||||
use ai_onboarding::ApiKeysWithProviders;
|
||||
use buffer_diff::BufferDiff;
|
||||
use client::UserStore;
|
||||
use collections::{HashMap, HashSet};
|
||||
@@ -33,7 +34,8 @@ use gpui::{
|
||||
};
|
||||
use language::{Buffer, Language, Point};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModelRequestMessage, MessageContent, ZED_CLOUD_PROVIDER_ID,
|
||||
ConfiguredModel, LanguageModelRegistry, LanguageModelRequestMessage, MessageContent,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use multi_buffer;
|
||||
use project::Project;
|
||||
@@ -65,6 +67,9 @@ use agent::{
|
||||
thread_store::{TextThreadStore, ThreadStore},
|
||||
};
|
||||
|
||||
pub const MIN_EDITOR_LINES: usize = 4;
|
||||
pub const MAX_EDITOR_LINES: usize = 8;
|
||||
|
||||
#[derive(RegisterComponent)]
|
||||
pub struct MessageEditor {
|
||||
thread: Entity<Thread>,
|
||||
@@ -88,9 +93,6 @@ pub struct MessageEditor {
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
const MIN_EDITOR_LINES: usize = 4;
|
||||
const MAX_EDITOR_LINES: usize = 8;
|
||||
|
||||
pub(crate) fn create_editor(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
@@ -711,11 +713,11 @@ impl MessageEditor {
|
||||
cx.listener(|this, _: &RejectAll, window, cx| this.handle_reject_all(window, cx)),
|
||||
)
|
||||
.capture_action(cx.listener(Self::paste))
|
||||
.gap_2()
|
||||
.p_2()
|
||||
.bg(editor_bg_color)
|
||||
.gap_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(editor_bg_color)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
@@ -1655,9 +1657,34 @@ impl Render for MessageEditor {
|
||||
|
||||
let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5;
|
||||
|
||||
let in_pro_trial = matches!(
|
||||
self.user_store.read(cx).current_plan(),
|
||||
Some(proto::Plan::ZedProTrial)
|
||||
);
|
||||
|
||||
let pro_user = matches!(
|
||||
self.user_store.read(cx).current_plan(),
|
||||
Some(proto::Plan::ZedPro)
|
||||
);
|
||||
|
||||
let configured_providers: Vec<(IconName, SharedString)> =
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.providers()
|
||||
.iter()
|
||||
.filter(|provider| {
|
||||
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
|
||||
})
|
||||
.map(|provider| (provider.icon(), provider.name().0.clone()))
|
||||
.collect();
|
||||
let has_existing_providers = configured_providers.len() > 0;
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.when(
|
||||
has_existing_providers && !in_pro_trial && !pro_user,
|
||||
|this| this.child(cx.new(ApiKeysWithProviders::new)),
|
||||
)
|
||||
.when(changed_buffers.len() > 0, |parent| {
|
||||
parent.child(self.render_edits_bar(&changed_buffers, window, cx))
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ mod agent_notification;
|
||||
mod burn_mode_tooltip;
|
||||
mod context_pill;
|
||||
mod end_trial_upsell;
|
||||
mod new_thread_button;
|
||||
mod onboarding_modal;
|
||||
pub mod preview;
|
||||
mod upsell;
|
||||
@@ -10,4 +11,5 @@ pub use agent_notification::*;
|
||||
pub use burn_mode_tooltip::*;
|
||||
pub use context_pill::*;
|
||||
pub use end_trial_upsell::*;
|
||||
pub use new_thread_button::*;
|
||||
pub use onboarding_modal::*;
|
||||
|
||||
75
crates/agent_ui/src/ui/new_thread_button.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use gpui::{ClickEvent, ElementId, IntoElement, ParentElement, Styled};
|
||||
use ui::prelude::*;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct NewThreadButton {
|
||||
id: ElementId,
|
||||
label: SharedString,
|
||||
icon: IconName,
|
||||
keybinding: Option<ui::KeyBinding>,
|
||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
}
|
||||
|
||||
impl NewThreadButton {
|
||||
pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>, icon: IconName) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
label: label.into(),
|
||||
icon,
|
||||
keybinding: None,
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn keybinding(mut self, keybinding: Option<ui::KeyBinding>) -> Self {
|
||||
self.keybinding = keybinding;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click<F>(mut self, handler: F) -> Self
|
||||
where
|
||||
F: Fn(&mut Window, &mut App) + 'static,
|
||||
{
|
||||
self.on_click = Some(Box::new(
|
||||
move |_: &ClickEvent, window: &mut Window, cx: &mut App| handler(window, cx),
|
||||
));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for NewThreadButton {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
.w_full()
|
||||
.py_1p5()
|
||||
.px_2()
|
||||
.gap_1()
|
||||
.justify_between()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.4))
|
||||
.bg(cx.theme().colors().element_active.opacity(0.2))
|
||||
.hover(|style| {
|
||||
style
|
||||
.bg(cx.theme().colors().element_hover)
|
||||
.border_color(cx.theme().colors().border)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(self.icon)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new(self.label).size(LabelSize::Small)),
|
||||
)
|
||||
.when_some(self.keybinding, |this, keybinding| {
|
||||
this.child(keybinding.size(rems_from_px(10.)))
|
||||
})
|
||||
.when_some(self.on_click, |this, on_click| {
|
||||
this.on_click(move |event, window, cx| on_click(event, window, cx))
|
||||
})
|
||||
}
|
||||
}
|
||||
135
crates/ai_onboarding/src/agent_api_keys_onboarding.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
use gpui::{Action, IntoElement, ParentElement, RenderOnce, point};
|
||||
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
|
||||
use ui::{Divider, List, prelude::*};
|
||||
|
||||
use crate::BulletItem;
|
||||
|
||||
pub struct ApiKeysWithProviders {
|
||||
configured_providers: Vec<(IconName, SharedString)>,
|
||||
}
|
||||
|
||||
impl ApiKeysWithProviders {
|
||||
pub fn new(cx: &mut Context<Self>) -> Self {
|
||||
cx.subscribe(
|
||||
&LanguageModelRegistry::global(cx),
|
||||
|this: &mut Self, _registry, event: &language_model::Event, cx| match event {
|
||||
language_model::Event::ProviderStateChanged
|
||||
| language_model::Event::AddedProvider(_)
|
||||
| language_model::Event::RemovedProvider(_) => {
|
||||
this.configured_providers = Self::compute_configured_providers(cx)
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
configured_providers: Self::compute_configured_providers(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_configured_providers(cx: &App) -> Vec<(IconName, SharedString)> {
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.providers()
|
||||
.iter()
|
||||
.filter(|provider| {
|
||||
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
|
||||
})
|
||||
.map(|provider| (provider.icon(), provider.name().0.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn has_providers(&self) -> bool {
|
||||
!self.configured_providers.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ApiKeysWithProviders {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let configured_providers_list =
|
||||
self.configured_providers
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|(icon, name)| {
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
|
||||
.child(Label::new(name))
|
||||
});
|
||||
|
||||
h_flex()
|
||||
.mx_2p5()
|
||||
.p_1()
|
||||
.pb_0()
|
||||
.gap_2()
|
||||
.rounded_t_lg()
|
||||
.border_t_1()
|
||||
.border_x_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.5))
|
||||
.bg(cx.theme().colors().background.alpha(0.5))
|
||||
.shadow(vec![gpui::BoxShadow {
|
||||
color: gpui::black().opacity(0.15),
|
||||
offset: point(px(1.), px(-1.)),
|
||||
blur_radius: px(3.),
|
||||
spread_radius: px(0.),
|
||||
}])
|
||||
.child(
|
||||
h_flex()
|
||||
.px_2p5()
|
||||
.py_1p5()
|
||||
.gap_2()
|
||||
.flex_wrap()
|
||||
.rounded_t(px(5.))
|
||||
.overflow_hidden()
|
||||
.border_t_1()
|
||||
.border_x_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.child(Icon::new(IconName::Info).size(IconSize::XSmall).color(Color::Muted))
|
||||
.child(Label::new("Or start now using API keys from your environment for the following providers:").color(Color::Muted))
|
||||
.children(configured_providers_list)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ApiKeysWithoutProviders;
|
||||
|
||||
impl ApiKeysWithoutProviders {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ApiKeysWithoutProviders {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
v_flex()
|
||||
.mt_2()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new("API Keys")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(Divider::horizontal()),
|
||||
)
|
||||
.child(List::new().child(BulletItem::new(
|
||||
"You can also use AI in Zed by bringing your own API keys",
|
||||
)))
|
||||
.child(
|
||||
Button::new("configure-providers", "Configure Providers")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.on_click(move |_, window, cx| {
|
||||
window.dispatch_action(
|
||||
zed_actions::agent::OpenConfiguration.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ impl ParentElement for AgentPanelOnboardingCard {
|
||||
impl RenderOnce for AgentPanelOnboardingCard {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
div()
|
||||
.m_4()
|
||||
.m_2p5()
|
||||
.p(px(3.))
|
||||
.elevation_2(cx)
|
||||
.rounded_lg()
|
||||
@@ -49,6 +49,7 @@ impl RenderOnce for AgentPanelOnboardingCard {
|
||||
.right_0()
|
||||
.w(px(400.))
|
||||
.h(px(92.))
|
||||
.rounded_md()
|
||||
.child(
|
||||
Vector::new(
|
||||
VectorName::AiGrid,
|
||||
@@ -61,11 +62,12 @@ impl RenderOnce for AgentPanelOnboardingCard {
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.right_0()
|
||||
.top_0p5()
|
||||
.right_0p5()
|
||||
.w(px(660.))
|
||||
.h(px(401.))
|
||||
.overflow_hidden()
|
||||
.rounded_md()
|
||||
.bg(linear_gradient(
|
||||
75.,
|
||||
linear_color_stop(
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use client::{Client, UserStore};
|
||||
use gpui::{Action, ClickEvent, Entity, IntoElement, ParentElement};
|
||||
use gpui::{Entity, IntoElement, ParentElement};
|
||||
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
|
||||
use ui::{Divider, List, prelude::*};
|
||||
use zed_actions::agent::{OpenConfiguration, ToggleModelSelector};
|
||||
use ui::prelude::*;
|
||||
|
||||
use crate::{AgentPanelOnboardingCard, BulletItem, ZedAiOnboarding};
|
||||
use crate::{AgentPanelOnboardingCard, ApiKeysWithoutProviders, ZedAiOnboarding};
|
||||
|
||||
pub struct AgentPanelOnboarding {
|
||||
user_store: Entity<UserStore>,
|
||||
@@ -53,93 +52,34 @@ impl AgentPanelOnboarding {
|
||||
.map(|provider| (provider.icon(), provider.name().0.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn configure_providers(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
window.dispatch_action(OpenConfiguration.boxed_clone(), cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_api_keys_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let has_existing_providers = self.configured_providers.len() > 0;
|
||||
let configure_provider_label = if has_existing_providers {
|
||||
"Configure Other Provider"
|
||||
} else {
|
||||
"Configure Providers"
|
||||
};
|
||||
|
||||
let content = if has_existing_providers {
|
||||
List::new()
|
||||
.child(BulletItem::new(
|
||||
"Or start now using API keys from your environment for the following providers:"
|
||||
))
|
||||
.child(
|
||||
h_flex()
|
||||
.px_5()
|
||||
.gap_2()
|
||||
.flex_wrap()
|
||||
.children(self.configured_providers.iter().cloned().map(|(icon, name)|
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
|
||||
.child(Label::new(name))
|
||||
))
|
||||
)
|
||||
.child(BulletItem::new(
|
||||
"No need for any of the plans or even to sign in",
|
||||
))
|
||||
} else {
|
||||
List::new()
|
||||
.child(BulletItem::new(
|
||||
"You can also use AI in Zed by bringing your own API keys",
|
||||
))
|
||||
.child(BulletItem::new(
|
||||
"No need for any of the plans or even to sign in",
|
||||
))
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.mt_2()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new("API Keys")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(Divider::horizontal()),
|
||||
)
|
||||
.child(content)
|
||||
.when(has_existing_providers, |this| {
|
||||
this.child(
|
||||
Button::new("pick-model", "Choose Model")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Button::new("configure-providers", configure_provider_label)
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.on_click(cx.listener(Self::configure_providers)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AgentPanelOnboarding {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let enrolled_in_trial = matches!(
|
||||
self.user_store.read(cx).current_plan(),
|
||||
Some(proto::Plan::ZedProTrial)
|
||||
);
|
||||
|
||||
AgentPanelOnboardingCard::new()
|
||||
.child(ZedAiOnboarding::new(
|
||||
self.client.clone(),
|
||||
&self.user_store,
|
||||
self.continue_with_zed_ai.clone(),
|
||||
cx,
|
||||
))
|
||||
.child(self.render_api_keys_section(cx))
|
||||
.child(
|
||||
ZedAiOnboarding::new(
|
||||
self.client.clone(),
|
||||
&self.user_store,
|
||||
self.continue_with_zed_ai.clone(),
|
||||
cx,
|
||||
)
|
||||
.with_dismiss({
|
||||
let callback = self.continue_with_zed_ai.clone();
|
||||
move |window, cx| callback(window, cx)
|
||||
}),
|
||||
)
|
||||
.map(|this| {
|
||||
if enrolled_in_trial || self.configured_providers.len() >= 1 {
|
||||
this
|
||||
} else {
|
||||
this.child(ApiKeysWithoutProviders::new())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
mod agent_api_keys_onboarding;
|
||||
mod agent_panel_onboarding_card;
|
||||
mod agent_panel_onboarding_content;
|
||||
mod edit_prediction_onboarding_content;
|
||||
mod young_account_banner;
|
||||
|
||||
pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProviders};
|
||||
pub use agent_panel_onboarding_card::AgentPanelOnboardingCard;
|
||||
pub use agent_panel_onboarding_content::AgentPanelOnboarding;
|
||||
pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
|
||||
@@ -12,7 +14,7 @@ use std::sync::Arc;
|
||||
|
||||
use client::{Client, UserStore, zed_urls};
|
||||
use gpui::{AnyElement, Entity, IntoElement, ParentElement, SharedString};
|
||||
use ui::{Divider, List, ListItem, RegisterComponent, TintColor, prelude::*};
|
||||
use ui::{Divider, List, ListItem, RegisterComponent, TintColor, Tooltip, prelude::*};
|
||||
|
||||
pub struct BulletItem {
|
||||
label: SharedString,
|
||||
@@ -69,6 +71,7 @@ pub struct ZedAiOnboarding {
|
||||
pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
pub accept_terms_of_service: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
pub dismiss_onboarding: Option<Arc<dyn Fn(&mut Window, &mut App)>>,
|
||||
}
|
||||
|
||||
impl ZedAiOnboarding {
|
||||
@@ -80,6 +83,7 @@ impl ZedAiOnboarding {
|
||||
) -> Self {
|
||||
let store = user_store.read(cx);
|
||||
let status = *client.status().borrow();
|
||||
|
||||
Self {
|
||||
sign_in_status: status.into(),
|
||||
has_accepted_terms_of_service: store.current_user_has_accepted_terms().unwrap_or(false),
|
||||
@@ -102,14 +106,22 @@ impl ZedAiOnboarding {
|
||||
})
|
||||
.detach();
|
||||
}),
|
||||
dismiss_onboarding: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_free_plan_section(&self, cx: &mut App) -> impl IntoElement {
|
||||
pub fn with_dismiss(
|
||||
mut self,
|
||||
dismiss_callback: impl Fn(&mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.dismiss_onboarding = Some(Arc::new(dismiss_callback));
|
||||
self
|
||||
}
|
||||
|
||||
fn free_plan_definition(&self, cx: &mut App) -> impl IntoElement {
|
||||
v_flex()
|
||||
.mt_2()
|
||||
.gap_1()
|
||||
.when(self.account_too_young, |this| this.opacity(0.4))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
@@ -119,6 +131,12 @@ impl ZedAiOnboarding {
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(
|
||||
Label::new("(Current Plan)")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Custom(cx.theme().colors().text_muted.opacity(0.6)))
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(Divider::horizontal()),
|
||||
)
|
||||
.child(
|
||||
@@ -130,65 +148,89 @@ impl ZedAiOnboarding {
|
||||
"2000 accepted edit predictions using our open-source Zeta model",
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
Button::new("continue", "Continue Free")
|
||||
.disabled(self.account_too_young)
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.on_click({
|
||||
let callback = self.continue_with_zed_ai.clone();
|
||||
move |_, window, cx| callback(window, cx)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_pro_plan_section(&self, cx: &mut App) -> impl IntoElement {
|
||||
let (button_label, button_url) = if self.account_too_young {
|
||||
("Start with Pro", zed_urls::upgrade_to_zed_pro_url(cx))
|
||||
} else {
|
||||
("Start Pro Trial", zed_urls::account_url(cx))
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.mt_2()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new("Pro")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Accent)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(Divider::horizontal()),
|
||||
)
|
||||
.child(
|
||||
List::new()
|
||||
.child(BulletItem::new("500 prompts per month with Claude models"))
|
||||
.child(BulletItem::new("Unlimited edit predictions"))
|
||||
.when(!self.account_too_young, |this| {
|
||||
this.child(BulletItem::new(
|
||||
"Try it out for 14 days with no charge, no credit card required",
|
||||
))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("pro", button_label)
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.on_click(move |_, _window, cx| cx.open_url(&button_url)),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_accept_terms_of_service(&self) -> Div {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(Headline::new("Before starting…"))
|
||||
.child(Label::new(
|
||||
"Make sure you have read and accepted Zed AI's terms of service.",
|
||||
fn pro_trial_definition(&self) -> impl IntoElement {
|
||||
List::new()
|
||||
.child(BulletItem::new(
|
||||
"150 prompts per month with the Claude models",
|
||||
))
|
||||
.child(BulletItem::new(
|
||||
"Unlimited accepted edit predictions using our open-source Zeta model",
|
||||
))
|
||||
}
|
||||
|
||||
fn pro_plan_definition(&self, cx: &mut App) -> impl IntoElement {
|
||||
v_flex().mt_2().gap_1().map(|this| {
|
||||
if self.account_too_young {
|
||||
this.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new("Pro")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Accent)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(Divider::horizontal()),
|
||||
)
|
||||
.child(
|
||||
List::new()
|
||||
.child(BulletItem::new("500 prompts per month with Claude models"))
|
||||
.child(BulletItem::new(
|
||||
"Unlimited accepted edit predictions using our open-source Zeta model",
|
||||
))
|
||||
.child(BulletItem::new("USD $20 per month")),
|
||||
)
|
||||
.child(
|
||||
Button::new("pro", "Start with Pro")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.on_click(move |_, _window, cx| {
|
||||
cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new("Pro Trial")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Accent)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(Divider::horizontal()),
|
||||
)
|
||||
.child(
|
||||
List::new()
|
||||
.child(self.pro_trial_definition())
|
||||
.child(BulletItem::new(
|
||||
"Try it out for 14 days with no charge and no credit card required",
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
Button::new("pro", "Start Pro Trial")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.on_click(move |_, _window, cx| {
|
||||
cx.open_url(&zed_urls::start_trial_url(cx))
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn render_accept_terms_of_service(&self) -> AnyElement {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.w_full()
|
||||
.child(Headline::new("Before starting…"))
|
||||
.child(
|
||||
Label::new("Make sure you have read and accepted Zed AI's terms of service.")
|
||||
.color(Color::Muted)
|
||||
.mb_2(),
|
||||
)
|
||||
.child(
|
||||
Button::new("terms_of_service", "View and Read the Terms of Service")
|
||||
.full_width()
|
||||
@@ -196,9 +238,7 @@ impl ZedAiOnboarding {
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(move |_, _window, cx| {
|
||||
cx.open_url("https://zed.dev/terms-of-service")
|
||||
}),
|
||||
.on_click(move |_, _window, cx| cx.open_url(&zed_urls::terms_of_service(cx))),
|
||||
)
|
||||
.child(
|
||||
Button::new("accept_terms", "I've read it and accept it")
|
||||
@@ -209,23 +249,23 @@ impl ZedAiOnboarding {
|
||||
move |_, window, cx| (callback)(window, cx)
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_sign_in_disclaimer(&self, _cx: &mut App) -> Div {
|
||||
const SIGN_IN_DISCLAIMER: &str =
|
||||
"To start using AI in Zed with our hosted models, sign in and subscribe to a plan.";
|
||||
fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
|
||||
let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.gap_1()
|
||||
.child(Headline::new("Welcome to Zed AI"))
|
||||
.child(div().w_full().child(Label::new(SIGN_IN_DISCLAIMER)))
|
||||
.child(
|
||||
Button::new("sign_in", "Sign In with GitHub")
|
||||
.icon(IconName::Github)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
Label::new("Sign in to start using AI in Zed with a free trial of the Pro plan, which includes:")
|
||||
.color(Color::Muted)
|
||||
.mb_2(),
|
||||
)
|
||||
.child(self.pro_trial_definition())
|
||||
.child(
|
||||
Button::new("sign_in", "Sign in to Start Trial")
|
||||
.disabled(signing_in)
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
@@ -234,36 +274,55 @@ impl ZedAiOnboarding {
|
||||
move |_, window, cx| callback(window, cx)
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_free_plan_onboarding(&self, cx: &mut App) -> Div {
|
||||
const PLANS_DESCRIPTION: &str = "Choose how you want to start.";
|
||||
fn render_free_plan_state(&self, cx: &mut App) -> AnyElement {
|
||||
let young_account_banner = YoungAccountBanner;
|
||||
|
||||
v_flex()
|
||||
.relative()
|
||||
.gap_1()
|
||||
.child(Headline::new("Welcome to Zed AI"))
|
||||
.child(
|
||||
Label::new(PLANS_DESCRIPTION)
|
||||
.size(LabelSize::Small)
|
||||
Label::new("Choose how you want to start.")
|
||||
.color(Color::Muted)
|
||||
.mt_1()
|
||||
.mb_3(),
|
||||
.mb_2(),
|
||||
)
|
||||
.when(self.account_too_young, |this| {
|
||||
this.child(young_account_banner)
|
||||
.map(|this| {
|
||||
if self.account_too_young {
|
||||
this.child(young_account_banner)
|
||||
} else {
|
||||
this.child(self.free_plan_definition(cx)).when_some(
|
||||
self.dismiss_onboarding.as_ref(),
|
||||
|this, dismiss_callback| {
|
||||
let callback = dismiss_callback.clone();
|
||||
|
||||
this.child(
|
||||
h_flex().absolute().top_0().right_0().child(
|
||||
IconButton::new("dismiss_onboarding", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Dismiss"))
|
||||
.on_click(move |_, window, cx| callback(window, cx)),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
.child(self.render_free_plan_section(cx))
|
||||
.child(self.render_pro_plan_section(cx))
|
||||
.child(self.pro_plan_definition(cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_trial_onboarding(&self, _cx: &mut App) -> Div {
|
||||
fn render_trial_state(&self, _cx: &mut App) -> AnyElement {
|
||||
v_flex()
|
||||
.child(Headline::new("Welcome to the trial of Zed Pro"))
|
||||
.relative()
|
||||
.gap_1()
|
||||
.child(Headline::new("Welcome to the Zed Pro free trial"))
|
||||
.child(
|
||||
Label::new("Here's what you get for the next 14 days:")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.mt_1(),
|
||||
.mb_2(),
|
||||
)
|
||||
.child(
|
||||
List::new()
|
||||
@@ -272,25 +331,31 @@ impl ZedAiOnboarding {
|
||||
"Unlimited edit predictions with Zeta, our open-source model",
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
Button::new("trial", "Start Trial")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.on_click({
|
||||
let callback = self.continue_with_zed_ai.clone();
|
||||
move |_, window, cx| callback(window, cx)
|
||||
}),
|
||||
.when_some(
|
||||
self.dismiss_onboarding.as_ref(),
|
||||
|this, dismiss_callback| {
|
||||
let callback = dismiss_callback.clone();
|
||||
this.child(
|
||||
h_flex().absolute().top_0().right_0().child(
|
||||
IconButton::new("dismiss_onboarding", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Dismiss"))
|
||||
.on_click(move |_, window, cx| callback(window, cx)),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_pro_plan_onboarding(&self, _cx: &mut App) -> Div {
|
||||
fn render_pro_plan_state(&self, _cx: &mut App) -> AnyElement {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(Headline::new("Welcome to Zed Pro"))
|
||||
.child(
|
||||
Label::new("Here's what you get:")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.mt_1(),
|
||||
.mb_2(),
|
||||
)
|
||||
.child(
|
||||
List::new()
|
||||
@@ -306,6 +371,7 @@ impl ZedAiOnboarding {
|
||||
move |_, window, cx| callback(window, cx)
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,9 +380,9 @@ impl RenderOnce for ZedAiOnboarding {
|
||||
if matches!(self.sign_in_status, SignInStatus::SignedIn) {
|
||||
if self.has_accepted_terms_of_service {
|
||||
match self.plan {
|
||||
None | Some(proto::Plan::Free) => self.render_free_plan_onboarding(cx),
|
||||
Some(proto::Plan::ZedProTrial) => self.render_trial_onboarding(cx),
|
||||
Some(proto::Plan::ZedPro) => self.render_pro_plan_onboarding(cx),
|
||||
None | Some(proto::Plan::Free) => self.render_free_plan_state(cx),
|
||||
Some(proto::Plan::ZedProTrial) => self.render_trial_state(cx),
|
||||
Some(proto::Plan::ZedPro) => self.render_pro_plan_state(cx),
|
||||
}
|
||||
} else {
|
||||
self.render_accept_terms_of_service()
|
||||
@@ -347,6 +413,7 @@ impl Component for ZedAiOnboarding {
|
||||
continue_with_zed_ai: Arc::new(|_, _| {}),
|
||||
sign_in: Arc::new(|_, _| {}),
|
||||
accept_terms_of_service: Arc::new(|_, _| {}),
|
||||
dismiss_onboarding: None,
|
||||
}
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ pub struct YoungAccountBanner;
|
||||
|
||||
impl RenderOnce for YoungAccountBanner {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
const YOUNG_ACCOUNT_DISCLAIMER: &str = "Given your GitHub account was created less than 30 days ago, we cannot put you in the Free plan or offer you a free trial of the Pro plan. We hope you'll understand, as this is unfortunately required to prevent abuse of our service. To continue, upgrade to Pro or use your own API keys for other providers.";
|
||||
const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, we cannot offer plans to GitHub accounts created fewer than 30 days ago. To request an exception, reach out to billing@zed.dev.";
|
||||
|
||||
let label = div()
|
||||
.w_full()
|
||||
|
||||
@@ -11,15 +11,18 @@ use client::{
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use fs::Fs;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, WeakEntity};
|
||||
use gpui::{
|
||||
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, ScreenCaptureSource,
|
||||
ScreenCaptureStream, Task, WeakEntity,
|
||||
};
|
||||
use gpui_tokio::Tokio;
|
||||
use language::LanguageRegistry;
|
||||
use livekit::{LocalTrackPublication, ParticipantIdentity, RoomEvent};
|
||||
use livekit_client::{self as livekit, TrackSid};
|
||||
use livekit_client::{self as livekit, AudioStream, TrackSid};
|
||||
use postage::{sink::Sink, stream::Stream, watch};
|
||||
use project::Project;
|
||||
use settings::Settings as _;
|
||||
use std::{any::Any, future::Future, mem, rc::Rc, sync::Arc, time::Duration};
|
||||
use std::{future::Future, mem, rc::Rc, sync::Arc, time::Duration};
|
||||
use util::{ResultExt, TryFutureExt, post_inc};
|
||||
|
||||
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
@@ -1251,12 +1254,21 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_screen_sharing(&self) -> bool {
|
||||
pub fn is_sharing_screen(&self) -> bool {
|
||||
self.live_kit.as_ref().map_or(false, |live_kit| {
|
||||
!matches!(live_kit.screen_track, LocalTrack::None)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn shared_screen_id(&self) -> Option<u64> {
|
||||
self.live_kit.as_ref().and_then(|lk| match lk.screen_track {
|
||||
LocalTrack::Published { ref _stream, .. } => {
|
||||
_stream.metadata().ok().map(|meta| meta.id)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_sharing_mic(&self) -> bool {
|
||||
self.live_kit.as_ref().map_or(false, |live_kit| {
|
||||
!matches!(live_kit.microphone_track, LocalTrack::None)
|
||||
@@ -1369,11 +1381,15 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn share_screen(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
pub fn share_screen(
|
||||
&mut self,
|
||||
source: Rc<dyn ScreenCaptureSource>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if self.status.is_offline() {
|
||||
return Task::ready(Err(anyhow!("room is offline")));
|
||||
}
|
||||
if self.is_screen_sharing() {
|
||||
if self.is_sharing_screen() {
|
||||
return Task::ready(Err(anyhow!("screen was already shared")));
|
||||
}
|
||||
|
||||
@@ -1386,20 +1402,8 @@ impl Room {
|
||||
return Task::ready(Err(anyhow!("live-kit was not initialized")));
|
||||
};
|
||||
|
||||
let sources = cx.screen_capture_sources();
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let sources = sources
|
||||
.await
|
||||
.map_err(|error| error.into())
|
||||
.and_then(|sources| sources);
|
||||
let source =
|
||||
sources.and_then(|sources| sources.into_iter().next().context("no display found"));
|
||||
|
||||
let publication = match source {
|
||||
Ok(source) => participant.publish_screenshare_track(&*source, cx).await,
|
||||
Err(error) => Err(error),
|
||||
};
|
||||
let publication = participant.publish_screenshare_track(&*source, cx).await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let live_kit = this
|
||||
@@ -1426,7 +1430,7 @@ impl Room {
|
||||
} else {
|
||||
live_kit.screen_track = LocalTrack::Published {
|
||||
track_publication: publication,
|
||||
_stream: Box::new(stream),
|
||||
_stream: stream,
|
||||
};
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1492,7 +1496,7 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unshare_screen(&mut self, cx: &mut Context<Self>) -> Result<()> {
|
||||
pub fn unshare_screen(&mut self, play_sound: bool, cx: &mut Context<Self>) -> Result<()> {
|
||||
anyhow::ensure!(!self.status.is_offline(), "room is offline");
|
||||
|
||||
let live_kit = self
|
||||
@@ -1516,7 +1520,10 @@ impl Room {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
Audio::play_sound(Sound::StopScreenshare, cx);
|
||||
if play_sound {
|
||||
Audio::play_sound(Sound::StopScreenshare, cx);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1624,8 +1631,8 @@ fn spawn_room_connection(
|
||||
|
||||
struct LiveKitRoom {
|
||||
room: Rc<livekit::Room>,
|
||||
screen_track: LocalTrack,
|
||||
microphone_track: LocalTrack,
|
||||
screen_track: LocalTrack<dyn ScreenCaptureStream>,
|
||||
microphone_track: LocalTrack<AudioStream>,
|
||||
/// Tracks whether we're currently in a muted state due to auto-mute from deafening or manual mute performed by user.
|
||||
muted_by_user: bool,
|
||||
deafened: bool,
|
||||
@@ -1663,18 +1670,18 @@ impl LiveKitRoom {
|
||||
}
|
||||
}
|
||||
|
||||
enum LocalTrack {
|
||||
enum LocalTrack<Stream: ?Sized> {
|
||||
None,
|
||||
Pending {
|
||||
publish_id: usize,
|
||||
},
|
||||
Published {
|
||||
track_publication: LocalTrackPublication,
|
||||
_stream: Box<dyn Any>,
|
||||
_stream: Box<Stream>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for LocalTrack {
|
||||
impl<T: ?Sized> Default for LocalTrack<T> {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
|
||||
@@ -18,7 +18,20 @@ pub fn account_url(cx: &App) -> String {
|
||||
format!("{server_url}/account", server_url = server_url(cx))
|
||||
}
|
||||
|
||||
/// Returns the URL to the start trial page on zed.dev.
|
||||
pub fn start_trial_url(cx: &App) -> String {
|
||||
format!(
|
||||
"{server_url}/account/start-trial",
|
||||
server_url = server_url(cx)
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the URL to the upgrade page on zed.dev.
|
||||
pub fn upgrade_to_zed_pro_url(cx: &App) -> String {
|
||||
format!("{server_url}/account/upgrade", server_url = server_url(cx))
|
||||
}
|
||||
|
||||
/// Returns the URL to Zed's terms of service.
|
||||
pub fn terms_of_service(cx: &App) -> String {
|
||||
format!("{server_url}/terms-of-service", server_url = server_url(cx))
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use anyhow::{Context as _, bail};
|
||||
use axum::routing::put;
|
||||
use axum::{Extension, Json, Router, extract, routing::post};
|
||||
use chrono::{DateTime, SecondsFormat, Utc};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::{HashMap, HashSet};
|
||||
use reqwest::StatusCode;
|
||||
use sea_orm::ActiveValue;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::{str::FromStr, sync::Arc, time::Duration};
|
||||
use stripe::{
|
||||
BillingPortalSession, CancellationDetailsReason, CreateBillingPortalSession,
|
||||
@@ -20,7 +18,6 @@ use stripe::{
|
||||
use util::{ResultExt, maybe};
|
||||
use zed_llm_client::LanguageModelProvider;
|
||||
|
||||
use crate::api::events::SnowflakeRow;
|
||||
use crate::db::billing_subscription::{
|
||||
StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind,
|
||||
};
|
||||
@@ -36,14 +33,13 @@ use crate::{
|
||||
db::{
|
||||
BillingSubscriptionId, CreateBillingCustomerParams, CreateBillingSubscriptionParams,
|
||||
CreateProcessedStripeEventParams, UpdateBillingCustomerParams,
|
||||
UpdateBillingPreferencesParams, UpdateBillingSubscriptionParams, billing_customer,
|
||||
UpdateBillingSubscriptionParams, billing_customer,
|
||||
},
|
||||
stripe_billing::StripeBilling,
|
||||
};
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/billing/preferences", put(update_billing_preferences))
|
||||
.route("/billing/subscriptions", post(create_billing_subscription))
|
||||
.route(
|
||||
"/billing/subscriptions/manage",
|
||||
@@ -55,108 +51,6 @@ pub fn router() -> Router {
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct BillingPreferencesResponse {
|
||||
trial_started_at: Option<String>,
|
||||
max_monthly_llm_usage_spending_in_cents: i32,
|
||||
model_request_overages_enabled: bool,
|
||||
model_request_overages_spend_limit_in_cents: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct UpdateBillingPreferencesBody {
|
||||
github_user_id: i32,
|
||||
#[serde(default)]
|
||||
max_monthly_llm_usage_spending_in_cents: i32,
|
||||
#[serde(default)]
|
||||
model_request_overages_enabled: bool,
|
||||
#[serde(default)]
|
||||
model_request_overages_spend_limit_in_cents: i32,
|
||||
}
|
||||
|
||||
async fn update_billing_preferences(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
Extension(rpc_server): Extension<Arc<crate::rpc::Server>>,
|
||||
extract::Json(body): extract::Json<UpdateBillingPreferencesBody>,
|
||||
) -> Result<Json<BillingPreferencesResponse>> {
|
||||
let user = app
|
||||
.db
|
||||
.get_user_by_github_user_id(body.github_user_id)
|
||||
.await?
|
||||
.context("user not found")?;
|
||||
|
||||
let billing_customer = app.db.get_billing_customer_by_user_id(user.id).await?;
|
||||
|
||||
let max_monthly_llm_usage_spending_in_cents =
|
||||
body.max_monthly_llm_usage_spending_in_cents.max(0);
|
||||
let model_request_overages_spend_limit_in_cents =
|
||||
body.model_request_overages_spend_limit_in_cents.max(0);
|
||||
|
||||
let billing_preferences =
|
||||
if let Some(_billing_preferences) = app.db.get_billing_preferences(user.id).await? {
|
||||
app.db
|
||||
.update_billing_preferences(
|
||||
user.id,
|
||||
&UpdateBillingPreferencesParams {
|
||||
max_monthly_llm_usage_spending_in_cents: ActiveValue::set(
|
||||
max_monthly_llm_usage_spending_in_cents,
|
||||
),
|
||||
model_request_overages_enabled: ActiveValue::set(
|
||||
body.model_request_overages_enabled,
|
||||
),
|
||||
model_request_overages_spend_limit_in_cents: ActiveValue::set(
|
||||
model_request_overages_spend_limit_in_cents,
|
||||
),
|
||||
},
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
app.db
|
||||
.create_billing_preferences(
|
||||
user.id,
|
||||
&crate::db::CreateBillingPreferencesParams {
|
||||
max_monthly_llm_usage_spending_in_cents,
|
||||
model_request_overages_enabled: body.model_request_overages_enabled,
|
||||
model_request_overages_spend_limit_in_cents,
|
||||
},
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
SnowflakeRow::new(
|
||||
"Billing Preferences Updated",
|
||||
Some(user.metrics_id),
|
||||
user.admin,
|
||||
None,
|
||||
json!({
|
||||
"user_id": user.id,
|
||||
"model_request_overages_enabled": billing_preferences.model_request_overages_enabled,
|
||||
"model_request_overages_spend_limit_in_cents": billing_preferences.model_request_overages_spend_limit_in_cents,
|
||||
"max_monthly_llm_usage_spending_in_cents": billing_preferences.max_monthly_llm_usage_spending_in_cents,
|
||||
}),
|
||||
)
|
||||
.write(&app.kinesis_client, &app.config.kinesis_stream)
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
rpc_server.refresh_llm_tokens_for_user(user.id).await;
|
||||
|
||||
Ok(Json(BillingPreferencesResponse {
|
||||
trial_started_at: billing_customer
|
||||
.and_then(|billing_customer| billing_customer.trial_started_at)
|
||||
.map(|trial_started_at| {
|
||||
trial_started_at
|
||||
.and_utc()
|
||||
.to_rfc3339_opts(SecondsFormat::Millis, true)
|
||||
}),
|
||||
max_monthly_llm_usage_spending_in_cents: billing_preferences
|
||||
.max_monthly_llm_usage_spending_in_cents,
|
||||
model_request_overages_enabled: billing_preferences.model_request_overages_enabled,
|
||||
model_request_overages_spend_limit_in_cents: billing_preferences
|
||||
.model_request_overages_spend_limit_in_cents,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum ProductCode {
|
||||
|
||||
@@ -42,9 +42,6 @@ pub use tests::TestDb;
|
||||
|
||||
pub use ids::*;
|
||||
pub use queries::billing_customers::{CreateBillingCustomerParams, UpdateBillingCustomerParams};
|
||||
pub use queries::billing_preferences::{
|
||||
CreateBillingPreferencesParams, UpdateBillingPreferencesParams,
|
||||
};
|
||||
pub use queries::billing_subscriptions::{
|
||||
CreateBillingSubscriptionParams, UpdateBillingSubscriptionParams,
|
||||
};
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
use anyhow::Context as _;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CreateBillingPreferencesParams {
|
||||
pub max_monthly_llm_usage_spending_in_cents: i32,
|
||||
pub model_request_overages_enabled: bool,
|
||||
pub model_request_overages_spend_limit_in_cents: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct UpdateBillingPreferencesParams {
|
||||
pub max_monthly_llm_usage_spending_in_cents: ActiveValue<i32>,
|
||||
pub model_request_overages_enabled: ActiveValue<bool>,
|
||||
pub model_request_overages_spend_limit_in_cents: ActiveValue<i32>,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
/// Returns the billing preferences for the given user, if they exist.
|
||||
pub async fn get_billing_preferences(
|
||||
@@ -30,62 +14,4 @@ impl Database {
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Creates new billing preferences for the given user.
|
||||
pub async fn create_billing_preferences(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
params: &CreateBillingPreferencesParams,
|
||||
) -> Result<billing_preference::Model> {
|
||||
self.transaction(|tx| async move {
|
||||
let preferences = billing_preference::Entity::insert(billing_preference::ActiveModel {
|
||||
user_id: ActiveValue::set(user_id),
|
||||
max_monthly_llm_usage_spending_in_cents: ActiveValue::set(
|
||||
params.max_monthly_llm_usage_spending_in_cents,
|
||||
),
|
||||
model_request_overages_enabled: ActiveValue::set(
|
||||
params.model_request_overages_enabled,
|
||||
),
|
||||
model_request_overages_spend_limit_in_cents: ActiveValue::set(
|
||||
params.model_request_overages_spend_limit_in_cents,
|
||||
),
|
||||
..Default::default()
|
||||
})
|
||||
.exec_with_returning(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(preferences)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Updates the billing preferences for the given user.
|
||||
pub async fn update_billing_preferences(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
params: &UpdateBillingPreferencesParams,
|
||||
) -> Result<billing_preference::Model> {
|
||||
self.transaction(|tx| async move {
|
||||
let preferences = billing_preference::Entity::update_many()
|
||||
.set(billing_preference::ActiveModel {
|
||||
max_monthly_llm_usage_spending_in_cents: params
|
||||
.max_monthly_llm_usage_spending_in_cents
|
||||
.clone(),
|
||||
model_request_overages_enabled: params.model_request_overages_enabled.clone(),
|
||||
model_request_overages_spend_limit_in_cents: params
|
||||
.model_request_overages_spend_limit_in_cents
|
||||
.clone(),
|
||||
..Default::default()
|
||||
})
|
||||
.filter(billing_preference::Column::UserId.eq(user_id))
|
||||
.exec_with_returning(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(preferences
|
||||
.into_iter()
|
||||
.next()
|
||||
.context("billing preferences not found")?)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4167,6 +4167,13 @@ async fn accept_terms_of_service(
|
||||
response.send(proto::AcceptTermsOfServiceResponse {
|
||||
accepted_tos_at: accepted_tos_at.timestamp() as u64,
|
||||
})?;
|
||||
|
||||
// When the user accepts the terms of service, we want to refresh their LLM
|
||||
// token to grant access.
|
||||
session
|
||||
.peer
|
||||
.send(session.connection_id, proto::RefreshLlmToken {})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -439,7 +439,7 @@ async fn test_basic_following(
|
||||
editor_a1.item_id()
|
||||
);
|
||||
|
||||
#[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
|
||||
// #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
|
||||
{
|
||||
use crate::rpc::RECONNECT_TIMEOUT;
|
||||
use gpui::TestScreenCaptureSource;
|
||||
@@ -456,11 +456,19 @@ async fn test_basic_following(
|
||||
.await
|
||||
.unwrap();
|
||||
cx_b.set_screen_capture_sources(vec![display]);
|
||||
let source = cx_b
|
||||
.read(|cx| cx.screen_capture_sources())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap();
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| {
|
||||
call.room()
|
||||
.unwrap()
|
||||
.update(cx, |room, cx| room.share_screen(cx))
|
||||
.update(cx, |room, cx| room.share_screen(source, cx))
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -277,11 +277,19 @@ async fn test_basic_calls(
|
||||
let events_b = active_call_events(cx_b);
|
||||
let events_c = active_call_events(cx_c);
|
||||
cx_a.set_screen_capture_sources(vec![display]);
|
||||
let screen_a = cx_a
|
||||
.update(|cx| cx.screen_capture_sources())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap();
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.room()
|
||||
.unwrap()
|
||||
.update(cx, |room, cx| room.share_screen(cx))
|
||||
.update(cx, |room, cx| room.share_screen(screen_a, cx))
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -6312,11 +6320,20 @@ async fn test_join_call_after_screen_was_shared(
|
||||
// User A shares their screen
|
||||
let display = gpui::TestScreenCaptureSource::new();
|
||||
cx_a.set_screen_capture_sources(vec![display]);
|
||||
let screen_a = cx_a
|
||||
.update(|cx| cx.screen_capture_sources())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap();
|
||||
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.room()
|
||||
.unwrap()
|
||||
.update(cx, |room, cx| room.share_screen(cx))
|
||||
.update(cx, |room, cx| room.share_screen(screen_a, cx))
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -144,10 +144,22 @@ pub fn init(cx: &mut App) {
|
||||
if let Some(room) = room {
|
||||
window.defer(cx, move |_window, cx| {
|
||||
room.update(cx, |room, cx| {
|
||||
if room.is_screen_sharing() {
|
||||
room.unshare_screen(cx).ok();
|
||||
if room.is_sharing_screen() {
|
||||
room.unshare_screen(true, cx).ok();
|
||||
} else {
|
||||
room.share_screen(cx).detach_and_log_err(cx);
|
||||
let sources = cx.screen_capture_sources();
|
||||
|
||||
cx.spawn(async move |room, cx| {
|
||||
let sources = sources.await??;
|
||||
let first = sources.into_iter().next();
|
||||
if let Some(first) = first {
|
||||
room.update(cx, |room, cx| room.share_screen(first, cx))?
|
||||
.await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -528,10 +540,10 @@ impl CollabPanel {
|
||||
project_id: project.id,
|
||||
worktree_root_names: project.worktree_root_names.clone(),
|
||||
host_user_id: user_id,
|
||||
is_last: projects.peek().is_none() && !room.is_screen_sharing(),
|
||||
is_last: projects.peek().is_none() && !room.is_sharing_screen(),
|
||||
});
|
||||
}
|
||||
if room.is_screen_sharing() {
|
||||
if room.is_sharing_screen() {
|
||||
self.entries.push(ListEntry::ParticipantScreen {
|
||||
peer_id: None,
|
||||
is_last: true,
|
||||
|
||||
@@ -242,7 +242,7 @@ impl CommandPaletteDelegate {
|
||||
self.selected_ix = cmp::min(self.selected_ix, self.matches.len() - 1);
|
||||
}
|
||||
}
|
||||
///
|
||||
|
||||
/// Hit count for each command in the palette.
|
||||
/// We only account for commands triggered directly via command palette and not by e.g. keystrokes because
|
||||
/// if a user already knows a keystroke for a command, they are unlikely to use a command palette to look for it.
|
||||
|
||||
@@ -6,9 +6,9 @@ pub mod test;
|
||||
pub mod transport;
|
||||
pub mod types;
|
||||
|
||||
use std::fmt::Display;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::{fmt::Display, path::PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use client::Client;
|
||||
@@ -31,7 +31,7 @@ impl Display for ContextServerId {
|
||||
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
|
||||
pub struct ContextServerCommand {
|
||||
#[serde(rename = "command")]
|
||||
pub path: String,
|
||||
pub path: PathBuf,
|
||||
pub args: Vec<String>,
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ impl DapRegistry {
|
||||
let name = adapter.name();
|
||||
let _previous_value = self.0.write().adapters.insert(name, adapter);
|
||||
}
|
||||
|
||||
pub fn add_locator(&self, locator: Arc<dyn DapLocator>) {
|
||||
self.0.write().locators.insert(locator.name(), locator);
|
||||
}
|
||||
|
||||
@@ -322,6 +322,8 @@ actions!(
|
||||
ApplyDiffHunk,
|
||||
/// Deletes the character before the cursor.
|
||||
Backspace,
|
||||
/// Shows git blame information for the current line.
|
||||
BlameHover,
|
||||
/// Cancels the current operation.
|
||||
Cancel,
|
||||
/// Cancels the running flycheck operation.
|
||||
|
||||
@@ -950,6 +950,7 @@ struct InlineBlamePopover {
|
||||
hide_task: Option<Task<()>>,
|
||||
popover_bounds: Option<Bounds<Pixels>>,
|
||||
popover_state: InlineBlamePopoverState,
|
||||
keyboard_grace: bool,
|
||||
}
|
||||
|
||||
enum SelectionDragState {
|
||||
@@ -6517,21 +6518,55 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blame_hover(&mut self, _: &BlameHover, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
let cursor = self.selections.newest::<Point>(cx).head();
|
||||
let Some((buffer, point, _)) = snapshot.buffer_snapshot.point_to_buffer_point(cursor)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(blame) = self.blame.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let row_info = RowInfo {
|
||||
buffer_id: Some(buffer.remote_id()),
|
||||
buffer_row: Some(point.row),
|
||||
..Default::default()
|
||||
};
|
||||
let Some(blame_entry) = blame
|
||||
.update(cx, |blame, cx| blame.blame_for_rows(&[row_info], cx).next())
|
||||
.flatten()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let anchor = self.selections.newest_anchor().head();
|
||||
let position = self.to_pixel_point(anchor, &snapshot, window);
|
||||
if let (Some(position), Some(last_bounds)) = (position, self.last_bounds) {
|
||||
self.show_blame_popover(&blame_entry, position + last_bounds.origin, true, cx);
|
||||
};
|
||||
}
|
||||
|
||||
fn show_blame_popover(
|
||||
&mut self,
|
||||
blame_entry: &BlameEntry,
|
||||
position: gpui::Point<Pixels>,
|
||||
ignore_timeout: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(state) = &mut self.inline_blame_popover {
|
||||
state.hide_task.take();
|
||||
} else {
|
||||
let delay = EditorSettings::get_global(cx).hover_popover_delay;
|
||||
let blame_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
|
||||
let blame_entry = blame_entry.clone();
|
||||
let show_task = cx.spawn(async move |editor, cx| {
|
||||
cx.background_executor()
|
||||
.timer(std::time::Duration::from_millis(delay))
|
||||
.await;
|
||||
if !ignore_timeout {
|
||||
cx.background_executor()
|
||||
.timer(std::time::Duration::from_millis(blame_popover_delay))
|
||||
.await;
|
||||
}
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.inline_blame_popover_show_task.take();
|
||||
@@ -6560,6 +6595,7 @@ impl Editor {
|
||||
commit_message: details,
|
||||
markdown,
|
||||
},
|
||||
keyboard_grace: ignore_timeout,
|
||||
});
|
||||
cx.notify();
|
||||
})
|
||||
|
||||
@@ -216,6 +216,7 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::newline_above);
|
||||
register_action(editor, window, Editor::newline_below);
|
||||
register_action(editor, window, Editor::backspace);
|
||||
register_action(editor, window, Editor::blame_hover);
|
||||
register_action(editor, window, Editor::delete);
|
||||
register_action(editor, window, Editor::tab);
|
||||
register_action(editor, window, Editor::backtab);
|
||||
@@ -1143,10 +1144,14 @@ impl EditorElement {
|
||||
.as_ref()
|
||||
.and_then(|state| state.popover_bounds)
|
||||
.map_or(false, |bounds| bounds.contains(&event.position));
|
||||
let keyboard_grace = editor
|
||||
.inline_blame_popover
|
||||
.as_ref()
|
||||
.map_or(false, |state| state.keyboard_grace);
|
||||
|
||||
if mouse_over_inline_blame || mouse_over_popover {
|
||||
editor.show_blame_popover(&blame_entry, event.position, cx);
|
||||
} else {
|
||||
editor.show_blame_popover(&blame_entry, event.position, false, cx);
|
||||
} else if !keyboard_grace {
|
||||
editor.hide_blame_popover(cx);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -422,6 +422,13 @@ impl AppContext for ExampleContext {
|
||||
self.app.update_entity(handle, update)
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> Self::Result<gpui::GpuiBorrow<'a, T>>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.app.as_mut(handle)
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
|
||||
@@ -3,7 +3,7 @@ mod dap;
|
||||
mod lsp;
|
||||
mod slash_command;
|
||||
|
||||
use std::ops::Range;
|
||||
use std::{ops::Range, path::PathBuf};
|
||||
|
||||
use util::redact::should_redact;
|
||||
|
||||
@@ -18,7 +18,7 @@ pub type EnvVars = Vec<(String, String)>;
|
||||
/// A command.
|
||||
pub struct Command {
|
||||
/// The command to execute.
|
||||
pub command: String,
|
||||
pub command: PathBuf,
|
||||
/// The arguments to pass to the command.
|
||||
pub args: Vec<String>,
|
||||
/// The environment variables to set for the command.
|
||||
|
||||
@@ -75,7 +75,7 @@ impl From<Range> for std::ops::Range<usize> {
|
||||
impl From<Command> for extension::Command {
|
||||
fn from(value: Command) -> Self {
|
||||
Self {
|
||||
command: value.command,
|
||||
command: value.command.into(),
|
||||
args: value.args,
|
||||
env: value.env,
|
||||
}
|
||||
@@ -958,7 +958,7 @@ impl ExtensionImports for WasmState {
|
||||
command,
|
||||
} => Ok(serde_json::to_string(&settings::ContextServerSettings {
|
||||
command: Some(settings::CommandSettings {
|
||||
path: Some(command.path),
|
||||
path: command.path.to_str().map(|path| path.to_string()),
|
||||
arguments: Some(command.args),
|
||||
env: command.env.map(|env| env.into_iter().collect()),
|
||||
}),
|
||||
|
||||
@@ -121,7 +121,7 @@ smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
strum.workspace = true
|
||||
sum_tree.workspace = true
|
||||
taffy = "0.4.3"
|
||||
taffy = "=0.5.1"
|
||||
thiserror.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
@@ -34,7 +34,7 @@ fn main() {
|
||||
});
|
||||
}
|
||||
|
||||
// Associate actions using the `actions!` macro (or `impl_actions!` macro)
|
||||
// Associate actions using the `actions!` macro (or `Action` derive macro)
|
||||
actions!(set_menus, [Quit]);
|
||||
|
||||
// Define the quit function that is registered with the App
|
||||
|
||||
130
crates/gpui/examples/tab_stop.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString,
|
||||
Stateful, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, size,
|
||||
};
|
||||
|
||||
actions!(example, [Tab, TabPrev]);
|
||||
|
||||
struct Example {
|
||||
items: Vec<FocusHandle>,
|
||||
message: SharedString,
|
||||
}
|
||||
|
||||
impl Example {
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let items = vec![
|
||||
cx.focus_handle().tab_index(1).tab_stop(true),
|
||||
cx.focus_handle().tab_index(2).tab_stop(true),
|
||||
cx.focus_handle().tab_index(3).tab_stop(true),
|
||||
cx.focus_handle(),
|
||||
cx.focus_handle().tab_index(2).tab_stop(true),
|
||||
];
|
||||
|
||||
window.focus(items.first().unwrap());
|
||||
Self {
|
||||
items,
|
||||
message: SharedString::from("Press `Tab`, `Shift-Tab` to switch focus."),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tab(&mut self, _: &Tab, window: &mut Window, _: &mut Context<Self>) {
|
||||
window.focus_next();
|
||||
self.message = SharedString::from("You have pressed `Tab`.");
|
||||
}
|
||||
|
||||
fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, _: &mut Context<Self>) {
|
||||
window.focus_prev();
|
||||
self.message = SharedString::from("You have pressed `Shift-Tab`.");
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Example {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn button(id: impl Into<ElementId>) -> Stateful<Div> {
|
||||
div()
|
||||
.id(id)
|
||||
.h_10()
|
||||
.flex_1()
|
||||
.flex()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.border_1()
|
||||
.border_color(gpui::black())
|
||||
.bg(gpui::black())
|
||||
.text_color(gpui::white())
|
||||
.focus(|this| this.border_color(gpui::blue()))
|
||||
.shadow_sm()
|
||||
}
|
||||
|
||||
div()
|
||||
.id("app")
|
||||
.on_action(cx.listener(Self::on_tab))
|
||||
.on_action(cx.listener(Self::on_tab_prev))
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.p_4()
|
||||
.gap_3()
|
||||
.bg(gpui::white())
|
||||
.text_color(gpui::black())
|
||||
.child(self.message.clone())
|
||||
.children(
|
||||
self.items
|
||||
.clone()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, item_handle)| {
|
||||
div()
|
||||
.id(("item", ix))
|
||||
.track_focus(&item_handle)
|
||||
.h_10()
|
||||
.w_full()
|
||||
.flex()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.border_1()
|
||||
.border_color(gpui::black())
|
||||
.when(
|
||||
item_handle.tab_stop && item_handle.is_focused(window),
|
||||
|this| this.border_color(gpui::blue()),
|
||||
)
|
||||
.map(|this| match item_handle.tab_stop {
|
||||
true => this
|
||||
.hover(|this| this.bg(gpui::black().opacity(0.1)))
|
||||
.child(format!("tab_index: {}", item_handle.tab_index)),
|
||||
false => this.opacity(0.4).child("tab_stop: false"),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.gap_3()
|
||||
.items_center()
|
||||
.child(button("el1").tab_index(4).child("Button 1"))
|
||||
.child(button("el2").tab_index(5).child("Button 2")),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("tab", Tab, None),
|
||||
KeyBinding::new("shift-tab", TabPrev, None),
|
||||
]);
|
||||
|
||||
let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| cx.new(|cx| Example::new(window, cx)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
@@ -448,15 +448,23 @@ impl App {
|
||||
}
|
||||
|
||||
pub(crate) fn update<R>(&mut self, update: impl FnOnce(&mut Self) -> R) -> R {
|
||||
self.pending_updates += 1;
|
||||
self.start_update();
|
||||
let result = update(self);
|
||||
self.finish_update();
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn start_update(&mut self) {
|
||||
self.pending_updates += 1;
|
||||
}
|
||||
|
||||
pub(crate) fn finish_update(&mut self) {
|
||||
if !self.flushing_effects && self.pending_updates == 1 {
|
||||
self.flushing_effects = true;
|
||||
self.flush_effects();
|
||||
self.flushing_effects = false;
|
||||
}
|
||||
self.pending_updates -= 1;
|
||||
result
|
||||
}
|
||||
|
||||
/// Arrange a callback to be invoked when the given entity calls `notify` on its respective context.
|
||||
@@ -688,7 +696,7 @@ impl App {
|
||||
/// Returns a list of available screen capture sources.
|
||||
pub fn screen_capture_sources(
|
||||
&self,
|
||||
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
|
||||
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||
self.platform.screen_capture_sources()
|
||||
}
|
||||
|
||||
@@ -868,7 +876,6 @@ impl App {
|
||||
loop {
|
||||
self.release_dropped_entities();
|
||||
self.release_dropped_focus_handles();
|
||||
|
||||
if let Some(effect) = self.pending_effects.pop_front() {
|
||||
match effect {
|
||||
Effect::Notify { emitter } => {
|
||||
@@ -947,8 +954,8 @@ impl App {
|
||||
self.focus_handles
|
||||
.clone()
|
||||
.write()
|
||||
.retain(|handle_id, count| {
|
||||
if count.load(SeqCst) == 0 {
|
||||
.retain(|handle_id, focus| {
|
||||
if focus.ref_count.load(SeqCst) == 0 {
|
||||
for window_handle in self.windows() {
|
||||
window_handle
|
||||
.update(self, |_, window, _| {
|
||||
@@ -1363,7 +1370,9 @@ impl App {
|
||||
self.keymap.clone()
|
||||
}
|
||||
|
||||
/// Register a global listener for actions invoked via the keyboard.
|
||||
/// Register a global handler for actions invoked via the keyboard. These handlers are run at
|
||||
/// the end of the bubble phase for actions, and so will only be invoked if there are no other
|
||||
/// handlers or if they called `cx.propagate()`.
|
||||
pub fn on_action<A: Action>(&mut self, listener: impl Fn(&A, &mut Self) + 'static) {
|
||||
self.global_action_listeners
|
||||
.entry(TypeId::of::<A>())
|
||||
@@ -1819,6 +1828,13 @@ impl AppContext for App {
|
||||
})
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> GpuiBorrow<'a, T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
GpuiBorrow::new(handle.clone(), self)
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
@@ -2015,3 +2031,79 @@ impl HttpClient for NullHttpClient {
|
||||
type_name::<Self>()
|
||||
}
|
||||
}
|
||||
|
||||
/// A mutable reference to an entity owned by GPUI
|
||||
pub struct GpuiBorrow<'a, T> {
|
||||
inner: Option<Lease<T>>,
|
||||
app: &'a mut App,
|
||||
}
|
||||
|
||||
impl<'a, T: 'static> GpuiBorrow<'a, T> {
|
||||
fn new(inner: Entity<T>, app: &'a mut App) -> Self {
|
||||
app.start_update();
|
||||
let lease = app.entities.lease(&inner);
|
||||
Self {
|
||||
inner: Some(lease),
|
||||
app,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: 'static> std::borrow::Borrow<T> for GpuiBorrow<'a, T> {
|
||||
fn borrow(&self) -> &T {
|
||||
self.inner.as_ref().unwrap().borrow()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: 'static> std::borrow::BorrowMut<T> for GpuiBorrow<'a, T> {
|
||||
fn borrow_mut(&mut self) -> &mut T {
|
||||
self.inner.as_mut().unwrap().borrow_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Drop for GpuiBorrow<'a, T> {
|
||||
fn drop(&mut self) {
|
||||
let lease = self.inner.take().unwrap();
|
||||
self.app.notify(lease.id);
|
||||
self.app.entities.end_lease(lease);
|
||||
self.app.finish_update();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use crate::{AppContext, TestAppContext};
|
||||
|
||||
#[test]
|
||||
fn test_gpui_borrow() {
|
||||
let cx = TestAppContext::single();
|
||||
let observation_count = Rc::new(RefCell::new(0));
|
||||
|
||||
let state = cx.update(|cx| {
|
||||
let state = cx.new(|_| false);
|
||||
cx.observe(&state, {
|
||||
let observation_count = observation_count.clone();
|
||||
move |_, _| {
|
||||
let mut count = observation_count.borrow_mut();
|
||||
*count += 1;
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
state
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
// Calling this like this so that we don't clobber the borrow_mut above
|
||||
*std::borrow::BorrowMut::borrow_mut(&mut state.as_mut(cx)) = true;
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
state.write(cx, false);
|
||||
});
|
||||
|
||||
assert_eq!(*observation_count.borrow(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
Entity, EventEmitter, Focusable, ForegroundExecutor, Global, PromptButton, PromptLevel, Render,
|
||||
Reservation, Result, Subscription, Task, VisualContext, Window, WindowHandle,
|
||||
};
|
||||
use anyhow::Context as _;
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use futures::channel::oneshot;
|
||||
use std::{future::Future, rc::Weak};
|
||||
@@ -58,6 +58,15 @@ impl AppContext for AsyncApp {
|
||||
Ok(app.update_entity(handle, update))
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, _handle: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
Err(anyhow!(
|
||||
"Cannot as_mut with an async context. Try calling update() first"
|
||||
))
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
@@ -364,6 +373,15 @@ impl AppContext for AsyncWindowContext {
|
||||
.update(self, |_, _, cx| cx.update_entity(handle, update))
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
Err(anyhow!(
|
||||
"Cannot use as_mut() from an async context, call `update`"
|
||||
))
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
|
||||
@@ -726,6 +726,13 @@ impl<T> AppContext for Context<'_, T> {
|
||||
self.app.update_entity(handle, update)
|
||||
}
|
||||
|
||||
fn as_mut<'a, E>(&'a mut self, handle: &Entity<E>) -> Self::Result<super::GpuiBorrow<'a, E>>
|
||||
where
|
||||
E: 'static,
|
||||
{
|
||||
self.app.as_mut(handle)
|
||||
}
|
||||
|
||||
fn read_entity<U, R>(
|
||||
&self,
|
||||
handle: &Entity<U>,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{App, AppContext, VisualContext, Window, seal::Sealed};
|
||||
use crate::{App, AppContext, GpuiBorrow, VisualContext, Window, seal::Sealed};
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::FxHashSet;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
@@ -105,7 +105,7 @@ impl EntityMap {
|
||||
|
||||
/// Move an entity to the stack.
|
||||
#[track_caller]
|
||||
pub fn lease<'a, T>(&mut self, pointer: &'a Entity<T>) -> Lease<'a, T> {
|
||||
pub fn lease<T>(&mut self, pointer: &Entity<T>) -> Lease<T> {
|
||||
self.assert_valid_context(pointer);
|
||||
let mut accessed_entities = self.accessed_entities.borrow_mut();
|
||||
accessed_entities.insert(pointer.entity_id);
|
||||
@@ -117,15 +117,14 @@ impl EntityMap {
|
||||
);
|
||||
Lease {
|
||||
entity,
|
||||
pointer,
|
||||
id: pointer.entity_id,
|
||||
entity_type: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an entity after moving it to the stack.
|
||||
pub fn end_lease<T>(&mut self, mut lease: Lease<T>) {
|
||||
self.entities
|
||||
.insert(lease.pointer.entity_id, lease.entity.take().unwrap());
|
||||
self.entities.insert(lease.id, lease.entity.take().unwrap());
|
||||
}
|
||||
|
||||
pub fn read<T: 'static>(&self, entity: &Entity<T>) -> &T {
|
||||
@@ -187,13 +186,13 @@ fn double_lease_panic<T>(operation: &str) -> ! {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) struct Lease<'a, T> {
|
||||
pub(crate) struct Lease<T> {
|
||||
entity: Option<Box<dyn Any>>,
|
||||
pub pointer: &'a Entity<T>,
|
||||
pub id: EntityId,
|
||||
entity_type: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: 'static> core::ops::Deref for Lease<'_, T> {
|
||||
impl<T: 'static> core::ops::Deref for Lease<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
@@ -201,13 +200,13 @@ impl<T: 'static> core::ops::Deref for Lease<'_, T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> core::ops::DerefMut for Lease<'_, T> {
|
||||
impl<T: 'static> core::ops::DerefMut for Lease<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.entity.as_mut().unwrap().downcast_mut().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for Lease<'_, T> {
|
||||
impl<T> Drop for Lease<T> {
|
||||
fn drop(&mut self) {
|
||||
if self.entity.is_some() && !panicking() {
|
||||
panic!("Leases must be ended with EntityMap::end_lease")
|
||||
@@ -437,6 +436,19 @@ impl<T: 'static> Entity<T> {
|
||||
cx.update_entity(self, update)
|
||||
}
|
||||
|
||||
/// Updates the entity referenced by this handle with the given function.
|
||||
pub fn as_mut<'a, C: AppContext>(&self, cx: &'a mut C) -> C::Result<GpuiBorrow<'a, T>> {
|
||||
cx.as_mut(self)
|
||||
}
|
||||
|
||||
/// Updates the entity referenced by this handle with the given function.
|
||||
pub fn write<C: AppContext>(&self, cx: &mut C, value: T) -> C::Result<()> {
|
||||
self.update(cx, |entity, cx| {
|
||||
*entity = value;
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
|
||||
/// Updates the entity referenced by this handle with the given function if
|
||||
/// the referenced entity still exists, within a visual context that has a window.
|
||||
/// Returns an error if the entity has been released.
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::{
|
||||
};
|
||||
use anyhow::{anyhow, bail};
|
||||
use futures::{Stream, StreamExt, channel::oneshot};
|
||||
use rand::{SeedableRng, rngs::StdRng};
|
||||
use std::{cell::RefCell, future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration};
|
||||
|
||||
/// A TestAppContext is provided to tests created with `#[gpui::test]`, it provides
|
||||
@@ -63,6 +64,13 @@ impl AppContext for TestAppContext {
|
||||
app.update_entity(handle, update)
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
panic!("Cannot use as_mut with a test app context. Try calling update() first")
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
@@ -134,6 +142,12 @@ impl TestAppContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a single TestAppContext, for non-multi-client tests
|
||||
pub fn single() -> Self {
|
||||
let dispatcher = TestDispatcher::new(StdRng::from_entropy());
|
||||
Self::build(dispatcher, None)
|
||||
}
|
||||
|
||||
/// The name of the test function that created this `TestAppContext`
|
||||
pub fn test_function_name(&self) -> Option<&'static str> {
|
||||
self.fn_name
|
||||
@@ -914,6 +928,13 @@ impl AppContext for VisualTestContext {
|
||||
self.cx.update_entity(handle, update)
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.cx.as_mut(handle)
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
|
||||
@@ -39,7 +39,7 @@ use crate::{
|
||||
use derive_more::{Deref, DerefMut};
|
||||
pub(crate) use smallvec::SmallVec;
|
||||
use std::{
|
||||
any::Any,
|
||||
any::{Any, type_name},
|
||||
fmt::{self, Debug, Display},
|
||||
mem, panic,
|
||||
};
|
||||
@@ -220,14 +220,17 @@ impl<C: RenderOnce> Element for Component<C> {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut element = self
|
||||
.component
|
||||
.take()
|
||||
.unwrap()
|
||||
.render(window, cx)
|
||||
.into_any_element();
|
||||
let layout_id = element.request_layout(window, cx);
|
||||
(layout_id, element)
|
||||
window.with_global_id(ElementId::Name(type_name::<C>().into()), |_, window| {
|
||||
let mut element = self
|
||||
.component
|
||||
.take()
|
||||
.unwrap()
|
||||
.render(window, cx)
|
||||
.into_any_element();
|
||||
|
||||
let layout_id = element.request_layout(window, cx);
|
||||
(layout_id, element)
|
||||
})
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
@@ -239,7 +242,9 @@ impl<C: RenderOnce> Element for Component<C> {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
element.prepaint(window, cx);
|
||||
window.with_global_id(ElementId::Name(type_name::<C>().into()), |_, window| {
|
||||
element.prepaint(window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(
|
||||
@@ -252,7 +257,9 @@ impl<C: RenderOnce> Element for Component<C> {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
element.paint(window, cx);
|
||||
window.with_global_id(ElementId::Name(type_name::<C>().into()), |_, window| {
|
||||
element.paint(window, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -619,6 +619,13 @@ pub trait InteractiveElement: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set index of the tab stop order.
|
||||
fn tab_index(mut self, index: isize) -> Self {
|
||||
self.interactivity().focusable = true;
|
||||
self.interactivity().tab_index = Some(index);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the keymap context for this element. This will be used to determine
|
||||
/// which action to dispatch from the keymap.
|
||||
fn key_context<C, E>(mut self, key_context: C) -> Self
|
||||
@@ -1462,6 +1469,7 @@ pub struct Interactivity {
|
||||
pub(crate) tooltip_builder: Option<TooltipBuilder>,
|
||||
pub(crate) window_control: Option<WindowControlArea>,
|
||||
pub(crate) hitbox_behavior: HitboxBehavior,
|
||||
pub(crate) tab_index: Option<isize>,
|
||||
|
||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||
pub(crate) source_location: Option<&'static core::panic::Location<'static>>,
|
||||
@@ -1521,12 +1529,17 @@ impl Interactivity {
|
||||
// as frames contain an element with this id.
|
||||
if self.focusable && self.tracked_focus_handle.is_none() {
|
||||
if let Some(element_state) = element_state.as_mut() {
|
||||
self.tracked_focus_handle = Some(
|
||||
element_state
|
||||
.focus_handle
|
||||
.get_or_insert_with(|| cx.focus_handle())
|
||||
.clone(),
|
||||
);
|
||||
let mut handle = element_state
|
||||
.focus_handle
|
||||
.get_or_insert_with(|| cx.focus_handle())
|
||||
.clone()
|
||||
.tab_stop(false);
|
||||
|
||||
if let Some(index) = self.tab_index {
|
||||
handle = handle.tab_index(index).tab_stop(true);
|
||||
}
|
||||
|
||||
self.tracked_focus_handle = Some(handle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1651,6 +1664,11 @@ impl Interactivity {
|
||||
window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Point<Pixels> {
|
||||
fn round_to_two_decimals(pixels: Pixels) -> Pixels {
|
||||
const ROUNDING_FACTOR: f32 = 100.0;
|
||||
(pixels * ROUNDING_FACTOR).round() / ROUNDING_FACTOR
|
||||
}
|
||||
|
||||
if let Some(scroll_offset) = self.scroll_offset.as_ref() {
|
||||
let mut scroll_to_bottom = false;
|
||||
let mut tracked_scroll_handle = self
|
||||
@@ -1665,8 +1683,16 @@ impl Interactivity {
|
||||
let rem_size = window.rem_size();
|
||||
let padding = style.padding.to_pixels(bounds.size.into(), rem_size);
|
||||
let padding_size = size(padding.left + padding.right, padding.top + padding.bottom);
|
||||
// The floating point values produced by Taffy and ours often vary
|
||||
// slightly after ~5 decimal places. This can lead to cases where after
|
||||
// subtracting these, the container becomes scrollable for less than
|
||||
// 0.00000x pixels. As we generally don't benefit from a precision that
|
||||
// high for the maximum scroll, we round the scroll max to 2 decimal
|
||||
// places here.
|
||||
let padded_content_size = self.content_size + padding_size;
|
||||
let scroll_max = (padded_content_size - bounds.size).max(&Size::default());
|
||||
let scroll_max = (padded_content_size - bounds.size)
|
||||
.map(round_to_two_decimals)
|
||||
.max(&Default::default());
|
||||
// Clamp scroll offset in case scroll max is smaller now (e.g., if children
|
||||
// were removed or the bounds became larger).
|
||||
let mut scroll_offset = scroll_offset.borrow_mut();
|
||||
@@ -1679,7 +1705,7 @@ impl Interactivity {
|
||||
}
|
||||
|
||||
if let Some(mut scroll_handle_state) = tracked_scroll_handle {
|
||||
scroll_handle_state.padded_content_size = padded_content_size;
|
||||
scroll_handle_state.max_offset = scroll_max;
|
||||
}
|
||||
|
||||
*scroll_offset
|
||||
@@ -1729,6 +1755,10 @@ impl Interactivity {
|
||||
return ((), element_state);
|
||||
}
|
||||
|
||||
if let Some(focus_handle) = &self.tracked_focus_handle {
|
||||
window.next_frame.tab_handles.insert(focus_handle);
|
||||
}
|
||||
|
||||
window.with_element_opacity(style.opacity, |window| {
|
||||
style.paint(bounds, window, cx, |window: &mut Window, cx: &mut App| {
|
||||
window.with_text_style(style.text_style().cloned(), |window| {
|
||||
@@ -2919,7 +2949,7 @@ impl ScrollAnchor {
|
||||
struct ScrollHandleState {
|
||||
offset: Rc<RefCell<Point<Pixels>>>,
|
||||
bounds: Bounds<Pixels>,
|
||||
padded_content_size: Size<Pixels>,
|
||||
max_offset: Size<Pixels>,
|
||||
child_bounds: Vec<Bounds<Pixels>>,
|
||||
scroll_to_bottom: bool,
|
||||
overflow: Point<Overflow>,
|
||||
@@ -2948,6 +2978,11 @@ impl ScrollHandle {
|
||||
*self.0.borrow().offset.borrow()
|
||||
}
|
||||
|
||||
/// Get the maximum scroll offset.
|
||||
pub fn max_offset(&self) -> Size<Pixels> {
|
||||
self.0.borrow().max_offset
|
||||
}
|
||||
|
||||
/// Get the top child that's scrolled into view.
|
||||
pub fn top_item(&self) -> usize {
|
||||
let state = self.0.borrow();
|
||||
@@ -2982,11 +3017,6 @@ impl ScrollHandle {
|
||||
self.0.borrow().child_bounds.get(ix).cloned()
|
||||
}
|
||||
|
||||
/// Get the size of the content with padding of the container.
|
||||
pub fn padded_content_size(&self) -> Size<Pixels> {
|
||||
self.0.borrow().padded_content_size
|
||||
}
|
||||
|
||||
/// scroll_to_item scrolls the minimal amount to ensure that the child is
|
||||
/// fully visible
|
||||
pub fn scroll_to_item(&self, ix: usize) {
|
||||
|
||||
@@ -411,9 +411,9 @@ impl ListState {
|
||||
self.0.borrow_mut().set_offset_from_scrollbar(point);
|
||||
}
|
||||
|
||||
/// Returns the size of items we have measured.
|
||||
/// Returns the maximum scroll offset according to the items we have measured.
|
||||
/// This value remains constant while dragging to prevent the scrollbar from moving away unexpectedly.
|
||||
pub fn content_size_for_scrollbar(&self) -> Size<Pixels> {
|
||||
pub fn max_offset_for_scrollbar(&self) -> Size<Pixels> {
|
||||
let state = self.0.borrow();
|
||||
let bounds = state.last_layout_bounds.unwrap_or_default();
|
||||
|
||||
@@ -421,7 +421,7 @@ impl ListState {
|
||||
.scrollbar_drag_start_height
|
||||
.unwrap_or_else(|| state.items.summary().height);
|
||||
|
||||
Size::new(bounds.size.width, height)
|
||||
Size::new(Pixels::ZERO, Pixels::ZERO.max(height - bounds.size.height))
|
||||
}
|
||||
|
||||
/// Returns the current scroll offset adjusted for the scrollbar
|
||||
|
||||
@@ -95,6 +95,7 @@ mod style;
|
||||
mod styled;
|
||||
mod subscription;
|
||||
mod svg_renderer;
|
||||
mod tab_stop;
|
||||
mod taffy;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
@@ -151,6 +152,7 @@ pub use style::*;
|
||||
pub use styled::*;
|
||||
pub use subscription::*;
|
||||
use svg_renderer::*;
|
||||
pub(crate) use tab_stop::*;
|
||||
pub use taffy::{AvailableSpace, LayoutId};
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use test::*;
|
||||
@@ -197,6 +199,11 @@ pub trait AppContext {
|
||||
where
|
||||
T: 'static;
|
||||
|
||||
/// Update a entity in the app context.
|
||||
fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> Self::Result<GpuiBorrow<'a, T>>
|
||||
where
|
||||
T: 'static;
|
||||
|
||||
/// Read a entity from the app context.
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
|
||||
@@ -85,7 +85,7 @@ pub(crate) use test::*;
|
||||
pub(crate) use windows::*;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use test::{TestDispatcher, TestScreenCaptureSource};
|
||||
pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream};
|
||||
|
||||
/// Returns a background executor for the current platform.
|
||||
pub fn background_executor() -> BackgroundExecutor {
|
||||
@@ -189,13 +189,12 @@ pub(crate) trait Platform: 'static {
|
||||
false
|
||||
}
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>>;
|
||||
fn screen_capture_sources(&self)
|
||||
-> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>>;
|
||||
#[cfg(not(feature = "screen-capture"))]
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> oneshot::Receiver<anyhow::Result<Vec<Box<dyn ScreenCaptureSource>>>> {
|
||||
) -> oneshot::Receiver<anyhow::Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||
let (sources_tx, sources_rx) = oneshot::channel();
|
||||
sources_tx
|
||||
.send(Err(anyhow::anyhow!(
|
||||
@@ -293,10 +292,23 @@ pub trait PlatformDisplay: Send + Sync + Debug {
|
||||
}
|
||||
}
|
||||
|
||||
/// Metadata for a given [ScreenCaptureSource]
|
||||
#[derive(Clone)]
|
||||
pub struct SourceMetadata {
|
||||
/// Opaque identifier of this screen.
|
||||
pub id: u64,
|
||||
/// Human-readable label for this source.
|
||||
pub label: Option<SharedString>,
|
||||
/// Whether this source is the main display.
|
||||
pub is_main: Option<bool>,
|
||||
/// Video resolution of this source.
|
||||
pub resolution: Size<DevicePixels>,
|
||||
}
|
||||
|
||||
/// A source of on-screen video content that can be captured.
|
||||
pub trait ScreenCaptureSource {
|
||||
/// Returns the video resolution of this source.
|
||||
fn resolution(&self) -> Result<Size<DevicePixels>>;
|
||||
/// Returns metadata for this source.
|
||||
fn metadata(&self) -> Result<SourceMetadata>;
|
||||
|
||||
/// Start capture video from this source, invoking the given callback
|
||||
/// with each frame.
|
||||
@@ -308,7 +320,10 @@ pub trait ScreenCaptureSource {
|
||||
}
|
||||
|
||||
/// A video stream captured from a screen.
|
||||
pub trait ScreenCaptureStream {}
|
||||
pub trait ScreenCaptureStream {
|
||||
/// Returns metadata for this source.
|
||||
fn metadata(&self) -> Result<SourceMetadata>;
|
||||
}
|
||||
|
||||
/// A frame of video captured from a screen.
|
||||
pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
|
||||
|
||||
@@ -73,7 +73,7 @@ impl LinuxClient for HeadlessClient {
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Box<dyn crate::ScreenCaptureSource>>>>
|
||||
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>>
|
||||
{
|
||||
let (mut tx, rx) = futures::channel::oneshot::channel();
|
||||
tx.send(Err(anyhow::anyhow!(
|
||||
|
||||
@@ -56,7 +56,7 @@ pub trait LinuxClient {
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> oneshot::Receiver<Result<Vec<Box<dyn crate::ScreenCaptureSource>>>>;
|
||||
) -> oneshot::Receiver<Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>>;
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
@@ -245,7 +245,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> oneshot::Receiver<Result<Vec<Box<dyn crate::ScreenCaptureSource>>>> {
|
||||
) -> oneshot::Receiver<Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>> {
|
||||
self.screen_capture_sources()
|
||||
}
|
||||
|
||||
@@ -828,6 +828,13 @@ impl crate::Keystroke {
|
||||
Keysym::Delete => "delete".to_owned(),
|
||||
Keysym::Escape => "escape".to_owned(),
|
||||
|
||||
Keysym::Left => "left".to_owned(),
|
||||
Keysym::Right => "right".to_owned(),
|
||||
Keysym::Up => "up".to_owned(),
|
||||
Keysym::Down => "down".to_owned(),
|
||||
Keysym::Home => "home".to_owned(),
|
||||
Keysym::End => "end".to_owned(),
|
||||
|
||||
_ => {
|
||||
let name = xkb::keysym_get_name(key_sym).to_lowercase();
|
||||
if key_sym.is_keypad_key() {
|
||||
|
||||
@@ -672,7 +672,7 @@ impl LinuxClient for WaylandClient {
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Box<dyn crate::ScreenCaptureSource>>>>
|
||||
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>>
|
||||
{
|
||||
// TODO: Get screen capture working on wayland. Be sure to try window resizing as that may
|
||||
// be tricky.
|
||||
|
||||
@@ -76,6 +76,7 @@ struct InProgressConfigure {
|
||||
size: Option<Size<Pixels>>,
|
||||
fullscreen: bool,
|
||||
maximized: bool,
|
||||
resizing: bool,
|
||||
tiling: Tiling,
|
||||
}
|
||||
|
||||
@@ -107,6 +108,7 @@ pub struct WaylandWindowState {
|
||||
active: bool,
|
||||
hovered: bool,
|
||||
in_progress_configure: Option<InProgressConfigure>,
|
||||
resize_throttle: bool,
|
||||
in_progress_window_controls: Option<WindowControls>,
|
||||
window_controls: WindowControls,
|
||||
inset: Option<Pixels>,
|
||||
@@ -176,6 +178,7 @@ impl WaylandWindowState {
|
||||
tiling: Tiling::default(),
|
||||
window_bounds: options.bounds,
|
||||
in_progress_configure: None,
|
||||
resize_throttle: false,
|
||||
client,
|
||||
appearance,
|
||||
handle,
|
||||
@@ -335,6 +338,7 @@ impl WaylandWindowStatePtr {
|
||||
pub fn frame(&self) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.surface.frame(&state.globals.qh, state.surface.id());
|
||||
state.resize_throttle = false;
|
||||
drop(state);
|
||||
|
||||
let mut cb = self.callbacks.borrow_mut();
|
||||
@@ -366,6 +370,12 @@ impl WaylandWindowStatePtr {
|
||||
state.fullscreen = configure.fullscreen;
|
||||
state.maximized = configure.maximized;
|
||||
state.tiling = configure.tiling;
|
||||
// Limit interactive resizes to once per vblank
|
||||
if configure.resizing && state.resize_throttle {
|
||||
return;
|
||||
} else if configure.resizing {
|
||||
state.resize_throttle = true;
|
||||
}
|
||||
if !configure.fullscreen && !configure.maximized {
|
||||
configure.size = if got_unmaximized {
|
||||
Some(state.window_bounds.size)
|
||||
@@ -472,6 +482,7 @@ impl WaylandWindowStatePtr {
|
||||
let mut tiling = Tiling::default();
|
||||
let mut fullscreen = false;
|
||||
let mut maximized = false;
|
||||
let mut resizing = false;
|
||||
|
||||
for state in states {
|
||||
match state {
|
||||
@@ -481,6 +492,7 @@ impl WaylandWindowStatePtr {
|
||||
xdg_toplevel::State::Fullscreen => {
|
||||
fullscreen = true;
|
||||
}
|
||||
xdg_toplevel::State::Resizing => resizing = true,
|
||||
xdg_toplevel::State::TiledTop => {
|
||||
tiling.top = true;
|
||||
}
|
||||
@@ -508,6 +520,7 @@ impl WaylandWindowStatePtr {
|
||||
size,
|
||||
fullscreen,
|
||||
maximized,
|
||||
resizing,
|
||||
tiling,
|
||||
});
|
||||
|
||||
|
||||
@@ -1448,7 +1448,7 @@ impl LinuxClient for X11Client {
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Box<dyn crate::ScreenCaptureSource>>>>
|
||||
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>>
|
||||
{
|
||||
crate::platform::scap_screen_capture::scap_screen_sources(
|
||||
&self.0.borrow().common.foreground_executor,
|
||||
|
||||
@@ -583,7 +583,7 @@ impl Platform for MacPlatform {
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> oneshot::Receiver<Result<Vec<Box<dyn crate::ScreenCaptureSource>>>> {
|
||||
) -> oneshot::Receiver<Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>> {
|
||||
super::screen_capture::get_sources()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
DevicePixels, ForegroundExecutor, Size,
|
||||
DevicePixels, ForegroundExecutor, SharedString, SourceMetadata,
|
||||
platform::{ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream},
|
||||
size,
|
||||
};
|
||||
@@ -7,8 +7,9 @@ use anyhow::{Result, anyhow};
|
||||
use block::ConcreteBlock;
|
||||
use cocoa::{
|
||||
base::{YES, id, nil},
|
||||
foundation::NSArray,
|
||||
foundation::{NSArray, NSString},
|
||||
};
|
||||
use collections::HashMap;
|
||||
use core_foundation::base::TCFType;
|
||||
use core_graphics::display::{
|
||||
CGDirectDisplayID, CGDisplayCopyDisplayMode, CGDisplayModeGetPixelHeight,
|
||||
@@ -32,11 +33,13 @@ use super::NSStringExt;
|
||||
#[derive(Clone)]
|
||||
pub struct MacScreenCaptureSource {
|
||||
sc_display: id,
|
||||
meta: Option<ScreenMeta>,
|
||||
}
|
||||
|
||||
pub struct MacScreenCaptureStream {
|
||||
sc_stream: id,
|
||||
sc_stream_output: id,
|
||||
meta: SourceMetadata,
|
||||
}
|
||||
|
||||
static mut DELEGATE_CLASS: *const Class = ptr::null();
|
||||
@@ -47,19 +50,31 @@ const FRAME_CALLBACK_IVAR: &str = "frame_callback";
|
||||
const SCStreamOutputTypeScreen: NSInteger = 0;
|
||||
|
||||
impl ScreenCaptureSource for MacScreenCaptureSource {
|
||||
fn resolution(&self) -> Result<Size<DevicePixels>> {
|
||||
unsafe {
|
||||
fn metadata(&self) -> Result<SourceMetadata> {
|
||||
let (display_id, size) = unsafe {
|
||||
let display_id: CGDirectDisplayID = msg_send![self.sc_display, displayID];
|
||||
let display_mode_ref = CGDisplayCopyDisplayMode(display_id);
|
||||
let width = CGDisplayModeGetPixelWidth(display_mode_ref);
|
||||
let height = CGDisplayModeGetPixelHeight(display_mode_ref);
|
||||
CGDisplayModeRelease(display_mode_ref);
|
||||
|
||||
Ok(size(
|
||||
DevicePixels(width as i32),
|
||||
DevicePixels(height as i32),
|
||||
))
|
||||
}
|
||||
(
|
||||
display_id,
|
||||
size(DevicePixels(width as i32), DevicePixels(height as i32)),
|
||||
)
|
||||
};
|
||||
let (label, is_main) = self
|
||||
.meta
|
||||
.clone()
|
||||
.map(|meta| (meta.label, meta.is_main))
|
||||
.unzip();
|
||||
|
||||
Ok(SourceMetadata {
|
||||
id: display_id as u64,
|
||||
label,
|
||||
is_main,
|
||||
resolution: size,
|
||||
})
|
||||
}
|
||||
|
||||
fn stream(
|
||||
@@ -89,9 +104,9 @@ impl ScreenCaptureSource for MacScreenCaptureSource {
|
||||
Box::into_raw(Box::new(frame_callback)) as *mut c_void,
|
||||
);
|
||||
|
||||
let resolution = self.resolution().unwrap();
|
||||
let _: id = msg_send![configuration, setWidth: resolution.width.0 as i64];
|
||||
let _: id = msg_send![configuration, setHeight: resolution.height.0 as i64];
|
||||
let meta = self.metadata().unwrap();
|
||||
let _: id = msg_send![configuration, setWidth: meta.resolution.width.0 as i64];
|
||||
let _: id = msg_send![configuration, setHeight: meta.resolution.height.0 as i64];
|
||||
let stream: id = msg_send![stream, initWithFilter:filter configuration:configuration delegate:delegate];
|
||||
|
||||
let (mut tx, rx) = oneshot::channel();
|
||||
@@ -110,6 +125,7 @@ impl ScreenCaptureSource for MacScreenCaptureSource {
|
||||
move |error: id| {
|
||||
let result = if error == nil {
|
||||
let stream = MacScreenCaptureStream {
|
||||
meta: meta.clone(),
|
||||
sc_stream: stream,
|
||||
sc_stream_output: output,
|
||||
};
|
||||
@@ -138,7 +154,11 @@ impl Drop for MacScreenCaptureSource {
|
||||
}
|
||||
}
|
||||
|
||||
impl ScreenCaptureStream for MacScreenCaptureStream {}
|
||||
impl ScreenCaptureStream for MacScreenCaptureStream {
|
||||
fn metadata(&self) -> Result<SourceMetadata> {
|
||||
Ok(self.meta.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MacScreenCaptureStream {
|
||||
fn drop(&mut self) {
|
||||
@@ -164,24 +184,74 @@ impl Drop for MacScreenCaptureStream {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_sources() -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
|
||||
#[derive(Clone)]
|
||||
struct ScreenMeta {
|
||||
label: SharedString,
|
||||
// Is this the screen with menu bar?
|
||||
is_main: bool,
|
||||
}
|
||||
|
||||
unsafe fn screen_id_to_human_label() -> HashMap<CGDirectDisplayID, ScreenMeta> {
|
||||
let screens: id = msg_send![class!(NSScreen), screens];
|
||||
let count: usize = msg_send![screens, count];
|
||||
let mut map = HashMap::default();
|
||||
let screen_number_key = unsafe { NSString::alloc(nil).init_str("NSScreenNumber") };
|
||||
for i in 0..count {
|
||||
let screen: id = msg_send![screens, objectAtIndex: i];
|
||||
let device_desc: id = msg_send![screen, deviceDescription];
|
||||
if device_desc == nil {
|
||||
continue;
|
||||
}
|
||||
|
||||
let nsnumber: id = msg_send![device_desc, objectForKey: screen_number_key];
|
||||
if nsnumber == nil {
|
||||
continue;
|
||||
}
|
||||
|
||||
let screen_id: u32 = msg_send![nsnumber, unsignedIntValue];
|
||||
|
||||
let name: id = msg_send![screen, localizedName];
|
||||
if name != nil {
|
||||
let cstr: *const std::os::raw::c_char = msg_send![name, UTF8String];
|
||||
let rust_str = unsafe {
|
||||
std::ffi::CStr::from_ptr(cstr)
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
};
|
||||
map.insert(
|
||||
screen_id,
|
||||
ScreenMeta {
|
||||
label: rust_str.into(),
|
||||
is_main: i == 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
pub(crate) fn get_sources() -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||
unsafe {
|
||||
let (mut tx, rx) = oneshot::channel();
|
||||
let tx = Rc::new(RefCell::new(Some(tx)));
|
||||
|
||||
let screen_id_to_label = screen_id_to_human_label();
|
||||
let block = ConcreteBlock::new(move |shareable_content: id, error: id| {
|
||||
let Some(mut tx) = tx.borrow_mut().take() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let result = if error == nil {
|
||||
let displays: id = msg_send![shareable_content, displays];
|
||||
let mut result = Vec::new();
|
||||
for i in 0..displays.count() {
|
||||
let display = displays.objectAtIndex(i);
|
||||
let id: CGDirectDisplayID = msg_send![display, displayID];
|
||||
let meta = screen_id_to_label.get(&id).cloned();
|
||||
let source = MacScreenCaptureSource {
|
||||
sc_display: msg_send![display, retain],
|
||||
meta,
|
||||
};
|
||||
result.push(Box::new(source) as Box<dyn ScreenCaptureSource>);
|
||||
result.push(Rc::new(source) as Rc<dyn ScreenCaptureSource>);
|
||||
}
|
||||
Ok(result)
|
||||
} else {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
//! Screen capture for Linux and Windows
|
||||
use crate::{
|
||||
DevicePixels, ForegroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
|
||||
Size, size,
|
||||
Size, SourceMetadata, size,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use futures::channel::oneshot;
|
||||
use scap::Target;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{self, AtomicBool};
|
||||
|
||||
@@ -15,7 +17,7 @@ use std::sync::atomic::{self, AtomicBool};
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn scap_screen_sources(
|
||||
foreground_executor: &ForegroundExecutor,
|
||||
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
|
||||
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||
let (sources_tx, sources_rx) = oneshot::channel();
|
||||
get_screen_targets(sources_tx);
|
||||
to_dyn_screen_capture_sources(sources_rx, foreground_executor)
|
||||
@@ -29,14 +31,14 @@ pub(crate) fn scap_screen_sources(
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn start_scap_default_target_source(
|
||||
foreground_executor: &ForegroundExecutor,
|
||||
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
|
||||
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||
let (sources_tx, sources_rx) = oneshot::channel();
|
||||
start_default_target_screen_capture(sources_tx);
|
||||
to_dyn_screen_capture_sources(sources_rx, foreground_executor)
|
||||
}
|
||||
|
||||
struct ScapCaptureSource {
|
||||
target: scap::Target,
|
||||
target: scap::Display,
|
||||
size: Size<DevicePixels>,
|
||||
}
|
||||
|
||||
@@ -52,7 +54,7 @@ fn get_screen_targets(sources_tx: oneshot::Sender<Result<Vec<ScapCaptureSource>>
|
||||
}
|
||||
};
|
||||
let sources = targets
|
||||
.iter()
|
||||
.into_iter()
|
||||
.filter_map(|target| match target {
|
||||
scap::Target::Display(display) => {
|
||||
let size = Size {
|
||||
@@ -60,7 +62,7 @@ fn get_screen_targets(sources_tx: oneshot::Sender<Result<Vec<ScapCaptureSource>>
|
||||
height: DevicePixels(display.height as i32),
|
||||
};
|
||||
Some(ScapCaptureSource {
|
||||
target: target.clone(),
|
||||
target: display,
|
||||
size,
|
||||
})
|
||||
}
|
||||
@@ -72,8 +74,13 @@ fn get_screen_targets(sources_tx: oneshot::Sender<Result<Vec<ScapCaptureSource>>
|
||||
}
|
||||
|
||||
impl ScreenCaptureSource for ScapCaptureSource {
|
||||
fn resolution(&self) -> Result<Size<DevicePixels>> {
|
||||
Ok(self.size)
|
||||
fn metadata(&self) -> Result<SourceMetadata> {
|
||||
Ok(SourceMetadata {
|
||||
resolution: self.size,
|
||||
label: Some(self.target.title.clone().into()),
|
||||
is_main: None,
|
||||
id: self.target.id as u64,
|
||||
})
|
||||
}
|
||||
|
||||
fn stream(
|
||||
@@ -85,13 +92,15 @@ impl ScreenCaptureSource for ScapCaptureSource {
|
||||
let target = self.target.clone();
|
||||
|
||||
// Due to use of blocking APIs, a dedicated thread is used.
|
||||
std::thread::spawn(move || match new_scap_capturer(Some(target)) {
|
||||
Ok(mut capturer) => {
|
||||
capturer.start_capture();
|
||||
run_capture(capturer, frame_callback, stream_tx);
|
||||
}
|
||||
Err(e) => {
|
||||
stream_tx.send(Err(e)).ok();
|
||||
std::thread::spawn(move || {
|
||||
match new_scap_capturer(Some(scap::Target::Display(target.clone()))) {
|
||||
Ok(mut capturer) => {
|
||||
capturer.start_capture();
|
||||
run_capture(capturer, target.clone(), frame_callback, stream_tx);
|
||||
}
|
||||
Err(e) => {
|
||||
stream_tx.send(Err(e)).ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -107,6 +116,7 @@ struct ScapDefaultTargetCaptureSource {
|
||||
// Callback for frames.
|
||||
Box<dyn Fn(ScreenCaptureFrame) + Send>,
|
||||
)>,
|
||||
target: scap::Display,
|
||||
size: Size<DevicePixels>,
|
||||
}
|
||||
|
||||
@@ -123,33 +133,48 @@ fn start_default_target_screen_capture(
|
||||
.get_next_frame()
|
||||
.context("Failed to get first frame of screenshare to get the size.")?;
|
||||
let size = frame_size(&first_frame);
|
||||
Ok((capturer, size))
|
||||
let target = capturer
|
||||
.target()
|
||||
.context("Unable to determine the target display.")?;
|
||||
let target = target.clone();
|
||||
Ok((capturer, size, target))
|
||||
});
|
||||
|
||||
match start_result {
|
||||
Err(e) => {
|
||||
sources_tx.send(Err(e)).ok();
|
||||
}
|
||||
Ok((capturer, size)) => {
|
||||
Ok((capturer, size, Target::Display(display))) => {
|
||||
let (stream_call_tx, stream_rx) = std::sync::mpsc::sync_channel(1);
|
||||
sources_tx
|
||||
.send(Ok(vec![ScapDefaultTargetCaptureSource {
|
||||
stream_call_tx,
|
||||
size,
|
||||
target: display.clone(),
|
||||
}]))
|
||||
.ok();
|
||||
let Ok((stream_tx, frame_callback)) = stream_rx.recv() else {
|
||||
return;
|
||||
};
|
||||
run_capture(capturer, frame_callback, stream_tx);
|
||||
run_capture(capturer, display, frame_callback, stream_tx);
|
||||
}
|
||||
Err(e) => {
|
||||
sources_tx.send(Err(e)).ok();
|
||||
}
|
||||
_ => {
|
||||
sources_tx
|
||||
.send(Err(anyhow!("The screen capture source is not a display")))
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
impl ScreenCaptureSource for ScapDefaultTargetCaptureSource {
|
||||
fn resolution(&self) -> Result<Size<DevicePixels>> {
|
||||
Ok(self.size)
|
||||
fn metadata(&self) -> Result<SourceMetadata> {
|
||||
Ok(SourceMetadata {
|
||||
resolution: self.size,
|
||||
label: None,
|
||||
is_main: None,
|
||||
id: self.target.id as u64,
|
||||
})
|
||||
}
|
||||
|
||||
fn stream(
|
||||
@@ -189,12 +214,19 @@ fn new_scap_capturer(target: Option<scap::Target>) -> Result<scap::capturer::Cap
|
||||
|
||||
fn run_capture(
|
||||
mut capturer: scap::capturer::Capturer,
|
||||
display: scap::Display,
|
||||
frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
|
||||
stream_tx: oneshot::Sender<Result<ScapStream>>,
|
||||
) {
|
||||
let cancel_stream = Arc::new(AtomicBool::new(false));
|
||||
let size = Size {
|
||||
width: DevicePixels(display.width as i32),
|
||||
height: DevicePixels(display.height as i32),
|
||||
};
|
||||
let stream_send_result = stream_tx.send(Ok(ScapStream {
|
||||
cancel_stream: cancel_stream.clone(),
|
||||
display,
|
||||
size,
|
||||
}));
|
||||
if let Err(_) = stream_send_result {
|
||||
return;
|
||||
@@ -213,9 +245,20 @@ fn run_capture(
|
||||
|
||||
struct ScapStream {
|
||||
cancel_stream: Arc<AtomicBool>,
|
||||
display: scap::Display,
|
||||
size: Size<DevicePixels>,
|
||||
}
|
||||
|
||||
impl ScreenCaptureStream for ScapStream {}
|
||||
impl ScreenCaptureStream for ScapStream {
|
||||
fn metadata(&self) -> Result<SourceMetadata> {
|
||||
Ok(SourceMetadata {
|
||||
resolution: self.size,
|
||||
label: Some(self.display.title.clone().into()),
|
||||
is_main: None,
|
||||
id: self.display.id as u64,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ScapStream {
|
||||
fn drop(&mut self) {
|
||||
@@ -237,12 +280,12 @@ fn frame_size(frame: &scap::frame::Frame) -> Size<DevicePixels> {
|
||||
}
|
||||
|
||||
/// This is used by `get_screen_targets` and `start_default_target_screen_capture` to turn their
|
||||
/// results into `Box<dyn ScreenCaptureSource>`. They need to `Send` their capture source, and so
|
||||
/// the capture source structs are used as `Box<dyn ScreenCaptureSource>` is not `Send`.
|
||||
/// results into `Rc<dyn ScreenCaptureSource>`. They need to `Send` their capture source, and so
|
||||
/// the capture source structs are used as `Rc<dyn ScreenCaptureSource>` is not `Send`.
|
||||
fn to_dyn_screen_capture_sources<T: ScreenCaptureSource + 'static>(
|
||||
sources_rx: oneshot::Receiver<Result<Vec<T>>>,
|
||||
foreground_executor: &ForegroundExecutor,
|
||||
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
|
||||
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||
let (dyn_sources_tx, dyn_sources_rx) = oneshot::channel();
|
||||
foreground_executor
|
||||
.spawn(async move {
|
||||
@@ -250,7 +293,7 @@ fn to_dyn_screen_capture_sources<T: ScreenCaptureSource + 'static>(
|
||||
Ok(Ok(results)) => dyn_sources_tx
|
||||
.send(Ok(results
|
||||
.into_iter()
|
||||
.map(|source| Box::new(source) as Box<dyn ScreenCaptureSource>)
|
||||
.map(|source| Rc::new(source) as Rc<dyn ScreenCaptureSource>)
|
||||
.collect::<Vec<_>>()))
|
||||
.ok(),
|
||||
Ok(Err(err)) => dyn_sources_tx.send(Err(err)).ok(),
|
||||
|
||||
@@ -8,4 +8,4 @@ pub(crate) use display::*;
|
||||
pub(crate) use platform::*;
|
||||
pub(crate) use window::*;
|
||||
|
||||
pub use platform::TestScreenCaptureSource;
|
||||
pub use platform::{TestScreenCaptureSource, TestScreenCaptureStream};
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
||||
ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
|
||||
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
|
||||
Size, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
||||
SourceMetadata, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::VecDeque;
|
||||
@@ -44,11 +44,17 @@ pub(crate) struct TestPlatform {
|
||||
/// A fake screen capture source, used for testing.
|
||||
pub struct TestScreenCaptureSource {}
|
||||
|
||||
/// A fake screen capture stream, used for testing.
|
||||
pub struct TestScreenCaptureStream {}
|
||||
|
||||
impl ScreenCaptureSource for TestScreenCaptureSource {
|
||||
fn resolution(&self) -> Result<Size<DevicePixels>> {
|
||||
Ok(size(DevicePixels(1), DevicePixels(1)))
|
||||
fn metadata(&self) -> Result<SourceMetadata> {
|
||||
Ok(SourceMetadata {
|
||||
id: 0,
|
||||
is_main: None,
|
||||
label: None,
|
||||
resolution: size(DevicePixels(1), DevicePixels(1)),
|
||||
})
|
||||
}
|
||||
|
||||
fn stream(
|
||||
@@ -64,7 +70,11 @@ impl ScreenCaptureSource for TestScreenCaptureSource {
|
||||
}
|
||||
}
|
||||
|
||||
impl ScreenCaptureStream for TestScreenCaptureStream {}
|
||||
impl ScreenCaptureStream for TestScreenCaptureStream {
|
||||
fn metadata(&self) -> Result<SourceMetadata> {
|
||||
TestScreenCaptureSource {}.metadata()
|
||||
}
|
||||
}
|
||||
|
||||
struct TestPrompt {
|
||||
msg: String,
|
||||
@@ -271,13 +281,13 @@ impl Platform for TestPlatform {
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
|
||||
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||
let (mut tx, rx) = oneshot::channel();
|
||||
tx.send(Ok(self
|
||||
.screen_capture_sources
|
||||
.borrow()
|
||||
.iter()
|
||||
.map(|source| Box::new(source.clone()) as Box<dyn ScreenCaptureSource>)
|
||||
.map(|source| Rc::new(source.clone()) as Rc<dyn ScreenCaptureSource>)
|
||||
.collect()))
|
||||
.ok();
|
||||
rx
|
||||
|
||||
@@ -440,7 +440,7 @@ impl Platform for WindowsPlatform {
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
|
||||
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||
crate::platform::scap_screen_capture::scap_screen_sources(&self.foreground_executor)
|
||||
}
|
||||
|
||||
|
||||
157
crates/gpui/src/tab_stop.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
use crate::{FocusHandle, FocusId};
|
||||
|
||||
/// Represents a collection of tab handles.
|
||||
///
|
||||
/// Used to manage the `Tab` event to switch between focus handles.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct TabHandles {
|
||||
handles: Vec<FocusHandle>,
|
||||
}
|
||||
|
||||
impl TabHandles {
|
||||
pub(crate) fn insert(&mut self, focus_handle: &FocusHandle) {
|
||||
if !focus_handle.tab_stop {
|
||||
return;
|
||||
}
|
||||
|
||||
let focus_handle = focus_handle.clone();
|
||||
|
||||
// Insert handle with same tab_index last
|
||||
if let Some(ix) = self
|
||||
.handles
|
||||
.iter()
|
||||
.position(|tab| tab.tab_index > focus_handle.tab_index)
|
||||
{
|
||||
self.handles.insert(ix, focus_handle);
|
||||
} else {
|
||||
self.handles.push(focus_handle);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clear(&mut self) {
|
||||
self.handles.clear();
|
||||
}
|
||||
|
||||
fn current_index(&self, focused_id: Option<&FocusId>) -> usize {
|
||||
self.handles
|
||||
.iter()
|
||||
.position(|h| Some(&h.id) == focused_id)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn next(&self, focused_id: Option<&FocusId>) -> Option<FocusHandle> {
|
||||
let ix = self.current_index(focused_id);
|
||||
|
||||
let mut next_ix = ix + 1;
|
||||
if next_ix + 1 > self.handles.len() {
|
||||
next_ix = 0;
|
||||
}
|
||||
|
||||
if let Some(next_handle) = self.handles.get(next_ix) {
|
||||
Some(next_handle.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn prev(&self, focused_id: Option<&FocusId>) -> Option<FocusHandle> {
|
||||
let ix = self.current_index(focused_id);
|
||||
let prev_ix;
|
||||
if ix == 0 {
|
||||
prev_ix = self.handles.len().saturating_sub(1);
|
||||
} else {
|
||||
prev_ix = ix.saturating_sub(1);
|
||||
}
|
||||
|
||||
if let Some(prev_handle) = self.handles.get(prev_ix) {
|
||||
Some(prev_handle.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{FocusHandle, FocusMap, TabHandles};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn test_tab_handles() {
|
||||
let focus_map = Arc::new(FocusMap::default());
|
||||
let mut tab = TabHandles::default();
|
||||
|
||||
let focus_handles = vec![
|
||||
FocusHandle::new(&focus_map).tab_stop(true).tab_index(0),
|
||||
FocusHandle::new(&focus_map).tab_stop(true).tab_index(1),
|
||||
FocusHandle::new(&focus_map).tab_stop(true).tab_index(1),
|
||||
FocusHandle::new(&focus_map),
|
||||
FocusHandle::new(&focus_map).tab_index(2),
|
||||
FocusHandle::new(&focus_map).tab_stop(true).tab_index(0),
|
||||
FocusHandle::new(&focus_map).tab_stop(true).tab_index(2),
|
||||
];
|
||||
|
||||
for handle in focus_handles.iter() {
|
||||
tab.insert(&handle);
|
||||
}
|
||||
assert_eq!(
|
||||
tab.handles
|
||||
.iter()
|
||||
.map(|handle| handle.id)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
focus_handles[0].id,
|
||||
focus_handles[5].id,
|
||||
focus_handles[1].id,
|
||||
focus_handles[2].id,
|
||||
focus_handles[6].id,
|
||||
]
|
||||
);
|
||||
|
||||
// next
|
||||
assert_eq!(tab.next(None), Some(tab.handles[1].clone()));
|
||||
assert_eq!(
|
||||
tab.next(Some(&tab.handles[0].id)),
|
||||
Some(tab.handles[1].clone())
|
||||
);
|
||||
assert_eq!(
|
||||
tab.next(Some(&tab.handles[1].id)),
|
||||
Some(tab.handles[2].clone())
|
||||
);
|
||||
assert_eq!(
|
||||
tab.next(Some(&tab.handles[2].id)),
|
||||
Some(tab.handles[3].clone())
|
||||
);
|
||||
assert_eq!(
|
||||
tab.next(Some(&tab.handles[3].id)),
|
||||
Some(tab.handles[4].clone())
|
||||
);
|
||||
assert_eq!(
|
||||
tab.next(Some(&tab.handles[4].id)),
|
||||
Some(tab.handles[0].clone())
|
||||
);
|
||||
|
||||
// prev
|
||||
assert_eq!(tab.prev(None), Some(tab.handles[4].clone()));
|
||||
assert_eq!(
|
||||
tab.prev(Some(&tab.handles[0].id)),
|
||||
Some(tab.handles[4].clone())
|
||||
);
|
||||
assert_eq!(
|
||||
tab.prev(Some(&tab.handles[1].id)),
|
||||
Some(tab.handles[0].clone())
|
||||
);
|
||||
assert_eq!(
|
||||
tab.prev(Some(&tab.handles[2].id)),
|
||||
Some(tab.handles[1].clone())
|
||||
);
|
||||
assert_eq!(
|
||||
tab.prev(Some(&tab.handles[3].id)),
|
||||
Some(tab.handles[2].clone())
|
||||
);
|
||||
assert_eq!(
|
||||
tab.prev(Some(&tab.handles[4].id)),
|
||||
Some(tab.handles[3].clone())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -182,7 +182,7 @@ impl TaffyLayoutEngine {
|
||||
.compute_layout_with_measure(
|
||||
id.into(),
|
||||
available_space.into(),
|
||||
|known_dimensions, available_space, _id, node_context| {
|
||||
|known_dimensions, available_space, _id, node_context, _style| {
|
||||
let Some(node_context) = node_context else {
|
||||
return taffy::geometry::Size::default();
|
||||
};
|
||||
|
||||
@@ -12,10 +12,11 @@ use crate::{
|
||||
PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptButton, PromptLevel, Quad,
|
||||
Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge,
|
||||
SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, Size,
|
||||
StrikethroughStyle, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle,
|
||||
TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, WindowAppearance,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, WindowOptions,
|
||||
WindowParams, WindowTextSystem, point, prelude::*, px, rems, size, transparent_black,
|
||||
StrikethroughStyle, Style, SubscriberSet, Subscription, TabHandles, TaffyLayoutEngine, Task,
|
||||
TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle,
|
||||
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations,
|
||||
WindowOptions, WindowParams, WindowTextSystem, point, prelude::*, px, rems, size,
|
||||
transparent_black,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use collections::{FxHashMap, FxHashSet};
|
||||
@@ -222,7 +223,12 @@ impl ArenaClearNeeded {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type FocusMap = RwLock<SlotMap<FocusId, AtomicUsize>>;
|
||||
pub(crate) type FocusMap = RwLock<SlotMap<FocusId, FocusRef>>;
|
||||
pub(crate) struct FocusRef {
|
||||
pub(crate) ref_count: AtomicUsize,
|
||||
pub(crate) tab_index: isize,
|
||||
pub(crate) tab_stop: bool,
|
||||
}
|
||||
|
||||
impl FocusId {
|
||||
/// Obtains whether the element associated with this handle is currently focused.
|
||||
@@ -258,6 +264,10 @@ impl FocusId {
|
||||
pub struct FocusHandle {
|
||||
pub(crate) id: FocusId,
|
||||
handles: Arc<FocusMap>,
|
||||
/// The index of this element in the tab order.
|
||||
pub tab_index: isize,
|
||||
/// Whether this element can be focused by tab navigation.
|
||||
pub tab_stop: bool,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for FocusHandle {
|
||||
@@ -268,25 +278,54 @@ impl std::fmt::Debug for FocusHandle {
|
||||
|
||||
impl FocusHandle {
|
||||
pub(crate) fn new(handles: &Arc<FocusMap>) -> Self {
|
||||
let id = handles.write().insert(AtomicUsize::new(1));
|
||||
let id = handles.write().insert(FocusRef {
|
||||
ref_count: AtomicUsize::new(1),
|
||||
tab_index: 0,
|
||||
tab_stop: false,
|
||||
});
|
||||
|
||||
Self {
|
||||
id,
|
||||
tab_index: 0,
|
||||
tab_stop: false,
|
||||
handles: handles.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn for_id(id: FocusId, handles: &Arc<FocusMap>) -> Option<Self> {
|
||||
let lock = handles.read();
|
||||
let ref_count = lock.get(id)?;
|
||||
if atomic_incr_if_not_zero(ref_count) == 0 {
|
||||
let focus = lock.get(id)?;
|
||||
if atomic_incr_if_not_zero(&focus.ref_count) == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(Self {
|
||||
id,
|
||||
tab_index: focus.tab_index,
|
||||
tab_stop: focus.tab_stop,
|
||||
handles: handles.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets the tab index of the element associated with this handle.
|
||||
pub fn tab_index(mut self, index: isize) -> Self {
|
||||
self.tab_index = index;
|
||||
if let Some(focus) = self.handles.write().get_mut(self.id) {
|
||||
focus.tab_index = index;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether the element associated with this handle is a tab stop.
|
||||
///
|
||||
/// When `false`, the element will not be included in the tab order.
|
||||
pub fn tab_stop(mut self, tab_stop: bool) -> Self {
|
||||
self.tab_stop = tab_stop;
|
||||
if let Some(focus) = self.handles.write().get_mut(self.id) {
|
||||
focus.tab_stop = tab_stop;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Converts this focus handle into a weak variant, which does not prevent it from being released.
|
||||
pub fn downgrade(&self) -> WeakFocusHandle {
|
||||
WeakFocusHandle {
|
||||
@@ -354,6 +393,7 @@ impl Drop for FocusHandle {
|
||||
.read()
|
||||
.get(self.id)
|
||||
.unwrap()
|
||||
.ref_count
|
||||
.fetch_sub(1, SeqCst);
|
||||
}
|
||||
}
|
||||
@@ -642,6 +682,7 @@ pub(crate) struct Frame {
|
||||
pub(crate) next_inspector_instance_ids: FxHashMap<Rc<crate::InspectorElementPath>, usize>,
|
||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||
pub(crate) inspector_hitboxes: FxHashMap<HitboxId, crate::InspectorElementId>,
|
||||
pub(crate) tab_handles: TabHandles,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
@@ -689,6 +730,7 @@ impl Frame {
|
||||
|
||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||
inspector_hitboxes: FxHashMap::default(),
|
||||
tab_handles: TabHandles::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -704,6 +746,7 @@ impl Frame {
|
||||
self.hitboxes.clear();
|
||||
self.window_control_hitboxes.clear();
|
||||
self.deferred_draws.clear();
|
||||
self.tab_handles.clear();
|
||||
self.focus = None;
|
||||
|
||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||
@@ -1289,6 +1332,28 @@ impl Window {
|
||||
self.focus_enabled = false;
|
||||
}
|
||||
|
||||
/// Move focus to next tab stop.
|
||||
pub fn focus_next(&mut self) {
|
||||
if !self.focus_enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(handle) = self.rendered_frame.tab_handles.next(self.focus.as_ref()) {
|
||||
self.focus(&handle)
|
||||
}
|
||||
}
|
||||
|
||||
/// Move focus to previous tab stop.
|
||||
pub fn focus_prev(&mut self) {
|
||||
if !self.focus_enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(handle) = self.rendered_frame.tab_handles.prev(self.focus.as_ref()) {
|
||||
self.focus(&handle)
|
||||
}
|
||||
}
|
||||
|
||||
/// Accessor for the text system.
|
||||
pub fn text_system(&self) -> &Arc<WindowTextSystem> {
|
||||
&self.text_system
|
||||
@@ -2424,6 +2489,53 @@ impl Window {
|
||||
result
|
||||
}
|
||||
|
||||
/// Use a piece of state that exists as long this element is being rendered in consecutive frames.
|
||||
pub fn use_keyed_state<S: 'static>(
|
||||
&mut self,
|
||||
key: impl Into<ElementId>,
|
||||
cx: &mut App,
|
||||
init: impl FnOnce(&mut Self, &mut App) -> S,
|
||||
) -> Entity<S> {
|
||||
let current_view = self.current_view();
|
||||
self.with_global_id(key.into(), |global_id, window| {
|
||||
window.with_element_state(global_id, |state: Option<Entity<S>>, window| {
|
||||
if let Some(state) = state {
|
||||
(state.clone(), state)
|
||||
} else {
|
||||
let new_state = cx.new(|cx| init(window, cx));
|
||||
cx.observe(&new_state, move |_, cx| {
|
||||
cx.notify(current_view);
|
||||
})
|
||||
.detach();
|
||||
(new_state.clone(), new_state)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Immediately push an element ID onto the stack. Useful for simplifying IDs in lists
|
||||
pub fn with_id<R>(&mut self, id: impl Into<ElementId>, f: impl FnOnce(&mut Self) -> R) -> R {
|
||||
self.with_global_id(id.into(), |_, window| f(window))
|
||||
}
|
||||
|
||||
/// Use a piece of state that exists as long this element is being rendered in consecutive frames, without needing to specify a key
|
||||
///
|
||||
/// NOTE: This method uses the location of the caller to generate an ID for this state.
|
||||
/// If this is not sufficient to identify your state (e.g. you're rendering a list item),
|
||||
/// you can provide a custom ElementID using the `use_keyed_state` method.
|
||||
#[track_caller]
|
||||
pub fn use_state<S: 'static>(
|
||||
&mut self,
|
||||
cx: &mut App,
|
||||
init: impl FnOnce(&mut Self, &mut App) -> S,
|
||||
) -> Entity<S> {
|
||||
self.use_keyed_state(
|
||||
ElementId::CodeLocation(*core::panic::Location::caller()),
|
||||
cx,
|
||||
init,
|
||||
)
|
||||
}
|
||||
|
||||
/// Updates or initializes state for an element with the given id that lives across multiple
|
||||
/// frames. If an element with this ID existed in the rendered frame, its state will be passed
|
||||
/// to the given closure. The state returned by the closure will be stored so it can be referenced
|
||||
@@ -4577,6 +4689,8 @@ pub enum ElementId {
|
||||
NamedInteger(SharedString, u64),
|
||||
/// A path.
|
||||
Path(Arc<std::path::Path>),
|
||||
/// A code location.
|
||||
CodeLocation(core::panic::Location<'static>),
|
||||
}
|
||||
|
||||
impl ElementId {
|
||||
@@ -4596,6 +4710,7 @@ impl Display for ElementId {
|
||||
ElementId::NamedInteger(s, i) => write!(f, "{}-{}", s, i)?,
|
||||
ElementId::Uuid(uuid) => write!(f, "{}", uuid)?,
|
||||
ElementId::Path(path) => write!(f, "{}", path.display())?,
|
||||
ElementId::CodeLocation(location) => write!(f, "{}", location)?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -53,6 +53,16 @@ pub fn derive_app_context(input: TokenStream) -> TokenStream {
|
||||
self.#app_variable.update_entity(handle, update)
|
||||
}
|
||||
|
||||
fn as_mut<'y, 'z, T>(
|
||||
&'y mut self,
|
||||
handle: &'z gpui::Entity<T>,
|
||||
) -> Self::Result<gpui::GpuiBorrow<'y, T>>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.#app_variable.as_mut(handle)
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &gpui::Entity<T>,
|
||||
|
||||
@@ -181,6 +181,9 @@ pub enum IconName {
|
||||
MicMute,
|
||||
Microscope,
|
||||
Minimize,
|
||||
NewFromSummary,
|
||||
NewTextThread,
|
||||
NewThread,
|
||||
Option,
|
||||
PageDown,
|
||||
PageUp,
|
||||
@@ -256,6 +259,9 @@ pub enum IconName {
|
||||
TextSnippet,
|
||||
ThumbsDown,
|
||||
ThumbsUp,
|
||||
TodoComplete,
|
||||
TodoPending,
|
||||
TodoProgress,
|
||||
ToolBulb,
|
||||
ToolCopy,
|
||||
ToolDeleteFile,
|
||||
|
||||
@@ -206,8 +206,8 @@ impl LanguageModelRegistry {
|
||||
None
|
||||
}
|
||||
|
||||
/// Check that we have at least one provider that is authenticated.
|
||||
fn has_authenticated_provider(&self, cx: &App) -> bool {
|
||||
/// Returns `true` if at least one provider that is authenticated.
|
||||
pub fn has_authenticated_provider(&self, cx: &App) -> bool {
|
||||
self.providers.values().any(|p| p.is_authenticated(cx))
|
||||
}
|
||||
|
||||
|
||||
@@ -1140,19 +1140,19 @@ impl RenderOnce for ZedAiConfiguration {
|
||||
let is_pro = self.plan == Some(proto::Plan::ZedPro);
|
||||
let subscription_text = match (self.plan, self.subscription_period) {
|
||||
(Some(proto::Plan::ZedPro), Some(_)) => {
|
||||
"You have access to Zed's hosted LLMs through your Pro subscription."
|
||||
"You have access to Zed's hosted models through your Pro subscription."
|
||||
}
|
||||
(Some(proto::Plan::ZedProTrial), Some(_)) => {
|
||||
"You have access to Zed's hosted LLMs through your Pro trial."
|
||||
"You have access to Zed's hosted models through your Pro trial."
|
||||
}
|
||||
(Some(proto::Plan::Free), Some(_)) => {
|
||||
"You have basic access to Zed's hosted LLMs through the Free plan."
|
||||
"You have basic access to Zed's hosted models through the Free plan."
|
||||
}
|
||||
_ => {
|
||||
if self.eligible_for_trial {
|
||||
"Subscribe for access to Zed's hosted LLMs. Start with a 14 day free trial."
|
||||
"Subscribe for access to Zed's hosted models. Start with a 14 day free trial."
|
||||
} else {
|
||||
"Subscribe for access to Zed's hosted LLMs."
|
||||
"Subscribe for access to Zed's hosted models."
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1166,7 +1166,7 @@ impl RenderOnce for ZedAiConfiguration {
|
||||
Button::new("start_trial", "Start 14-day Free Pro Trial")
|
||||
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx)))
|
||||
.on_click(|_, _, cx| cx.open_url(&zed_urls::start_trial_url(cx)))
|
||||
.into_any_element()
|
||||
} else {
|
||||
Button::new("upgrade", "Upgrade to Pro")
|
||||
|
||||
@@ -410,8 +410,20 @@ pub fn into_mistral(
|
||||
.push_part(mistral::MessagePart::Text { text: text.clone() });
|
||||
}
|
||||
MessageContent::RedactedThinking(_) => {}
|
||||
MessageContent::ToolUse(_) | MessageContent::ToolResult(_) => {
|
||||
// Tool content is not supported in User messages for Mistral
|
||||
MessageContent::ToolUse(_) => {
|
||||
// Tool use is not supported in User messages for Mistral
|
||||
}
|
||||
MessageContent::ToolResult(tool_result) => {
|
||||
let tool_content = match &tool_result.content {
|
||||
LanguageModelToolResultContent::Text(text) => text.to_string(),
|
||||
LanguageModelToolResultContent::Image(_) => {
|
||||
"[Tool responded with an image, but Zed doesn't support these in Mistral models yet]".to_string()
|
||||
}
|
||||
};
|
||||
messages.push(mistral::RequestMessage::Tool {
|
||||
content: tool_content,
|
||||
tool_call_id: tool_result.tool_use_id.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -482,24 +494,6 @@ pub fn into_mistral(
|
||||
}
|
||||
}
|
||||
|
||||
for message in &request.messages {
|
||||
for content in &message.content {
|
||||
if let MessageContent::ToolResult(tool_result) = content {
|
||||
let content = match &tool_result.content {
|
||||
LanguageModelToolResultContent::Text(text) => text.to_string(),
|
||||
LanguageModelToolResultContent::Image(_) => {
|
||||
"[Tool responded with an image, but Zed doesn't support these in Mistral models yet]".to_string()
|
||||
}
|
||||
};
|
||||
|
||||
messages.push(mistral::RequestMessage::Tool {
|
||||
content,
|
||||
tool_call_id: tool_result.tool_use_id.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The Mistral API requires that tool messages be followed by assistant messages,
|
||||
// not user messages. When we have a tool->user sequence in the conversation,
|
||||
// we need to insert a placeholder assistant message to maintain proper conversation
|
||||
|
||||
@@ -18,7 +18,6 @@ client.workspace = true
|
||||
collections.workspace = true
|
||||
copilot.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
use std::{collections::hash_map, path::PathBuf, rc::Rc, time::Duration};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use client::proto;
|
||||
use collections::{HashMap, HashSet};
|
||||
use collections::HashSet;
|
||||
use editor::{Editor, EditorEvent};
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use gpui::{Corner, Entity, Subscription, Task, WeakEntity, actions};
|
||||
use language::{BinaryStatus, BufferId, LocalFile, ServerHealth};
|
||||
use language::{BinaryStatus, BufferId, ServerHealth};
|
||||
use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector};
|
||||
use project::{LspStore, LspStoreEvent, project_settings::ProjectSettings};
|
||||
use project::{LspStore, LspStoreEvent, Worktree, project_settings::ProjectSettings};
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use ui::{
|
||||
Context, ContextMenu, ContextMenuEntry, ContextMenuItem, DocumentationAside, DocumentationSide,
|
||||
@@ -36,8 +40,7 @@ pub struct LspTool {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct LanguageServerState {
|
||||
items: Vec<LspItem>,
|
||||
other_servers_start_index: Option<usize>,
|
||||
items: Vec<LspMenuItem>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
lsp_store: WeakEntity<LspStore>,
|
||||
active_editor: Option<ActiveEditor>,
|
||||
@@ -63,8 +66,13 @@ impl std::fmt::Debug for ActiveEditor {
|
||||
struct LanguageServers {
|
||||
health_statuses: HashMap<LanguageServerId, LanguageServerHealthStatus>,
|
||||
binary_statuses: HashMap<LanguageServerName, LanguageServerBinaryStatus>,
|
||||
servers_per_buffer_abs_path:
|
||||
HashMap<PathBuf, HashMap<LanguageServerId, Option<LanguageServerName>>>,
|
||||
servers_per_buffer_abs_path: HashMap<PathBuf, ServersForPath>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ServersForPath {
|
||||
servers: HashMap<LanguageServerId, Option<LanguageServerName>>,
|
||||
worktree: Option<WeakEntity<Worktree>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -120,8 +128,8 @@ impl LanguageServerState {
|
||||
};
|
||||
|
||||
let mut first_button_encountered = false;
|
||||
for (i, item) in self.items.iter().enumerate() {
|
||||
if let LspItem::ToggleServersButton { restart } = item {
|
||||
for item in &self.items {
|
||||
if let LspMenuItem::ToggleServersButton { restart } = item {
|
||||
let label = if *restart {
|
||||
"Restart All Servers"
|
||||
} else {
|
||||
@@ -140,22 +148,19 @@ impl LanguageServerState {
|
||||
};
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let buffer_store = project.read(cx).buffer_store().clone();
|
||||
let worktree_store = project.read(cx).worktree_store();
|
||||
|
||||
let buffers = state
|
||||
.read(cx)
|
||||
.language_servers
|
||||
.servers_per_buffer_abs_path
|
||||
.keys()
|
||||
.filter_map(|abs_path| {
|
||||
worktree_store.read(cx).find_worktree(abs_path, cx)
|
||||
})
|
||||
.filter_map(|(worktree, relative_path)| {
|
||||
let entry =
|
||||
worktree.read(cx).entry_for_path(&relative_path)?;
|
||||
project.read(cx).path_for_entry(entry.id, cx)
|
||||
})
|
||||
.filter_map(|project_path| {
|
||||
.iter()
|
||||
.filter_map(|(abs_path, servers)| {
|
||||
let worktree =
|
||||
servers.worktree.as_ref()?.upgrade()?.read(cx);
|
||||
let relative_path =
|
||||
abs_path.strip_prefix(&worktree.abs_path()).ok()?;
|
||||
let entry = worktree.entry_for_path(&relative_path)?;
|
||||
let project_path =
|
||||
project.read(cx).path_for_entry(entry.id, cx)?;
|
||||
buffer_store.read(cx).get_by_path(&project_path)
|
||||
})
|
||||
.collect();
|
||||
@@ -165,13 +170,16 @@ impl LanguageServerState {
|
||||
.iter()
|
||||
// Do not try to use IDs as we have stopped all servers already, when allowing to restart them all
|
||||
.flat_map(|item| match item {
|
||||
LspItem::ToggleServersButton { .. } => None,
|
||||
LspItem::WithHealthCheck(_, status, ..) => Some(
|
||||
LanguageServerSelector::Name(status.name.clone()),
|
||||
),
|
||||
LspItem::WithBinaryStatus(_, server_name, ..) => Some(
|
||||
LanguageServerSelector::Name(server_name.clone()),
|
||||
LspMenuItem::Header { .. } => None,
|
||||
LspMenuItem::ToggleServersButton { .. } => None,
|
||||
LspMenuItem::WithHealthCheck { health, .. } => Some(
|
||||
LanguageServerSelector::Name(health.name.clone()),
|
||||
),
|
||||
LspMenuItem::WithBinaryStatus {
|
||||
server_name, ..
|
||||
} => Some(LanguageServerSelector::Name(
|
||||
server_name.clone(),
|
||||
)),
|
||||
})
|
||||
.collect();
|
||||
lsp_store.restart_language_servers_for_buffers(
|
||||
@@ -190,13 +198,17 @@ impl LanguageServerState {
|
||||
}
|
||||
menu = menu.item(button);
|
||||
continue;
|
||||
};
|
||||
} else if let LspMenuItem::Header { header, separator } = item {
|
||||
menu = menu
|
||||
.when(*separator, |menu| menu.separator())
|
||||
.when_some(header.as_ref(), |menu, header| menu.header(header));
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(server_info) = item.server_info() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let server_selector = server_info.server_selector();
|
||||
// TODO currently, Zed remote does not work well with the LSP logs
|
||||
// https://github.com/zed-industries/zed/issues/28557
|
||||
@@ -205,6 +217,7 @@ impl LanguageServerState {
|
||||
|
||||
let status_color = server_info
|
||||
.binary_status
|
||||
.as_ref()
|
||||
.and_then(|binary_status| match binary_status.status {
|
||||
BinaryStatus::None => None,
|
||||
BinaryStatus::CheckingForUpdate
|
||||
@@ -223,17 +236,20 @@ impl LanguageServerState {
|
||||
})
|
||||
.unwrap_or(Color::Success);
|
||||
|
||||
if self
|
||||
.other_servers_start_index
|
||||
.is_some_and(|index| index == i)
|
||||
{
|
||||
menu = menu.separator().header("Other Buffers");
|
||||
}
|
||||
|
||||
if i == 0 && self.other_servers_start_index.is_some() {
|
||||
menu = menu.header("Current Buffer");
|
||||
}
|
||||
let message = server_info
|
||||
.message
|
||||
.as_ref()
|
||||
.or_else(|| server_info.binary_status.as_ref()?.message.as_ref())
|
||||
.cloned();
|
||||
let hover_label = if has_logs {
|
||||
Some("View Logs")
|
||||
} else if message.is_some() {
|
||||
Some("View Message")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let server_name = server_info.name.clone();
|
||||
menu = menu.item(ContextMenuItem::custom_entry(
|
||||
move |_, _| {
|
||||
h_flex()
|
||||
@@ -245,42 +261,99 @@ impl LanguageServerState {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Indicator::dot().color(status_color))
|
||||
.child(Label::new(server_info.name.0.clone())),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.visible_on_hover("menu_item")
|
||||
.child(
|
||||
Label::new("View Logs")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronRight)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
.child(Label::new(server_name.0.clone())),
|
||||
)
|
||||
.when_some(hover_label, |div, hover_label| {
|
||||
div.child(
|
||||
h_flex()
|
||||
.visible_on_hover("menu_item")
|
||||
.child(
|
||||
Label::new(hover_label)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronRight)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
},
|
||||
{
|
||||
let lsp_logs = lsp_logs.clone();
|
||||
let message = message.clone();
|
||||
let server_selector = server_selector.clone();
|
||||
let server_name = server_info.name.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
move |window, cx| {
|
||||
if !has_logs {
|
||||
if has_logs {
|
||||
lsp_logs.update(cx, |lsp_logs, cx| {
|
||||
lsp_logs.open_server_trace(
|
||||
workspace.clone(),
|
||||
server_selector.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
} else if let Some(message) = &message {
|
||||
let Some(create_buffer) = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.project()
|
||||
.update(cx, |project, cx| project.create_buffer(cx))
|
||||
})
|
||||
.ok()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let window = window.window_handle();
|
||||
let workspace = workspace.clone();
|
||||
let message = message.clone();
|
||||
let server_name = server_name.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
let buffer = create_buffer.await?;
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
[(
|
||||
0..0,
|
||||
format!("Language server {server_name}:\n\n{message}"),
|
||||
)],
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
buffer.set_capability(language::Capability::ReadOnly, cx);
|
||||
})?;
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
window.update(cx, |_, window, cx| {
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(cx.new(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_buffer(buffer, None, window, cx);
|
||||
editor.set_read_only(true);
|
||||
editor
|
||||
})),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})??;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
} else {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
lsp_logs.update(cx, |lsp_logs, cx| {
|
||||
lsp_logs.open_server_trace(
|
||||
workspace.clone(),
|
||||
server_selector.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
server_info.message.map(|server_message| {
|
||||
message.map(|server_message| {
|
||||
DocumentationAside::new(
|
||||
DocumentationSide::Right,
|
||||
Rc::new(move |_| Label::new(server_message.clone()).into_any_element()),
|
||||
@@ -345,81 +418,95 @@ impl LanguageServers {
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ServerData<'a> {
|
||||
WithHealthCheck(
|
||||
LanguageServerId,
|
||||
&'a LanguageServerHealthStatus,
|
||||
Option<&'a LanguageServerBinaryStatus>,
|
||||
),
|
||||
WithBinaryStatus(
|
||||
Option<LanguageServerId>,
|
||||
&'a LanguageServerName,
|
||||
&'a LanguageServerBinaryStatus,
|
||||
),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum LspItem {
|
||||
WithHealthCheck(
|
||||
LanguageServerId,
|
||||
LanguageServerHealthStatus,
|
||||
Option<LanguageServerBinaryStatus>,
|
||||
),
|
||||
WithBinaryStatus(
|
||||
Option<LanguageServerId>,
|
||||
LanguageServerName,
|
||||
LanguageServerBinaryStatus,
|
||||
),
|
||||
ToggleServersButton {
|
||||
restart: bool,
|
||||
WithHealthCheck {
|
||||
server_id: LanguageServerId,
|
||||
health: &'a LanguageServerHealthStatus,
|
||||
binary_status: Option<&'a LanguageServerBinaryStatus>,
|
||||
},
|
||||
WithBinaryStatus {
|
||||
server_id: Option<LanguageServerId>,
|
||||
server_name: &'a LanguageServerName,
|
||||
binary_status: &'a LanguageServerBinaryStatus,
|
||||
},
|
||||
}
|
||||
|
||||
impl LspItem {
|
||||
#[derive(Debug)]
|
||||
enum LspMenuItem {
|
||||
WithHealthCheck {
|
||||
server_id: LanguageServerId,
|
||||
health: LanguageServerHealthStatus,
|
||||
binary_status: Option<LanguageServerBinaryStatus>,
|
||||
},
|
||||
WithBinaryStatus {
|
||||
server_id: Option<LanguageServerId>,
|
||||
server_name: LanguageServerName,
|
||||
binary_status: LanguageServerBinaryStatus,
|
||||
},
|
||||
ToggleServersButton {
|
||||
restart: bool,
|
||||
},
|
||||
Header {
|
||||
header: Option<SharedString>,
|
||||
separator: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl LspMenuItem {
|
||||
fn server_info(&self) -> Option<ServerInfo> {
|
||||
match self {
|
||||
LspItem::ToggleServersButton { .. } => None,
|
||||
LspItem::WithHealthCheck(
|
||||
language_server_id,
|
||||
language_server_health_status,
|
||||
language_server_binary_status,
|
||||
) => Some(ServerInfo {
|
||||
name: language_server_health_status.name.clone(),
|
||||
id: Some(*language_server_id),
|
||||
health: language_server_health_status.health(),
|
||||
binary_status: language_server_binary_status.clone(),
|
||||
message: language_server_health_status.message(),
|
||||
}),
|
||||
LspItem::WithBinaryStatus(
|
||||
Self::Header { .. } => None,
|
||||
Self::ToggleServersButton { .. } => None,
|
||||
Self::WithHealthCheck {
|
||||
server_id,
|
||||
language_server_name,
|
||||
language_server_binary_status,
|
||||
) => Some(ServerInfo {
|
||||
name: language_server_name.clone(),
|
||||
health,
|
||||
binary_status,
|
||||
..
|
||||
} => Some(ServerInfo {
|
||||
name: health.name.clone(),
|
||||
id: Some(*server_id),
|
||||
health: health.health(),
|
||||
binary_status: binary_status.clone(),
|
||||
message: health.message(),
|
||||
}),
|
||||
Self::WithBinaryStatus {
|
||||
server_id,
|
||||
server_name,
|
||||
binary_status,
|
||||
..
|
||||
} => Some(ServerInfo {
|
||||
name: server_name.clone(),
|
||||
id: *server_id,
|
||||
health: None,
|
||||
binary_status: Some(language_server_binary_status.clone()),
|
||||
message: language_server_binary_status.message.clone(),
|
||||
binary_status: Some(binary_status.clone()),
|
||||
message: binary_status.message.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerData<'_> {
|
||||
fn name(&self) -> &LanguageServerName {
|
||||
fn into_lsp_item(self) -> LspMenuItem {
|
||||
match self {
|
||||
Self::WithHealthCheck(_, state, _) => &state.name,
|
||||
Self::WithBinaryStatus(_, name, ..) => name,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_lsp_item(self) -> LspItem {
|
||||
match self {
|
||||
Self::WithHealthCheck(id, name, status) => {
|
||||
LspItem::WithHealthCheck(id, name.clone(), status.cloned())
|
||||
}
|
||||
Self::WithBinaryStatus(server_id, name, status) => {
|
||||
LspItem::WithBinaryStatus(server_id, name.clone(), status.clone())
|
||||
}
|
||||
Self::WithHealthCheck {
|
||||
server_id,
|
||||
health,
|
||||
binary_status,
|
||||
..
|
||||
} => LspMenuItem::WithHealthCheck {
|
||||
server_id,
|
||||
health: health.clone(),
|
||||
binary_status: binary_status.cloned(),
|
||||
},
|
||||
Self::WithBinaryStatus {
|
||||
server_id,
|
||||
server_name,
|
||||
binary_status,
|
||||
..
|
||||
} => LspMenuItem::WithBinaryStatus {
|
||||
server_id,
|
||||
server_name: server_name.clone(),
|
||||
binary_status: binary_status.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -452,7 +539,6 @@ impl LspTool {
|
||||
let state = cx.new(|_| LanguageServerState {
|
||||
workspace: workspace.weak_handle(),
|
||||
items: Vec::new(),
|
||||
other_servers_start_index: None,
|
||||
lsp_store: lsp_store.downgrade(),
|
||||
active_editor: None,
|
||||
language_servers: LanguageServers::default(),
|
||||
@@ -542,13 +628,28 @@ impl LspTool {
|
||||
message: proto::update_language_server::Variant::RegisteredForBuffer(update),
|
||||
..
|
||||
} => {
|
||||
self.server_state.update(cx, |state, _| {
|
||||
state
|
||||
self.server_state.update(cx, |state, cx| {
|
||||
let Ok(worktree) = state.workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.find_worktree(Path::new(&update.buffer_abs_path), cx)
|
||||
.map(|(worktree, _)| worktree.downgrade())
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
let entry = state
|
||||
.language_servers
|
||||
.servers_per_buffer_abs_path
|
||||
.entry(PathBuf::from(&update.buffer_abs_path))
|
||||
.or_default()
|
||||
.insert(*language_server_id, name.clone());
|
||||
.or_insert_with(|| ServersForPath {
|
||||
servers: HashMap::default(),
|
||||
worktree: worktree.clone(),
|
||||
});
|
||||
entry.servers.insert(*language_server_id, name.clone());
|
||||
if worktree.is_some() {
|
||||
entry.worktree = worktree;
|
||||
}
|
||||
});
|
||||
updated = true;
|
||||
}
|
||||
@@ -562,94 +663,95 @@ impl LspTool {
|
||||
|
||||
fn regenerate_items(&mut self, cx: &mut App) {
|
||||
self.server_state.update(cx, |state, cx| {
|
||||
let editor_buffers = state
|
||||
let active_worktrees = state
|
||||
.active_editor
|
||||
.as_ref()
|
||||
.map(|active_editor| active_editor.editor_buffers.clone())
|
||||
.unwrap_or_default();
|
||||
let editor_buffer_paths = editor_buffers
|
||||
.iter()
|
||||
.filter_map(|buffer_id| {
|
||||
let buffer_path = state
|
||||
.lsp_store
|
||||
.update(cx, |lsp_store, cx| {
|
||||
Some(
|
||||
project::File::from_dyn(
|
||||
lsp_store
|
||||
.buffer_store()
|
||||
.read(cx)
|
||||
.get(*buffer_id)?
|
||||
.read(cx)
|
||||
.file(),
|
||||
)?
|
||||
.abs_path(cx),
|
||||
)
|
||||
.into_iter()
|
||||
.flat_map(|active_editor| {
|
||||
active_editor
|
||||
.editor
|
||||
.upgrade()
|
||||
.into_iter()
|
||||
.flat_map(|active_editor| {
|
||||
active_editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.all_buffers()
|
||||
.into_iter()
|
||||
.filter_map(|buffer| {
|
||||
project::File::from_dyn(buffer.read(cx).file())
|
||||
})
|
||||
.map(|buffer_file| buffer_file.worktree.clone())
|
||||
})
|
||||
.ok()??;
|
||||
Some(buffer_path)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let mut servers_with_health_checks = HashSet::default();
|
||||
let mut server_ids_with_health_checks = HashSet::default();
|
||||
let mut buffer_servers =
|
||||
Vec::with_capacity(state.language_servers.health_statuses.len());
|
||||
let mut other_servers =
|
||||
Vec::with_capacity(state.language_servers.health_statuses.len());
|
||||
let buffer_server_ids = editor_buffer_paths
|
||||
.iter()
|
||||
.filter_map(|buffer_path| {
|
||||
state
|
||||
.language_servers
|
||||
.servers_per_buffer_abs_path
|
||||
.get(buffer_path)
|
||||
})
|
||||
.flatten()
|
||||
.fold(HashMap::default(), |mut acc, (server_id, name)| {
|
||||
match acc.entry(*server_id) {
|
||||
hash_map::Entry::Occupied(mut o) => {
|
||||
let old_name: &mut Option<&LanguageServerName> = o.get_mut();
|
||||
if old_name.is_none() {
|
||||
*old_name = name.as_ref();
|
||||
}
|
||||
}
|
||||
hash_map::Entry::Vacant(v) => {
|
||||
v.insert(name.as_ref());
|
||||
let mut server_ids_to_worktrees =
|
||||
HashMap::<LanguageServerId, Entity<Worktree>>::default();
|
||||
let mut server_names_to_worktrees = HashMap::<
|
||||
LanguageServerName,
|
||||
HashSet<(Entity<Worktree>, LanguageServerId)>,
|
||||
>::default();
|
||||
for servers_for_path in state.language_servers.servers_per_buffer_abs_path.values() {
|
||||
if let Some(worktree) = servers_for_path
|
||||
.worktree
|
||||
.as_ref()
|
||||
.and_then(|worktree| worktree.upgrade())
|
||||
{
|
||||
for (server_id, server_name) in &servers_for_path.servers {
|
||||
server_ids_to_worktrees.insert(*server_id, worktree.clone());
|
||||
if let Some(server_name) = server_name {
|
||||
server_names_to_worktrees
|
||||
.entry(server_name.clone())
|
||||
.or_default()
|
||||
.insert((worktree.clone(), *server_id));
|
||||
}
|
||||
}
|
||||
acc
|
||||
}
|
||||
}
|
||||
|
||||
let mut servers_per_worktree = BTreeMap::<SharedString, Vec<ServerData>>::new();
|
||||
let mut servers_without_worktree = Vec::<ServerData>::new();
|
||||
let mut servers_with_health_checks = HashSet::default();
|
||||
|
||||
for (server_id, health) in &state.language_servers.health_statuses {
|
||||
let worktree = server_ids_to_worktrees.get(server_id).or_else(|| {
|
||||
let worktrees = server_names_to_worktrees.get(&health.name)?;
|
||||
worktrees
|
||||
.iter()
|
||||
.find(|(worktree, _)| active_worktrees.contains(worktree))
|
||||
.or_else(|| worktrees.iter().next())
|
||||
.map(|(worktree, _)| worktree)
|
||||
});
|
||||
for (server_id, server_state) in &state.language_servers.health_statuses {
|
||||
let binary_status = state
|
||||
.language_servers
|
||||
.binary_statuses
|
||||
.get(&server_state.name);
|
||||
servers_with_health_checks.insert(&server_state.name);
|
||||
server_ids_with_health_checks.insert(*server_id);
|
||||
if buffer_server_ids.contains_key(server_id) {
|
||||
buffer_servers.push(ServerData::WithHealthCheck(
|
||||
*server_id,
|
||||
server_state,
|
||||
binary_status,
|
||||
));
|
||||
} else {
|
||||
other_servers.push(ServerData::WithHealthCheck(
|
||||
*server_id,
|
||||
server_state,
|
||||
binary_status,
|
||||
));
|
||||
servers_with_health_checks.insert(&health.name);
|
||||
let worktree_name =
|
||||
worktree.map(|worktree| SharedString::new(worktree.read(cx).root_name()));
|
||||
|
||||
let binary_status = state.language_servers.binary_statuses.get(&health.name);
|
||||
let server_data = ServerData::WithHealthCheck {
|
||||
server_id: *server_id,
|
||||
health,
|
||||
binary_status,
|
||||
};
|
||||
match worktree_name {
|
||||
Some(worktree_name) => servers_per_worktree
|
||||
.entry(worktree_name.clone())
|
||||
.or_default()
|
||||
.push(server_data),
|
||||
None => servers_without_worktree.push(server_data),
|
||||
}
|
||||
}
|
||||
|
||||
let mut can_stop_all = !state.language_servers.health_statuses.is_empty();
|
||||
let mut can_restart_all = state.language_servers.health_statuses.is_empty();
|
||||
for (server_name, status) in state
|
||||
for (server_name, binary_status) in state
|
||||
.language_servers
|
||||
.binary_statuses
|
||||
.iter()
|
||||
.filter(|(name, _)| !servers_with_health_checks.contains(name))
|
||||
{
|
||||
match status.status {
|
||||
match binary_status.status {
|
||||
BinaryStatus::None => {
|
||||
can_restart_all = false;
|
||||
can_stop_all |= true;
|
||||
@@ -674,52 +776,73 @@ impl LspTool {
|
||||
BinaryStatus::Failed { .. } => {}
|
||||
}
|
||||
|
||||
let matching_server_id = state
|
||||
.language_servers
|
||||
.servers_per_buffer_abs_path
|
||||
.iter()
|
||||
.filter(|(path, _)| editor_buffer_paths.contains(path))
|
||||
.flat_map(|(_, server_associations)| server_associations.iter())
|
||||
.find_map(|(id, name)| {
|
||||
if name.as_ref() == Some(server_name) {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
match server_names_to_worktrees.get(server_name) {
|
||||
Some(worktrees_for_name) => {
|
||||
match worktrees_for_name
|
||||
.iter()
|
||||
.find(|(worktree, _)| active_worktrees.contains(worktree))
|
||||
.or_else(|| worktrees_for_name.iter().next())
|
||||
{
|
||||
Some((worktree, server_id)) => {
|
||||
let worktree_name =
|
||||
SharedString::new(worktree.read(cx).root_name());
|
||||
servers_per_worktree
|
||||
.entry(worktree_name.clone())
|
||||
.or_default()
|
||||
.push(ServerData::WithBinaryStatus {
|
||||
server_name,
|
||||
binary_status,
|
||||
server_id: Some(*server_id),
|
||||
});
|
||||
}
|
||||
None => servers_without_worktree.push(ServerData::WithBinaryStatus {
|
||||
server_name,
|
||||
binary_status,
|
||||
server_id: None,
|
||||
}),
|
||||
}
|
||||
});
|
||||
if let Some(server_id) = matching_server_id {
|
||||
buffer_servers.push(ServerData::WithBinaryStatus(
|
||||
Some(server_id),
|
||||
}
|
||||
None => servers_without_worktree.push(ServerData::WithBinaryStatus {
|
||||
server_name,
|
||||
status,
|
||||
));
|
||||
} else {
|
||||
other_servers.push(ServerData::WithBinaryStatus(None, server_name, status));
|
||||
binary_status,
|
||||
server_id: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
buffer_servers.sort_by_key(|data| data.name().clone());
|
||||
other_servers.sort_by_key(|data| data.name().clone());
|
||||
|
||||
let mut other_servers_start_index = None;
|
||||
let mut new_lsp_items =
|
||||
Vec::with_capacity(buffer_servers.len() + other_servers.len() + 1);
|
||||
new_lsp_items.extend(buffer_servers.into_iter().map(ServerData::into_lsp_item));
|
||||
if !new_lsp_items.is_empty() {
|
||||
other_servers_start_index = Some(new_lsp_items.len());
|
||||
Vec::with_capacity(servers_per_worktree.len() + servers_without_worktree.len() + 2);
|
||||
for (worktree_name, worktree_servers) in servers_per_worktree {
|
||||
if worktree_servers.is_empty() {
|
||||
continue;
|
||||
}
|
||||
new_lsp_items.push(LspMenuItem::Header {
|
||||
header: Some(worktree_name),
|
||||
separator: false,
|
||||
});
|
||||
new_lsp_items.extend(worktree_servers.into_iter().map(ServerData::into_lsp_item));
|
||||
}
|
||||
if !servers_without_worktree.is_empty() {
|
||||
new_lsp_items.push(LspMenuItem::Header {
|
||||
header: Some(SharedString::from("Unknown worktree")),
|
||||
separator: false,
|
||||
});
|
||||
new_lsp_items.extend(
|
||||
servers_without_worktree
|
||||
.into_iter()
|
||||
.map(ServerData::into_lsp_item),
|
||||
);
|
||||
}
|
||||
new_lsp_items.extend(other_servers.into_iter().map(ServerData::into_lsp_item));
|
||||
if !new_lsp_items.is_empty() {
|
||||
if can_stop_all {
|
||||
new_lsp_items.push(LspItem::ToggleServersButton { restart: true });
|
||||
new_lsp_items.push(LspItem::ToggleServersButton { restart: false });
|
||||
new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: true });
|
||||
new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: false });
|
||||
} else if can_restart_all {
|
||||
new_lsp_items.push(LspItem::ToggleServersButton { restart: true });
|
||||
new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: true });
|
||||
}
|
||||
}
|
||||
|
||||
state.items = new_lsp_items;
|
||||
state.other_servers_start_index = other_servers_start_index;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -841,10 +964,7 @@ impl StatusItemView for LspTool {
|
||||
|
||||
impl Render for LspTool {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
|
||||
if !cx.is_staff()
|
||||
|| self.server_state.read(cx).language_servers.is_empty()
|
||||
|| self.lsp_menu.is_none()
|
||||
{
|
||||
if self.server_state.read(cx).language_servers.is_empty() || self.lsp_menu.is_none() {
|
||||
return div();
|
||||
}
|
||||
|
||||
@@ -852,12 +972,12 @@ impl Render for LspTool {
|
||||
let mut has_warnings = false;
|
||||
let mut has_other_notifications = false;
|
||||
let state = self.server_state.read(cx);
|
||||
for server in state.language_servers.health_statuses.values() {
|
||||
if let Some(binary_status) = &state.language_servers.binary_statuses.get(&server.name) {
|
||||
has_errors |= matches!(binary_status.status, BinaryStatus::Failed { .. });
|
||||
has_other_notifications |= binary_status.message.is_some();
|
||||
}
|
||||
for binary_status in state.language_servers.binary_statuses.values() {
|
||||
has_errors |= matches!(binary_status.status, BinaryStatus::Failed { .. });
|
||||
has_other_notifications |= binary_status.message.is_some();
|
||||
}
|
||||
|
||||
for server in state.language_servers.health_statuses.values() {
|
||||
if let Some((message, health)) = &server.health {
|
||||
has_other_notifications |= message.is_some();
|
||||
match health {
|
||||
|
||||
@@ -231,6 +231,13 @@ impl JsonLspAdapter {
|
||||
))
|
||||
}
|
||||
|
||||
schemas
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
.extend(cx.all_action_names().into_iter().map(|&name| {
|
||||
project::lsp_store::json_language_server_ext::url_schema_for_action(name)
|
||||
}));
|
||||
|
||||
// This can be viewed via `dev: open language server logs` -> `json-language-server` ->
|
||||
// `Server Info`
|
||||
serde_json::json!({
|
||||
|
||||
@@ -273,6 +273,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
|
||||
"Astro",
|
||||
"CSS",
|
||||
"ERB",
|
||||
"HTML/ERB",
|
||||
"HEEX",
|
||||
"HTML",
|
||||
"JavaScript",
|
||||
|
||||
@@ -179,6 +179,7 @@ impl LspAdapter for TailwindLspAdapter {
|
||||
("Elixir".to_string(), "phoenix-heex".to_string()),
|
||||
("HEEX".to_string(), "phoenix-heex".to_string()),
|
||||
("ERB".to_string(), "erb".to_string()),
|
||||
("HTML/ERB".to_string(), "erb".to_string()),
|
||||
("PHP".to_string(), "php".to_string()),
|
||||
("Vue.js".to_string(), "vue".to_string()),
|
||||
])
|
||||
|
||||
@@ -326,11 +326,11 @@ pub(crate) async fn capture_local_video_track(
|
||||
capture_source: &dyn ScreenCaptureSource,
|
||||
cx: &mut gpui::AsyncApp,
|
||||
) -> Result<(crate::LocalVideoTrack, Box<dyn ScreenCaptureStream>)> {
|
||||
let resolution = capture_source.resolution()?;
|
||||
let metadata = capture_source.metadata()?;
|
||||
let track_source = gpui_tokio::Tokio::spawn(cx, async move {
|
||||
NativeVideoSource::new(VideoResolution {
|
||||
width: resolution.width.0 as u32,
|
||||
height: resolution.height.0 as u32,
|
||||
width: metadata.resolution.width.0 as u32,
|
||||
height: metadata.resolution.height.0 as u32,
|
||||
})
|
||||
})?
|
||||
.await?;
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use gpui::{AsyncApp, ScreenCaptureSource, ScreenCaptureStream};
|
||||
use gpui::{AsyncApp, ScreenCaptureSource, ScreenCaptureStream, TestScreenCaptureStream};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LocalParticipant {
|
||||
@@ -119,7 +119,3 @@ impl RemoteParticipant {
|
||||
self.identity.clone()
|
||||
}
|
||||
}
|
||||
|
||||
struct TestScreenCaptureStream;
|
||||
|
||||
impl gpui::ScreenCaptureStream for TestScreenCaptureStream {}
|
||||
|
||||
@@ -25,7 +25,7 @@ fn replace_string_action(
|
||||
None
|
||||
}
|
||||
|
||||
/// "ctrl-k ctrl-1": "inline_completion::ToggleMenu" -> "edit_prediction::ToggleMenu"
|
||||
/// "space": "outline_panel::Open" -> "outline_panel::OpenSelectedEntry"
|
||||
static STRING_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
|
||||
HashMap::from_iter([("outline_panel::Open", "outline_panel::OpenSelectedEntry")])
|
||||
});
|
||||
|
||||
28
crates/onboarding/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "onboarding"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/onboarding.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
db.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
1
crates/onboarding/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../LICENSE-GPL
|
||||
352
crates/onboarding/src/onboarding.rs
Normal file
@@ -0,0 +1,352 @@
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use feature_flags::{FeatureFlag, FeatureFlagViewExt as _};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
IntoElement, Render, SharedString, Subscription, Task, WeakEntity, Window, actions,
|
||||
};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use std::sync::Arc;
|
||||
use theme::{ThemeMode, ThemeSettings};
|
||||
use ui::{
|
||||
ButtonCommon as _, ButtonSize, ButtonStyle, Clickable as _, Color, Divider, FluentBuilder,
|
||||
Headline, InteractiveElement, KeyBinding, Label, LabelCommon, ParentElement as _,
|
||||
StatefulInteractiveElement, Styled, ToggleButton, Toggleable as _, Vector, VectorName, div,
|
||||
h_flex, rems, v_container, v_flex,
|
||||
};
|
||||
use workspace::{
|
||||
AppState, Workspace, WorkspaceId,
|
||||
dock::DockPosition,
|
||||
item::{Item, ItemEvent},
|
||||
open_new, with_active_or_new_workspace,
|
||||
};
|
||||
|
||||
pub struct OnBoardingFeatureFlag {}
|
||||
|
||||
impl FeatureFlag for OnBoardingFeatureFlag {
|
||||
const NAME: &'static str = "onboarding";
|
||||
}
|
||||
|
||||
pub const FIRST_OPEN: &str = "first_open";
|
||||
|
||||
actions!(
|
||||
zed,
|
||||
[
|
||||
/// Opens the onboarding view.
|
||||
OpenOnboarding
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.on_action(|_: &OpenOnboarding, cx| {
|
||||
with_active_or_new_workspace(cx, |workspace, window, cx| {
|
||||
workspace
|
||||
.with_local_workspace(window, cx, |workspace, window, cx| {
|
||||
let existing = workspace
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.items()
|
||||
.find_map(|item| item.downcast::<Onboarding>());
|
||||
|
||||
if let Some(existing) = existing {
|
||||
workspace.activate_item(&existing, true, true, window, cx);
|
||||
} else {
|
||||
let settings_page = Onboarding::new(workspace.weak_handle(), cx);
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(settings_page),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
});
|
||||
cx.observe_new::<Workspace>(|_, window, cx| {
|
||||
let Some(window) = window else {
|
||||
return;
|
||||
};
|
||||
|
||||
let onboarding_actions = [std::any::TypeId::of::<OpenOnboarding>()];
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_action_types(&onboarding_actions);
|
||||
});
|
||||
|
||||
cx.observe_flag::<OnBoardingFeatureFlag, _>(window, move |is_enabled, _, _, cx| {
|
||||
if is_enabled {
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.show_action_types(onboarding_actions.iter());
|
||||
});
|
||||
} else {
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_action_types(&onboarding_actions);
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn show_onboarding_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyhow::Result<()>> {
|
||||
open_new(
|
||||
Default::default(),
|
||||
app_state,
|
||||
cx,
|
||||
|workspace, window, cx| {
|
||||
{
|
||||
workspace.toggle_dock(DockPosition::Left, window, cx);
|
||||
let onboarding_page = Onboarding::new(workspace.weak_handle(), cx);
|
||||
workspace.add_item_to_center(Box::new(onboarding_page.clone()), window, cx);
|
||||
|
||||
window.focus(&onboarding_page.focus_handle(cx));
|
||||
|
||||
cx.notify();
|
||||
};
|
||||
db::write_and_log(cx, || {
|
||||
KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string())
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn read_theme_selection(cx: &App) -> ThemeMode {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
settings
|
||||
.theme_selection
|
||||
.as_ref()
|
||||
.and_then(|selection| selection.mode())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn write_theme_selection(theme_mode: ThemeMode, cx: &App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
update_settings_file::<ThemeSettings>(fs, cx, move |settings, _| {
|
||||
settings.set_mode(theme_mode);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum SelectedPage {
|
||||
Basics,
|
||||
Editing,
|
||||
AiSetup,
|
||||
}
|
||||
|
||||
struct Onboarding {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
selected_page: SelectedPage,
|
||||
_settings_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl Onboarding {
|
||||
fn new(workspace: WeakEntity<Workspace>, cx: &mut App) -> Entity<Self> {
|
||||
cx.new(|cx| Self {
|
||||
workspace,
|
||||
focus_handle: cx.focus_handle(),
|
||||
selected_page: SelectedPage::Basics,
|
||||
_settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
|
||||
})
|
||||
}
|
||||
|
||||
fn render_page_nav(
|
||||
&mut self,
|
||||
page: SelectedPage,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let text = match page {
|
||||
SelectedPage::Basics => "Basics",
|
||||
SelectedPage::Editing => "Editing",
|
||||
SelectedPage::AiSetup => "AI Setup",
|
||||
};
|
||||
let binding = match page {
|
||||
SelectedPage::Basics => {
|
||||
KeyBinding::new(vec![gpui::Keystroke::parse("cmd-1").unwrap()], cx)
|
||||
}
|
||||
SelectedPage::Editing => {
|
||||
KeyBinding::new(vec![gpui::Keystroke::parse("cmd-2").unwrap()], cx)
|
||||
}
|
||||
SelectedPage::AiSetup => {
|
||||
KeyBinding::new(vec![gpui::Keystroke::parse("cmd-3").unwrap()], cx)
|
||||
}
|
||||
};
|
||||
let selected = self.selected_page == page;
|
||||
h_flex()
|
||||
.id(text)
|
||||
.rounded_sm()
|
||||
.child(text)
|
||||
.child(binding)
|
||||
.h_8()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
.py_0p5()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.map(|this| {
|
||||
if selected {
|
||||
this.bg(Color::Selected.color(cx))
|
||||
.border_l_1()
|
||||
.border_color(Color::Accent.color(cx))
|
||||
} else {
|
||||
this.text_color(Color::Muted.color(cx))
|
||||
}
|
||||
})
|
||||
.hover(|style| {
|
||||
if selected {
|
||||
style.bg(Color::Selected.color(cx).opacity(0.6))
|
||||
} else {
|
||||
style.bg(Color::Selected.color(cx).opacity(0.3))
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.selected_page = page;
|
||||
cx.notify();
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_page(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
match self.selected_page {
|
||||
SelectedPage::Basics => self.render_basics_page(window, cx).into_any_element(),
|
||||
SelectedPage::Editing => self.render_editing_page(window, cx).into_any_element(),
|
||||
SelectedPage::AiSetup => self.render_ai_setup_page(window, cx).into_any_element(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_basics_page(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let theme_mode = read_theme_selection(cx);
|
||||
|
||||
v_container().child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(Label::new("Theme"))
|
||||
.child(
|
||||
h_flex()
|
||||
.rounded_md()
|
||||
.child(
|
||||
ToggleButton::new("light", "Light")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.toggle_state(theme_mode == ThemeMode::Light)
|
||||
.on_click(|_, _, cx| write_theme_selection(ThemeMode::Light, cx))
|
||||
.first(),
|
||||
)
|
||||
.child(
|
||||
ToggleButton::new("dark", "Dark")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.toggle_state(theme_mode == ThemeMode::Dark)
|
||||
.on_click(|_, _, cx| write_theme_selection(ThemeMode::Dark, cx))
|
||||
.last(),
|
||||
)
|
||||
.child(
|
||||
ToggleButton::new("system", "System")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.toggle_state(theme_mode == ThemeMode::System)
|
||||
.on_click(|_, _, cx| write_theme_selection(ThemeMode::System, cx))
|
||||
.middle(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_editing_page(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
// div().child("editing page")
|
||||
"Right"
|
||||
}
|
||||
|
||||
fn render_ai_setup_page(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
div().child("ai setup page")
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Onboarding {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
h_flex()
|
||||
.image_cache(gpui::retain_all("onboarding-page"))
|
||||
.key_context("onboarding-page")
|
||||
.px_24()
|
||||
.py_12()
|
||||
.items_start()
|
||||
.child(
|
||||
v_flex()
|
||||
.w_1_3()
|
||||
.h_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.pt_0p5()
|
||||
.child(Vector::square(VectorName::ZedLogo, rems(2.)))
|
||||
.child(
|
||||
v_flex()
|
||||
.left_1()
|
||||
.items_center()
|
||||
.child(Headline::new("Welcome to Zed"))
|
||||
.child(
|
||||
Label::new("The editor for what's next")
|
||||
.color(Color::Muted)
|
||||
.italic(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.p_1()
|
||||
.child(Divider::horizontal_dashed())
|
||||
.child(
|
||||
v_flex().gap_1().children([
|
||||
self.render_page_nav(SelectedPage::Basics, window, cx)
|
||||
.into_element(),
|
||||
self.render_page_nav(SelectedPage::Editing, window, cx)
|
||||
.into_element(),
|
||||
self.render_page_nav(SelectedPage::AiSetup, window, cx)
|
||||
.into_element(),
|
||||
]),
|
||||
),
|
||||
)
|
||||
// .child(Divider::vertical_dashed())
|
||||
.child(div().w_2_3().h_full().child(self.render_page(window, cx)))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ItemEvent> for Onboarding {}
|
||||
|
||||
impl Focusable for Onboarding {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for Onboarding {
|
||||
type Event = ItemEvent;
|
||||
|
||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
||||
"Onboarding".into()
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
Some("Onboarding Page Opened")
|
||||
}
|
||||
|
||||
fn show_toolbar(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: Option<WorkspaceId>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Entity<Self>> {
|
||||
Some(Onboarding::new(self.workspace.clone(), cx))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
|
||||
f(*event)
|
||||
}
|
||||
}
|
||||
@@ -610,7 +610,7 @@ mod tests {
|
||||
use context_server::test::create_fake_transport;
|
||||
use gpui::{AppContext, TestAppContext, UpdateGlobal as _};
|
||||
use serde_json::json;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use std::{cell::RefCell, path::PathBuf, rc::Rc};
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
@@ -931,7 +931,7 @@ mod tests {
|
||||
ContextServerSettings::Custom {
|
||||
enabled: true,
|
||||
command: ContextServerCommand {
|
||||
path: "somebinary".to_string(),
|
||||
path: "somebinary".into(),
|
||||
args: vec!["arg".to_string()],
|
||||
env: None,
|
||||
},
|
||||
@@ -971,7 +971,7 @@ mod tests {
|
||||
ContextServerSettings::Custom {
|
||||
enabled: true,
|
||||
command: ContextServerCommand {
|
||||
path: "somebinary".to_string(),
|
||||
path: "somebinary".into(),
|
||||
args: vec!["anotherArg".to_string()],
|
||||
env: None,
|
||||
},
|
||||
@@ -1053,7 +1053,7 @@ mod tests {
|
||||
ContextServerSettings::Custom {
|
||||
enabled: true,
|
||||
command: ContextServerCommand {
|
||||
path: "somebinary".to_string(),
|
||||
path: "somebinary".into(),
|
||||
args: vec!["arg".to_string()],
|
||||
env: None,
|
||||
},
|
||||
@@ -1104,7 +1104,7 @@ mod tests {
|
||||
ContextServerSettings::Custom {
|
||||
enabled: false,
|
||||
command: ContextServerCommand {
|
||||
path: "somebinary".to_string(),
|
||||
path: "somebinary".into(),
|
||||
args: vec!["arg".to_string()],
|
||||
env: None,
|
||||
},
|
||||
@@ -1132,7 +1132,7 @@ mod tests {
|
||||
ContextServerSettings::Custom {
|
||||
enabled: true,
|
||||
command: ContextServerCommand {
|
||||
path: "somebinary".to_string(),
|
||||
path: "somebinary".into(),
|
||||
args: vec!["arg".to_string()],
|
||||
env: None,
|
||||
},
|
||||
@@ -1184,7 +1184,7 @@ mod tests {
|
||||
ContextServerSettings::Custom {
|
||||
enabled: true,
|
||||
command: ContextServerCommand {
|
||||
path: "somebinary".to_string(),
|
||||
path: "somebinary".into(),
|
||||
args: vec!["arg".to_string()],
|
||||
env: None,
|
||||
},
|
||||
@@ -1256,11 +1256,11 @@ mod tests {
|
||||
}
|
||||
|
||||
struct FakeContextServerDescriptor {
|
||||
path: String,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl FakeContextServerDescriptor {
|
||||
fn new(path: impl Into<String>) -> Self {
|
||||
fn new(path: impl Into<PathBuf>) -> Self {
|
||||
Self { path: path.into() }
|
||||
}
|
||||
}
|
||||
|
||||