Compare commits

..

2 Commits

Author SHA1 Message Date
Richard Feldman
1ff8521612 wip 2025-03-20 09:36:42 -04:00
Richard Feldman
ca22d5d4a3 Add shell_parser crate 2025-03-19 22:19:16 -04:00
176 changed files with 16967 additions and 12942 deletions

View File

@@ -19,10 +19,6 @@
# https://github.com/zed-industries/zed/pull/2394
eca93c124a488b4e538946cd2d313bd571aa2b86
# 2024-02-15 Format YAML files
# https://github.com/zed-industries/zed/pull/7887
a161a7d0c95ca7505bf9218bfae640ee5444c88b
# 2024-02-25 Format JSON files in assets/
# https://github.com/zed-industries/zed/pull/8405
ffdda588b41f7d9d270ffe76cab116f828ad545e

View File

@@ -209,6 +209,7 @@ jobs:
cargo check -p workspace
cargo build -p remote_server
cargo check -p gpui --examples
script/check-rust-livekit-macos
# Since the macOS runners are stateful, so we need to remove the config file to prevent potential bug.
- name: Clean CI config file
@@ -234,7 +235,7 @@ jobs:
clean: false
- name: Cache dependencies
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "buildjet"
@@ -286,7 +287,7 @@ jobs:
clean: false
- name: Cache dependencies
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "buildjet"
@@ -333,7 +334,7 @@ jobs:
Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.ZED_WORKSPACE }}" -Recurse
- name: Cache dependencies
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
workspaces: ${{ env.ZED_WORKSPACE }}
@@ -392,7 +393,7 @@ jobs:
Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.ZED_WORKSPACE }}" -Recurse
- name: Cache dependencies
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
workspaces: ${{ env.ZED_WORKSPACE }}

View File

@@ -22,7 +22,7 @@ jobs:
clean: false
- name: Cache dependencies
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "github"

361
Cargo.lock generated
View File

@@ -607,7 +607,6 @@ version = "0.1.0"
dependencies = [
"anthropic",
"anyhow",
"collections",
"deepseek",
"feature_flags",
"fs",
@@ -1075,7 +1074,7 @@ source = "git+https://github.com/zed-industries/async-tls?rev=1e759a4b5e370f87dc
dependencies = [
"futures-core",
"futures-io",
"rustls 0.23.25",
"rustls 0.23.23",
"rustls-pemfile 2.2.0",
"webpki-roots",
]
@@ -2298,10 +2297,10 @@ dependencies = [
"fs",
"futures 0.3.31",
"gpui",
"gpui_tokio",
"http_client",
"language",
"livekit_client",
"livekit_client_macos",
"log",
"postage",
"project",
@@ -2357,7 +2356,7 @@ dependencies = [
"cap-primitives",
"cap-std",
"io-lifetimes",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -2385,7 +2384,7 @@ dependencies = [
"ipnet",
"maybe-owned",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
"winx",
]
@@ -2437,7 +2436,8 @@ dependencies = [
[[package]]
name = "cargo_metadata"
version = "0.19.2"
source = "git+https://github.com/zed-industries/cargo_metadata?rev=ce8171bad673923d61a77b6761d0dc4aff63398a#ce8171bad673923d61a77b6761d0dc4aff63398a"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba"
dependencies = [
"camino",
"cargo-platform",
@@ -2563,15 +2563,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "cgl"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff"
dependencies = [
"libc",
]
[[package]]
name = "channel"
version = "0.1.0"
@@ -2728,7 +2719,7 @@ dependencies = [
"anyhow",
"clap",
"collections",
"core-foundation 0.10.0",
"core-foundation 0.9.4",
"core-services",
"exec",
"fork",
@@ -2928,7 +2919,6 @@ dependencies = [
"git_ui",
"google_ai",
"gpui",
"gpui_tokio",
"hex",
"http_client",
"hyper 0.14.32",
@@ -2938,6 +2928,7 @@ dependencies = [
"language_model",
"livekit_api",
"livekit_client",
"livekit_client_macos",
"log",
"lsp",
"menu",
@@ -3363,19 +3354,6 @@ dependencies = [
"libc",
]
[[package]]
name = "core-graphics2"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e4583956b9806b69f73fcb23aee05eb3620efc282972f08f6a6db7504f8334d"
dependencies = [
"bitflags 2.8.0",
"block",
"cfg-if",
"core-foundation 0.10.0",
"libc",
]
[[package]]
name = "core-services"
version = "0.2.1"
@@ -3387,30 +3365,16 @@ dependencies = [
[[package]]
name = "core-text"
version = "21.0.0"
version = "20.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130"
checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5"
dependencies = [
"core-foundation 0.10.0",
"core-graphics 0.24.0",
"core-foundation 0.9.4",
"core-graphics 0.23.2",
"foreign-types 0.5.0",
"libc",
]
[[package]]
name = "core-video"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d45e71d5be22206bed53c3c3cb99315fc4c3d31b8963808c6bc4538168c4f8ef"
dependencies = [
"block",
"core-foundation 0.10.0",
"core-graphics2",
"io-surface",
"libc",
"metal",
]
[[package]]
name = "core_maths"
version = "0.1.1"
@@ -4111,9 +4075,9 @@ dependencies = [
[[package]]
name = "deranged"
version = "0.4.0"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
"serde",
@@ -4628,7 +4592,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -4963,13 +4927,25 @@ dependencies = [
name = "feedback"
version = "0.1.0"
dependencies = [
"anyhow",
"bitflags 2.8.0",
"client",
"db",
"editor",
"futures 0.3.31",
"gpui",
"http_client",
"human_bytes",
"language",
"log",
"menu",
"project",
"regex",
"release_channel",
"serde",
"serde_derive",
"serde_json",
"smol",
"sysinfo",
"ui",
"urlencoding",
@@ -5112,12 +5088,12 @@ checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2"
[[package]]
name = "font-kit"
version = "0.14.1"
source = "git+https://github.com/zed-industries/font-kit?rev=5474cfad4b719a72ec8ed2cb7327b2b01fd10568#5474cfad4b719a72ec8ed2cb7327b2b01fd10568"
source = "git+https://github.com/zed-industries/font-kit?rev=40391b7#40391b7c0041d8a8572af2afa3de32ae088f0120"
dependencies = [
"bitflags 2.8.0",
"byteorder",
"core-foundation 0.10.0",
"core-graphics 0.24.0",
"core-foundation 0.9.4",
"core-graphics 0.23.2",
"core-text",
"dirs 5.0.1",
"dwrote",
@@ -5291,7 +5267,7 @@ checksum = "5e2e6123af26f0f2c51cc66869137080199406754903cc926a7690401ce09cb4"
dependencies = [
"io-lifetimes",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -5315,7 +5291,7 @@ name = "fsevent"
version = "0.1.0"
dependencies = [
"bitflags 2.8.0",
"core-foundation 0.10.0",
"core-foundation 0.9.4",
"fsevent-sys 3.1.0",
"parking_lot",
"tempfile",
@@ -5852,11 +5828,10 @@ dependencies = [
"cbindgen 0.28.0",
"cocoa 0.26.0",
"collections",
"core-foundation 0.10.0",
"core-foundation 0.9.4",
"core-foundation-sys",
"core-graphics 0.24.0",
"core-graphics 0.23.2",
"core-text",
"core-video",
"cosmic-text",
"ctor",
"derive_more",
@@ -6369,7 +6344,7 @@ dependencies = [
name = "http_client_tls"
version = "0.1.0"
dependencies = [
"rustls 0.23.25",
"rustls 0.23.23",
"rustls-platform-verifier",
]
@@ -6467,7 +6442,7 @@ dependencies = [
"http 1.2.0",
"hyper 1.5.1",
"hyper-util",
"rustls 0.23.25",
"rustls 0.23.23",
"rustls-native-certs 0.8.1",
"rustls-pki-types",
"tokio",
@@ -6951,7 +6926,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65"
dependencies = [
"io-lifetimes",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -6960,19 +6935,6 @@ version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983"
[[package]]
name = "io-surface"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8283575d5f0b2e7447ec0840363879d71c0fa325d4c699d5b45208ea4a51f45e"
dependencies = [
"cgl",
"core-foundation 0.10.0",
"core-foundation-sys",
"leaky-cow",
"libc",
]
[[package]]
name = "iovec"
version = "0.1.4"
@@ -7553,21 +7515,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "leak"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd100e01f1154f2908dfa7d02219aeab25d0b9c7fa955164192e3245255a0c73"
[[package]]
name = "leaky-cow"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40a8225d44241fd324a8af2806ba635fc7c8a7e9a7de4d5cf3ef54e71f5926fc"
dependencies = [
"leak",
]
[[package]]
name = "leb128"
version = "0.2.5"
@@ -7625,7 +7572,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
"windows-targets 0.48.5",
]
[[package]]
@@ -7636,9 +7583,9 @@ checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
[[package]]
name = "libmimalloc-sys"
version = "0.1.40"
version = "0.1.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07d0e07885d6a754b9c7993f2625187ad694ee985d60f23355ff0e7077261502"
checksum = "23aa6811d3bd4deb8a84dde645f943476d13b248d818edcf8ce0b2f37f036b44"
dependencies = [
"cc",
"libc",
@@ -7668,8 +7615,8 @@ dependencies = [
[[package]]
name = "libwebrtc"
version = "0.3.10"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=4941ff2352c9a243113607e808fc784450aa2ff3#4941ff2352c9a243113607e808fc784450aa2ff3"
version = "0.3.7"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=811ceae29fabee455f110c56cd66b3f49a7e5003#811ceae29fabee455f110c56cd66b3f49a7e5003"
dependencies = [
"cxx",
"jni",
@@ -7753,13 +7700,12 @@ checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
[[package]]
name = "livekit"
version = "0.7.7"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=4941ff2352c9a243113607e808fc784450aa2ff3#4941ff2352c9a243113607e808fc784450aa2ff3"
version = "0.7.0"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=811ceae29fabee455f110c56cd66b3f49a7e5003#811ceae29fabee455f110c56cd66b3f49a7e5003"
dependencies = [
"chrono",
"futures-util",
"lazy_static",
"libloading",
"libwebrtc",
"livekit-api",
"livekit-protocol",
@@ -7776,10 +7722,10 @@ dependencies = [
[[package]]
name = "livekit-api"
version = "0.4.2"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=4941ff2352c9a243113607e808fc784450aa2ff3#4941ff2352c9a243113607e808fc784450aa2ff3"
version = "0.4.1"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=811ceae29fabee455f110c56cd66b3f49a7e5003#811ceae29fabee455f110c56cd66b3f49a7e5003"
dependencies = [
"base64 0.21.7",
"async-tungstenite",
"futures-util",
"http 0.2.12",
"jsonwebtoken",
@@ -7787,9 +7733,7 @@ dependencies = [
"livekit-runtime",
"log",
"parking_lot",
"pbjson-types",
"prost 0.12.6",
"rand 0.9.0",
"reqwest 0.11.27",
"scopeguard",
"serde",
@@ -7797,14 +7741,14 @@ dependencies = [
"sha2",
"thiserror 1.0.69",
"tokio",
"tokio-tungstenite 0.26.2",
"tokio-tungstenite 0.20.1",
"url",
]
[[package]]
name = "livekit-protocol"
version = "0.3.9"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=4941ff2352c9a243113607e808fc784450aa2ff3#4941ff2352c9a243113607e808fc784450aa2ff3"
version = "0.3.6"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=811ceae29fabee455f110c56cd66b3f49a7e5003#811ceae29fabee455f110c56cd66b3f49a7e5003"
dependencies = [
"futures-util",
"livekit-runtime",
@@ -7820,11 +7764,13 @@ dependencies = [
[[package]]
name = "livekit-runtime"
version = "0.4.0"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=4941ff2352c9a243113607e808fc784450aa2ff3#4941ff2352c9a243113607e808fc784450aa2ff3"
version = "0.3.1"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=811ceae29fabee455f110c56cd66b3f49a7e5003#811ceae29fabee455f110c56cd66b3f49a7e5003"
dependencies = [
"tokio",
"tokio-stream",
"async-io",
"async-std",
"async-task",
"futures 0.3.31",
]
[[package]]
@@ -7849,21 +7795,19 @@ dependencies = [
"anyhow",
"async-trait",
"collections",
"core-foundation 0.10.0",
"core-video",
"core-foundation 0.9.4",
"coreaudio-rs 0.12.1",
"cpal",
"futures 0.3.31",
"gpui",
"gpui_tokio",
"http_client_tls",
"http 0.2.12",
"http_client",
"image",
"libwebrtc",
"livekit",
"livekit_api",
"log",
"media",
"nanoid",
"objc",
"parking_lot",
"postage",
"serde",
@@ -7871,10 +7815,32 @@ dependencies = [
"sha2",
"simplelog",
"smallvec",
"tokio-tungstenite 0.26.2",
"util",
]
[[package]]
name = "livekit_client_macos"
version = "0.1.0"
dependencies = [
"anyhow",
"async-broadcast",
"async-trait",
"collections",
"core-foundation 0.9.4",
"futures 0.3.31",
"gpui",
"livekit_api",
"log",
"media",
"nanoid",
"parking_lot",
"postage",
"serde",
"serde_json",
"sha2",
"simplelog",
]
[[package]]
name = "lmdb-master-sys"
version = "0.2.4"
@@ -8235,8 +8201,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"bindgen 0.70.1",
"core-foundation 0.10.0",
"core-video",
"core-foundation 0.9.4",
"ctor",
"foreign-types 0.5.0",
"metal",
@@ -8286,9 +8251,9 @@ dependencies = [
[[package]]
name = "metal"
version = "0.29.0"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21"
checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e"
dependencies = [
"bitflags 2.8.0",
"block",
@@ -8315,9 +8280,9 @@ dependencies = [
[[package]]
name = "mimalloc"
version = "0.1.44"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99585191385958383e13f6b822e6b6d8d9cf928e7d286ceb092da92b43c87bc1"
checksum = "68914350ae34959d83f732418d51e2427a794055d0b9529f48259ac07af65633"
dependencies = [
"libmimalloc-sys",
]
@@ -10147,15 +10112,6 @@ dependencies = [
"indexmap",
]
[[package]]
name = "pgvector"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0e8871b6d7ca78348c6cd29b911b94851f3429f0cd403130ca17f26c1fb91a6"
dependencies = [
"serde",
]
[[package]]
name = "phf"
version = "0.11.2"
@@ -10636,7 +10592,6 @@ dependencies = [
"smol",
"snippet",
"snippet_provider",
"sum_tree",
"task",
"tempfile",
"terminal",
@@ -11006,7 +10961,7 @@ dependencies = [
"quinn-proto",
"quinn-udp",
"rustc-hash 2.1.1",
"rustls 0.23.25",
"rustls 0.23.23",
"socket2",
"thiserror 2.0.12",
"tokio",
@@ -11024,7 +10979,7 @@ dependencies = [
"rand 0.8.5",
"ring",
"rustc-hash 2.1.1",
"rustls 0.23.25",
"rustls 0.23.23",
"rustls-pki-types",
"slab",
"thiserror 2.0.12",
@@ -11044,7 +10999,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -11621,7 +11576,7 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls 0.23.25",
"rustls 0.23.23",
"rustls-native-certs 0.8.1",
"rustls-pemfile 2.2.0",
"rustls-pki-types",
@@ -11969,7 +11924,7 @@ dependencies = [
"libc",
"linux-raw-sys",
"once_cell",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -11997,16 +11952,16 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.25"
version = "0.23.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c"
checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395"
dependencies = [
"aws-lc-rs",
"log",
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki 0.103.0",
"rustls-webpki 0.102.8",
"subtle",
"zeroize",
]
@@ -12055,32 +12010,32 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.11.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b"
dependencies = [
"web-time",
]
[[package]]
name = "rustls-platform-verifier"
version = "0.5.1"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5467026f437b4cb2a533865eaa73eb840019a0916f4b9ec563c6e617e086c9"
checksum = "e012c45844a1790332c9386ed4ca3a06def221092eda277e6f079728f8ea99da"
dependencies = [
"core-foundation 0.10.0",
"core-foundation-sys",
"jni",
"log",
"once_cell",
"rustls 0.23.25",
"rustls 0.23.23",
"rustls-native-certs 0.8.1",
"rustls-platform-verifier-android",
"rustls-webpki 0.103.0",
"rustls-webpki 0.102.8",
"security-framework 3.0.1",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -12101,9 +12056,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
version = "0.103.0"
version = "0.102.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0aa4eeac2588ffff23e9d7a7e9b3f971c5fb5b7ebc9452745e0c232c64f83b2f"
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
dependencies = [
"aws-lc-rs",
"ring",
@@ -12302,18 +12257,17 @@ dependencies = [
[[package]]
name = "sea-orm"
version = "1.1.7"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3417812d38049e8ec3d588c03570f8c60de811d2453fb48e424045a1600ffd86"
checksum = "00733e5418e8ae3758cdb988c3654174e716230cc53ee2cb884207cf86a23029"
dependencies = [
"async-stream",
"async-trait",
"bigdecimal",
"chrono",
"futures-util",
"futures 0.3.31",
"log",
"ouroboros",
"pgvector",
"rust_decimal",
"sea-orm-macros",
"sea-query",
@@ -12331,9 +12285,9 @@ dependencies = [
[[package]]
name = "sea-orm-macros"
version = "1.1.7"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d705ba84e1c74c8ac27784e4ac6f21584058c1dc0cadb9d39b43e109fcf8139c"
checksum = "a98408f82fb4875d41ef469a79944a7da29767c7b3e4028e22188a3dd613b10f"
dependencies = [
"heck 0.4.1",
"proc-macro2",
@@ -12566,9 +12520,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.140"
version = "1.0.139"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
dependencies = [
"indexmap",
"itoa",
@@ -12624,9 +12578,9 @@ dependencies = [
[[package]]
name = "serde_repr"
version = "0.1.20"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [
"proc-macro2",
"quote",
@@ -12752,6 +12706,13 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
[[package]]
name = "shell_parser"
version = "0.1.0"
dependencies = [
"shlex",
]
[[package]]
name = "shellexpand"
version = "2.1.2"
@@ -13139,7 +13100,7 @@ dependencies = [
"once_cell",
"percent-encoding",
"rust_decimal",
"rustls 0.23.25",
"rustls 0.23.23",
"rustls-pemfile 2.2.0",
"serde",
"serde_json",
@@ -13762,7 +13723,7 @@ dependencies = [
"fd-lock",
"io-lifetimes",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
"winx",
]
@@ -13906,7 +13867,7 @@ dependencies = [
"getrandom 0.3.1",
"once_cell",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -14180,9 +14141,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.40"
version = "0.3.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d9c75b47bdff86fa3334a3db91356b8d7d86a9b839dab7d0bdc5c3d3a077618"
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
dependencies = [
"deranged",
"itoa",
@@ -14197,15 +14158,15 @@ dependencies = [
[[package]]
name = "time-core"
version = "0.1.4"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.21"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29aa485584182073ed57fd5004aa09c371f021325014694e432313345865fd04"
checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
dependencies = [
"num-conv",
"time-core",
@@ -14215,7 +14176,7 @@ dependencies = [
name = "time_format"
version = "0.1.0"
dependencies = [
"core-foundation 0.10.0",
"core-foundation 0.9.4",
"core-foundation-sys",
"sys-locale",
"time",
@@ -14403,7 +14364,7 @@ version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37"
dependencies = [
"rustls 0.23.25",
"rustls 0.23.23",
"tokio",
]
@@ -14439,7 +14400,10 @@ checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c"
dependencies = [
"futures-util",
"log",
"rustls 0.21.12",
"rustls-native-certs 0.6.3",
"tokio",
"tokio-rustls 0.24.1",
"tungstenite 0.20.1",
]
@@ -14455,21 +14419,6 @@ dependencies = [
"tungstenite 0.21.0",
]
[[package]]
name = "tokio-tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
dependencies = [
"futures-util",
"log",
"rustls 0.23.25",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.1",
"tungstenite 0.26.2",
]
[[package]]
name = "tokio-util"
version = "0.7.13"
@@ -14968,6 +14917,7 @@ dependencies = [
"httparse",
"log",
"rand 0.8.5",
"rustls 0.21.12",
"sha1",
"thiserror 1.0.69",
"url",
@@ -15011,25 +14961,6 @@ dependencies = [
"utf-8",
]
[[package]]
name = "tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
dependencies = [
"bytes 1.10.1",
"data-encoding",
"http 1.2.0",
"httparse",
"log",
"rand 0.9.0",
"rustls 0.23.25",
"rustls-pki-types",
"sha1",
"thiserror 2.0.12",
"utf-8",
]
[[package]]
name = "typeid"
version = "1.0.2"
@@ -15216,9 +15147,9 @@ checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
[[package]]
name = "unindent"
version = "0.2.4"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce"
[[package]]
name = "untrusted"
@@ -16165,8 +16096,8 @@ dependencies = [
[[package]]
name = "webrtc-sys"
version = "0.3.7"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=4941ff2352c9a243113607e808fc784450aa2ff3#4941ff2352c9a243113607e808fc784450aa2ff3"
version = "0.3.5"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=811ceae29fabee455f110c56cd66b3f49a7e5003#811ceae29fabee455f110c56cd66b3f49a7e5003"
dependencies = [
"cc",
"cxx",
@@ -16178,8 +16109,8 @@ dependencies = [
[[package]]
name = "webrtc-sys-build"
version = "0.3.6"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=4941ff2352c9a243113607e808fc784450aa2ff3#4941ff2352c9a243113607e808fc784450aa2ff3"
version = "0.3.5"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=811ceae29fabee455f110c56cd66b3f49a7e5003#811ceae29fabee455f110c56cd66b3f49a7e5003"
dependencies = [
"fs2",
"regex",
@@ -16330,7 +16261,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -16869,9 +16800,9 @@ dependencies = [
[[package]]
name = "winresource"
version = "0.1.20"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba4a67c78ee5782c0c1cb41bebc7e12c6e79644daa1650ebbc1de5d5b08593f7"
checksum = "7276691b353ad4547af8c3268488d1311f4be791ffdc0c65b8cfa8f41eed693b"
dependencies = [
"toml 0.8.20",
"version_check",
@@ -16890,7 +16821,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d"
dependencies = [
"bitflags 2.8.0",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]

View File

@@ -86,6 +86,7 @@ members = [
"crates/languages",
"crates/livekit_api",
"crates/livekit_client",
"crates/livekit_client_macos",
"crates/lmstudio",
"crates/lsp",
"crates/markdown",
@@ -130,6 +131,7 @@ members = [
"crates/session",
"crates/settings",
"crates/settings_ui",
"crates/shell_parser",
"crates/snippet",
"crates/snippet_provider",
"crates/snippets_ui",
@@ -288,6 +290,7 @@ language_tools = { path = "crates/language_tools" }
languages = { path = "crates/languages" }
livekit_api = { path = "crates/livekit_api" }
livekit_client = { path = "crates/livekit_client" }
livekit_client_macos = { path = "crates/livekit_client_macos" }
lmstudio = { path = "crates/lmstudio" }
lsp = { path = "crates/lsp" }
markdown = { path = "crates/markdown" }
@@ -407,16 +410,15 @@ blade-util = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f
naga = { version = "23.1.0", features = ["wgsl-in"] }
blake3 = "1.5.3"
bytes = "1.0"
cargo_metadata = { git = "https://github.com/zed-industries/cargo_metadata", rev = "ce8171bad673923d61a77b6761d0dc4aff63398a"}
cargo_metadata = "0.19"
cargo_toml = "0.21"
chrono = { version = "0.4", features = ["serde"] }
circular-buffer = "1.0"
clap = { version = "4.4", features = ["derive"] }
cocoa = "0.26"
cocoa-foundation = "0.2.0"
core-video = { version = "0.4.3", features = ["metal"] }
convert_case = "0.8.0"
core-foundation = "0.10.0"
core-foundation = "0.9.3"
core-foundation-sys = "0.8.6"
ctor = "0.4.0"
dashmap = "6.0"
@@ -454,6 +456,11 @@ libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
linkme = "0.3.31"
livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "811ceae29fabee455f110c56cd66b3f49a7e5003", features = [
"dispatcher",
"services-dispatcher",
"rustls-tls-native-roots",
], default-features = false }
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
markup5ever_rcdom = "0.3.0"
mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
@@ -541,7 +548,6 @@ time = { version = "0.3", features = [
tiny_http = "0.8"
toml = "0.8"
tokio = { version = "1" }
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"]}
tower-http = "0.4.4"
tree-sitter = { version = "0.25.3", features = ["wasm"] }
tree-sitter-bash = "0.23"
@@ -587,7 +593,7 @@ which = "6.0.0"
wit-component = "0.221"
zed_llm_client = "0.4"
zstd = "0.11"
metal = "0.29"
metal = "0.31"
[workspace.dependencies.async-stripe]
git = "https://github.com/zed-industries/async-stripe"

View File

@@ -14,19 +14,5 @@ Be concise and direct in your responses.
The user has opened a project that contains the following root directories/files:
{{#each worktrees}}
- `{{root_name}}` (absolute path: `{{abs_path}}`)
- {{root_name}} (absolute path: {{abs_path}})
{{/each}}
{{#if has_rules}}
There are rules that apply to these root directories:
{{#each worktrees}}
{{#if rules_file}}
`{{root_name}}/{{rules_file.rel_path}}`:
``````
{{{rules_file.text}}}
``````
{{/if}}
{{/each}}
{{/if}}

View File

@@ -1,16 +1,14 @@
use crate::thread::{
LastRestoreCheckpoint, MessageId, RequestKind, Thread, ThreadError, ThreadEvent,
};
use crate::thread::{MessageId, RequestKind, Thread, ThreadError, ThreadEvent};
use crate::thread_store::ThreadStore;
use crate::tool_use::{ToolUse, ToolUseStatus};
use crate::ui::ContextPill;
use collections::HashMap;
use editor::{Editor, MultiBuffer};
use gpui::{
list, percentage, pulsating_between, AbsoluteLength, Animation, AnimationExt, AnyElement, App,
ClickEvent, DefiniteLength, EdgesRefinement, Empty, Entity, Focusable, Length, ListAlignment,
ListOffset, ListState, StyleRefinement, Subscription, Task, TextStyleRefinement,
Transformation, UnderlineStyle, WeakEntity,
list, percentage, AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent,
DefiniteLength, EdgesRefinement, Empty, Entity, Focusable, Length, ListAlignment, ListOffset,
ListState, StyleRefinement, Subscription, Task, TextStyleRefinement, Transformation,
UnderlineStyle,
};
use language::{Buffer, LanguageRegistry};
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
@@ -20,9 +18,9 @@ use settings::Settings as _;
use std::sync::Arc;
use std::time::Duration;
use theme::ThemeSettings;
use ui::{prelude::*, Disclosure, KeyBinding, Tooltip};
use ui::Color;
use ui::{prelude::*, Disclosure, KeyBinding};
use util::ResultExt as _;
use workspace::{OpenOptions, Workspace};
use crate::context_store::{refresh_context_store_text, ContextStore};
@@ -31,13 +29,11 @@ pub struct ActiveThread {
thread_store: Entity<ThreadStore>,
thread: Entity<Thread>,
context_store: Entity<ContextStore>,
workspace: WeakEntity<Workspace>,
save_thread_task: Option<Task<()>>,
messages: Vec<MessageId>,
list_state: ListState,
rendered_messages_by_id: HashMap<MessageId, Entity<Markdown>>,
rendered_scripting_tool_uses: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
rendered_tool_use_labels: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
editing_message: Option<(MessageId, EditMessageState)>,
expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
last_error: Option<ThreadError>,
@@ -54,7 +50,6 @@ impl ActiveThread {
thread_store: Entity<ThreadStore>,
language_registry: Arc<LanguageRegistry>,
context_store: Entity<ContextStore>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -68,12 +63,10 @@ impl ActiveThread {
thread_store,
thread: thread.clone(),
context_store,
workspace,
save_thread_task: None,
messages: Vec::new(),
rendered_messages_by_id: HashMap::default(),
rendered_scripting_tool_uses: HashMap::default(),
rendered_tool_use_labels: HashMap::default(),
expanded_tool_uses: HashMap::default(),
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
let this = cx.entity().downgrade();
@@ -90,29 +83,10 @@ impl ActiveThread {
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
this.push_message(&message.id, message.text.clone(), window, cx);
for tool_use in thread.read(cx).tool_uses_for_message(message.id, cx) {
this.render_tool_use_label_markdown(
tool_use.id.clone(),
tool_use.ui_text.clone(),
window,
cx,
);
}
for tool_use in thread
.read(cx)
.scripting_tool_uses_for_message(message.id, cx)
{
this.render_tool_use_label_markdown(
tool_use.id.clone(),
tool_use.ui_text.clone(),
window,
cx,
);
for tool_use in thread.read(cx).scripting_tool_uses_for_message(message.id) {
this.render_scripting_tool_use_markdown(
tool_use.id.clone(),
tool_use.ui_text.as_ref(),
tool_use.name.as_ref(),
tool_use.input.clone(),
window,
cx,
@@ -310,19 +284,6 @@ impl ActiveThread {
.insert(tool_use_id, lua_script);
}
fn render_tool_use_label_markdown(
&mut self,
tool_use_id: LanguageModelToolUseId,
tool_label: impl Into<SharedString>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.rendered_tool_use_labels.insert(
tool_use_id,
self.render_markdown(tool_label.into(), window, cx),
);
}
fn handle_thread_event(
&mut self,
_thread: &Entity<Thread>,
@@ -377,18 +338,9 @@ impl ActiveThread {
cx.notify();
}
ThreadEvent::UsePendingTools => {
let tool_uses = self
.thread
.update(cx, |thread, cx| thread.use_pending_tools(cx));
for tool_use in tool_uses {
self.render_tool_use_label_markdown(
tool_use.id,
tool_use.ui_text.clone(),
window,
cx,
);
}
self.thread.update(cx, |thread, cx| {
thread.use_pending_tools(cx);
});
}
ThreadEvent::ToolFinished {
pending_tool_use,
@@ -397,12 +349,6 @@ impl ActiveThread {
} => {
let canceled = *canceled;
if let Some(tool_use) = pending_tool_use {
self.render_tool_use_label_markdown(
tool_use.id.clone(),
SharedString::from(tool_use.ui_text.clone()),
window,
cx,
);
self.render_scripting_tool_use_markdown(
tool_use.id.clone(),
tool_use.name.as_ref(),
@@ -464,7 +410,6 @@ impl ActiveThread {
}
}
}
ThreadEvent::CheckpointChanged => cx.notify(),
}
}
@@ -607,8 +552,8 @@ impl ActiveThread {
// Get all the data we need from thread before we start using it in closures
let checkpoint = thread.checkpoint_for_message(message_id);
let context = thread.context_for_message(message_id);
let tool_uses = thread.tool_uses_for_message(message_id, cx);
let scripting_tool_uses = thread.scripting_tool_uses_for_message(message_id, cx);
let tool_uses = thread.tool_uses_for_message(message_id);
let scripting_tool_uses = thread.scripting_tool_uses_for_message(message_id);
// Don't render user messages that are just there for returning tool results.
if message.role == Role::User
@@ -761,25 +706,27 @@ impl ActiveThread {
)
.child(div().p_2().child(message_content)),
),
Role::Assistant => v_flex()
.id(("message-container", ix))
.child(div().py_3().px_4().child(message_content))
.when(
!tool_uses.is_empty() || !scripting_tool_uses.is_empty(),
|parent| {
parent.child(
v_flex()
.children(
tool_uses
.into_iter()
.map(|tool_use| self.render_tool_use(tool_use, cx)),
)
.children(scripting_tool_uses.into_iter().map(|tool_use| {
self.render_scripting_tool_use(tool_use, window, cx)
})),
)
},
),
Role::Assistant => {
v_flex()
.id(("message-container", ix))
.child(div().py_3().px_4().child(message_content))
.when(
!tool_uses.is_empty() || !scripting_tool_uses.is_empty(),
|parent| {
parent.child(
v_flex()
.children(
tool_uses
.into_iter()
.map(|tool_use| self.render_tool_use(tool_use, cx)),
)
.children(scripting_tool_uses.into_iter().map(|tool_use| {
self.render_scripting_tool_use(tool_use, cx)
})),
)
},
)
}
Role::System => div().id(("message-container", ix)).py_1().px_2().child(
v_flex()
.bg(colors.editor_background)
@@ -789,64 +736,21 @@ impl ActiveThread {
};
v_flex()
.when(ix == 0, |parent| parent.child(self.render_rules_item(cx)))
.when_some(checkpoint, |parent, checkpoint| {
let mut is_pending = false;
let mut error = None;
if let Some(last_restore_checkpoint) =
self.thread.read(cx).last_restore_checkpoint()
{
if last_restore_checkpoint.message_id() == message_id {
match last_restore_checkpoint {
LastRestoreCheckpoint::Pending { .. } => is_pending = true,
LastRestoreCheckpoint::Error { error: err, .. } => {
error = Some(err.clone());
}
}
}
}
let restore_checkpoint_button =
Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
.icon(if error.is_some() {
IconName::XCircle
} else {
IconName::Undo
})
.size(ButtonSize::Compact)
.disabled(is_pending)
.icon_color(if error.is_some() {
Some(Color::Error)
} else {
None
})
.on_click(cx.listener(move |this, _, _window, cx| {
this.thread.update(cx, |thread, cx| {
thread
.restore_checkpoint(checkpoint.clone(), cx)
.detach_and_log_err(cx);
});
}));
let restore_checkpoint_button = if is_pending {
restore_checkpoint_button
.with_animation(
("pulsating-restore-checkpoint-button", ix),
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.6, 1.)),
|label, delta| label.alpha(delta),
)
.into_any_element()
} else if let Some(error) = error {
restore_checkpoint_button
.tooltip(Tooltip::text(error.to_string()))
.into_any_element()
} else {
restore_checkpoint_button.into_any_element()
};
parent.child(h_flex().pl_2().child(restore_checkpoint_button))
parent.child(
h_flex().pl_2().child(
Button::new("restore-checkpoint", "Restore Checkpoint")
.icon(IconName::Undo)
.size(ButtonSize::Compact)
.on_click(cx.listener(move |this, _, _window, cx| {
this.thread.update(cx, |thread, cx| {
thread
.restore_checkpoint(checkpoint.clone(), cx)
.detach_and_log_err(cx);
});
})),
),
)
})
.child(styled_message)
.into_any()
@@ -897,10 +801,11 @@ impl ActiveThread {
}
}),
))
.child(div().text_ui_sm(cx).children(
self.rendered_tool_use_labels.get(&tool_use.id).cloned(),
))
.truncate(),
.child(
Label::new(tool_use.name)
.size(LabelSize::Small)
.buffer_font(cx),
),
)
.child({
let (icon_name, color, animated) = match &tool_use.status {
@@ -1028,7 +933,6 @@ impl ActiveThread {
fn render_scripting_tool_use(
&self,
tool_use: ToolUse,
window: &Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let is_open = self
@@ -1074,12 +978,7 @@ impl ActiveThread {
}
}),
))
.child(div().text_ui_sm(cx).child(self.render_markdown(
tool_use.ui_text.clone(),
window,
cx,
)))
.truncate(),
.child(Label::new(tool_use.name)),
)
.child(
Label::new(match tool_use.status {
@@ -1143,86 +1042,6 @@ impl ActiveThread {
}),
)
}
fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
else {
return div().into_any();
};
let rules_files = system_prompt_context
.worktrees
.iter()
.filter_map(|worktree| worktree.rules_file.as_ref())
.collect::<Vec<_>>();
let label_text = match rules_files.as_slice() {
&[] => return div().into_any(),
&[rules_file] => {
format!("Using {:?} file", rules_file.rel_path)
}
rules_files => {
format!("Using {} rules files", rules_files.len())
}
};
div()
.pt_1()
.px_2p5()
.child(
h_flex()
.group("rules-item")
.w_full()
.gap_2()
.justify_between()
.child(
h_flex()
.gap_1p5()
.child(
Icon::new(IconName::File)
.size(IconSize::XSmall)
.color(Color::Disabled),
)
.child(
Label::new(label_text)
.size(LabelSize::XSmall)
.color(Color::Muted)
.buffer_font(cx),
),
)
.child(
div().visible_on_hover("rules-item").child(
Button::new("open-rules", "Open Rules")
.label_size(LabelSize::XSmall)
.on_click(cx.listener(Self::handle_open_rules)),
),
),
)
.into_any()
}
fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
else {
return;
};
let abs_paths = system_prompt_context
.worktrees
.iter()
.flat_map(|worktree| worktree.rules_file.as_ref())
.map(|rules_file| rules_file.abs_path.to_path_buf())
.collect::<Vec<_>>();
if let Ok(task) = self.workspace.update(cx, move |workspace, cx| {
// TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
// files clear. For example, if rules file 1 is already open but rules file 2 is not,
// this would open and focus rules file 2 in a tab that is not next to rules file 1.
workspace.open_paths(abs_paths, OpenOptions::default(), None, window, cx)
}) {
task.detach();
}
}
}
impl Render for ActiveThread {

View File

@@ -1,4 +1,5 @@
mod active_thread;
mod agent_profile;
mod assistant_configuration;
mod assistant_model_selector;
mod assistant_panel;

View File

@@ -195,7 +195,6 @@ impl AssistantConfiguration {
let tool_count = tools.len();
v_flex()
.id(SharedString::from(context_server.id()))
.border_1()
.rounded_sm()
.border_color(cx.theme().colors().border)

View File

@@ -174,7 +174,6 @@ impl AssistantPanel {
thread_store.clone(),
language_registry.clone(),
message_editor_context_store.clone(),
workspace.clone(),
window,
cx,
)
@@ -253,7 +252,6 @@ impl AssistantPanel {
self.thread_store.clone(),
self.language_registry.clone(),
message_editor_context_store.clone(),
self.workspace.clone(),
window,
cx,
)
@@ -391,7 +389,6 @@ impl AssistantPanel {
this.thread_store.clone(),
this.language_registry.clone(),
message_editor_context_store.clone(),
this.workspace.clone(),
window,
cx,
)
@@ -458,7 +455,7 @@ impl AssistantPanel {
workspace.update_in(cx, |workspace, window, cx| {
let thread = thread.read(cx);
let markdown = thread.to_markdown(cx)?;
let markdown = thread.to_markdown()?;
let thread_summary = thread
.summary()
.map(|summary| summary.to_string())
@@ -925,8 +922,8 @@ impl AssistantPanel {
ThreadError::MaxMonthlySpendReached => {
self.render_max_monthly_spend_reached_error(cx)
}
ThreadError::Message { header, message } => {
self.render_error_message(header, message, cx)
ThreadError::Message(error_message) => {
self.render_error_message(&error_message, cx)
}
})
.into_any(),
@@ -1029,8 +1026,7 @@ impl AssistantPanel {
fn render_error_message(
&self,
header: SharedString,
message: SharedString,
error_message: &SharedString,
cx: &mut Context<Self>,
) -> AnyElement {
v_flex()
@@ -1040,14 +1036,17 @@ impl AssistantPanel {
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new(header).weight(FontWeight::MEDIUM)),
.child(
Label::new("Error interacting with language model")
.weight(FontWeight::MEDIUM),
),
)
.child(
div()
.id("error-message")
.max_h_32()
.overflow_y_scroll()
.child(Label::new(message)),
.child(Label::new(error_message.clone())),
)
.child(
h_flex()

View File

@@ -33,7 +33,7 @@ use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::thread::{RequestKind, Thread};
use crate::thread_store::ThreadStore;
use crate::tool_selector::ToolSelector;
use crate::{Chat, ChatMode, RemoveAllContext, ThreadEvent, ToggleContextPicker};
use crate::{Chat, ChatMode, RemoveAllContext, ToggleContextPicker};
pub struct MessageEditor {
thread: Entity<Thread>,
@@ -206,23 +206,12 @@ impl MessageEditor {
let refresh_task =
refresh_context_store_text(self.context_store.clone(), &HashSet::default(), cx);
let system_prompt_context_task = self.thread.read(cx).load_system_prompt_context(cx);
let thread = self.thread.clone();
let context_store = self.context_store.clone();
let git_store = self.project.read(cx).git_store();
let checkpoint = git_store.read(cx).checkpoint(cx);
cx.spawn(async move |_, cx| {
refresh_task.await;
let (system_prompt_context, load_error) = system_prompt_context_task.await;
thread
.update(cx, |thread, cx| {
thread.set_system_prompt_context(system_prompt_context);
if let Some(load_error) = load_error {
cx.emit(ThreadEvent::ShowError(load_error));
}
})
.ok();
let checkpoint = checkpoint.await.log_err();
thread
.update(cx, |thread, cx| {

View File

@@ -6,7 +6,6 @@ use anyhow::{Context as _, Result};
use assistant_tool::{ActionLog, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::{BTreeMap, HashMap, HashSet};
use fs::Fs;
use futures::future::Shared;
use futures::{FutureExt, StreamExt as _};
use git;
@@ -17,14 +16,12 @@ use language_model::{
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
Role, StopReason, TokenUsage,
};
use project::git_store::{GitStore, GitStoreCheckpoint};
use project::{Project, Worktree};
use prompt_store::{
AssistantSystemPromptContext, PromptBuilder, RulesFile, WorktreeInfoForSystemPrompt,
};
use project::git::GitStoreCheckpoint;
use project::Project;
use prompt_store::{AssistantSystemPromptWorktree, PromptBuilder};
use scripting_tool::{ScriptingSession, ScriptingTool};
use serde::{Deserialize, Serialize};
use util::{maybe, post_inc, ResultExt as _, TryFutureExt as _};
use util::{post_inc, ResultExt, TryFutureExt as _};
use uuid::Uuid;
use crate::context::{attach_context_to_message, ContextId, ContextSnapshot};
@@ -99,25 +96,6 @@ pub struct ThreadCheckpoint {
git_checkpoint: GitStoreCheckpoint,
}
pub enum LastRestoreCheckpoint {
Pending {
message_id: MessageId,
},
Error {
message_id: MessageId,
error: String,
},
}
impl LastRestoreCheckpoint {
pub fn message_id(&self) -> MessageId {
match self {
LastRestoreCheckpoint::Pending { message_id } => *message_id,
LastRestoreCheckpoint::Error { message_id, .. } => *message_id,
}
}
}
/// A thread of conversation with the LLM.
pub struct Thread {
id: ThreadId,
@@ -128,7 +106,6 @@ pub struct Thread {
next_message_id: MessageId,
context: BTreeMap<ContextId, ContextSnapshot>,
context_by_message: HashMap<MessageId, Vec<ContextId>>,
system_prompt_context: Option<AssistantSystemPromptContext>,
checkpoints_by_message: HashMap<MessageId, GitStoreCheckpoint>,
completion_count: usize,
pending_completions: Vec<PendingCompletion>,
@@ -137,7 +114,6 @@ pub struct Thread {
tools: Arc<ToolWorkingSet>,
tool_use: ToolUseState,
action_log: Entity<ActionLog>,
last_restore_checkpoint: Option<LastRestoreCheckpoint>,
scripting_session: Entity<ScriptingSession>,
scripting_tool_use: ToolUseState,
initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>,
@@ -160,17 +136,15 @@ impl Thread {
next_message_id: MessageId(0),
context: BTreeMap::default(),
context_by_message: HashMap::default(),
system_prompt_context: None,
checkpoints_by_message: HashMap::default(),
completion_count: 0,
pending_completions: Vec::new(),
project: project.clone(),
prompt_builder,
tools: tools.clone(),
last_restore_checkpoint: None,
tool_use: ToolUseState::new(tools.clone()),
tools,
tool_use: ToolUseState::new(),
scripting_session: cx.new(|cx| ScriptingSession::new(project.clone(), cx)),
scripting_tool_use: ToolUseState::new(tools),
scripting_tool_use: ToolUseState::new(),
action_log: cx.new(|_| ActionLog::new()),
initial_project_snapshot: {
let project_snapshot = Self::project_snapshot(project, cx);
@@ -197,12 +171,11 @@ impl Thread {
.map(|message| message.id.0 + 1)
.unwrap_or(0),
);
let tool_use =
ToolUseState::from_serialized_messages(tools.clone(), &serialized.messages, |name| {
name != ScriptingTool::NAME
});
let tool_use = ToolUseState::from_serialized_messages(&serialized.messages, |name| {
name != ScriptingTool::NAME
});
let scripting_tool_use =
ToolUseState::from_serialized_messages(tools.clone(), &serialized.messages, |name| {
ToolUseState::from_serialized_messages(&serialized.messages, |name| {
name == ScriptingTool::NAME
});
let scripting_session = cx.new(|cx| ScriptingSession::new(project.clone(), cx));
@@ -224,11 +197,9 @@ impl Thread {
next_message_id,
context: BTreeMap::default(),
context_by_message: HashMap::default(),
system_prompt_context: None,
checkpoints_by_message: HashMap::default(),
completion_count: 0,
pending_completions: Vec::new(),
last_restore_checkpoint: None,
project,
prompt_builder,
tools,
@@ -301,38 +272,17 @@ impl Thread {
checkpoint: ThreadCheckpoint,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.last_restore_checkpoint = Some(LastRestoreCheckpoint::Pending {
message_id: checkpoint.message_id,
});
cx.emit(ThreadEvent::CheckpointChanged);
let project = self.project.read(cx);
let restore = project
.git_store()
.read(cx)
.restore_checkpoint(checkpoint.git_checkpoint, cx);
cx.spawn(async move |this, cx| {
let result = restore.await;
this.update(cx, |this, cx| {
if let Err(err) = result.as_ref() {
this.last_restore_checkpoint = Some(LastRestoreCheckpoint::Error {
message_id: checkpoint.message_id,
error: err.to_string(),
});
} else {
this.last_restore_checkpoint = None;
this.truncate(checkpoint.message_id, cx);
}
cx.emit(ThreadEvent::CheckpointChanged);
})?;
result
restore.await?;
this.update(cx, |this, cx| this.truncate(checkpoint.message_id, cx))
})
}
pub fn last_restore_checkpoint(&self) -> Option<&LastRestoreCheckpoint> {
self.last_restore_checkpoint.as_ref()
}
pub fn truncate(&mut self, message_id: MessageId, cx: &mut Context<Self>) {
let Some(message_ix) = self
.messages
@@ -372,12 +322,12 @@ impl Thread {
all_pending_tool_uses.all(|tool_use| tool_use.status.is_error())
}
pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> {
self.tool_use.tool_uses_for_message(id, cx)
pub fn tool_uses_for_message(&self, id: MessageId) -> Vec<ToolUse> {
self.tool_use.tool_uses_for_message(id)
}
pub fn scripting_tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> {
self.scripting_tool_use.tool_uses_for_message(id, cx)
pub fn scripting_tool_uses_for_message(&self, id: MessageId) -> Vec<ToolUse> {
self.scripting_tool_use.tool_uses_for_message(id)
}
pub fn tool_results_for_message(&self, id: MessageId) -> Vec<&LanguageModelToolResult> {
@@ -492,7 +442,7 @@ impl Thread {
let initial_project_snapshot = self.initial_project_snapshot.clone();
cx.spawn(async move |this, cx| {
let initial_project_snapshot = initial_project_snapshot.await;
this.read_with(cx, |this, cx| SerializedThread {
this.read_with(cx, |this, _| SerializedThread {
summary: this.summary_or_default(),
updated_at: this.updated_at(),
messages: this
@@ -502,9 +452,9 @@ impl Thread {
role: message.role,
text: message.text.clone(),
tool_uses: this
.tool_uses_for_message(message.id, cx)
.tool_uses_for_message(message.id)
.into_iter()
.chain(this.scripting_tool_uses_for_message(message.id, cx))
.chain(this.scripting_tool_uses_for_message(message.id))
.map(|tool_use| SerializedToolUse {
id: tool_use.id,
name: tool_use.name,
@@ -528,116 +478,6 @@ impl Thread {
})
}
pub fn set_system_prompt_context(&mut self, context: AssistantSystemPromptContext) {
self.system_prompt_context = Some(context);
}
pub fn system_prompt_context(&self) -> &Option<AssistantSystemPromptContext> {
&self.system_prompt_context
}
pub fn load_system_prompt_context(
&self,
cx: &App,
) -> Task<(AssistantSystemPromptContext, Option<ThreadError>)> {
let project = self.project.read(cx);
let tasks = project
.visible_worktrees(cx)
.map(|worktree| {
Self::load_worktree_info_for_system_prompt(
project.fs().clone(),
worktree.read(cx),
cx,
)
})
.collect::<Vec<_>>();
cx.spawn(async |_cx| {
let results = futures::future::join_all(tasks).await;
let mut first_err = None;
let worktrees = results
.into_iter()
.map(|(worktree, err)| {
if first_err.is_none() && err.is_some() {
first_err = err;
}
worktree
})
.collect::<Vec<_>>();
(AssistantSystemPromptContext::new(worktrees), first_err)
})
}
fn load_worktree_info_for_system_prompt(
fs: Arc<dyn Fs>,
worktree: &Worktree,
cx: &App,
) -> Task<(WorktreeInfoForSystemPrompt, Option<ThreadError>)> {
let root_name = worktree.root_name().into();
let abs_path = worktree.abs_path();
// Note that Cline supports `.clinerules` being a directory, but that is not currently
// supported. This doesn't seem to occur often in GitHub repositories.
const RULES_FILE_NAMES: [&'static str; 5] = [
".rules",
".cursorrules",
".windsurfrules",
".clinerules",
"CLAUDE.md",
];
let selected_rules_file = RULES_FILE_NAMES
.into_iter()
.filter_map(|name| {
worktree
.entry_for_path(name)
.filter(|entry| entry.is_file())
.map(|entry| (entry.path.clone(), worktree.absolutize(&entry.path)))
})
.next();
if let Some((rel_rules_path, abs_rules_path)) = selected_rules_file {
cx.spawn(async move |_| {
let rules_file_result = maybe!(async move {
let abs_rules_path = abs_rules_path?;
let text = fs.load(&abs_rules_path).await.with_context(|| {
format!("Failed to load assistant rules file {:?}", abs_rules_path)
})?;
anyhow::Ok(RulesFile {
rel_path: rel_rules_path,
abs_path: abs_rules_path.into(),
text: text.trim().to_string(),
})
})
.await;
let (rules_file, rules_file_error) = match rules_file_result {
Ok(rules_file) => (Some(rules_file), None),
Err(err) => (
None,
Some(ThreadError::Message {
header: "Error loading rules file".into(),
message: format!("{err}").into(),
}),
),
};
let worktree_info = WorktreeInfoForSystemPrompt {
root_name,
abs_path,
rules_file,
};
(worktree_info, rules_file_error)
})
} else {
Task::ready((
WorktreeInfoForSystemPrompt {
root_name,
abs_path,
rules_file: None,
},
None,
))
}
}
pub fn send_to_model(
&mut self,
model: Arc<dyn LanguageModel>,
@@ -675,30 +515,36 @@ impl Thread {
request_kind: RequestKind,
cx: &App,
) -> LanguageModelRequest {
let worktree_root_names = self
.project
.read(cx)
.visible_worktrees(cx)
.map(|worktree| {
let worktree = worktree.read(cx);
AssistantSystemPromptWorktree {
root_name: worktree.root_name().into(),
abs_path: worktree.abs_path(),
}
})
.collect::<Vec<_>>();
let system_prompt = self
.prompt_builder
.generate_assistant_system_prompt(worktree_root_names)
.context("failed to generate assistant system prompt")
.log_err()
.unwrap_or_default();
let mut request = LanguageModelRequest {
messages: vec![],
messages: vec![LanguageModelRequestMessage {
role: Role::System,
content: vec![MessageContent::Text(system_prompt)],
cache: true,
}],
tools: Vec::new(),
stop: Vec::new(),
temperature: None,
};
if let Some(system_prompt_context) = self.system_prompt_context.as_ref() {
if let Some(system_prompt) = self
.prompt_builder
.generate_assistant_system_prompt(system_prompt_context)
.context("failed to generate assistant system prompt")
.log_err()
{
request.messages.push(LanguageModelRequestMessage {
role: Role::System,
content: vec![MessageContent::Text(system_prompt)],
cache: true,
});
}
} else {
log::error!("system_prompt_context not set.")
}
let mut referenced_context_ids = HashSet::default();
for message in &self.messages {
@@ -853,17 +699,13 @@ impl Thread {
.rfind(|message| message.role == Role::Assistant)
{
if tool_use.name.as_ref() == ScriptingTool::NAME {
thread.scripting_tool_use.request_tool_use(
last_assistant_message.id,
tool_use,
cx,
);
thread
.scripting_tool_use
.request_tool_use(last_assistant_message.id, tool_use);
} else {
thread.tool_use.request_tool_use(
last_assistant_message.id,
tool_use,
cx,
);
thread
.tool_use
.request_tool_use(last_assistant_message.id, tool_use);
}
}
}
@@ -915,10 +757,9 @@ impl Thread {
.map(|err| err.to_string())
.collect::<Vec<_>>()
.join("\n");
cx.emit(ThreadEvent::ShowError(ThreadError::Message {
header: "Error interacting with language model".into(),
message: SharedString::from(error_message.clone()),
}));
cx.emit(ThreadEvent::ShowError(ThreadError::Message(
SharedString::from(error_message.clone()),
)));
}
thread.cancel_last_completion(cx);
@@ -1004,10 +845,7 @@ impl Thread {
});
}
pub fn use_pending_tools(
&mut self,
cx: &mut Context<Self>,
) -> impl IntoIterator<Item = PendingToolUse> {
pub fn use_pending_tools(&mut self, cx: &mut Context<Self>) {
let request = self.to_completion_request(RequestKind::Chat, cx);
let pending_tool_uses = self
.tool_use
@@ -1017,22 +855,17 @@ impl Thread {
.cloned()
.collect::<Vec<_>>();
for tool_use in pending_tool_uses.iter() {
for tool_use in pending_tool_uses {
if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
let task = tool.run(
tool_use.input.clone(),
tool_use.input,
&request.messages,
self.project.clone(),
self.action_log.clone(),
cx,
);
self.insert_tool_output(
tool_use.id.clone(),
tool_use.ui_text.clone().into(),
task,
cx,
);
self.insert_tool_output(tool_use.id.clone(), task, cx);
}
}
@@ -1044,8 +877,8 @@ impl Thread {
.cloned()
.collect::<Vec<_>>();
for scripting_tool_use in pending_scripting_tool_uses.iter() {
let task = match ScriptingTool::deserialize_input(scripting_tool_use.input.clone()) {
for scripting_tool_use in pending_scripting_tool_uses {
let task = match ScriptingTool::deserialize_input(scripting_tool_use.input) {
Err(err) => Task::ready(Err(err.into())),
Ok(input) => {
let (script_id, script_task) =
@@ -1072,20 +905,13 @@ impl Thread {
}
};
let ui_text: SharedString = scripting_tool_use.name.clone().into();
self.insert_scripting_tool_output(scripting_tool_use.id.clone(), ui_text, task, cx);
self.insert_scripting_tool_output(scripting_tool_use.id.clone(), task, cx);
}
pending_tool_uses
.into_iter()
.chain(pending_scripting_tool_uses)
}
pub fn insert_tool_output(
&mut self,
tool_use_id: LanguageModelToolUseId,
ui_text: SharedString,
output: Task<Result<String>>,
cx: &mut Context<Self>,
) {
@@ -1110,13 +936,12 @@ impl Thread {
});
self.tool_use
.run_pending_tool(tool_use_id, ui_text, insert_output_task);
.run_pending_tool(tool_use_id, insert_output_task);
}
pub fn insert_scripting_tool_output(
&mut self,
tool_use_id: LanguageModelToolUseId,
ui_text: SharedString,
output: Task<Result<String>>,
cx: &mut Context<Self>,
) {
@@ -1141,7 +966,7 @@ impl Thread {
});
self.scripting_tool_use
.run_pending_tool(tool_use_id, ui_text, insert_output_task);
.run_pending_tool(tool_use_id, insert_output_task);
}
pub fn attach_tool_results(
@@ -1219,11 +1044,10 @@ impl Thread {
project: Entity<Project>,
cx: &mut Context<Self>,
) -> Task<Arc<ProjectSnapshot>> {
let git_store = project.read(cx).git_store().clone();
let worktree_snapshots: Vec<_> = project
.read(cx)
.visible_worktrees(cx)
.map(|worktree| Self::worktree_snapshot(worktree, git_store.clone(), cx))
.map(|worktree| Self::worktree_snapshot(worktree, cx))
.collect();
cx.spawn(async move |_, cx| {
@@ -1252,11 +1076,7 @@ impl Thread {
})
}
fn worktree_snapshot(
worktree: Entity<project::Worktree>,
git_store: Entity<GitStore>,
cx: &App,
) -> Task<WorktreeSnapshot> {
fn worktree_snapshot(worktree: Entity<project::Worktree>, cx: &App) -> Task<WorktreeSnapshot> {
cx.spawn(async move |cx| {
// Get worktree path and snapshot
let worktree_info = cx.update(|app_cx| {
@@ -1273,40 +1093,42 @@ impl Thread {
};
};
let repo_info = git_store
.update(cx, |git_store, cx| {
git_store
.repositories()
.values()
.find(|repo| repo.read(cx).worktree_id == snapshot.id())
.and_then(|repo| {
let repo = repo.read(cx);
Some((repo.branch().cloned(), repo.local_repository()?))
})
})
.ok()
.flatten();
// Extract git information
let git_state = match repo_info {
let git_state = match snapshot.repositories().first() {
None => None,
Some((branch, repo)) => {
let current_branch = branch.map(|branch| branch.name.to_string());
let remote_url = repo.remote_url("origin");
let head_sha = repo.head_sha();
Some(repo_entry) => {
// Get branch information
let current_branch = repo_entry.branch().map(|branch| branch.name.to_string());
// Get diff asynchronously
let diff = repo
.diff(git::repository::DiffType::HeadToWorktree, cx.clone())
.await
.ok();
// Get repository info
let repo_result = worktree.read_with(cx, |worktree, _cx| {
if let project::Worktree::Local(local_worktree) = &worktree {
local_worktree.get_local_repo(repo_entry).map(|local_repo| {
let repo = local_repo.repo();
(repo.remote_url("origin"), repo.head_sha(), repo.clone())
})
} else {
None
}
});
Some(GitState {
remote_url,
head_sha,
current_branch,
diff,
})
match repo_result {
Ok(Some((remote_url, head_sha, repository))) => {
// Get diff asynchronously
let diff = repository
.diff(git::repository::DiffType::HeadToWorktree, cx.clone())
.await
.ok();
Some(GitState {
remote_url,
head_sha,
current_branch,
diff,
})
}
Err(_) | Ok(None) => None,
}
}
};
@@ -1317,7 +1139,7 @@ impl Thread {
})
}
pub fn to_markdown(&self, cx: &App) -> Result<String> {
pub fn to_markdown(&self) -> Result<String> {
let mut markdown = Vec::new();
if let Some(summary) = self.summary() {
@@ -1336,7 +1158,7 @@ impl Thread {
)?;
writeln!(markdown, "{}\n", message.text)?;
for tool_use in self.tool_uses_for_message(message.id, cx) {
for tool_use in self.tool_uses_for_message(message.id) {
writeln!(
markdown,
"**Use Tool: {} ({})**",
@@ -1382,10 +1204,7 @@ impl Thread {
pub enum ThreadError {
PaymentRequired,
MaxMonthlySpendReached,
Message {
header: SharedString,
message: SharedString,
},
Message(SharedString),
}
#[derive(Debug, Clone)]
@@ -1407,7 +1226,6 @@ pub enum ThreadEvent {
/// Whether the tool was canceled by the user.
canceled: bool,
},
CheckpointChanged,
}
impl EventEmitter<ThreadEvent> for Thread {}

View File

@@ -20,7 +20,7 @@ use prompt_store::PromptBuilder;
use serde::{Deserialize, Serialize};
use util::ResultExt as _;
use crate::thread::{MessageId, ProjectSnapshot, Thread, ThreadEvent, ThreadId};
use crate::thread::{MessageId, ProjectSnapshot, Thread, ThreadId};
pub fn init(cx: &mut App) {
ThreadsDatabase::init(cx);
@@ -113,7 +113,7 @@ impl ThreadStore {
.await?
.ok_or_else(|| anyhow!("no thread found with ID: {id:?}"))?;
let thread = this.update(cx, |this, cx| {
this.update(cx, |this, cx| {
cx.new(|cx| {
Thread::deserialize(
id.clone(),
@@ -124,19 +124,7 @@ impl ThreadStore {
cx,
)
})
})?;
let (system_prompt_context, load_error) = thread
.update(cx, |thread, cx| thread.load_system_prompt_context(cx))?
.await;
thread.update(cx, |thread, cx| {
thread.set_system_prompt_context(system_prompt_context);
if let Some(load_error) = load_error {
cx.emit(ThreadEvent::ShowError(load_error));
}
})?;
Ok(thread)
})
})
}

View File

@@ -1,50 +1,23 @@
use std::sync::Arc;
use assistant_settings::{AgentProfile, AssistantSettings};
use assistant_tool::{ToolSource, ToolWorkingSet};
use collections::HashMap;
use gpui::{Entity, Subscription};
use gpui::Entity;
use scripting_tool::ScriptingTool;
use settings::{Settings as _, SettingsStore};
use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
use crate::agent_profile::AgentProfile;
pub struct ToolSelector {
profiles: HashMap<Arc<str>, AgentProfile>,
profiles: Vec<AgentProfile>,
tools: Arc<ToolWorkingSet>,
_subscriptions: Vec<Subscription>,
}
impl ToolSelector {
pub fn new(tools: Arc<ToolWorkingSet>, cx: &mut Context<Self>) -> Self {
let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
this.refresh_profiles(cx);
});
let mut this = Self {
profiles: HashMap::default(),
pub fn new(tools: Arc<ToolWorkingSet>, _cx: &mut Context<Self>) -> Self {
Self {
profiles: vec![AgentProfile::read_only(), AgentProfile::code_writer()],
tools,
_subscriptions: vec![settings_subscription],
};
this.refresh_profiles(cx);
this
}
fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
let settings = AssistantSettings::get_global(cx);
let mut profiles = settings.profiles.clone();
let read_only = AgentProfile::read_only();
if !profiles.contains_key(read_only.name.as_ref()) {
profiles.insert(read_only.name.clone().into(), read_only);
}
let code_writer = AgentProfile::code_writer();
if !profiles.contains_key(code_writer.name.as_ref()) {
profiles.insert(code_writer.name.clone().into(), code_writer);
}
self.profiles = profiles;
}
fn build_context_menu(
@@ -58,7 +31,7 @@ impl ToolSelector {
let icon_position = IconPosition::End;
menu = menu.header("Profiles");
for (_id, profile) in profiles.clone() {
for profile in profiles.clone() {
menu = menu.toggleable_entry(profile.name.clone(), false, icon_position, None, {
let tools = tool_set.clone();
move |_window, cx| {
@@ -71,10 +44,6 @@ impl ToolSelector {
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
.collect::<Vec<_>>(),
);
if profile.tools.contains_key(ScriptingTool::NAME) {
tools.enable_scripting_tool();
}
}
});
}

View File

@@ -1,11 +1,10 @@
use std::sync::Arc;
use anyhow::Result;
use assistant_tool::ToolWorkingSet;
use collections::HashMap;
use futures::future::Shared;
use futures::FutureExt as _;
use gpui::{App, SharedString, Task};
use gpui::{SharedString, Task};
use language_model::{
LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
LanguageModelToolUseId, MessageContent, Role,
@@ -18,7 +17,6 @@ use crate::thread_store::SerializedMessage;
pub struct ToolUse {
pub id: LanguageModelToolUseId,
pub name: SharedString,
pub ui_text: SharedString,
pub status: ToolUseStatus,
pub input: serde_json::Value,
}
@@ -32,7 +30,6 @@ pub enum ToolUseStatus {
}
pub struct ToolUseState {
tools: Arc<ToolWorkingSet>,
tool_uses_by_assistant_message: HashMap<MessageId, Vec<LanguageModelToolUse>>,
tool_uses_by_user_message: HashMap<MessageId, Vec<LanguageModelToolUseId>>,
tool_results: HashMap<LanguageModelToolUseId, LanguageModelToolResult>,
@@ -40,9 +37,8 @@ pub struct ToolUseState {
}
impl ToolUseState {
pub fn new(tools: Arc<ToolWorkingSet>) -> Self {
pub fn new() -> Self {
Self {
tools,
tool_uses_by_assistant_message: HashMap::default(),
tool_uses_by_user_message: HashMap::default(),
tool_results: HashMap::default(),
@@ -54,11 +50,10 @@ impl ToolUseState {
///
/// Accepts a function to filter the tools that should be used to populate the state.
pub fn from_serialized_messages(
tools: Arc<ToolWorkingSet>,
messages: &[SerializedMessage],
mut filter_by_tool_name: impl FnMut(&str) -> bool,
) -> Self {
let mut this = Self::new(tools);
let mut this = Self::new();
let mut tool_names_by_id = HashMap::default();
for message in messages {
@@ -143,7 +138,7 @@ impl ToolUseState {
self.pending_tool_uses_by_id.values().collect()
}
pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> {
pub fn tool_uses_for_message(&self, id: MessageId) -> Vec<ToolUse> {
let Some(tool_uses_for_message) = &self.tool_uses_by_assistant_message.get(&id) else {
return Vec::new();
};
@@ -178,7 +173,6 @@ impl ToolUseState {
tool_uses.push(ToolUse {
id: tool_use.id.clone(),
name: tool_use.name.clone().into(),
ui_text: self.tool_ui_label(&tool_use.name, &tool_use.input, cx),
input: tool_use.input.clone(),
status,
})
@@ -187,19 +181,6 @@ impl ToolUseState {
tool_uses
}
pub fn tool_ui_label(
&self,
tool_name: &str,
input: &serde_json::Value,
cx: &App,
) -> SharedString {
if let Some(tool) = self.tools.tool(tool_name, cx) {
tool.ui_text(input).into()
} else {
"Unknown tool".into()
}
}
pub fn tool_results_for_message(&self, message_id: MessageId) -> Vec<&LanguageModelToolResult> {
let empty = Vec::new();
@@ -228,7 +209,6 @@ impl ToolUseState {
&mut self,
assistant_message_id: MessageId,
tool_use: LanguageModelToolUse,
cx: &App,
) {
self.tool_uses_by_assistant_message
.entry(assistant_message_id)
@@ -248,24 +228,15 @@ impl ToolUseState {
PendingToolUse {
assistant_message_id,
id: tool_use.id,
name: tool_use.name.clone(),
ui_text: self
.tool_ui_label(&tool_use.name, &tool_use.input, cx)
.into(),
name: tool_use.name,
input: tool_use.input,
status: PendingToolUseStatus::Idle,
},
);
}
pub fn run_pending_tool(
&mut self,
tool_use_id: LanguageModelToolUseId,
ui_text: SharedString,
task: Task<()>,
) {
pub fn run_pending_tool(&mut self, tool_use_id: LanguageModelToolUseId, task: Task<()>) {
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
tool_use.ui_text = ui_text.into();
tool_use.status = PendingToolUseStatus::Running {
_task: task.shared(),
};
@@ -364,7 +335,6 @@ pub struct PendingToolUse {
#[allow(unused)]
pub assistant_message_id: MessageId,
pub name: Arc<str>,
pub ui_text: Arc<str>,
pub input: serde_json::Value,
pub status: PendingToolUseStatus,
}

View File

@@ -6,6 +6,15 @@ fn main() {
if cfg!(target_os = "macos") {
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7");
println!("cargo:rerun-if-env-changed=ZED_BUNDLE");
if std::env::var("ZED_BUNDLE").ok().as_deref() == Some("true") {
// Find WebRTC.framework in the Frameworks folder when running as part of an application bundle.
println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path/../Frameworks");
} else {
// Find WebRTC.framework as a sibling of the executable when running outside of an application bundle.
println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
}
// Weakly link ReplayKit to ensure Zed can be used on macOS 10.15+.
println!("cargo:rustc-link-arg=-Wl,-weak_framework,ReplayKit");

View File

@@ -79,25 +79,10 @@ impl Eval {
let start_time = std::time::SystemTime::now();
let (system_prompt_context, load_error) = cx
.update(|cx| {
assistant
.read(cx)
.thread
.read(cx)
.load_system_prompt_context(cx)
})?
.await;
if let Some(load_error) = load_error {
return Err(anyhow!("{:?}", load_error));
};
assistant.update(cx, |assistant, cx| {
assistant.thread.update(cx, |thread, cx| {
let context = vec![];
thread.insert_user_message(self.user_prompt.clone(), context, None, cx);
thread.set_system_prompt_context(system_prompt_context);
thread.send_to_model(model, RequestKind::Chat, cx);
});
})?;

View File

@@ -128,7 +128,12 @@ impl HeadlessAssistant {
}
}
}
_ => {}
ThreadEvent::StreamedCompletion
| ThreadEvent::SummaryChanged
| ThreadEvent::StreamedAssistantText(_, _)
| ThreadEvent::MessageAdded(_)
| ThreadEvent::MessageEdited(_)
| ThreadEvent::MessageDeleted(_) => {}
}
}
}

View File

@@ -14,7 +14,6 @@ path = "src/assistant_settings.rs"
[dependencies]
anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
collections.workspace = true
feature_flags.workspace = true
gpui.workspace = true
language_model.workspace = true

View File

@@ -1,10 +1,7 @@
mod agent_profile;
use std::sync::Arc;
use ::open_ai::Model as OpenAiModel;
use anthropic::Model as AnthropicModel;
use collections::HashMap;
use deepseek::Model as DeepseekModel;
use feature_flags::FeatureFlagAppExt;
use gpui::{App, Pixels};
@@ -15,8 +12,6 @@ use schemars::{schema::Schema, JsonSchema};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
pub use crate::agent_profile::*;
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AssistantDockPosition {
@@ -71,7 +66,6 @@ pub struct AssistantSettings {
pub inline_alternatives: Vec<LanguageModelSelection>,
pub using_outdated_settings_version: bool,
pub enable_experimental_live_diffs: bool,
pub profiles: HashMap<Arc<str>, AgentProfile>,
}
impl AssistantSettings {
@@ -172,7 +166,6 @@ impl AssistantSettingsContent {
editor_model: None,
inline_alternatives: None,
enable_experimental_live_diffs: None,
profiles: None,
},
VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
},
@@ -194,7 +187,6 @@ impl AssistantSettingsContent {
editor_model: None,
inline_alternatives: None,
enable_experimental_live_diffs: None,
profiles: None,
},
}
}
@@ -324,7 +316,6 @@ impl Default for VersionedAssistantSettingsContent {
editor_model: None,
inline_alternatives: None,
enable_experimental_live_diffs: None,
profiles: None,
})
}
}
@@ -361,8 +352,6 @@ pub struct AssistantSettingsContentV2 {
///
/// Default: false
enable_experimental_live_diffs: Option<bool>,
#[schemars(skip)]
profiles: Option<HashMap<Arc<str>, AgentProfileContent>>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
@@ -399,12 +388,6 @@ impl Default for LanguageModelSelection {
}
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
pub struct AgentProfileContent {
pub name: Arc<str>,
pub tools: HashMap<Arc<str>, bool>,
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
pub struct AssistantSettingsContentV1 {
/// Whether the Assistant is enabled.
@@ -499,24 +482,6 @@ impl Settings for AssistantSettings {
&mut settings.enable_experimental_live_diffs,
value.enable_experimental_live_diffs,
);
merge(
&mut settings.profiles,
value.profiles.map(|profiles| {
profiles
.into_iter()
.map(|(id, profile)| {
(
id,
AgentProfile {
name: profile.name.into(),
tools: profile.tools,
context_servers: HashMap::default(),
},
)
})
.collect()
}),
);
}
Ok(settings)
@@ -581,7 +546,6 @@ mod tests {
default_width: None,
default_height: None,
enable_experimental_live_diffs: None,
profiles: None,
}),
)
},

View File

@@ -5,7 +5,8 @@ use std::sync::Arc;
use anyhow::Result;
use collections::{HashMap, HashSet};
use gpui::{App, Context, Entity, SharedString, Task};
use gpui::Context;
use gpui::{App, Entity, SharedString, Task};
use language::Buffer;
use language_model::LanguageModelRequestMessage;
use project::Project;
@@ -43,9 +44,6 @@ pub trait Tool: 'static + Send + Sync {
serde_json::Value::Object(serde_json::Map::default())
}
/// Returns markdown to be displayed in the UI for this tool.
fn ui_text(&self, input: &serde_json::Value) -> String;
/// Runs the tool with the provided input.
fn run(
self: Arc<Self>,

View File

@@ -32,13 +32,6 @@ impl Tool for BashTool {
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<BashToolInput>(input.clone()) {
Ok(input) => format!("`$ {}`", input.command),
Err(_) => "Run bash command".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,

View File

@@ -39,13 +39,6 @@ impl Tool for DeletePathTool {
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<DeletePathToolInput>(input.clone()) {
Ok(input) => format!("Delete “`{}`”", input.path),
Err(_) => "Delete path".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
@@ -66,12 +59,13 @@ impl Tool for DeletePathTool {
{
Some(deletion_task) => cx.background_spawn(async move {
match deletion_task.await {
Ok(()) => Ok(format!("Deleted {path_str}")),
Err(err) => Err(anyhow!("Failed to delete {path_str}: {err}")),
Ok(()) => Ok(format!("Deleted {}", &path_str)),
Err(err) => Err(anyhow!("Failed to delete {}: {}", &path_str, err)),
}
}),
None => Task::ready(Err(anyhow!(
"Couldn't delete {path_str} because that path isn't in this project."
"Couldn't delete {} because that path isn't in this project.",
path_str
))),
}
}

View File

@@ -46,17 +46,6 @@ impl Tool for DiagnosticsTool {
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
if let Some(path) = serde_json::from_value::<DiagnosticsToolInput>(input.clone())
.ok()
.and_then(|input| input.path)
{
format!("Check diagnostics for “`{}`”", path.display())
} else {
"Check project diagnostics".to_string()
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
@@ -65,15 +54,14 @@ impl Tool for DiagnosticsTool {
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
if let Some(path) = serde_json::from_value::<DiagnosticsToolInput>(input)
.ok()
.and_then(|input| input.path)
{
let input = match serde_json::from_value::<DiagnosticsToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
if let Some(path) = input.path {
let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
return Task::ready(Err(anyhow!(
"Could not find path {} in project",
path.display()
)));
return Task::ready(Err(anyhow!("Could not find path in project")));
};
let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));

View File

@@ -24,7 +24,10 @@ use util::ResultExt;
pub struct EditFilesToolInput {
/// High-level edit instructions. These will be interpreted by a smaller
/// model, so explain the changes you want that model to make and which
/// file paths need changing. The description should be concise and clear.
/// file paths need changing.
///
/// The description should be concise and clear. We will show this
/// description to the user as well.
///
/// WARNING: When specifying which file paths need changing, you MUST
/// start each path with one of the project's root directories.
@@ -55,21 +58,6 @@ pub struct EditFilesToolInput {
/// Notice how we never specify code snippets in the instructions!
/// </example>
pub edit_instructions: String,
/// A user-friendly description of what changes are being made.
/// This will be shown to the user in the UI to describe the edit operation. The screen real estate for this UI will be extremely
/// constrained, so make the description extremely terse.
///
/// <example>
/// For fixing a broken authentication system:
/// "Fix auth bug in login flow"
/// </example>
///
/// <example>
/// For adding unit tests to a module:
/// "Add tests for user profile logic"
/// </example>
pub display_description: String,
}
pub struct EditFilesTool;
@@ -88,13 +76,6 @@ impl Tool for EditFilesTool {
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<EditFilesToolInput>(input.clone()) {
Ok(input) => input.display_description,
Err(_) => "Edit files".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
@@ -145,39 +126,24 @@ impl Tool for EditFilesTool {
struct EditToolRequest {
parser: EditActionParser,
editor_response: EditorResponse,
output: String,
changed_buffers: HashSet<Entity<language::Buffer>>,
bad_searches: Vec<BadSearch>,
project: Entity<Project>,
action_log: Entity<ActionLog>,
tool_log: Option<(Entity<EditToolLog>, EditToolRequestId)>,
}
enum EditorResponse {
/// The editor model hasn't produced any actions yet.
/// If we don't have any by the end, we'll return its message to the architect model.
Message(String),
/// The editor model produced at least one action.
Actions {
applied: Vec<AppliedAction>,
search_errors: Vec<SearchError>,
},
}
struct AppliedAction {
source: String,
buffer: Entity<language::Buffer>,
#[derive(Debug)]
enum DiffResult {
BadSearch(BadSearch),
Diff(language::Diff),
}
#[derive(Debug)]
enum SearchError {
NoMatch {
file_path: String,
search: String,
},
EmptyBuffer {
file_path: String,
search: String,
exists: bool,
},
struct BadSearch {
file_path: String,
search: String,
}
impl EditToolRequest {
@@ -234,7 +200,10 @@ impl EditToolRequest {
let mut request = Self {
parser: EditActionParser::new(),
editor_response: EditorResponse::Message(String::with_capacity(256)),
// we start with the success header so we don't need to shift the output in the common case
output: Self::SUCCESS_OUTPUT_HEADER.to_string(),
changed_buffers: HashSet::default(),
bad_searches: Vec::new(),
action_log,
project,
tool_log,
@@ -251,12 +220,6 @@ impl EditToolRequest {
async fn process_response_chunk(&mut self, chunk: &str, cx: &mut AsyncApp) -> Result<()> {
let new_actions = self.parser.parse_chunk(chunk);
if let EditorResponse::Message(ref mut message) = self.editor_response {
if new_actions.is_empty() {
message.push_str(chunk);
}
}
if let Some((ref log, req_id)) = self.tool_log {
log.update(cx, |log, cx| {
log.push_editor_response_chunk(req_id, chunk, &new_actions, cx)
@@ -287,11 +250,6 @@ impl EditToolRequest {
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
.await?;
enum DiffResult {
Diff(language::Diff),
SearchError(SearchError),
}
let result = match action {
EditAction::Replace {
old,
@@ -301,39 +259,7 @@ impl EditToolRequest {
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
cx.background_executor()
.spawn(async move {
if snapshot.is_empty() {
let exists = snapshot
.file()
.map_or(false, |file| file.disk_state().exists());
let error = SearchError::EmptyBuffer {
file_path: file_path.display().to_string(),
exists,
search: old,
};
return anyhow::Ok(DiffResult::SearchError(error));
}
let replace_result =
// Try to match exactly
replace_exact(&old, &new, &snapshot)
.await
// If that fails, try being flexible about indentation
.or_else(|| replace_with_flexible_indent(&old, &new, &snapshot));
let Some(diff) = replace_result else {
let error = SearchError::NoMatch {
search: old,
file_path: file_path.display().to_string(),
};
return Ok(DiffResult::SearchError(error));
};
Ok(DiffResult::Diff(diff))
})
.spawn(Self::replace_diff(old, new, file_path, snapshot))
.await
}
EditAction::Write { content, .. } => Ok(DiffResult::Diff(
@@ -344,179 +270,139 @@ impl EditToolRequest {
}?;
match result {
DiffResult::SearchError(error) => {
self.push_search_error(error);
DiffResult::BadSearch(invalid_replace) => {
self.bad_searches.push(invalid_replace);
}
DiffResult::Diff(diff) => {
let _clock = buffer.update(cx, |buffer, cx| buffer.apply_diff(diff, cx))?;
self.push_applied_action(AppliedAction { source, buffer });
write!(&mut self.output, "\n\n{}", source)?;
self.changed_buffers.insert(buffer);
}
}
anyhow::Ok(())
Ok(())
}
fn push_search_error(&mut self, error: SearchError) {
match &mut self.editor_response {
EditorResponse::Message(_) => {
self.editor_response = EditorResponse::Actions {
applied: Vec::new(),
search_errors: vec![error],
};
}
EditorResponse::Actions { search_errors, .. } => {
search_errors.push(error);
}
}
async fn replace_diff(
old: String,
new: String,
file_path: std::path::PathBuf,
snapshot: language::BufferSnapshot,
) -> Result<DiffResult> {
let result =
// Try to match exactly
replace_exact(&old, &new, &snapshot)
.await
// If that fails, try being flexible about indentation
.or_else(|| replace_with_flexible_indent(&old, &new, &snapshot));
let Some(diff) = result else {
return anyhow::Ok(DiffResult::BadSearch(BadSearch {
search: old,
file_path: file_path.display().to_string(),
}));
};
anyhow::Ok(DiffResult::Diff(diff))
}
fn push_applied_action(&mut self, action: AppliedAction) {
match &mut self.editor_response {
EditorResponse::Message(_) => {
self.editor_response = EditorResponse::Actions {
applied: vec![action],
search_errors: Vec::new(),
};
}
EditorResponse::Actions { applied, .. } => {
applied.push(action);
}
}
}
const SUCCESS_OUTPUT_HEADER: &str = "Successfully applied. Here's a list of changes:";
const ERROR_OUTPUT_HEADER_NO_EDITS: &str = "I couldn't apply any edits!";
const ERROR_OUTPUT_HEADER_WITH_EDITS: &str =
"Errors occurred. First, here's a list of the edits we managed to apply:";
async fn finalize(self, cx: &mut AsyncApp) -> Result<String> {
match self.editor_response {
EditorResponse::Message(message) => Err(anyhow!(
"No edits were applied! You might need to provide more context.\n\n{}",
message
)),
EditorResponse::Actions {
applied,
search_errors,
} => {
let mut output = String::with_capacity(1024);
let changed_buffer_count = self.changed_buffers.len();
let parse_errors = self.parser.errors();
let has_errors = !search_errors.is_empty() || !parse_errors.is_empty();
// Save each buffer once at the end
for buffer in &self.changed_buffers {
self.project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.await?;
}
if has_errors {
let error_count = search_errors.len() + parse_errors.len();
self.action_log
.update(cx, |log, cx| log.buffer_edited(self.changed_buffers, cx))
.log_err();
if applied.is_empty() {
writeln!(
&mut output,
"{} errors occurred! No edits were applied.",
error_count,
)?;
} else {
writeln!(
&mut output,
"{} errors occurred, but {} edits were correctly applied.",
error_count,
applied.len(),
)?;
let errors = self.parser.errors();
writeln!(
&mut output,
"# {} SEARCH/REPLACE block(s) applied:\n\nDo not re-send these since they are already applied!\n",
applied.len()
)?;
}
} else {
write!(
&mut output,
"Successfully applied! Here's a list of applied edits:"
)?;
}
if errors.is_empty() && self.bad_searches.is_empty() {
if changed_buffer_count == 0 {
return Err(anyhow!(
"The instructions didn't lead to any changes. You might need to consult the file contents first."
));
}
let mut changed_buffers = HashSet::default();
Ok(self.output)
} else {
let mut output = self.output;
for action in applied {
changed_buffers.insert(action.buffer);
write!(&mut output, "\n\n{}", action.source)?;
}
if output.is_empty() {
output.replace_range(
0..Self::SUCCESS_OUTPUT_HEADER.len(),
Self::ERROR_OUTPUT_HEADER_NO_EDITS,
);
} else {
output.replace_range(
0..Self::SUCCESS_OUTPUT_HEADER.len(),
Self::ERROR_OUTPUT_HEADER_WITH_EDITS,
);
}
for buffer in &changed_buffers {
self.project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.await?;
}
if !self.bad_searches.is_empty() {
writeln!(
&mut output,
"\n\n# {} SEARCH/REPLACE block(s) failed to match:\n",
self.bad_searches.len()
)?;
self.action_log
.update(cx, |log, cx| log.buffer_edited(changed_buffers.clone(), cx))
.log_err();
if !search_errors.is_empty() {
for replace in self.bad_searches {
writeln!(
&mut output,
"\n\n## {} SEARCH/REPLACE block(s) failed to match:\n",
search_errors.len()
)?;
for error in search_errors {
match error {
SearchError::NoMatch { file_path, search } => {
writeln!(
&mut output,
"### No exact match in: `{}`\n```\n{}\n```\n",
file_path, search,
)?;
}
SearchError::EmptyBuffer {
file_path,
exists: true,
search,
} => {
writeln!(
&mut output,
"### No match because `{}` is empty:\n```\n{}\n```\n",
file_path, search,
)?;
}
SearchError::EmptyBuffer {
file_path,
exists: false,
search,
} => {
writeln!(
&mut output,
"### No match because `{}` does not exist:\n```\n{}\n```\n",
file_path, search,
)?;
}
}
}
write!(&mut output,
"The SEARCH section must exactly match an existing block of lines including all white \
space, comments, indentation, docstrings, etc."
"## No exact match in: {}\n```\n{}\n```\n",
replace.file_path, replace.search,
)?;
}
if !parse_errors.is_empty() {
writeln!(
&mut output,
"\n\n## {} SEARCH/REPLACE blocks failed to parse:",
parse_errors.len()
)?;
write!(&mut output,
"The SEARCH section must exactly match an existing block of lines including all white \
space, comments, indentation, docstrings, etc."
)?;
}
for error in parse_errors {
writeln!(&mut output, "- {}", error)?;
}
}
if !errors.is_empty() {
writeln!(
&mut output,
"\n\n# {} SEARCH/REPLACE blocks failed to parse:",
errors.len()
)?;
if has_errors {
writeln!(&mut output,
"\n\nYou can fix errors by running the tool again. You can include instructions, \
but errors are part of the conversation so you don't need to repeat them.",
)?;
Err(anyhow!(output))
} else {
Ok(output)
for error in errors {
writeln!(&mut output, "- {}", error)?;
}
}
if changed_buffer_count > 0 {
writeln!(
&mut output,
"\n\nThe other SEARCH/REPLACE blocks were applied successfully. Do not re-send them!",
)?;
}
writeln!(
&mut output,
"{}You can fix errors by running the tool again. You can include instructions, \
but errors are part of the conversation so you don't need to repeat them.",
if changed_buffer_count == 0 {
"\n\n"
} else {
""
}
)?;
Err(anyhow!(output))
}
}
}

View File

@@ -122,13 +122,6 @@ impl Tool for FetchTool {
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<FetchToolInput>(input.clone()) {
Ok(input) => format!("Fetch `{}`", input.url),
Err(_) => "Fetch URL".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,

View File

@@ -50,13 +50,6 @@ impl Tool for ListDirectoryTool {
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<ListDirectoryToolInput>(input.clone()) {
Ok(input) => format!("List the `{}` directory's contents", input.path.display()),
Err(_) => "List directory".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
@@ -71,10 +64,7 @@ impl Tool for ListDirectoryTool {
};
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!(
"Path {} not found in project",
input.path.display()
)));
return Task::ready(Err(anyhow!("Path not found in project")));
};
let Some(worktree) = project
.read(cx)
@@ -89,7 +79,7 @@ impl Tool for ListDirectoryTool {
};
if !entry.is_dir() {
return Task::ready(Err(anyhow!("{} is not a directory.", input.path.display())));
return Task::ready(Err(anyhow!("{} is a file.", input.path.display())));
}
let mut output = String::new();

View File

@@ -40,10 +40,6 @@ impl Tool for NowTool {
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, _input: &serde_json::Value) -> String {
"Get current time".to_string()
}
fn run(
self: Arc<Self>,
input: serde_json::Value,

View File

@@ -48,13 +48,6 @@ impl Tool for PathSearchTool {
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<PathSearchToolInput>(input.clone()) {
Ok(input) => format!("Find paths matching “`{}`”", input.glob),
Err(_) => "Search paths".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
@@ -69,7 +62,7 @@ impl Tool for PathSearchTool {
};
let path_matcher = match PathMatcher::new(&[glob.clone()]) {
Ok(matcher) => matcher,
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {}", err))),
};
let snapshots: Vec<Snapshot> = project
.read(cx)

View File

@@ -53,13 +53,6 @@ impl Tool for ReadFileTool {
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<ReadFileToolInput>(input.clone()) {
Ok(input) => format!("Read file `{}`", input.path.display()),
Err(_) => "Read file".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
@@ -74,10 +67,7 @@ impl Tool for ReadFileTool {
};
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!(
"Path {} not found in project",
&input.path.display()
)));
return Task::ready(Err(anyhow!("Path not found in project")));
};
cx.spawn(async move |cx| {

View File

@@ -22,17 +22,10 @@ pub struct RegexSearchToolInput {
/// Optional starting position for paginated results (0-based).
/// When not provided, starts from the beginning.
#[serde(default)]
pub offset: Option<u32>,
pub offset: Option<usize>,
}
impl RegexSearchToolInput {
/// Which page of search results this is.
pub fn page(&self) -> u32 {
1 + (self.offset.unwrap_or(0) / RESULTS_PER_PAGE)
}
}
const RESULTS_PER_PAGE: u32 = 20;
const RESULTS_PER_PAGE: usize = 20;
pub struct RegexSearchTool;
@@ -50,24 +43,6 @@ impl Tool for RegexSearchTool {
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<RegexSearchToolInput>(input.clone()) {
Ok(input) => {
let page = input.page();
if page > 1 {
format!(
"Get page {page} of search results for regex “`{}`”",
input.regex
)
} else {
format!("Search files for regex “`{}`”", input.regex)
}
}
Err(_) => "Search with regex".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
@@ -179,7 +154,7 @@ impl Tool for RegexSearchTool {
offset + matches_found,
offset + RESULTS_PER_PAGE,
))
} else {
} else {
Ok(format!("Found {matches_found} matches:\n{output}"))
}
})

View File

@@ -31,10 +31,6 @@ impl Tool for ThinkingTool {
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, _input: &serde_json::Value) -> String {
"Thinking".to_string()
}
fn run(
self: Arc<Self>,
input: serde_json::Value,

View File

@@ -18,6 +18,7 @@ test-support = [
"collections/test-support",
"gpui/test-support",
"livekit_client/test-support",
"livekit_client_macos/test-support",
"project/test-support",
"util/test-support"
]
@@ -40,8 +41,12 @@ serde_derive.workspace = true
settings.workspace = true
telemetry.workspace = true
util.workspace = true
gpui_tokio.workspace = true
livekit_client.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
livekit_client_macos = { workspace = true }
[target.'cfg(not(target_os = "macos"))'.dependencies]
livekit_client = { workspace = true }
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }
@@ -52,4 +57,9 @@ language = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
http_client = { workspace = true, features = ["test-support"] }
[target.'cfg(target_os = "macos")'.dev-dependencies]
livekit_client_macos = { workspace = true, features = ["test-support"] }
[target.'cfg(not(target_os = "macos"))'.dev-dependencies]
livekit_client = { workspace = true, features = ["test-support"] }

View File

@@ -1,5 +1,13 @@
pub mod call_settings;
mod call_impl;
#[cfg(target_os = "macos")]
mod macos;
pub use call_impl::*;
#[cfg(target_os = "macos")]
pub use macos::*;
#[cfg(not(target_os = "macos"))]
mod cross_platform;
#[cfg(not(target_os = "macos"))]
pub use cross_platform::*;

View File

@@ -17,7 +17,9 @@ use room::Event;
use settings::Settings;
use std::sync::Arc;
pub use livekit_client::{RemoteVideoTrack, RemoteVideoTrackView, RemoteVideoTrackViewEvent};
pub use livekit_client::{
track::RemoteVideoTrack, RemoteVideoTrackView, RemoteVideoTrackViewEvent,
};
pub use participant::ParticipantLocation;
pub use room::Room;
@@ -26,6 +28,10 @@ struct GlobalActiveCall(Entity<ActiveCall>);
impl Global for GlobalActiveCall {}
pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
livekit_client::init(
cx.background_executor().dispatcher.clone(),
cx.http_client(),
);
CallSettings::register(cx);
let active_call = cx.new(|cx| ActiveCall::new(client, user_store, cx));

View File

@@ -0,0 +1,84 @@
#![cfg_attr(all(target_os = "windows", target_env = "gnu"), allow(unused))]
use anyhow::{anyhow, Result};
use client::{proto, ParticipantIndex, User};
use collections::HashMap;
use gpui::WeakEntity;
use livekit_client::AudioStream;
use project::Project;
use std::sync::Arc;
#[cfg(not(all(target_os = "windows", target_env = "gnu")))]
pub use livekit_client::id::TrackSid;
pub use livekit_client::track::{RemoteAudioTrack, RemoteVideoTrack};
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ParticipantLocation {
SharedProject { project_id: u64 },
UnsharedProject,
External,
}
impl ParticipantLocation {
pub fn from_proto(location: Option<proto::ParticipantLocation>) -> Result<Self> {
match location.and_then(|l| l.variant) {
Some(proto::participant_location::Variant::SharedProject(project)) => {
Ok(Self::SharedProject {
project_id: project.id,
})
}
Some(proto::participant_location::Variant::UnsharedProject(_)) => {
Ok(Self::UnsharedProject)
}
Some(proto::participant_location::Variant::External(_)) => Ok(Self::External),
None => Err(anyhow!("participant location was not provided")),
}
}
}
#[derive(Clone, Default)]
pub struct LocalParticipant {
pub projects: Vec<proto::ParticipantProject>,
pub active_project: Option<WeakEntity<Project>>,
pub role: proto::ChannelRole,
}
impl LocalParticipant {
pub fn can_write(&self) -> bool {
matches!(
self.role,
proto::ChannelRole::Admin | proto::ChannelRole::Member
)
}
}
pub struct RemoteParticipant {
pub user: Arc<User>,
pub peer_id: proto::PeerId,
pub role: proto::ChannelRole,
pub projects: Vec<proto::ParticipantProject>,
pub location: ParticipantLocation,
pub participant_index: ParticipantIndex,
pub muted: bool,
pub speaking: bool,
#[cfg(not(all(target_os = "windows", target_env = "gnu")))]
pub video_tracks: HashMap<TrackSid, RemoteVideoTrack>,
#[cfg(not(all(target_os = "windows", target_env = "gnu")))]
pub audio_tracks: HashMap<TrackSid, (RemoteAudioTrack, AudioStream)>,
}
impl RemoteParticipant {
pub fn has_video_tracks(&self) -> bool {
#[cfg(not(all(target_os = "windows", target_env = "gnu")))]
return !self.video_tracks.is_empty();
#[cfg(all(target_os = "windows", target_env = "gnu"))]
return false;
}
pub fn can_write(&self) -> bool {
matches!(
self.role,
proto::ChannelRole::Admin | proto::ChannelRole::Member
)
}
}

View File

@@ -1,3 +1,5 @@
#![cfg_attr(all(target_os = "windows", target_env = "gnu"), allow(unused))]
use crate::{
call_settings::CallSettings,
participant::{LocalParticipant, ParticipantLocation, RemoteParticipant},
@@ -12,9 +14,19 @@ 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_tokio::Tokio;
use language::LanguageRegistry;
use livekit::{play_remote_audio_track, LocalTrackPublication, ParticipantIdentity, RoomEvent};
#[cfg(not(all(target_os = "windows", target_env = "gnu")))]
use livekit::{
capture_local_audio_track, capture_local_video_track,
id::ParticipantIdentity,
options::{TrackPublishOptions, VideoCodec},
play_remote_audio_track,
publication::LocalTrackPublication,
track::{TrackKind, TrackSource},
RoomEvent, RoomOptions,
};
#[cfg(all(target_os = "windows", target_env = "gnu"))]
use livekit::{publication::LocalTrackPublication, RoomEvent};
use livekit_client as livekit;
use postage::{sink::Sink, stream::Stream, watch};
use project::Project;
@@ -92,7 +104,11 @@ impl Room {
!self.shared_projects.is_empty()
}
pub fn is_connected(&self, _: &App) -> bool {
#[cfg(all(
any(test, feature = "test-support"),
not(all(target_os = "windows", target_env = "gnu"))
))]
pub fn is_connected(&self) -> bool {
if let Some(live_kit) = self.live_kit.as_ref() {
live_kit.room.connection_state() == livekit::ConnectionState::Connected
} else {
@@ -453,27 +469,18 @@ impl Room {
let project = handle.read(cx);
if let Some(project_id) = project.remote_id() {
projects.insert(project_id, handle.clone());
let mut worktrees = Vec::new();
let mut repositories = Vec::new();
for worktree in project.worktrees(cx) {
let worktree = worktree.read(cx);
worktrees.push(proto::RejoinWorktree {
id: worktree.id().to_proto(),
scan_id: worktree.completed_scan_id() as u64,
});
}
for (entry_id, repository) in project.repositories(cx) {
let repository = repository.read(cx);
repositories.push(proto::RejoinRepository {
id: entry_id.to_proto(),
scan_id: repository.completed_scan_id as u64,
});
}
rejoined_projects.push(proto::RejoinProject {
id: project_id,
worktrees,
repositories,
worktrees: project
.worktrees(cx)
.map(|worktree| {
let worktree = worktree.read(cx);
proto::RejoinWorktree {
id: worktree.id().to_proto(),
scan_id: worktree.completed_scan_id() as u64,
}
})
.collect(),
});
}
return true;
@@ -673,6 +680,12 @@ impl Room {
}
}
#[cfg(all(target_os = "windows", target_env = "gnu"))]
fn start_room_connection(&self, mut room: proto::Room, cx: &mut Context<Self>) -> Task<()> {
Task::ready(())
}
#[cfg(not(all(target_os = "windows", target_env = "gnu")))]
fn start_room_connection(&self, mut room: proto::Room, cx: &mut Context<Self>) -> Task<()> {
// Filter ourselves out from the room's participants.
let local_participant_ix = room
@@ -825,6 +838,7 @@ impl Room {
muted: true,
speaking: false,
video_tracks: Default::default(),
#[cfg(not(all(target_os = "windows", target_env = "gnu")))]
audio_tracks: Default::default(),
},
);
@@ -927,6 +941,7 @@ impl Room {
);
match event {
#[cfg(not(all(target_os = "windows", target_env = "gnu")))]
RoomEvent::TrackSubscribed {
track,
participant,
@@ -941,27 +956,18 @@ impl Room {
)
})?;
if self.live_kit.as_ref().map_or(true, |kit| kit.deafened) {
if matches!(track, livekit_client::RemoteTrack::Audio(_)) {
track.set_enabled(false, cx);
}
track.rtc_track().set_enabled(false);
}
match track {
livekit_client::RemoteTrack::Audio(track) => {
livekit::track::RemoteTrack::Audio(track) => {
cx.emit(Event::RemoteAudioTracksChanged {
participant_id: participant.peer_id,
});
let apm = self
.live_kit
.as_ref()
.unwrap()
.room
.audio_processing_module();
let stream =
play_remote_audio_track(apm, &track, cx.background_executor())?;
let stream = play_remote_audio_track(&track, cx.background_executor())?;
participant.audio_tracks.insert(track_id, (track, stream));
participant.muted = publication.is_muted();
}
livekit_client::RemoteTrack::Video(track) => {
livekit::track::RemoteTrack::Video(track) => {
cx.emit(Event::RemoteVideoTracksChanged {
participant_id: participant.peer_id,
});
@@ -970,6 +976,7 @@ impl Room {
}
}
#[cfg(not(all(target_os = "windows", target_env = "gnu")))]
RoomEvent::TrackUnsubscribed {
track, participant, ..
} => {
@@ -981,14 +988,14 @@ impl Room {
)
})?;
match track {
livekit_client::RemoteTrack::Audio(track) => {
livekit::track::RemoteTrack::Audio(track) => {
participant.audio_tracks.remove(&track.sid());
participant.muted = true;
cx.emit(Event::RemoteAudioTracksChanged {
participant_id: participant.peer_id,
});
}
livekit_client::RemoteTrack::Video(track) => {
livekit::track::RemoteTrack::Video(track) => {
participant.video_tracks.remove(&track.sid());
cx.emit(Event::RemoteVideoTracksChanged {
participant_id: participant.peer_id,
@@ -997,6 +1004,7 @@ impl Room {
}
}
#[cfg(not(all(target_os = "windows", target_env = "gnu")))]
RoomEvent::ActiveSpeakersChanged { speakers } => {
let mut speaker_ids = speakers
.into_iter()
@@ -1013,6 +1021,7 @@ impl Room {
}
}
#[cfg(not(all(target_os = "windows", target_env = "gnu")))]
RoomEvent::TrackMuted {
participant,
publication,
@@ -1037,6 +1046,7 @@ impl Room {
}
}
#[cfg(not(all(target_os = "windows", target_env = "gnu")))]
RoomEvent::LocalTrackUnpublished { publication, .. } => {
log::info!("unpublished track {}", publication.sid());
if let Some(room) = &mut self.live_kit {
@@ -1059,10 +1069,12 @@ impl Room {
}
}
#[cfg(not(all(target_os = "windows", target_env = "gnu")))]
RoomEvent::LocalTrackPublished { publication, .. } => {
log::info!("published track {:?}", publication.sid());
}
#[cfg(not(all(target_os = "windows", target_env = "gnu")))]
RoomEvent::Disconnected { reason } => {
log::info!("disconnected from room: {reason:?}");
self.leave(cx).detach_and_log_err(cx);
@@ -1290,6 +1302,13 @@ impl Room {
pub fn can_use_microphone(&self) -> bool {
use proto::ChannelRole::*;
#[cfg(not(any(test, feature = "test-support")))]
{
if cfg!(all(target_os = "windows", target_env = "gnu")) {
return false;
}
}
match self.local_participant.role {
Admin | Member | Talker => true,
Guest | Banned => false,
@@ -1304,27 +1323,40 @@ impl Room {
}
}
#[cfg(all(target_os = "windows", target_env = "gnu"))]
pub fn share_microphone(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
Task::ready(Err(anyhow!("MinGW is not supported yet")))
}
#[cfg(not(all(target_os = "windows", target_env = "gnu")))]
#[track_caller]
pub fn share_microphone(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
}
let (participant, publish_id, apm) = if let Some(live_kit) = self.live_kit.as_mut() {
let (participant, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() {
let publish_id = post_inc(&mut live_kit.next_publish_id);
live_kit.microphone_track = LocalTrack::Pending { publish_id };
cx.notify();
(
live_kit.room.local_participant(),
publish_id,
live_kit.room.audio_processing_module(),
)
(live_kit.room.local_participant(), publish_id)
} else {
return Task::ready(Err(anyhow!("live-kit was not initialized")));
};
cx.spawn(async move |this, cx| {
let publication = participant.publish_microphone_track(apm, cx).await;
let (track, stream) = capture_local_audio_track(cx.background_executor())?.await;
let publication = participant
.publish_track(
livekit::track::LocalTrack::Audio(track),
TrackPublishOptions {
source: TrackSource::Microphone,
..Default::default()
},
)
.await
.map_err(|error| anyhow!("failed to publish track: {error}"));
this.update(cx, |this, cx| {
let live_kit = this
.live_kit
@@ -1341,15 +1373,15 @@ impl Room {
};
match publication {
Ok((publication, stream)) => {
Ok(publication) => {
if canceled {
cx.spawn(async move |_, cx| {
participant.unpublish_track(publication.sid(), cx).await
cx.background_spawn(async move {
participant.unpublish_track(&publication.sid()).await
})
.detach_and_log_err(cx)
} else {
if live_kit.muted_by_user || live_kit.deafened {
publication.mute(cx);
publication.mute();
}
live_kit.microphone_track = LocalTrack::Published {
track_publication: publication,
@@ -1373,6 +1405,12 @@ impl Room {
})
}
#[cfg(all(target_os = "windows", target_env = "gnu"))]
pub fn share_screen(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
Task::ready(Err(anyhow!("MinGW is not supported yet")))
}
#[cfg(not(all(target_os = "windows", target_env = "gnu")))]
pub fn share_screen(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
@@ -1396,7 +1434,19 @@ impl Room {
let sources = sources.await??;
let source = sources.first().ok_or_else(|| anyhow!("no display found"))?;
let publication = participant.publish_screenshare_track(&**source, cx).await;
let (track, stream) = capture_local_video_track(&**source).await?;
let publication = participant
.publish_track(
livekit::track::LocalTrack::Video(track),
TrackPublishOptions {
source: TrackSource::Screenshare,
video_codec: VideoCodec::H264,
..Default::default()
},
)
.await
.map_err(|error| anyhow!("error publishing screen track {error:?}"));
this.update(cx, |this, cx| {
let live_kit = this
@@ -1414,10 +1464,10 @@ impl Room {
};
match publication {
Ok((publication, stream)) => {
Ok(publication) => {
if canceled {
cx.spawn(async move |_, cx| {
participant.unpublish_track(publication.sid(), cx).await
cx.background_spawn(async move {
participant.unpublish_track(&publication.sid()).await
})
.detach()
} else {
@@ -1507,11 +1557,14 @@ impl Room {
LocalTrack::Published {
track_publication, ..
} => {
#[cfg(not(all(target_os = "windows", target_env = "gnu")))]
{
let local_participant = live_kit.room.local_participant();
let sid = track_publication.sid();
cx.spawn(async move |_, cx| local_participant.unpublish_track(sid, cx).await)
.detach_and_log_err(cx);
cx.background_spawn(
async move { local_participant.unpublish_track(&sid).await },
)
.detach_and_log_err(cx);
cx.notify();
}
@@ -1522,13 +1575,14 @@ impl Room {
}
fn set_deafened(&mut self, deafened: bool, cx: &mut Context<Self>) -> Option<()> {
#[cfg(not(all(target_os = "windows", target_env = "gnu")))]
{
let live_kit = self.live_kit.as_mut()?;
cx.notify();
for (_, participant) in live_kit.room.remote_participants() {
for (_, publication) in participant.track_publications() {
if publication.is_audio() {
publication.set_enabled(!deafened, cx);
if publication.kind() == TrackKind::Audio {
publication.set_enabled(!deafened);
}
}
}
@@ -1559,13 +1613,14 @@ impl Room {
LocalTrack::Published {
track_publication, ..
} => {
let guard = Tokio::handle(cx);
if should_mute {
track_publication.mute(cx)
} else {
track_publication.unmute(cx)
#[cfg(not(all(target_os = "windows", target_env = "gnu")))]
{
if should_mute {
track_publication.mute()
} else {
track_publication.unmute()
}
}
drop(guard);
None
}
@@ -1573,19 +1628,30 @@ impl Room {
}
}
#[cfg(all(target_os = "windows", target_env = "gnu"))]
fn spawn_room_connection(
livekit_connection_info: Option<proto::LiveKitConnectionInfo>,
cx: &mut Context<'_, Room>,
) {
}
#[cfg(not(all(target_os = "windows", target_env = "gnu")))]
fn spawn_room_connection(
livekit_connection_info: Option<proto::LiveKitConnectionInfo>,
cx: &mut Context<'_, Room>,
) {
if let Some(connection_info) = livekit_connection_info {
cx.spawn(async move |this, cx| {
let (room, mut events) =
livekit::Room::connect(connection_info.server_url, connection_info.token, cx)
.await?;
let (room, mut events) = livekit::Room::connect(
&connection_info.server_url,
&connection_info.token,
RoomOptions::default(),
)
.await?;
this.update(cx, |this, cx| {
let _handle_updates = cx.spawn(async move |this, cx| {
while let Some(event) = events.next().await {
while let Some(event) = events.recv().await {
if this
.update(cx, |this, cx| {
this.livekit_room_updated(event, cx).warn_on_err();
@@ -1634,6 +1700,10 @@ struct LiveKitRoom {
}
impl LiveKitRoom {
#[cfg(all(target_os = "windows", target_env = "gnu"))]
fn stop_publishing(&mut self, _cx: &mut Context<Room>) {}
#[cfg(not(all(target_os = "windows", target_env = "gnu")))]
fn stop_publishing(&mut self, cx: &mut Context<Room>) {
let mut tracks_to_unpublish = Vec::new();
if let LocalTrack::Published {
@@ -1653,9 +1723,9 @@ impl LiveKitRoom {
}
let participant = self.room.local_participant();
cx.spawn(async move |_, cx| {
cx.background_spawn(async move {
for sid in tracks_to_unpublish {
participant.unpublish_track(sid, cx).await.log_err();
participant.unpublish_track(&sid).await.log_err();
}
})
.detach();

View File

@@ -0,0 +1,521 @@
pub mod participant;
pub mod room;
use crate::call_settings::CallSettings;
use anyhow::{anyhow, Result};
use audio::Audio;
use client::{proto, ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
use collections::HashSet;
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
use gpui::{
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, Subscription, Task,
WeakEntity,
};
use postage::watch;
use project::Project;
use room::Event;
use settings::Settings;
use std::sync::Arc;
pub use participant::ParticipantLocation;
pub use room::Room;
struct GlobalActiveCall(Entity<ActiveCall>);
impl Global for GlobalActiveCall {}
pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
CallSettings::register(cx);
let active_call = cx.new(|cx| ActiveCall::new(client, user_store, cx));
cx.set_global(GlobalActiveCall(active_call));
}
pub struct OneAtATime {
cancel: Option<oneshot::Sender<()>>,
}
impl OneAtATime {
/// spawn a task in the given context.
/// if another task is spawned before that resolves, or if the OneAtATime itself is dropped, the first task will be cancelled and return Ok(None)
/// otherwise you'll see the result of the task.
fn spawn<F, Fut, R>(&mut self, cx: &mut App, f: F) -> Task<Result<Option<R>>>
where
F: 'static + FnOnce(AsyncApp) -> Fut,
Fut: Future<Output = Result<R>>,
R: 'static,
{
let (tx, rx) = oneshot::channel();
self.cancel.replace(tx);
cx.spawn(async move |cx| {
futures::select_biased! {
_ = rx.fuse() => Ok(None),
result = f(cx.clone()).fuse() => result.map(Some),
}
})
}
fn running(&self) -> bool {
self.cancel
.as_ref()
.is_some_and(|cancel| !cancel.is_canceled())
}
}
#[derive(Clone)]
pub struct IncomingCall {
pub room_id: u64,
pub calling_user: Arc<User>,
pub participants: Vec<Arc<User>>,
pub initial_project: Option<proto::ParticipantProject>,
}
/// Singleton global maintaining the user's participation in a room across workspaces.
pub struct ActiveCall {
room: Option<(Entity<Room>, Vec<Subscription>)>,
pending_room_creation: Option<Shared<Task<Result<Entity<Room>, Arc<anyhow::Error>>>>>,
location: Option<WeakEntity<Project>>,
_join_debouncer: OneAtATime,
pending_invites: HashSet<u64>,
incoming_call: (
watch::Sender<Option<IncomingCall>>,
watch::Receiver<Option<IncomingCall>>,
),
client: Arc<Client>,
user_store: Entity<UserStore>,
_subscriptions: Vec<client::Subscription>,
}
impl EventEmitter<Event> for ActiveCall {}
impl ActiveCall {
fn new(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut Context<Self>) -> Self {
Self {
room: None,
pending_room_creation: None,
location: None,
pending_invites: Default::default(),
incoming_call: watch::channel(),
_join_debouncer: OneAtATime { cancel: None },
_subscriptions: vec![
client.add_request_handler(cx.weak_entity(), Self::handle_incoming_call),
client.add_message_handler(cx.weak_entity(), Self::handle_call_canceled),
],
client,
user_store,
}
}
pub fn channel_id(&self, cx: &App) -> Option<ChannelId> {
self.room()?.read(cx).channel_id()
}
async fn handle_incoming_call(
this: Entity<Self>,
envelope: TypedEnvelope<proto::IncomingCall>,
mut cx: AsyncApp,
) -> Result<proto::Ack> {
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let call = IncomingCall {
room_id: envelope.payload.room_id,
participants: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_users(envelope.payload.participant_user_ids, cx)
})?
.await?,
calling_user: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_user(envelope.payload.calling_user_id, cx)
})?
.await?,
initial_project: envelope.payload.initial_project,
};
this.update(&mut cx, |this, _| {
*this.incoming_call.0.borrow_mut() = Some(call);
})?;
Ok(proto::Ack {})
}
async fn handle_call_canceled(
this: Entity<Self>,
envelope: TypedEnvelope<proto::CallCanceled>,
mut cx: AsyncApp,
) -> Result<()> {
this.update(&mut cx, |this, _| {
let mut incoming_call = this.incoming_call.0.borrow_mut();
if incoming_call
.as_ref()
.map_or(false, |call| call.room_id == envelope.payload.room_id)
{
incoming_call.take();
}
})?;
Ok(())
}
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalActiveCall>().0.clone()
}
pub fn try_global(cx: &App) -> Option<Entity<Self>> {
cx.try_global::<GlobalActiveCall>()
.map(|call| call.0.clone())
}
pub fn invite(
&mut self,
called_user_id: u64,
initial_project: Option<Entity<Project>>,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
if !self.pending_invites.insert(called_user_id) {
return Task::ready(Err(anyhow!("user was already invited")));
}
cx.notify();
if self._join_debouncer.running() {
return Task::ready(Ok(()));
}
let room = if let Some(room) = self.room().cloned() {
Some(Task::ready(Ok(room)).shared())
} else {
self.pending_room_creation.clone()
};
let invite = if let Some(room) = room {
cx.spawn(async move |_, cx| {
let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
let initial_project_id = if let Some(initial_project) = initial_project {
Some(
room.update(cx, |room, cx| room.share_project(initial_project, cx))?
.await?,
)
} else {
None
};
room.update(cx, move |room, cx| {
room.call(called_user_id, initial_project_id, cx)
})?
.await?;
anyhow::Ok(())
})
} else {
let client = self.client.clone();
let user_store = self.user_store.clone();
let room = cx
.spawn(async move |this, cx| {
let create_room = async {
let room = cx
.update(|cx| {
Room::create(
called_user_id,
initial_project,
client,
user_store,
cx,
)
})?
.await?;
this.update(cx, |this, cx| this.set_room(Some(room.clone()), cx))?
.await?;
anyhow::Ok(room)
};
let room = create_room.await;
this.update(cx, |this, _| this.pending_room_creation = None)?;
room.map_err(Arc::new)
})
.shared();
self.pending_room_creation = Some(room.clone());
cx.background_spawn(async move {
room.await.map_err(|err| anyhow!("{:?}", err))?;
anyhow::Ok(())
})
};
cx.spawn(async move |this, cx| {
let result = invite.await;
if result.is_ok() {
this.update(cx, |this, cx| {
this.report_call_event("Participant Invited", cx)
})?;
} else {
//TODO: report collaboration error
log::error!("invite failed: {:?}", result);
}
this.update(cx, |this, cx| {
this.pending_invites.remove(&called_user_id);
cx.notify();
})?;
result
})
}
pub fn cancel_invite(
&mut self,
called_user_id: u64,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let room_id = if let Some(room) = self.room() {
room.read(cx).id()
} else {
return Task::ready(Err(anyhow!("no active call")));
};
let client = self.client.clone();
cx.background_spawn(async move {
client
.request(proto::CancelCall {
room_id,
called_user_id,
})
.await?;
anyhow::Ok(())
})
}
pub fn incoming(&self) -> watch::Receiver<Option<IncomingCall>> {
self.incoming_call.1.clone()
}
pub fn accept_incoming(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
if self.room.is_some() {
return Task::ready(Err(anyhow!("cannot join while on another call")));
}
let call = if let Some(call) = self.incoming_call.0.borrow_mut().take() {
call
} else {
return Task::ready(Err(anyhow!("no incoming call")));
};
if self.pending_room_creation.is_some() {
return Task::ready(Ok(()));
}
let room_id = call.room_id;
let client = self.client.clone();
let user_store = self.user_store.clone();
let join = self._join_debouncer.spawn(cx, move |mut cx| async move {
Room::join(room_id, client, user_store, &mut cx).await
});
cx.spawn(async move |this, cx| {
let room = join.await?;
this.update(cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(cx, |this, cx| {
this.report_call_event("Incoming Call Accepted", cx)
})?;
Ok(())
})
}
pub fn decline_incoming(&mut self, _: &mut Context<Self>) -> Result<()> {
let call = self
.incoming_call
.0
.borrow_mut()
.take()
.ok_or_else(|| anyhow!("no incoming call"))?;
telemetry::event!("Incoming Call Declined", room_id = call.room_id);
self.client.send(proto::DeclineCall {
room_id: call.room_id,
})?;
Ok(())
}
pub fn join_channel(
&mut self,
channel_id: ChannelId,
cx: &mut Context<Self>,
) -> Task<Result<Option<Entity<Room>>>> {
if let Some(room) = self.room().cloned() {
if room.read(cx).channel_id() == Some(channel_id) {
return Task::ready(Ok(Some(room)));
} else {
room.update(cx, |room, cx| room.clear_state(cx));
}
}
if self.pending_room_creation.is_some() {
return Task::ready(Ok(None));
}
let client = self.client.clone();
let user_store = self.user_store.clone();
let join = self._join_debouncer.spawn(cx, move |mut cx| async move {
Room::join_channel(channel_id, client, user_store, &mut cx).await
});
cx.spawn(async move |this, cx| {
let room = join.await?;
this.update(cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(cx, |this, cx| this.report_call_event("Channel Joined", cx))?;
Ok(room)
})
}
pub fn hang_up(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
cx.notify();
self.report_call_event("Call Ended", cx);
Audio::end_call(cx);
let channel_id = self.channel_id(cx);
if let Some((room, _)) = self.room.take() {
cx.emit(Event::RoomLeft { channel_id });
room.update(cx, |room, cx| room.leave(cx))
} else {
Task::ready(Ok(()))
}
}
pub fn share_project(
&mut self,
project: Entity<Project>,
cx: &mut Context<Self>,
) -> Task<Result<u64>> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("Project Shared", cx);
room.update(cx, |room, cx| room.share_project(project, cx))
} else {
Task::ready(Err(anyhow!("no active call")))
}
}
pub fn unshare_project(
&mut self,
project: Entity<Project>,
cx: &mut Context<Self>,
) -> Result<()> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("Project Unshared", cx);
room.update(cx, |room, cx| room.unshare_project(project, cx))
} else {
Err(anyhow!("no active call"))
}
}
pub fn location(&self) -> Option<&WeakEntity<Project>> {
self.location.as_ref()
}
pub fn set_location(
&mut self,
project: Option<&Entity<Project>>,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
if project.is_some() || !*ZED_ALWAYS_ACTIVE {
self.location = project.map(|project| project.downgrade());
if let Some((room, _)) = self.room.as_ref() {
return room.update(cx, |room, cx| room.set_location(project, cx));
}
}
Task::ready(Ok(()))
}
fn set_room(&mut self, room: Option<Entity<Room>>, cx: &mut Context<Self>) -> Task<Result<()>> {
if room.as_ref() == self.room.as_ref().map(|room| &room.0) {
Task::ready(Ok(()))
} else {
cx.notify();
if let Some(room) = room {
if room.read(cx).status().is_offline() {
self.room = None;
Task::ready(Ok(()))
} else {
let subscriptions = vec![
cx.observe(&room, |this, room, cx| {
if room.read(cx).status().is_offline() {
this.set_room(None, cx).detach_and_log_err(cx);
}
cx.notify();
}),
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
];
self.room = Some((room.clone(), subscriptions));
let location = self
.location
.as_ref()
.and_then(|location| location.upgrade());
let channel_id = room.read(cx).channel_id();
cx.emit(Event::RoomJoined { channel_id });
room.update(cx, |room, cx| room.set_location(location.as_ref(), cx))
}
} else {
self.room = None;
Task::ready(Ok(()))
}
}
}
pub fn room(&self) -> Option<&Entity<Room>> {
self.room.as_ref().map(|(room, _)| room)
}
pub fn client(&self) -> Arc<Client> {
self.client.clone()
}
pub fn pending_invites(&self) -> &HashSet<u64> {
&self.pending_invites
}
pub fn report_call_event(&self, operation: &'static str, cx: &mut App) {
if let Some(room) = self.room() {
let room = room.read(cx);
telemetry::event!(
operation,
room_id = room.id(),
channel_id = room.channel_id()
);
}
}
}
#[cfg(test)]
mod test {
use gpui::TestAppContext;
use crate::OneAtATime;
#[gpui::test]
async fn test_one_at_a_time(cx: &mut TestAppContext) {
let mut one_at_a_time = OneAtATime { cancel: None };
assert_eq!(
cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(1) }))
.await
.unwrap(),
Some(1)
);
let (a, b) = cx.update(|cx| {
(
one_at_a_time.spawn(cx, |_| async {
panic!("");
}),
one_at_a_time.spawn(cx, |_| async { Ok(3) }),
)
});
assert_eq!(a.await.unwrap(), None::<u32>);
assert_eq!(b.await.unwrap(), Some(3));
let promise = cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(4) }));
drop(one_at_a_time);
assert_eq!(promise.await.unwrap(), None);
}
}

View File

@@ -1,14 +1,13 @@
use anyhow::{anyhow, Result};
use client::{proto, ParticipantIndex, User};
use client::ParticipantIndex;
use client::{proto, User};
use collections::HashMap;
use gpui::WeakEntity;
use livekit_client::AudioStream;
pub use livekit_client_macos::Frame;
pub use livekit_client_macos::{RemoteAudioTrack, RemoteVideoTrack};
use project::Project;
use std::sync::Arc;
pub use livekit_client::TrackSid;
pub use livekit_client::{RemoteAudioTrack, RemoteVideoTrack};
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ParticipantLocation {
SharedProject { project_id: u64 },
@@ -49,6 +48,7 @@ impl LocalParticipant {
}
}
#[derive(Clone, Debug)]
pub struct RemoteParticipant {
pub user: Arc<User>,
pub peer_id: proto::PeerId,
@@ -58,13 +58,13 @@ pub struct RemoteParticipant {
pub participant_index: ParticipantIndex,
pub muted: bool,
pub speaking: bool,
pub video_tracks: HashMap<TrackSid, RemoteVideoTrack>,
pub audio_tracks: HashMap<TrackSid, (RemoteAudioTrack, AudioStream)>,
pub video_tracks: HashMap<livekit_client_macos::Sid, Arc<RemoteVideoTrack>>,
pub audio_tracks: HashMap<livekit_client_macos::Sid, Arc<RemoteAudioTrack>>,
}
impl RemoteParticipant {
pub fn has_video_tracks(&self) -> bool {
return !self.video_tracks.is_empty();
!self.video_tracks.is_empty()
}
pub fn can_write(&self) -> bool {

File diff suppressed because it is too large Load Diff

View File

@@ -706,11 +706,10 @@ mod mac_os {
use anyhow::{anyhow, Context as _, Result};
use core_foundation::{
array::{CFArray, CFIndex},
base::TCFType as _,
string::kCFStringEncodingUTF8,
url::{CFURLCreateWithBytes, CFURL},
};
use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec};
use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
use serde::Deserialize;
use std::{
ffi::OsStr,
@@ -737,6 +736,7 @@ mod mac_os {
},
LocalPath {
executable: PathBuf,
plist: InfoPlist,
},
}
@@ -773,16 +773,34 @@ mod mac_os {
plist,
})
}
_ => Ok(Bundle::LocalPath {
executable: bundle_path,
}),
_ => {
println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
let plist_path = bundle_path
.parent()
.with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
.join("WebRTC.framework/Resources/Info.plist");
let plist =
plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
format!("Reading dev bundle plist file at {plist_path:?}")
})?;
Ok(Bundle::LocalPath {
executable: bundle_path,
plist,
})
}
}
}
}
impl InstalledApp for Bundle {
fn zed_version_string(&self) -> String {
format!("Zed {} {}", self.version(), self.path().display(),)
let is_dev = matches!(self, Self::LocalPath { .. });
format!(
"Zed {}{} {}",
self.plist().bundle_short_version_string,
if is_dev { " (dev)" } else { "" },
self.path().display(),
)
}
fn launch(&self, url: String) -> anyhow::Result<()> {
@@ -861,10 +879,10 @@ mod mac_os {
}
impl Bundle {
fn version(&self) -> String {
fn plist(&self) -> &InfoPlist {
match self {
Self::App { plist, .. } => plist.bundle_short_version_string.clone(),
Self::LocalPath { .. } => "<development>".to_string(),
Self::App { plist, .. } => plist,
Self::LocalPath { plist, .. } => plist,
}
}

View File

@@ -100,15 +100,13 @@ extension.workspace = true
file_finder.workspace = true
fs = { workspace = true, features = ["test-support"] }
git = { workspace = true, features = ["test-support"] }
git_hosting_providers.workspace = true
git_ui = { workspace = true, features = ["test-support"] }
git_hosting_providers.workspace = true
gpui = { workspace = true, features = ["test-support"] }
gpui_tokio.workspace = true
hyper.workspace = true
indoc.workspace = true
language = { workspace = true, features = ["test-support"] }
language_model = { workspace = true, features = ["test-support"] }
livekit_client = { workspace = true, features = ["test-support"] }
lsp = { workspace = true, features = ["test-support"] }
menu.workspace = true
multi_buffer = { workspace = true, features = ["test-support"] }
@@ -133,5 +131,11 @@ util.workspace = true
workspace = { workspace = true, features = ["test-support"] }
worktree = { workspace = true, features = ["test-support"] }
[target.'cfg(target_os = "macos")'.dev-dependencies]
livekit_client_macos = { workspace = true, features = ["test-support"] }
[target.'cfg(not(target_os = "macos"))'.dev-dependencies]
livekit_client = { workspace = true, features = ["test-support"] }
[package.metadata.cargo-machete]
ignored = ["async-stripe"]

View File

@@ -15,13 +15,9 @@ CREATE TABLE "users" (
"github_user_created_at" TIMESTAMP WITHOUT TIME ZONE,
"custom_llm_monthly_allowance_in_cents" INTEGER
);
CREATE UNIQUE INDEX "index_users_github_login" ON "users" ("github_login");
CREATE UNIQUE INDEX "index_invite_code_users" ON "users" ("invite_code");
CREATE INDEX "index_users_on_email_address" ON "users" ("email_address");
CREATE UNIQUE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id");
CREATE TABLE "access_tokens" (
@@ -30,7 +26,6 @@ CREATE TABLE "access_tokens" (
"impersonated_user_id" INTEGER REFERENCES users (id),
"hash" VARCHAR(128)
);
CREATE INDEX "index_access_tokens_user_id" ON "access_tokens" ("user_id");
CREATE TABLE "contacts" (
@@ -41,9 +36,7 @@ CREATE TABLE "contacts" (
"should_notify" BOOLEAN NOT NULL,
"accepted" BOOLEAN NOT NULL
);
CREATE UNIQUE INDEX "index_contacts_user_ids" ON "contacts" ("user_id_a", "user_id_b");
CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b");
CREATE TABLE "rooms" (
@@ -52,7 +45,6 @@ CREATE TABLE "rooms" (
"environment" VARCHAR,
"channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
CREATE TABLE "projects" (
@@ -63,9 +55,7 @@ CREATE TABLE "projects" (
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
"unregistered" BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id");
CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id");
CREATE TABLE "worktrees" (
@@ -77,9 +67,8 @@ CREATE TABLE "worktrees" (
"scan_id" INTEGER NOT NULL,
"is_complete" BOOL NOT NULL DEFAULT FALSE,
"completed_scan_id" INTEGER NOT NULL,
PRIMARY KEY (project_id, id)
PRIMARY KEY(project_id, id)
);
CREATE INDEX "index_worktrees_on_project_id" ON "worktrees" ("project_id");
CREATE TABLE "worktree_entries" (
@@ -98,33 +87,32 @@ CREATE TABLE "worktree_entries" (
"is_deleted" BOOL NOT NULL,
"git_status" INTEGER,
"is_fifo" BOOL NOT NULL,
PRIMARY KEY (project_id, worktree_id, id),
FOREIGN KEY (project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
PRIMARY KEY(project_id, worktree_id, id),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_worktree_entries_on_project_id" ON "worktree_entries" ("project_id");
CREATE INDEX "index_worktree_entries_on_project_id_and_worktree_id" ON "worktree_entries" ("project_id", "worktree_id");
CREATE TABLE "project_repositories" (
CREATE TABLE "worktree_repositories" (
"project_id" INTEGER NOT NULL,
"abs_path" VARCHAR,
"id" INTEGER NOT NULL,
"entry_ids" VARCHAR,
"legacy_worktree_id" INTEGER,
"worktree_id" INTEGER NOT NULL,
"work_directory_id" INTEGER NOT NULL,
"branch" VARCHAR,
"scan_id" INTEGER NOT NULL,
"is_deleted" BOOL NOT NULL,
"current_merge_conflicts" VARCHAR,
"branch_summary" VARCHAR,
PRIMARY KEY (project_id, id)
PRIMARY KEY(project_id, worktree_id, work_directory_id),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id");
CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id");
CREATE INDEX "index_project_repositories_on_project_id" ON "project_repositories" ("project_id");
CREATE TABLE "project_repository_statuses" (
CREATE TABLE "worktree_repository_statuses" (
"project_id" INTEGER NOT NULL,
"repository_id" INTEGER NOT NULL,
"worktree_id" INT8 NOT NULL,
"work_directory_id" INT8 NOT NULL,
"repo_path" VARCHAR NOT NULL,
"status" INT8 NOT NULL,
"status_kind" INT4 NOT NULL,
@@ -132,12 +120,13 @@ CREATE TABLE "project_repository_statuses" (
"second_status" INT4 NULL,
"scan_id" INT8 NOT NULL,
"is_deleted" BOOL NOT NULL,
PRIMARY KEY (project_id, repository_id, repo_path)
PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_project_repos_statuses_on_project_id" ON "project_repository_statuses" ("project_id");
CREATE INDEX "index_project_repos_statuses_on_project_id_and_repo_id" ON "project_repository_statuses" ("project_id", "repository_id");
CREATE INDEX "index_wt_repos_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id");
CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id" ON "worktree_repository_statuses" ("project_id", "worktree_id");
CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id_and_wd_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id");
CREATE TABLE "worktree_settings_files" (
"project_id" INTEGER NOT NULL,
@@ -145,12 +134,10 @@ CREATE TABLE "worktree_settings_files" (
"path" VARCHAR NOT NULL,
"content" TEXT,
"kind" VARCHAR,
PRIMARY KEY (project_id, worktree_id, path),
FOREIGN KEY (project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
PRIMARY KEY(project_id, worktree_id, path),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_worktree_settings_files_on_project_id" ON "worktree_settings_files" ("project_id");
CREATE INDEX "index_worktree_settings_files_on_project_id_and_worktree_id" ON "worktree_settings_files" ("project_id", "worktree_id");
CREATE TABLE "worktree_diagnostic_summaries" (
@@ -160,21 +147,18 @@ CREATE TABLE "worktree_diagnostic_summaries" (
"language_server_id" INTEGER NOT NULL,
"error_count" INTEGER NOT NULL,
"warning_count" INTEGER NOT NULL,
PRIMARY KEY (project_id, worktree_id, path),
FOREIGN KEY (project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
PRIMARY KEY(project_id, worktree_id, path),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_worktree_diagnostic_summaries_on_project_id" ON "worktree_diagnostic_summaries" ("project_id");
CREATE INDEX "index_worktree_diagnostic_summaries_on_project_id_and_worktree_id" ON "worktree_diagnostic_summaries" ("project_id", "worktree_id");
CREATE TABLE "language_servers" (
"id" INTEGER NOT NULL,
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
"name" VARCHAR NOT NULL,
PRIMARY KEY (project_id, id)
PRIMARY KEY(project_id, id)
);
CREATE INDEX "index_language_servers_on_project_id" ON "language_servers" ("project_id");
CREATE TABLE "project_collaborators" (
@@ -186,20 +170,11 @@ CREATE TABLE "project_collaborators" (
"replica_id" INTEGER NOT NULL,
"is_host" BOOLEAN NOT NULL
);
CREATE INDEX "index_project_collaborators_on_project_id" ON "project_collaborators" ("project_id");
CREATE UNIQUE INDEX "index_project_collaborators_on_project_id_and_replica_id" ON "project_collaborators" ("project_id", "replica_id");
CREATE INDEX "index_project_collaborators_on_connection_server_id" ON "project_collaborators" ("connection_server_id");
CREATE INDEX "index_project_collaborators_on_connection_id" ON "project_collaborators" ("connection_id");
CREATE UNIQUE INDEX "index_project_collaborators_on_project_id_connection_id_and_server_id" ON "project_collaborators" (
"project_id",
"connection_id",
"connection_server_id"
);
CREATE UNIQUE INDEX "index_project_collaborators_on_project_id_connection_id_and_server_id" ON "project_collaborators" ("project_id", "connection_id", "connection_server_id");
CREATE TABLE "room_participants" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -218,21 +193,12 @@ CREATE TABLE "room_participants" (
"role" TEXT,
"in_call" BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_id");
CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id");
CREATE INDEX "index_room_participants_on_answering_connection_server_id" ON "room_participants" ("answering_connection_server_id");
CREATE INDEX "index_room_participants_on_calling_connection_server_id" ON "room_participants" ("calling_connection_server_id");
CREATE INDEX "index_room_participants_on_answering_connection_id" ON "room_participants" ("answering_connection_id");
CREATE UNIQUE INDEX "index_room_participants_on_answering_connection_id_and_answering_connection_server_id" ON "room_participants" (
"answering_connection_id",
"answering_connection_server_id"
);
CREATE UNIQUE INDEX "index_room_participants_on_answering_connection_id_and_answering_connection_server_id" ON "room_participants" ("answering_connection_id", "answering_connection_server_id");
CREATE TABLE "servers" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -248,15 +214,9 @@ CREATE TABLE "followers" (
"follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
"follower_connection_id" INTEGER NOT NULL
);
CREATE UNIQUE INDEX "index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id" ON "followers" (
"project_id",
"leader_connection_server_id",
"leader_connection_id",
"follower_connection_server_id",
"follower_connection_id"
);
CREATE UNIQUE INDEX
"index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id"
ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id");
CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");
CREATE TABLE "channels" (
@@ -277,7 +237,6 @@ CREATE TABLE IF NOT EXISTS "channel_chat_participants" (
"connection_id" INTEGER NOT NULL,
"connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE
);
CREATE INDEX "index_channel_chat_participants_on_channel_id" ON "channel_chat_participants" ("channel_id");
CREATE TABLE IF NOT EXISTS "channel_messages" (
@@ -290,9 +249,7 @@ CREATE TABLE IF NOT EXISTS "channel_messages" (
"nonce" BLOB NOT NULL,
"reply_to_message_id" INTEGER DEFAULT NULL
);
CREATE INDEX "index_channel_messages_on_channel_id" ON "channel_messages" ("channel_id");
CREATE UNIQUE INDEX "index_channel_messages_on_sender_id_nonce" ON "channel_messages" ("sender_id", "nonce");
CREATE TABLE "channel_message_mentions" (
@@ -300,7 +257,7 @@ CREATE TABLE "channel_message_mentions" (
"start_offset" INTEGER NOT NULL,
"end_offset" INTEGER NOT NULL,
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
PRIMARY KEY (message_id, start_offset)
PRIMARY KEY(message_id, start_offset)
);
CREATE TABLE "channel_members" (
@@ -331,7 +288,7 @@ CREATE TABLE "buffer_operations" (
"replica_id" INTEGER NOT NULL,
"lamport_timestamp" INTEGER NOT NULL,
"value" BLOB NOT NULL,
PRIMARY KEY (buffer_id, epoch, lamport_timestamp, replica_id)
PRIMARY KEY(buffer_id, epoch, lamport_timestamp, replica_id)
);
CREATE TABLE "buffer_snapshots" (
@@ -339,7 +296,7 @@ CREATE TABLE "buffer_snapshots" (
"epoch" INTEGER NOT NULL,
"text" TEXT NOT NULL,
"operation_serialization_version" INTEGER NOT NULL,
PRIMARY KEY (buffer_id, epoch)
PRIMARY KEY(buffer_id, epoch)
);
CREATE TABLE "channel_buffer_collaborators" (
@@ -353,18 +310,11 @@ CREATE TABLE "channel_buffer_collaborators" (
);
CREATE INDEX "index_channel_buffer_collaborators_on_channel_id" ON "channel_buffer_collaborators" ("channel_id");
CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_and_replica_id" ON "channel_buffer_collaborators" ("channel_id", "replica_id");
CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id");
CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id");
CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("channel_id", "connection_id", "connection_server_id");
CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" (
"channel_id",
"connection_id",
"connection_server_id"
);
CREATE TABLE "feature_flags" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -374,6 +324,7 @@ CREATE TABLE "feature_flags" (
CREATE INDEX "index_feature_flags" ON "feature_flags" ("id");
CREATE TABLE "user_features" (
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
"feature_id" INTEGER NOT NULL REFERENCES feature_flags (id) ON DELETE CASCADE,
@@ -381,11 +332,10 @@ CREATE TABLE "user_features" (
);
CREATE UNIQUE INDEX "index_user_features_user_id_and_feature_id" ON "user_features" ("user_id", "feature_id");
CREATE INDEX "index_user_features_on_user_id" ON "user_features" ("user_id");
CREATE INDEX "index_user_features_on_feature_id" ON "user_features" ("feature_id");
CREATE TABLE "observed_buffer_edits" (
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
"buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE,
@@ -424,10 +374,13 @@ CREATE TABLE "notifications" (
"response" BOOLEAN
);
CREATE INDEX "index_notifications_on_recipient_id_is_read_kind_entity_id" ON "notifications" ("recipient_id", "is_read", "kind", "entity_id");
CREATE INDEX
"index_notifications_on_recipient_id_is_read_kind_entity_id"
ON "notifications"
("recipient_id", "is_read", "kind", "entity_id");
CREATE TABLE contributors (
user_id INTEGER REFERENCES users (id),
user_id INTEGER REFERENCES users(id),
signed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id)
);
@@ -441,7 +394,7 @@ CREATE TABLE extensions (
);
CREATE TABLE extension_versions (
extension_id INTEGER REFERENCES extensions (id),
extension_id INTEGER REFERENCES extensions(id),
version TEXT NOT NULL,
published_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
authors TEXT NOT NULL,
@@ -463,7 +416,6 @@ CREATE TABLE extension_versions (
);
CREATE UNIQUE INDEX "index_extensions_external_id" ON "extensions" ("external_id");
CREATE INDEX "index_extensions_total_download_count" ON "extensions" ("total_download_count");
CREATE TABLE rate_buckets (
@@ -472,15 +424,14 @@ CREATE TABLE rate_buckets (
token_count INT NOT NULL,
last_refill TIMESTAMP WITHOUT TIME ZONE NOT NULL,
PRIMARY KEY (user_id, rate_limit_name),
FOREIGN KEY (user_id) REFERENCES users (id)
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE INDEX idx_user_id_rate_limit ON rate_buckets (user_id, rate_limit_name);
CREATE TABLE IF NOT EXISTS billing_preferences (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
user_id INTEGER NOT NULL REFERENCES users (id),
user_id INTEGER NOT NULL REFERENCES users(id),
max_monthly_llm_usage_spending_in_cents INTEGER NOT NULL
);
@@ -489,19 +440,18 @@ CREATE UNIQUE INDEX "uix_billing_preferences_on_user_id" ON billing_preferences
CREATE TABLE IF NOT EXISTS billing_customers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
user_id INTEGER NOT NULL REFERENCES users (id),
user_id INTEGER NOT NULL REFERENCES users(id),
has_overdue_invoices BOOLEAN NOT NULL DEFAULT FALSE,
stripe_customer_id TEXT NOT NULL
);
CREATE UNIQUE INDEX "uix_billing_customers_on_user_id" ON billing_customers (user_id);
CREATE UNIQUE INDEX "uix_billing_customers_on_stripe_customer_id" ON billing_customers (stripe_customer_id);
CREATE TABLE IF NOT EXISTS billing_subscriptions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
billing_customer_id INTEGER NOT NULL REFERENCES billing_customers (id),
billing_customer_id INTEGER NOT NULL REFERENCES billing_customers(id),
stripe_subscription_id TEXT NOT NULL,
stripe_subscription_status TEXT NOT NULL,
stripe_cancel_at TIMESTAMP,
@@ -509,7 +459,6 @@ CREATE TABLE IF NOT EXISTS billing_subscriptions (
);
CREATE INDEX "ix_billing_subscriptions_on_billing_customer_id" ON billing_subscriptions (billing_customer_id);
CREATE UNIQUE INDEX "uix_billing_subscriptions_on_stripe_subscription_id" ON billing_subscriptions (stripe_subscription_id);
CREATE TABLE IF NOT EXISTS processed_stripe_events (
@@ -530,5 +479,4 @@ CREATE TABLE IF NOT EXISTS "breakpoints" (
"path" TEXT NOT NULL,
"kind" VARCHAR NOT NULL
);
CREATE INDEX "index_breakpoints_on_project_id" ON "breakpoints" ("project_id");

View File

@@ -1,32 +0,0 @@
CREATE TABLE "project_repositories" (
"project_id" INTEGER NOT NULL,
"abs_path" VARCHAR,
"id" INT8 NOT NULL,
"legacy_worktree_id" INT8,
"entry_ids" VARCHAR,
"branch" VARCHAR,
"scan_id" INT8 NOT NULL,
"is_deleted" BOOL NOT NULL,
"current_merge_conflicts" VARCHAR,
"branch_summary" VARCHAR,
PRIMARY KEY (project_id, id)
);
CREATE INDEX "index_project_repositories_on_project_id" ON "project_repositories" ("project_id");
CREATE TABLE "project_repository_statuses" (
"project_id" INTEGER NOT NULL,
"repository_id" INT8 NOT NULL,
"repo_path" VARCHAR NOT NULL,
"status" INT8 NOT NULL,
"status_kind" INT4 NOT NULL,
"first_status" INT4 NULL,
"second_status" INT4 NULL,
"scan_id" INT8 NOT NULL,
"is_deleted" BOOL NOT NULL,
PRIMARY KEY (project_id, repository_id, repo_path)
);
CREATE INDEX "index_project_repos_statuses_on_project_id" ON "project_repository_statuses" ("project_id");
CREATE INDEX "index_project_repos_statuses_on_project_id_and_repo_id" ON "project_repository_statuses" ("project_id", "repository_id");

View File

@@ -9,7 +9,6 @@ use anyhow::anyhow;
use collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use dashmap::DashMap;
use futures::StreamExt;
use project_repository_statuses::StatusKind;
use rand::{prelude::StdRng, Rng, SeedableRng};
use rpc::ExtensionProvides;
use rpc::{
@@ -37,6 +36,7 @@ use std::{
};
use time::PrimitiveDateTime;
use tokio::sync::{Mutex, OwnedMutexGuard};
use worktree_repository_statuses::StatusKind;
use worktree_settings_file::LocalSettingsKind;
#[cfg(test)]
@@ -658,8 +658,6 @@ pub struct RejoinedProject {
pub old_connection_id: ConnectionId,
pub collaborators: Vec<ProjectCollaborator>,
pub worktrees: Vec<RejoinedWorktree>,
pub updated_repositories: Vec<proto::UpdateRepository>,
pub removed_repositories: Vec<u64>,
pub language_servers: Vec<proto::LanguageServer>,
}
@@ -728,7 +726,6 @@ pub struct Project {
pub role: ChannelRole,
pub collaborators: Vec<ProjectCollaborator>,
pub worktrees: BTreeMap<u64, Worktree>,
pub repositories: Vec<proto::UpdateRepository>,
pub language_servers: Vec<proto::LanguageServer>,
}
@@ -763,7 +760,7 @@ pub struct Worktree {
pub root_name: String,
pub visible: bool,
pub entries: Vec<proto::Entry>,
pub legacy_repository_entries: BTreeMap<u64, proto::RepositoryEntry>,
pub repository_entries: BTreeMap<u64, proto::RepositoryEntry>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub settings_files: Vec<WorktreeSettingsFile>,
pub scan_id: u64,
@@ -813,7 +810,7 @@ impl LocalSettingsKind {
}
fn db_status_to_proto(
entry: project_repository_statuses::Model,
entry: worktree_repository_statuses::Model,
) -> anyhow::Result<proto::StatusEntry> {
use proto::git_file_status::{Tracked, Unmerged, Variant};

View File

@@ -324,135 +324,119 @@ impl Database {
.await?;
}
// Backward-compatibility for old Zed clients.
//
// Remove this block when Zed 1.80 stable has been out for a week.
{
if !update.updated_repositories.is_empty() {
project_repository::Entity::insert_many(
update.updated_repositories.iter().map(|repository| {
project_repository::ActiveModel {
project_id: ActiveValue::set(project_id),
legacy_worktree_id: ActiveValue::set(Some(worktree_id)),
id: ActiveValue::set(repository.work_directory_id as i64),
scan_id: ActiveValue::set(update.scan_id as i64),
is_deleted: ActiveValue::set(false),
branch_summary: ActiveValue::Set(
repository
.branch_summary
.as_ref()
.map(|summary| serde_json::to_string(summary).unwrap()),
),
current_merge_conflicts: ActiveValue::Set(Some(
serde_json::to_string(&repository.current_merge_conflicts)
.unwrap(),
)),
if !update.updated_repositories.is_empty() {
worktree_repository::Entity::insert_many(update.updated_repositories.iter().map(
|repository| {
worktree_repository::ActiveModel {
project_id: ActiveValue::set(project_id),
worktree_id: ActiveValue::set(worktree_id),
work_directory_id: ActiveValue::set(
repository.work_directory_id as i64,
),
scan_id: ActiveValue::set(update.scan_id as i64),
branch: ActiveValue::set(repository.branch.clone()),
is_deleted: ActiveValue::set(false),
branch_summary: ActiveValue::Set(
repository
.branch_summary
.as_ref()
.map(|summary| serde_json::to_string(summary).unwrap()),
),
current_merge_conflicts: ActiveValue::Set(Some(
serde_json::to_string(&repository.current_merge_conflicts).unwrap(),
)),
}
},
))
.on_conflict(
OnConflict::columns([
worktree_repository::Column::ProjectId,
worktree_repository::Column::WorktreeId,
worktree_repository::Column::WorkDirectoryId,
])
.update_columns([
worktree_repository::Column::ScanId,
worktree_repository::Column::Branch,
worktree_repository::Column::BranchSummary,
worktree_repository::Column::CurrentMergeConflicts,
])
.to_owned(),
)
.exec(&*tx)
.await?;
// Old clients do not use abs path or entry ids.
abs_path: ActiveValue::set(String::new()),
entry_ids: ActiveValue::set("[]".into()),
}
}),
let has_any_statuses = update
.updated_repositories
.iter()
.any(|repository| !repository.updated_statuses.is_empty());
if has_any_statuses {
worktree_repository_statuses::Entity::insert_many(
update.updated_repositories.iter().flat_map(
|repository: &proto::RepositoryEntry| {
repository.updated_statuses.iter().map(|status_entry| {
let (repo_path, status_kind, first_status, second_status) =
proto_status_to_db(status_entry.clone());
worktree_repository_statuses::ActiveModel {
project_id: ActiveValue::set(project_id),
worktree_id: ActiveValue::set(worktree_id),
work_directory_id: ActiveValue::set(
repository.work_directory_id as i64,
),
scan_id: ActiveValue::set(update.scan_id as i64),
is_deleted: ActiveValue::set(false),
repo_path: ActiveValue::set(repo_path),
status: ActiveValue::set(0),
status_kind: ActiveValue::set(status_kind),
first_status: ActiveValue::set(first_status),
second_status: ActiveValue::set(second_status),
}
})
},
),
)
.on_conflict(
OnConflict::columns([
project_repository::Column::ProjectId,
project_repository::Column::Id,
worktree_repository_statuses::Column::ProjectId,
worktree_repository_statuses::Column::WorktreeId,
worktree_repository_statuses::Column::WorkDirectoryId,
worktree_repository_statuses::Column::RepoPath,
])
.update_columns([
project_repository::Column::ScanId,
project_repository::Column::BranchSummary,
project_repository::Column::CurrentMergeConflicts,
worktree_repository_statuses::Column::ScanId,
worktree_repository_statuses::Column::StatusKind,
worktree_repository_statuses::Column::FirstStatus,
worktree_repository_statuses::Column::SecondStatus,
])
.to_owned(),
)
.exec(&*tx)
.await?;
let has_any_statuses = update
.updated_repositories
.iter()
.any(|repository| !repository.updated_statuses.is_empty());
if has_any_statuses {
project_repository_statuses::Entity::insert_many(
update.updated_repositories.iter().flat_map(
|repository: &proto::RepositoryEntry| {
repository.updated_statuses.iter().map(|status_entry| {
let (repo_path, status_kind, first_status, second_status) =
proto_status_to_db(status_entry.clone());
project_repository_statuses::ActiveModel {
project_id: ActiveValue::set(project_id),
repository_id: ActiveValue::set(
repository.work_directory_id as i64,
),
scan_id: ActiveValue::set(update.scan_id as i64),
is_deleted: ActiveValue::set(false),
repo_path: ActiveValue::set(repo_path),
status: ActiveValue::set(0),
status_kind: ActiveValue::set(status_kind),
first_status: ActiveValue::set(first_status),
second_status: ActiveValue::set(second_status),
}
})
},
),
)
.on_conflict(
OnConflict::columns([
project_repository_statuses::Column::ProjectId,
project_repository_statuses::Column::RepositoryId,
project_repository_statuses::Column::RepoPath,
])
.update_columns([
project_repository_statuses::Column::ScanId,
project_repository_statuses::Column::StatusKind,
project_repository_statuses::Column::FirstStatus,
project_repository_statuses::Column::SecondStatus,
])
.to_owned(),
)
.exec(&*tx)
.await?;
}
for repo in &update.updated_repositories {
if !repo.removed_statuses.is_empty() {
project_repository_statuses::Entity::update_many()
.filter(
project_repository_statuses::Column::ProjectId
.eq(project_id)
.and(
project_repository_statuses::Column::RepositoryId
.eq(repo.work_directory_id),
)
.and(
project_repository_statuses::Column::RepoPath
.is_in(repo.removed_statuses.iter()),
),
)
.set(project_repository_statuses::ActiveModel {
is_deleted: ActiveValue::Set(true),
scan_id: ActiveValue::Set(update.scan_id as i64),
..Default::default()
})
.exec(&*tx)
.await?;
}
}
}
if !update.removed_repositories.is_empty() {
project_repository::Entity::update_many()
let has_any_removed_statuses = update
.updated_repositories
.iter()
.any(|repository| !repository.removed_statuses.is_empty());
if has_any_removed_statuses {
worktree_repository_statuses::Entity::update_many()
.filter(
project_repository::Column::ProjectId
worktree_repository_statuses::Column::ProjectId
.eq(project_id)
.and(project_repository::Column::LegacyWorktreeId.eq(worktree_id))
.and(project_repository::Column::Id.is_in(
update.removed_repositories.iter().map(|id| *id as i64),
)),
.and(
worktree_repository_statuses::Column::WorktreeId
.eq(worktree_id),
)
.and(
worktree_repository_statuses::Column::RepoPath.is_in(
update.updated_repositories.iter().flat_map(|repository| {
repository.removed_statuses.iter()
}),
),
),
)
.set(project_repository::ActiveModel {
.set(worktree_repository_statuses::ActiveModel {
is_deleted: ActiveValue::Set(true),
scan_id: ActiveValue::Set(update.scan_id as i64),
..Default::default()
@@ -462,109 +446,18 @@ impl Database {
}
}
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
Ok(connection_ids)
})
.await
}
pub async fn update_repository(
&self,
update: &proto::UpdateRepository,
_connection: ConnectionId,
) -> Result<TransactionGuard<Vec<ConnectionId>>> {
let project_id = ProjectId::from_proto(update.project_id);
let repository_id = update.id as i64;
self.project_transaction(project_id, |tx| async move {
project_repository::Entity::insert(project_repository::ActiveModel {
project_id: ActiveValue::set(project_id),
id: ActiveValue::set(repository_id),
legacy_worktree_id: ActiveValue::set(None),
abs_path: ActiveValue::set(update.abs_path.clone()),
entry_ids: ActiveValue::Set(serde_json::to_string(&update.entry_ids).unwrap()),
scan_id: ActiveValue::set(update.scan_id as i64),
is_deleted: ActiveValue::set(false),
branch_summary: ActiveValue::Set(
update
.branch_summary
.as_ref()
.map(|summary| serde_json::to_string(summary).unwrap()),
),
current_merge_conflicts: ActiveValue::Set(Some(
serde_json::to_string(&update.current_merge_conflicts).unwrap(),
)),
})
.on_conflict(
OnConflict::columns([
project_repository::Column::ProjectId,
project_repository::Column::Id,
])
.update_columns([
project_repository::Column::ScanId,
project_repository::Column::BranchSummary,
project_repository::Column::EntryIds,
project_repository::Column::AbsPath,
project_repository::Column::CurrentMergeConflicts,
])
.to_owned(),
)
.exec(&*tx)
.await?;
let has_any_statuses = !update.updated_statuses.is_empty();
if has_any_statuses {
project_repository_statuses::Entity::insert_many(
update.updated_statuses.iter().map(|status_entry| {
let (repo_path, status_kind, first_status, second_status) =
proto_status_to_db(status_entry.clone());
project_repository_statuses::ActiveModel {
project_id: ActiveValue::set(project_id),
repository_id: ActiveValue::set(repository_id),
scan_id: ActiveValue::set(update.scan_id as i64),
is_deleted: ActiveValue::set(false),
repo_path: ActiveValue::set(repo_path),
status: ActiveValue::set(0),
status_kind: ActiveValue::set(status_kind),
first_status: ActiveValue::set(first_status),
second_status: ActiveValue::set(second_status),
}
}),
)
.on_conflict(
OnConflict::columns([
project_repository_statuses::Column::ProjectId,
project_repository_statuses::Column::RepositoryId,
project_repository_statuses::Column::RepoPath,
])
.update_columns([
project_repository_statuses::Column::ScanId,
project_repository_statuses::Column::StatusKind,
project_repository_statuses::Column::FirstStatus,
project_repository_statuses::Column::SecondStatus,
])
.to_owned(),
)
.exec(&*tx)
.await?;
}
let has_any_removed_statuses = !update.removed_statuses.is_empty();
if has_any_removed_statuses {
project_repository_statuses::Entity::update_many()
if !update.removed_repositories.is_empty() {
worktree_repository::Entity::update_many()
.filter(
project_repository_statuses::Column::ProjectId
worktree_repository::Column::ProjectId
.eq(project_id)
.and(worktree_repository::Column::WorktreeId.eq(worktree_id))
.and(
project_repository_statuses::Column::RepositoryId.eq(repository_id),
)
.and(
project_repository_statuses::Column::RepoPath
.is_in(update.removed_statuses.iter()),
worktree_repository::Column::WorkDirectoryId
.is_in(update.removed_repositories.iter().map(|id| *id as i64)),
),
)
.set(project_repository_statuses::ActiveModel {
.set(worktree_repository::ActiveModel {
is_deleted: ActiveValue::Set(true),
scan_id: ActiveValue::Set(update.scan_id as i64),
..Default::default()
@@ -579,34 +472,6 @@ impl Database {
.await
}
pub async fn remove_repository(
&self,
remove: &proto::RemoveRepository,
_connection: ConnectionId,
) -> Result<TransactionGuard<Vec<ConnectionId>>> {
let project_id = ProjectId::from_proto(remove.project_id);
let repository_id = remove.id as i64;
self.project_transaction(project_id, |tx| async move {
project_repository::Entity::update_many()
.filter(
project_repository::Column::ProjectId
.eq(project_id)
.and(project_repository::Column::Id.eq(repository_id)),
)
.set(project_repository::ActiveModel {
is_deleted: ActiveValue::Set(true),
// scan_id: ActiveValue::Set(update.scan_id as i64),
..Default::default()
})
.exec(&*tx)
.await?;
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
Ok(connection_ids)
})
.await
}
/// Updates the diagnostic summary for the given connection.
pub async fn update_diagnostic_summary(
&self,
@@ -838,11 +703,11 @@ impl Database {
root_name: db_worktree.root_name,
visible: db_worktree.visible,
entries: Default::default(),
repository_entries: Default::default(),
diagnostic_summaries: Default::default(),
settings_files: Default::default(),
scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_scan_id as u64,
legacy_repository_entries: Default::default(),
},
)
})
@@ -885,77 +750,65 @@ impl Database {
}
// Populate repository entries.
let mut repositories = Vec::new();
{
let db_repository_entries = project_repository::Entity::find()
let db_repository_entries = worktree_repository::Entity::find()
.filter(
Condition::all()
.add(project_repository::Column::ProjectId.eq(project.id))
.add(project_repository::Column::IsDeleted.eq(false)),
.add(worktree_repository::Column::ProjectId.eq(project.id))
.add(worktree_repository::Column::IsDeleted.eq(false)),
)
.all(tx)
.await?;
for db_repository_entry in db_repository_entries {
let mut repository_statuses = project_repository_statuses::Entity::find()
.filter(
Condition::all()
.add(project_repository_statuses::Column::ProjectId.eq(project.id))
.add(
project_repository_statuses::Column::RepositoryId
.eq(db_repository_entry.id),
)
.add(project_repository_statuses::Column::IsDeleted.eq(false)),
)
.stream(tx)
.await?;
let mut updated_statuses = Vec::new();
while let Some(status_entry) = repository_statuses.next().await {
let status_entry = status_entry?;
updated_statuses.push(db_status_to_proto(status_entry)?);
}
let current_merge_conflicts = db_repository_entry
.current_merge_conflicts
.as_ref()
.map(|conflicts| serde_json::from_str(&conflicts))
.transpose()?
.unwrap_or_default();
let branch_summary = db_repository_entry
.branch_summary
.as_ref()
.map(|branch_summary| serde_json::from_str(&branch_summary))
.transpose()?
.unwrap_or_default();
let entry_ids = serde_json::from_str(&db_repository_entry.entry_ids)
.context("failed to deserialize repository's entry ids")?;
if let Some(worktree_id) = db_repository_entry.legacy_worktree_id {
if let Some(worktree) = worktrees.get_mut(&(worktree_id as u64)) {
worktree.legacy_repository_entries.insert(
db_repository_entry.id as u64,
proto::RepositoryEntry {
work_directory_id: db_repository_entry.id as u64,
updated_statuses,
removed_statuses: Vec::new(),
current_merge_conflicts,
branch_summary,
},
);
if let Some(worktree) = worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
{
let mut repository_statuses = worktree_repository_statuses::Entity::find()
.filter(
Condition::all()
.add(worktree_repository_statuses::Column::ProjectId.eq(project.id))
.add(
worktree_repository_statuses::Column::WorktreeId
.eq(worktree.id),
)
.add(
worktree_repository_statuses::Column::WorkDirectoryId
.eq(db_repository_entry.work_directory_id),
)
.add(worktree_repository_statuses::Column::IsDeleted.eq(false)),
)
.stream(tx)
.await?;
let mut updated_statuses = Vec::new();
while let Some(status_entry) = repository_statuses.next().await {
let status_entry: worktree_repository_statuses::Model = status_entry?;
updated_statuses.push(db_status_to_proto(status_entry)?);
}
} else {
repositories.push(proto::UpdateRepository {
project_id: db_repository_entry.project_id.0 as u64,
id: db_repository_entry.id as u64,
abs_path: db_repository_entry.abs_path,
entry_ids,
updated_statuses,
removed_statuses: Vec::new(),
current_merge_conflicts,
branch_summary,
scan_id: db_repository_entry.scan_id as u64,
});
let current_merge_conflicts = db_repository_entry
.current_merge_conflicts
.as_ref()
.map(|conflicts| serde_json::from_str(&conflicts))
.transpose()?
.unwrap_or_default();
let branch_summary = db_repository_entry
.branch_summary
.as_ref()
.map(|branch_summary| serde_json::from_str(&branch_summary))
.transpose()?
.unwrap_or_default();
worktree.repository_entries.insert(
db_repository_entry.work_directory_id as u64,
proto::RepositoryEntry {
work_directory_id: db_repository_entry.work_directory_id as u64,
branch: db_repository_entry.branch,
updated_statuses,
removed_statuses: Vec::new(),
current_merge_conflicts,
branch_summary,
},
);
}
}
}
@@ -1018,7 +871,6 @@ impl Database {
})
.collect(),
worktrees,
repositories,
language_servers: language_servers
.into_iter()
.map(|language_server| proto::LanguageServer {

View File

@@ -1,5 +1,3 @@
use anyhow::Context as _;
use super::*;
impl Database {
@@ -608,11 +606,6 @@ impl Database {
let mut worktrees = Vec::new();
let db_worktrees = project.find_related(worktree::Entity).all(tx).await?;
let db_repos = project
.find_related(project_repository::Entity)
.all(tx)
.await?;
for db_worktree in db_worktrees {
let mut worktree = RejoinedWorktree {
id: db_worktree.id as u64,
@@ -680,112 +673,96 @@ impl Database {
}
}
worktrees.push(worktree);
}
let mut removed_repositories = Vec::new();
let mut updated_repositories = Vec::new();
for db_repo in db_repos {
let rejoined_repository = rejoined_project
.repositories
.iter()
.find(|repo| repo.id == db_repo.id as u64);
let repository_filter = if let Some(rejoined_repository) = rejoined_repository {
project_repository::Column::ScanId.gt(rejoined_repository.scan_id)
} else {
project_repository::Column::IsDeleted.eq(false)
};
let db_repositories = project_repository::Entity::find()
.filter(
Condition::all()
.add(project_repository::Column::ProjectId.eq(project.id))
.add(repository_filter),
)
.all(tx)
.await?;
for db_repository in db_repositories.into_iter() {
if db_repository.is_deleted {
removed_repositories.push(db_repository.id as u64);
// Repository Entries
{
let repository_entry_filter = if let Some(rejoined_worktree) = rejoined_worktree {
worktree_repository::Column::ScanId.gt(rejoined_worktree.scan_id)
} else {
let status_entry_filter = if let Some(rejoined_repository) = rejoined_repository
{
project_repository_statuses::Column::ScanId.gt(rejoined_repository.scan_id)
worktree_repository::Column::IsDeleted.eq(false)
};
let db_repositories = worktree_repository::Entity::find()
.filter(
Condition::all()
.add(worktree_repository::Column::ProjectId.eq(project.id))
.add(worktree_repository::Column::WorktreeId.eq(worktree.id))
.add(repository_entry_filter),
)
.all(tx)
.await?;
for db_repository in db_repositories.into_iter() {
if db_repository.is_deleted {
worktree
.removed_repositories
.push(db_repository.work_directory_id as u64);
} else {
project_repository_statuses::Column::IsDeleted.eq(false)
};
let mut db_statuses = project_repository_statuses::Entity::find()
.filter(
Condition::all()
.add(project_repository_statuses::Column::ProjectId.eq(project.id))
.add(
project_repository_statuses::Column::RepositoryId
.eq(db_repository.id),
)
.add(status_entry_filter),
)
.stream(tx)
.await?;
let mut removed_statuses = Vec::new();
let mut updated_statuses = Vec::new();
while let Some(db_status) = db_statuses.next().await {
let db_status: project_repository_statuses::Model = db_status?;
if db_status.is_deleted {
removed_statuses.push(db_status.repo_path);
} else {
updated_statuses.push(db_status_to_proto(db_status)?);
}
}
let current_merge_conflicts = db_repository
.current_merge_conflicts
.as_ref()
.map(|conflicts| serde_json::from_str(&conflicts))
.transpose()?
.unwrap_or_default();
let branch_summary = db_repository
.branch_summary
.as_ref()
.map(|branch_summary| serde_json::from_str(&branch_summary))
.transpose()?
.unwrap_or_default();
let entry_ids = serde_json::from_str(&db_repository.entry_ids)
.context("failed to deserialize repository's entry ids")?;
if let Some(legacy_worktree_id) = db_repository.legacy_worktree_id {
if let Some(worktree) = worktrees
.iter_mut()
.find(|worktree| worktree.id as i64 == legacy_worktree_id)
let status_entry_filter = if let Some(rejoined_worktree) = rejoined_worktree
{
worktree.updated_repositories.push(proto::RepositoryEntry {
work_directory_id: db_repository.id as u64,
updated_statuses,
removed_statuses,
current_merge_conflicts,
branch_summary,
});
worktree_repository_statuses::Column::ScanId
.gt(rejoined_worktree.scan_id)
} else {
worktree_repository_statuses::Column::IsDeleted.eq(false)
};
let mut db_statuses = worktree_repository_statuses::Entity::find()
.filter(
Condition::all()
.add(
worktree_repository_statuses::Column::ProjectId
.eq(project.id),
)
.add(
worktree_repository_statuses::Column::WorktreeId
.eq(worktree.id),
)
.add(
worktree_repository_statuses::Column::WorkDirectoryId
.eq(db_repository.work_directory_id),
)
.add(status_entry_filter),
)
.stream(tx)
.await?;
let mut removed_statuses = Vec::new();
let mut updated_statuses = Vec::new();
while let Some(db_status) = db_statuses.next().await {
let db_status: worktree_repository_statuses::Model = db_status?;
if db_status.is_deleted {
removed_statuses.push(db_status.repo_path);
} else {
updated_statuses.push(db_status_to_proto(db_status)?);
}
}
} else {
updated_repositories.push(proto::UpdateRepository {
entry_ids,
let current_merge_conflicts = db_repository
.current_merge_conflicts
.as_ref()
.map(|conflicts| serde_json::from_str(&conflicts))
.transpose()?
.unwrap_or_default();
let branch_summary = db_repository
.branch_summary
.as_ref()
.map(|branch_summary| serde_json::from_str(&branch_summary))
.transpose()?
.unwrap_or_default();
worktree.updated_repositories.push(proto::RepositoryEntry {
work_directory_id: db_repository.work_directory_id as u64,
branch: db_repository.branch,
updated_statuses,
removed_statuses,
current_merge_conflicts,
branch_summary,
project_id: project_id.to_proto(),
id: db_repository.id as u64,
abs_path: db_repository.abs_path,
scan_id: db_repository.scan_id as u64,
});
}
}
}
worktrees.push(worktree);
}
let language_servers = project
@@ -855,8 +832,6 @@ impl Database {
id: project_id,
old_connection_id,
collaborators,
updated_repositories,
removed_repositories,
worktrees,
language_servers,
}))

View File

@@ -26,8 +26,6 @@ pub mod observed_channel_messages;
pub mod processed_stripe_event;
pub mod project;
pub mod project_collaborator;
pub mod project_repository;
pub mod project_repository_statuses;
pub mod rate_buckets;
pub mod room;
pub mod room_participant;
@@ -38,4 +36,6 @@ pub mod user_feature;
pub mod worktree;
pub mod worktree_diagnostic_summary;
pub mod worktree_entry;
pub mod worktree_repository;
pub mod worktree_repository_statuses;
pub mod worktree_settings_file;

View File

@@ -45,8 +45,6 @@ pub enum Relation {
Room,
#[sea_orm(has_many = "super::worktree::Entity")]
Worktrees,
#[sea_orm(has_many = "super::project_repository::Entity")]
Repositories,
#[sea_orm(has_many = "super::project_collaborator::Entity")]
Collaborators,
#[sea_orm(has_many = "super::language_server::Entity")]
@@ -71,12 +69,6 @@ impl Related<super::worktree::Entity> for Entity {
}
}
impl Related<super::project_repository::Entity> for Entity {
fn to() -> RelationDef {
Relation::Repositories.def()
}
}
impl Related<super::project_collaborator::Entity> for Entity {
fn to() -> RelationDef {
Relation::Collaborators.def()

View File

@@ -2,17 +2,16 @@ use crate::db::ProjectId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "project_repositories")]
#[sea_orm(table_name = "worktree_repositories")]
pub struct Model {
#[sea_orm(primary_key)]
pub project_id: ProjectId,
#[sea_orm(primary_key)]
pub id: i64,
pub abs_path: String,
pub legacy_worktree_id: Option<i64>,
// JSON array containing 1 or more integer project entry ids
pub entry_ids: String,
pub worktree_id: i64,
#[sea_orm(primary_key)]
pub work_directory_id: i64,
pub scan_id: i64,
pub branch: Option<String>,
pub is_deleted: bool,
// JSON array typed string
pub current_merge_conflicts: Option<String>,
@@ -21,19 +20,6 @@ pub struct Model {
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::project::Entity",
from = "Column::ProjectId",
to = "super::project::Column::Id"
)]
Project,
}
impl Related<super::project::Entity> for Entity {
fn to() -> RelationDef {
Relation::Project.def()
}
}
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -2,12 +2,14 @@ use crate::db::ProjectId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "project_repository_statuses")]
#[sea_orm(table_name = "worktree_repository_statuses")]
pub struct Model {
#[sea_orm(primary_key)]
pub project_id: ProjectId,
#[sea_orm(primary_key)]
pub repository_id: i64,
pub worktree_id: i64,
#[sea_orm(primary_key)]
pub work_directory_id: i64,
#[sea_orm(primary_key)]
pub repo_path: String,
/// Old single-code status field, no longer used but kept here to mirror the DB schema.

View File

@@ -37,7 +37,6 @@ use core::fmt::{self, Debug, Formatter};
use http_client::HttpClient;
use open_ai::{OpenAiEmbeddingModel, OPEN_AI_API_URL};
use reqwest_client::ReqwestClient;
use rpc::proto::split_repository_update;
use sha2::Digest;
use supermaven_api::{CreateExternalUserRequest, SupermavenAdminApi};
@@ -292,8 +291,6 @@ impl Server {
.add_message_handler(leave_project)
.add_request_handler(update_project)
.add_request_handler(update_worktree)
.add_request_handler(update_repository)
.add_request_handler(remove_repository)
.add_message_handler(start_language_server)
.add_message_handler(update_language_server)
.add_message_handler(update_diagnostic_summary)
@@ -1467,7 +1464,7 @@ fn notify_rejoined_projects(
removed_repositories: worktree.removed_repositories,
};
for update in proto::split_worktree_update(message) {
session.peer.send(session.connection_id, update)?;
session.peer.send(session.connection_id, update.clone())?;
}
// Stream this worktree's diagnostics.
@@ -1496,23 +1493,21 @@ fn notify_rejoined_projects(
}
}
for repository in mem::take(&mut project.updated_repositories) {
for update in split_repository_update(repository) {
session.peer.send(session.connection_id, update)?;
}
}
for id in mem::take(&mut project.removed_repositories) {
for language_server in &project.language_servers {
session.peer.send(
session.connection_id,
proto::RemoveRepository {
proto::UpdateLanguageServer {
project_id: project.id.to_proto(),
id,
language_server_id: language_server.id,
variant: Some(
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
proto::LspDiskBasedDiagnosticsUpdated {},
),
),
},
)?;
}
}
Ok(())
}
@@ -1898,7 +1893,7 @@ fn join_project_internal(
removed_entries: Default::default(),
scan_id: worktree.scan_id,
is_last_update: worktree.scan_id == worktree.completed_scan_id,
updated_repositories: worktree.legacy_repository_entries.into_values().collect(),
updated_repositories: worktree.repository_entries.into_values().collect(),
removed_repositories: Default::default(),
};
for update in proto::split_worktree_update(message) {
@@ -1931,12 +1926,6 @@ fn join_project_internal(
}
}
for repository in mem::take(&mut project.repositories) {
for update in split_repository_update(repository) {
session.peer.send(session.connection_id, update)?;
}
}
for language_server in &project.language_servers {
session.peer.send(
session.connection_id,
@@ -2029,54 +2018,6 @@ async fn update_worktree(
Ok(())
}
async fn update_repository(
request: proto::UpdateRepository,
response: Response<proto::UpdateRepository>,
session: Session,
) -> Result<()> {
let guest_connection_ids = session
.db()
.await
.update_repository(&request, session.connection_id)
.await?;
broadcast(
Some(session.connection_id),
guest_connection_ids.iter().copied(),
|connection_id| {
session
.peer
.forward_send(session.connection_id, connection_id, request.clone())
},
);
response.send(proto::Ack {})?;
Ok(())
}
async fn remove_repository(
request: proto::RemoveRepository,
response: Response<proto::RemoveRepository>,
session: Session,
) -> Result<()> {
let guest_connection_ids = session
.db()
.await
.remove_repository(&request, session.connection_id)
.await?;
broadcast(
Some(session.connection_id),
guest_connection_ids.iter().copied(),
|connection_id| {
session
.peer
.forward_send(session.connection_id, connection_id, request.clone())
},
);
response.send(proto::Ack {})?;
Ok(())
}
/// Updates other participants with changes to the diagnostics
async fn update_diagnostic_summary(
message: proto::UpdateDiagnosticSummary,

View File

@@ -387,7 +387,7 @@ async fn test_channel_room(
executor.run_until_parked();
let room_a =
cx_a.read(|cx| active_call_a.read_with(cx, |call, _| call.room().unwrap().clone()));
cx_a.read(|cx| room_a.read_with(cx, |room, cx| assert!(room.is_connected(cx))));
cx_a.read(|cx| room_a.read_with(cx, |room, _| assert!(room.is_connected())));
cx_a.read(|cx| {
client_a.channel_store().read_with(cx, |channels, _| {
@@ -461,7 +461,7 @@ async fn test_channel_room(
let room_a =
cx_a.read(|cx| active_call_a.read_with(cx, |call, _| call.room().unwrap().clone()));
cx_a.read(|cx| room_a.read_with(cx, |room, cx| assert!(room.is_connected(cx))));
cx_a.read(|cx| room_a.read_with(cx, |room, _| assert!(room.is_connected())));
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
@@ -472,7 +472,7 @@ async fn test_channel_room(
let room_b =
cx_b.read(|cx| active_call_b.read_with(cx, |call, _| call.room().unwrap().clone()));
cx_b.read(|cx| room_b.read_with(cx, |room, cx| assert!(room.is_connected(cx))));
cx_b.read(|cx| room_b.read_with(cx, |room, _| assert!(room.is_connected())));
assert_eq!(
room_participants(&room_b, cx_b),
RoomParticipants {
@@ -556,7 +556,7 @@ async fn test_channel_room(
let room_a =
cx_a.read(|cx| active_call_a.read_with(cx, |call, _| call.room().unwrap().clone()));
cx_a.read(|cx| room_a.read_with(cx, |room, cx| assert!(room.is_connected(cx))));
cx_a.read(|cx| room_a.read_with(cx, |room, _| assert!(room.is_connected())));
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
@@ -567,7 +567,7 @@ async fn test_channel_room(
let room_b =
cx_b.read(|cx| active_call_b.read_with(cx, |call, _| call.room().unwrap().clone()));
cx_b.read(|cx| room_b.read_with(cx, |room, cx| assert!(room.is_connected(cx))));
cx_b.read(|cx| room_b.read_with(cx, |room, _| assert!(room.is_connected())));
assert_eq!(
room_participants(&room_b, cx_b),
RoomParticipants {

View File

@@ -435,114 +435,118 @@ async fn test_basic_following(
editor_a1.item_id()
);
use crate::rpc::RECONNECT_TIMEOUT;
use gpui::TestScreenCaptureSource;
use workspace::{
dock::{test::TestPanel, DockPosition},
item::test::TestItem,
shared_screen::SharedScreen,
};
// TODO: Re-enable this test once we can replace our swift Livekit SDK with the rust SDK
#[cfg(not(target_os = "macos"))]
{
use crate::rpc::RECONNECT_TIMEOUT;
use gpui::TestScreenCaptureSource;
use workspace::{
dock::{test::TestPanel, DockPosition},
item::test::TestItem,
shared_screen::SharedScreen,
};
// Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
let display = TestScreenCaptureSource::new();
active_call_b
.update(cx_b, |call, cx| call.set_location(None, cx))
.await
.unwrap();
cx_b.set_screen_capture_sources(vec![display]);
active_call_b
.update(cx_b, |call, cx| {
call.room()
// Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
let display = TestScreenCaptureSource::new();
active_call_b
.update(cx_b, |call, cx| call.set_location(None, cx))
.await
.unwrap();
cx_b.set_screen_capture_sources(vec![display]);
active_call_b
.update(cx_b, |call, cx| {
call.room()
.unwrap()
.update(cx, |room, cx| room.share_screen(cx))
})
.await
.unwrap(); // This is what breaks
executor.run_until_parked();
let shared_screen = workspace_a.update(cx_a, |workspace, cx| {
workspace
.active_item(cx)
.expect("no active item")
.downcast::<SharedScreen>()
.expect("active item isn't a shared screen")
});
// Client B activates Zed again, which causes the previous editor to become focused again.
active_call_b
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
.await
.unwrap();
executor.run_until_parked();
workspace_a.update(cx_a, |workspace, cx| {
assert_eq!(
workspace.active_item(cx).unwrap().item_id(),
editor_a1.item_id()
)
});
// Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.activate_item(&multibuffer_editor_b, true, true, window, cx)
});
executor.run_until_parked();
workspace_a.update(cx_a, |workspace, cx| {
assert_eq!(
workspace.active_item(cx).unwrap().item_id(),
multibuffer_editor_a.item_id()
)
});
// Client B activates a panel, and the previously-opened screen-sharing item gets activated.
let panel = cx_b.new(|cx| TestPanel::new(DockPosition::Left, cx));
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.add_panel(panel, window, cx);
workspace.toggle_panel_focus::<TestPanel>(window, cx);
});
executor.run_until_parked();
assert_eq!(
workspace_a.update(cx_a, |workspace, cx| workspace
.active_item(cx)
.unwrap()
.update(cx, |room, cx| room.share_screen(cx))
})
.await
.unwrap();
executor.run_until_parked();
let shared_screen = workspace_a.update(cx_a, |workspace, cx| {
workspace
.active_item(cx)
.expect("no active item")
.downcast::<SharedScreen>()
.expect("active item isn't a shared screen")
});
.item_id()),
shared_screen.item_id()
);
// Client B activates Zed again, which causes the previous editor to become focused again.
active_call_b
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
.await
.unwrap();
executor.run_until_parked();
workspace_a.update(cx_a, |workspace, cx| {
// Toggling the focus back to the pane causes client A to return to the multibuffer.
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.toggle_panel_focus::<TestPanel>(window, cx);
});
executor.run_until_parked();
workspace_a.update(cx_a, |workspace, cx| {
assert_eq!(
workspace.active_item(cx).unwrap().item_id(),
multibuffer_editor_a.item_id()
)
});
// Client B activates an item that doesn't implement following,
// so the previously-opened screen-sharing item gets activated.
let unfollowable_item = cx_b.new(TestItem::new);
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.active_pane().update(cx, |pane, cx| {
pane.add_item(Box::new(unfollowable_item), true, true, None, window, cx)
})
});
executor.run_until_parked();
assert_eq!(
workspace.active_item(cx).unwrap().item_id(),
editor_a1.item_id()
)
});
workspace_a.update(cx_a, |workspace, cx| workspace
.active_item(cx)
.unwrap()
.item_id()),
shared_screen.item_id()
);
// Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.activate_item(&multibuffer_editor_b, true, true, window, cx)
});
executor.run_until_parked();
workspace_a.update(cx_a, |workspace, cx| {
// Following interrupts when client B disconnects.
client_b.disconnect(&cx_b.to_async());
executor.advance_clock(RECONNECT_TIMEOUT);
assert_eq!(
workspace.active_item(cx).unwrap().item_id(),
multibuffer_editor_a.item_id()
)
});
// Client B activates a panel, and the previously-opened screen-sharing item gets activated.
let panel = cx_b.new(|cx| TestPanel::new(DockPosition::Left, cx));
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.add_panel(panel, window, cx);
workspace.toggle_panel_focus::<TestPanel>(window, cx);
});
executor.run_until_parked();
assert_eq!(
workspace_a.update(cx_a, |workspace, cx| workspace
.active_item(cx)
.unwrap()
.item_id()),
shared_screen.item_id()
);
// Toggling the focus back to the pane causes client A to return to the multibuffer.
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.toggle_panel_focus::<TestPanel>(window, cx);
});
executor.run_until_parked();
workspace_a.update(cx_a, |workspace, cx| {
assert_eq!(
workspace.active_item(cx).unwrap().item_id(),
multibuffer_editor_a.item_id()
)
});
// Client B activates an item that doesn't implement following,
// so the previously-opened screen-sharing item gets activated.
let unfollowable_item = cx_b.new(TestItem::new);
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.active_pane().update(cx, |pane, cx| {
pane.add_item(Box::new(unfollowable_item), true, true, None, window, cx)
})
});
executor.run_until_parked();
assert_eq!(
workspace_a.update(cx_a, |workspace, cx| workspace
.active_item(cx)
.unwrap()
.item_id()),
shared_screen.item_id()
);
// Following interrupts when client B disconnects.
client_b.disconnect(&cx_b.to_async());
executor.advance_clock(RECONNECT_TIMEOUT);
assert_eq!(
workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
None
);
workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
None
);
}
}
#[gpui::test]

View File

@@ -243,56 +243,60 @@ async fn test_basic_calls(
}
);
// User A shares their screen
let display = gpui::TestScreenCaptureSource::new();
let events_b = active_call_events(cx_b);
let events_c = active_call_events(cx_c);
cx_a.set_screen_capture_sources(vec![display]);
active_call_a
.update(cx_a, |call, cx| {
call.room()
.unwrap()
.update(cx, |room, cx| room.share_screen(cx))
})
.await
.unwrap();
// TODO: Re-enable this test once we can replace our swift Livekit SDK with the rust SDK
#[cfg(not(target_os = "macos"))]
{
// User A shares their screen
let display = gpui::TestScreenCaptureSource::new();
let events_b = active_call_events(cx_b);
let events_c = active_call_events(cx_c);
cx_a.set_screen_capture_sources(vec![display]);
active_call_a
.update(cx_a, |call, cx| {
call.room()
.unwrap()
.update(cx, |room, cx| room.share_screen(cx))
})
.await
.unwrap();
executor.run_until_parked();
executor.run_until_parked();
// User B observes the remote screen sharing track.
assert_eq!(events_b.borrow().len(), 1);
let event_b = events_b.borrow().first().unwrap().clone();
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_b {
assert_eq!(participant_id, client_a.peer_id().unwrap());
// User B observes the remote screen sharing track.
assert_eq!(events_b.borrow().len(), 1);
let event_b = events_b.borrow().first().unwrap().clone();
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_b {
assert_eq!(participant_id, client_a.peer_id().unwrap());
room_b.read_with(cx_b, |room, _| {
assert_eq!(
room.remote_participants()[&client_a.user_id().unwrap()]
.video_tracks
.len(),
1
);
});
} else {
panic!("unexpected event")
}
room_b.read_with(cx_b, |room, _| {
assert_eq!(
room.remote_participants()[&client_a.user_id().unwrap()]
.video_tracks
.len(),
1
);
});
} else {
panic!("unexpected event")
}
// User C observes the remote screen sharing track.
assert_eq!(events_c.borrow().len(), 1);
let event_c = events_c.borrow().first().unwrap().clone();
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_c {
assert_eq!(participant_id, client_a.peer_id().unwrap());
// User C observes the remote screen sharing track.
assert_eq!(events_c.borrow().len(), 1);
let event_c = events_c.borrow().first().unwrap().clone();
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_c {
assert_eq!(participant_id, client_a.peer_id().unwrap());
room_c.read_with(cx_c, |room, _| {
assert_eq!(
room.remote_participants()[&client_a.user_id().unwrap()]
.video_tracks
.len(),
1
);
});
} else {
panic!("unexpected event")
room_c.read_with(cx_c, |room, _| {
assert_eq!(
room.remote_participants()[&client_a.user_id().unwrap()]
.video_tracks
.len(),
1
);
});
} else {
panic!("unexpected event")
}
}
// User A leaves the room.
@@ -2081,7 +2085,17 @@ async fn test_mute_deafen(
audio_tracks_playing: participant
.audio_tracks
.values()
.map(|(track, _)| track.enabled())
.map({
#[cfg(target_os = "macos")]
{
|track| track.is_playing()
}
#[cfg(not(target_os = "macos"))]
{
|(track, _)| track.rtc_track().enabled()
}
})
.collect(),
})
.collect::<Vec<_>>()
@@ -2833,7 +2847,7 @@ async fn test_git_diff_base_change(
});
}
#[gpui::test(iterations = 10)]
#[gpui::test]
async fn test_git_branch_name(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
@@ -2881,10 +2895,9 @@ async fn test_git_branch_name(
let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1);
let worktree = worktrees[0].clone();
let snapshot = worktree.read(cx).snapshot();
let repo = snapshot.repositories().first().unwrap();
let root_entry = worktree.read(cx).snapshot().root_git_entry().unwrap();
assert_eq!(
repo.branch().map(|branch| branch.name.to_string()),
root_entry.branch().map(|branch| branch.name.to_string()),
branch_name
);
}
@@ -6148,6 +6161,8 @@ async fn test_contact_requests(
}
}
// TODO: Re-enable this test once we can replace our swift Livekit SDK with the rust SDK
#[cfg(not(target_os = "macos"))]
#[gpui::test(iterations = 10)]
async fn test_join_call_after_screen_was_shared(
executor: BackgroundExecutor,
@@ -6756,7 +6771,7 @@ async fn test_remote_git_branches(
.map(ToString::to_string)
.collect::<HashSet<_>>();
let (project_a, _) = client_a.build_local_project("/project", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project("/project", cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
@@ -6769,6 +6784,8 @@ async fn test_remote_git_branches(
let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
let root_path = ProjectPath::root_path(worktree_id);
let branches_b = cx_b
.update(|cx| repo_b.update(cx, |repository, _| repository.branches()))
.await
@@ -6793,15 +6810,11 @@ async fn test_remote_git_branches(
let host_branch = cx_a.update(|cx| {
project_a.update(cx, |project, cx| {
project
.repositories(cx)
.values()
.next()
.unwrap()
.read(cx)
.current_branch()
.unwrap()
.clone()
project.worktree_store().update(cx, |worktree_store, cx| {
worktree_store
.current_branch(root_path.clone(), cx)
.unwrap()
})
})
});
@@ -6830,15 +6843,9 @@ async fn test_remote_git_branches(
let host_branch = cx_a.update(|cx| {
project_a.update(cx, |project, cx| {
project
.repositories(cx)
.values()
.next()
.unwrap()
.read(cx)
.current_branch()
.unwrap()
.clone()
project.worktree_store().update(cx, |worktree_store, cx| {
worktree_store.current_branch(root_path, cx).unwrap()
})
})
});

View File

@@ -258,7 +258,7 @@ async fn test_ssh_collaboration_git_branches(
});
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
let (project_a, _) = client_a
let (project_a, worktree_id) = client_a
.build_ssh_project("/project", client_ssh, cx_a)
.await;
@@ -277,6 +277,7 @@ async fn test_ssh_collaboration_git_branches(
executor.run_until_parked();
let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
let root_path = ProjectPath::root_path(worktree_id);
let branches_b = cx_b
.update(|cx| repo_b.read(cx).branches())
@@ -302,17 +303,13 @@ async fn test_ssh_collaboration_git_branches(
let server_branch = server_cx.update(|cx| {
headless_project.update(cx, |headless_project, cx| {
headless_project.git_store.update(cx, |git_store, cx| {
git_store
.repositories()
.values()
.next()
.unwrap()
.read(cx)
.current_branch()
.unwrap()
.clone()
})
headless_project
.worktree_store
.update(cx, |worktree_store, cx| {
worktree_store
.current_branch(root_path.clone(), cx)
.unwrap()
})
})
});
@@ -341,17 +338,11 @@ async fn test_ssh_collaboration_git_branches(
let server_branch = server_cx.update(|cx| {
headless_project.update(cx, |headless_project, cx| {
headless_project.git_store.update(cx, |git_store, cx| {
git_store
.repositories()
.values()
.next()
.unwrap()
.read(cx)
.current_branch()
.unwrap()
.clone()
})
headless_project
.worktree_store
.update(cx, |worktree_store, cx| {
worktree_store.current_branch(root_path, cx).unwrap()
})
})
});

View File

@@ -45,8 +45,12 @@ use std::{
};
use workspace::{Workspace, WorkspaceStore};
#[cfg(not(target_os = "macos"))]
use livekit_client::test::TestServer as LivekitTestServer;
#[cfg(target_os = "macos")]
use livekit_client_macos::TestServer as LivekitTestServer;
pub struct TestServer {
pub app_state: Arc<AppState>,
pub test_livekit_server: Arc<LivekitTestServer>,
@@ -161,7 +165,6 @@ impl TestServer {
let fs = FakeFs::new(cx.executor());
cx.update(|cx| {
gpui_tokio::init(cx);
if cx.has_global::<SettingsStore>() {
panic!("Same cx used to create two test clients")
}

View File

@@ -56,10 +56,6 @@ impl Tool for ContextServerTool {
}
}
fn ui_text(&self, _input: &serde_json::Value) -> String {
format!("Run MCP tool `{}`", self.tool.name)
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
@@ -69,43 +65,42 @@ impl Tool for ContextServerTool {
cx: &mut App,
) -> Task<Result<String>> {
if let Some(server) = self.server_manager.read(cx).get_server(&self.server_id) {
let tool_name = self.tool.name.clone();
let server_clone = server.clone();
let input_clone = input.clone();
cx.foreground_executor().spawn({
let tool_name = self.tool.name.clone();
async move {
let Some(protocol) = server.client() else {
bail!("Context server not initialized");
};
cx.spawn(async move |_cx| {
let Some(protocol) = server_clone.client() else {
bail!("Context server not initialized");
};
let arguments = if let serde_json::Value::Object(map) = input {
Some(map.into_iter().collect())
} else {
None
};
let arguments = if let serde_json::Value::Object(map) = input_clone {
Some(map.into_iter().collect())
} else {
None
};
log::trace!(
"Running tool: {} with arguments: {:?}",
tool_name,
arguments
);
let response = protocol.run_tool(tool_name, arguments).await?;
log::trace!(
"Running tool: {} with arguments: {:?}",
tool_name,
arguments
);
let response = protocol.run_tool(tool_name, arguments).await?;
let mut result = String::new();
for content in response.content {
match content {
types::ToolResponseContent::Text { text } => {
result.push_str(&text);
}
types::ToolResponseContent::Image { .. } => {
log::warn!("Ignoring image content from tool response");
}
types::ToolResponseContent::Resource { .. } => {
log::warn!("Ignoring resource content from tool response");
let mut result = String::new();
for content in response.content {
match content {
types::ToolResponseContent::Text { text } => {
result.push_str(&text);
}
types::ToolResponseContent::Image { .. } => {
log::warn!("Ignoring image content from tool response");
}
types::ToolResponseContent::Resource { .. } => {
log::warn!("Ignoring resource content from tool response");
}
}
}
Ok(result)
}
Ok(result)
})
} else {
Task::ready(Err(anyhow!("Context server not found")))

View File

@@ -51,13 +51,9 @@ impl ExtensionContextServerProxy for ContextServerFactoryRegistryProxy {
})
})?;
let mut command = extension
let command = extension
.context_server_command(id.clone(), extension_project)
.await?;
command.command = extension
.path_from_extension(command.command.as_ref())
.to_string_lossy()
.to_string();
log::info!("loaded command for context server {id}: {command:?}");

View File

@@ -3,8 +3,8 @@ use anyhow::{anyhow, Result};
use collections::HashMap;
use command_palette_hooks::CommandPaletteFilter;
use dap::{
client::SessionId, debugger_settings::DebuggerSettings, ContinuedEvent, DebugAdapterConfig,
LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
client::SessionId, debugger_settings::DebuggerSettings, ContinuedEvent, LoadedSourceEvent,
ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
};
use futures::{channel::mpsc, SinkExt as _};
use gpui::{
@@ -21,7 +21,6 @@ use settings::Settings;
use std::{any::TypeId, path::PathBuf};
use terminal_view::terminal_panel::TerminalPanel;
use ui::prelude::*;
use util::ResultExt;
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
pane, Continue, Disconnect, Pane, Pause, Restart, StepBack, StepInto, StepOut, StepOver, Stop,
@@ -52,7 +51,6 @@ pub struct DebugPanel {
project: WeakEntity<Project>,
workspace: WeakEntity<Workspace>,
_subscriptions: Vec<Subscription>,
pub(crate) last_inert_config: Option<DebugAdapterConfig>,
}
impl DebugPanel {
@@ -65,7 +63,6 @@ impl DebugPanel {
let project = workspace.project().clone();
let dap_store = project.read(cx).dap_store();
let weak_workspace = workspace.weak_handle();
let debug_panel = cx.weak_entity();
let pane = cx.new(|cx| {
let mut pane = Pane::new(
workspace.weak_handle(),
@@ -84,7 +81,6 @@ impl DebugPanel {
pane.set_render_tab_bar_buttons(cx, {
let project = project.clone();
let weak_workspace = weak_workspace.clone();
let debug_panel = debug_panel.clone();
move |_, _, cx| {
let project = project.clone();
let weak_workspace = weak_workspace.clone();
@@ -95,34 +91,21 @@ impl DebugPanel {
.child(
IconButton::new("new-debug-session", IconName::Plus)
.icon_size(IconSize::Small)
.on_click({
let debug_panel = debug_panel.clone();
cx.listener(move |pane, _, window, cx| {
let config = debug_panel
.read_with(cx, |this: &DebugPanel, _| {
this.last_inert_config.clone()
})
.log_err()
.flatten();
pane.add_item(
Box::new(DebugSession::inert(
project.clone(),
weak_workspace.clone(),
debug_panel.clone(),
config,
window,
cx,
)),
false,
false,
None,
.on_click(cx.listener(move |pane, _, window, cx| {
pane.add_item(
Box::new(DebugSession::inert(
project.clone(),
weak_workspace.clone(),
window,
cx,
);
})
}),
)),
false,
false,
None,
window,
cx,
);
})),
)
.into_any_element(),
),
@@ -133,8 +116,6 @@ impl DebugPanel {
Box::new(DebugSession::inert(
project.clone(),
weak_workspace.clone(),
debug_panel.clone(),
None,
window,
cx,
)),
@@ -157,7 +138,6 @@ impl DebugPanel {
pane,
size: px(300.),
_subscriptions,
last_inert_config: None,
project: project.downgrade(),
workspace: workspace.weak_handle(),
};
@@ -300,14 +280,8 @@ impl DebugPanel {
// We already have an item for this session.
return;
}
let session_item = DebugSession::running(
project,
self.workspace.clone(),
session,
cx.weak_entity(),
window,
cx,
);
let session_item =
DebugSession::running(project, self.workspace.clone(), session, window, cx);
self.pane.update(cx, |pane, cx| {
pane.add_item(Box::new(session_item), true, true, None, window, cx);
@@ -530,16 +504,12 @@ impl Panel for DebugPanel {
let Some(project) = self.project.clone().upgrade() else {
return;
};
let config = self.last_inert_config.clone();
let panel = cx.weak_entity();
// todo: We need to revisit it when we start adding stopped items to pane (as that'll cause us to add two items).
self.pane.update(cx, |this, cx| {
this.add_item(
Box::new(DebugSession::inert(
project,
self.workspace.clone(),
panel,
config,
window,
cx,
)),

View File

@@ -6,7 +6,6 @@ mod starting;
use std::time::Duration;
use dap::client::SessionId;
use dap::DebugAdapterConfig;
use failed::FailedState;
use gpui::{
percentage, Animation, AnimationExt, AnyElement, App, Entity, EventEmitter, FocusHandle,
@@ -20,14 +19,11 @@ use rpc::proto::{self, PeerId};
use running::RunningState;
use starting::{StartingEvent, StartingState};
use ui::prelude::*;
use util::ResultExt;
use workspace::{
item::{self, Item},
FollowableItem, ViewId, Workspace,
};
use crate::debugger_panel::DebugPanel;
pub(crate) enum DebugSessionState {
Inert(Entity<InertState>),
Starting(Entity<StartingState>),
@@ -48,7 +44,6 @@ pub struct DebugSession {
remote_id: Option<workspace::ViewId>,
mode: DebugSessionState,
dap_store: WeakEntity<DapStore>,
debug_panel: WeakEntity<DebugPanel>,
worktree_store: WeakEntity<WorktreeStore>,
workspace: WeakEntity<Workspace>,
_subscriptions: [Subscription; 1],
@@ -72,8 +67,6 @@ impl DebugSession {
pub(super) fn inert(
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
debug_panel: WeakEntity<DebugPanel>,
config: Option<DebugAdapterConfig>,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
@@ -84,8 +77,7 @@ impl DebugSession {
.and_then(|tree| tree.read(cx).abs_path().to_str().map(|str| str.to_string()))
.unwrap_or_default();
let inert =
cx.new(|cx| InertState::new(workspace.clone(), &default_cwd, config, window, cx));
let inert = cx.new(|cx| InertState::new(workspace.clone(), &default_cwd, window, cx));
let project = project.read(cx);
let dap_store = project.dap_store().downgrade();
@@ -97,7 +89,6 @@ impl DebugSession {
mode: DebugSessionState::Inert(inert),
dap_store,
worktree_store,
debug_panel,
workspace,
_subscriptions,
}
@@ -108,7 +99,6 @@ impl DebugSession {
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
session: Entity<Session>,
debug_panel: WeakEntity<DebugPanel>,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
@@ -121,7 +111,6 @@ impl DebugSession {
remote_id: None,
mode: DebugSessionState::Running(mode),
dap_store: project.read(cx).dap_store().downgrade(),
debug_panel,
worktree_store: project.read(cx).worktree_store().downgrade(),
workspace,
})
@@ -159,11 +148,6 @@ impl DebugSession {
let dap_store = self.dap_store.clone();
let InertEvent::Spawned { config } = event;
let config = config.clone();
self.debug_panel
.update(cx, |this, _| this.last_inert_config = Some(config.clone()))
.log_err();
let worktree = self
.worktree_store
.update(cx, |this, _| this.worktrees().next())

View File

@@ -32,15 +32,6 @@ impl SpawnMode {
}
}
impl From<DebugRequestType> for SpawnMode {
fn from(request: DebugRequestType) -> Self {
match request {
DebugRequestType::Launch => SpawnMode::Launch,
DebugRequestType::Attach(_) => SpawnMode::Attach,
}
}
}
pub(crate) struct InertState {
focus_handle: FocusHandle,
selected_debugger: Option<SharedString>,
@@ -55,56 +46,27 @@ impl InertState {
pub(super) fn new(
workspace: WeakEntity<Workspace>,
default_cwd: &str,
debug_config: Option<DebugAdapterConfig>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let selected_debugger = debug_config.as_ref().and_then(|config| match config.kind {
DebugAdapterKind::Lldb => Some("LLDB".into()),
DebugAdapterKind::Go(_) => Some("Delve".into()),
DebugAdapterKind::Php(_) => Some("PHP".into()),
DebugAdapterKind::Javascript(_) => Some("JavaScript".into()),
DebugAdapterKind::Python(_) => Some("Debugpy".into()),
_ => None,
});
let spawn_mode = debug_config
.as_ref()
.map(|config| config.request.clone().into())
.unwrap_or_default();
let program = debug_config
.as_ref()
.and_then(|config| config.program.to_owned());
let program_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
if let Some(program) = program {
editor.insert(&program, window, cx);
} else {
editor.set_placeholder_text("Program path", cx);
}
editor.set_placeholder_text("Program path", cx);
editor
});
let cwd = debug_config
.and_then(|config| config.cwd.map(|cwd| cwd.to_owned()))
.unwrap_or_else(|| PathBuf::from(default_cwd));
let cwd_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
editor.insert(cwd.to_str().unwrap_or_else(|| default_cwd), window, cx);
editor.insert(default_cwd, window, cx);
editor.set_placeholder_text("Working directory", cx);
editor
});
Self {
workspace,
cwd_editor,
program_editor,
selected_debugger,
spawn_mode,
selected_debugger: None,
focus_handle: cx.focus_handle(),
spawn_mode: SpawnMode::default(),
popover_handle: Default::default(),
}
}

View File

@@ -602,7 +602,7 @@ async fn test_handle_start_debugging_reverse_request(
});
let child_client = child_session.update(cx, |session, _| session.adapter_client().unwrap());
child_client
client
.on_request::<dap::requests::Threads, _>(move |_, _| {
Ok(dap::ThreadsResponse {
threads: vec![dap::Thread {
@@ -645,230 +645,6 @@ async fn test_handle_start_debugging_reverse_request(
shutdown_session.await.unwrap();
}
#[gpui::test]
async fn test_shutdown_children_when_parent_session_shutdown(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
"/project",
json!({
"main.rs": "First line\nSecond line\nThird line\nFourth line",
}),
)
.await;
let project = Project::test(fs, ["/project".as_ref()], cx).await;
let dap_store = project.update(cx, |project, _| project.dap_store());
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
});
let parent_session = task.await.unwrap();
let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
client
.on_request::<dap::requests::Threads, _>(move |_, _| {
Ok(dap::ThreadsResponse {
threads: vec![dap::Thread {
id: 1,
name: "Thread 1".into(),
}],
})
})
.await;
client.on_response::<StartDebugging, _>(move |_| {}).await;
// start first child session
client
.fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
configuration: json!({}),
request: StartDebuggingRequestArgumentsRequest::Launch,
})
.await;
cx.run_until_parked();
// start second child session
client
.fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
configuration: json!({}),
request: StartDebuggingRequestArgumentsRequest::Launch,
})
.await;
cx.run_until_parked();
// configure first child session
let first_child_session = dap_store.read_with(cx, |dap_store, _| {
dap_store.session_by_id(SessionId(1)).unwrap()
});
let first_child_client =
first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
first_child_client
.on_request::<Disconnect, _>(move |_, _| Ok(()))
.await;
// configure second child session
let second_child_session = dap_store.read_with(cx, |dap_store, _| {
dap_store.session_by_id(SessionId(2)).unwrap()
});
let second_child_client =
second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
second_child_client
.on_request::<Disconnect, _>(move |_, _| Ok(()))
.await;
cx.run_until_parked();
// shutdown parent session
dap_store
.update(cx, |dap_store, cx| {
dap_store.shutdown_session(parent_session.read(cx).session_id(), cx)
})
.await
.unwrap();
// assert parent session and all children sessions are shutdown
dap_store.update(cx, |dap_store, cx| {
assert!(dap_store
.session_by_id(parent_session.read(cx).session_id())
.is_none());
assert!(dap_store
.session_by_id(first_child_session.read(cx).session_id())
.is_none());
assert!(dap_store
.session_by_id(second_child_session.read(cx).session_id())
.is_none());
});
}
#[gpui::test]
async fn test_shutdown_parent_session_if_all_children_are_shutdown(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
"/project",
json!({
"main.rs": "First line\nSecond line\nThird line\nFourth line",
}),
)
.await;
let project = Project::test(fs, ["/project".as_ref()], cx).await;
let dap_store = project.update(cx, |project, _| project.dap_store());
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
});
let parent_session = task.await.unwrap();
let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_response::<StartDebugging, _>(move |_| {}).await;
// start first child session
client
.fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
configuration: json!({}),
request: StartDebuggingRequestArgumentsRequest::Launch,
})
.await;
cx.run_until_parked();
// start second child session
client
.fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
configuration: json!({}),
request: StartDebuggingRequestArgumentsRequest::Launch,
})
.await;
cx.run_until_parked();
// configure first child session
let first_child_session = dap_store.read_with(cx, |dap_store, _| {
dap_store.session_by_id(SessionId(1)).unwrap()
});
let first_child_client =
first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
first_child_client
.on_request::<Disconnect, _>(move |_, _| Ok(()))
.await;
// configure second child session
let second_child_session = dap_store.read_with(cx, |dap_store, _| {
dap_store.session_by_id(SessionId(2)).unwrap()
});
let second_child_client =
second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
second_child_client
.on_request::<Disconnect, _>(move |_, _| Ok(()))
.await;
cx.run_until_parked();
// shutdown first child session
dap_store
.update(cx, |dap_store, cx| {
dap_store.shutdown_session(first_child_session.read(cx).session_id(), cx)
})
.await
.unwrap();
// assert parent session and second child session still exist
dap_store.update(cx, |dap_store, cx| {
assert!(dap_store
.session_by_id(parent_session.read(cx).session_id())
.is_some());
assert!(dap_store
.session_by_id(first_child_session.read(cx).session_id())
.is_none());
assert!(dap_store
.session_by_id(second_child_session.read(cx).session_id())
.is_some());
});
// shutdown first child session
dap_store
.update(cx, |dap_store, cx| {
dap_store.shutdown_session(second_child_session.read(cx).session_id(), cx)
})
.await
.unwrap();
// assert parent session got shutdown by second child session
// because it was the last child
dap_store.update(cx, |dap_store, cx| {
assert!(dap_store
.session_by_id(parent_session.read(cx).session_id())
.is_none());
assert!(dap_store
.session_by_id(second_child_session.read(cx).session_id())
.is_none());
});
}
#[gpui::test]
async fn test_debug_panel_item_thread_status_reset_on_failure(
executor: BackgroundExecutor,

View File

@@ -275,7 +275,6 @@ actions!(
ConvertToUpperCamelCase,
ConvertToUpperCase,
Copy,
CopyAndTrim,
CopyFileLocation,
CopyHighlightJson,
CopyFileName,

View File

@@ -665,11 +665,10 @@ impl CompletionsMenu {
.collect()
};
let mut additional_matches = Vec::new();
// Deprioritize all candidates where the query's start does not match the start of any word in the candidate
// Remove all candidates where the query's start does not match the start of any word in the candidate
if let Some(query) = query {
if let Some(query_start) = query.chars().next() {
let (primary, secondary) = matches.into_iter().partition(|string_match| {
matches.retain(|string_match| {
split_words(&string_match.string).any(|word| {
// Check that the first codepoint of the word as lowercase matches the first
// codepoint of the query as lowercase
@@ -679,8 +678,6 @@ impl CompletionsMenu {
.all(|(word_cp, query_cp)| word_cp == query_cp)
})
});
matches = primary;
additional_matches = secondary;
}
}
@@ -743,8 +740,6 @@ impl CompletionsMenu {
}
drop(completions);
matches.extend(additional_matches);
*self.entries.borrow_mut() = matches;
self.selected_item = 0;
// This keeps the display consistent when y_flipped.

View File

@@ -9429,15 +9429,7 @@ impl Editor {
self.do_paste(&text, metadata, false, window, cx);
}
pub fn copy_and_trim(&mut self, _: &CopyAndTrim, _: &mut Window, cx: &mut Context<Self>) {
self.do_copy(true, cx);
}
pub fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
self.do_copy(false, cx);
}
fn do_copy(&self, strip_leading_indents: bool, cx: &mut Context<Self>) {
let selections = self.selections.all::<Point>(cx);
let buffer = self.buffer.read(cx).read(cx);
let mut text = String::new();
@@ -9446,7 +9438,7 @@ impl Editor {
{
let max_point = buffer.max_point();
let mut is_first = true;
for selection in &selections {
for selection in selections.iter() {
let mut start = selection.start;
let mut end = selection.end;
let is_entire_line = selection.is_empty() || self.selections.line_mode;
@@ -9454,55 +9446,21 @@ impl Editor {
start = Point::new(start.row, 0);
end = cmp::min(max_point, Point::new(end.row + 1, 0));
}
let mut trimmed_selections = Vec::new();
if strip_leading_indents && end.row.saturating_sub(start.row) > 0 {
let row = MultiBufferRow(start.row);
let first_indent = buffer.indent_size_for_line(row);
if first_indent.len == 0 || start.column > first_indent.len {
trimmed_selections.push(start..end);
} else {
trimmed_selections.push(
Point::new(row.0, first_indent.len)
..Point::new(row.0, buffer.line_len(row)),
);
for row in start.row + 1..=end.row {
let row_indent_size = buffer.indent_size_for_line(MultiBufferRow(row));
if row_indent_size.len >= first_indent.len {
trimmed_selections.push(
Point::new(row, first_indent.len)
..Point::new(row, buffer.line_len(MultiBufferRow(row))),
);
} else {
trimmed_selections.clear();
trimmed_selections.push(start..end);
break;
}
}
}
if is_first {
is_first = false;
} else {
trimmed_selections.push(start..end);
text += "\n";
}
for trimmed_range in trimmed_selections {
if is_first {
is_first = false;
} else {
text += "\n";
}
let mut len = 0;
for chunk in buffer.text_for_range(trimmed_range.start..trimmed_range.end) {
text.push_str(chunk);
len += chunk.len();
}
clipboard_selections.push(ClipboardSelection {
len,
is_entire_line,
first_line_indent: buffer
.indent_size_for_line(MultiBufferRow(trimmed_range.start.row))
.len,
});
let mut len = 0;
for chunk in buffer.text_for_range(start..end) {
text.push_str(chunk);
len += chunk.len();
}
clipboard_selections.push(ClipboardSelection {
len,
is_entire_line,
first_line_indent: buffer.indent_size_for_line(MultiBufferRow(start.row)).len,
});
}
}

View File

@@ -4918,180 +4918,6 @@ async fn test_clipboard(cx: &mut TestAppContext) {
tˇhe lazy dog"});
}
#[gpui::test]
async fn test_copy_trim(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.set_state(
r#" «for selection in selections.iter() {
let mut start = selection.start;
let mut end = selection.end;
let is_entire_line = selection.is_empty() || self.selections.line_mode;
if is_entire_line {
start = Point::new(start.row, 0);ˇ»
end = cmp::min(max_point, Point::new(end.row + 1, 0));
}
"#,
);
cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
assert_eq!(
cx.read_from_clipboard()
.and_then(|item| item.text().as_deref().map(str::to_string)),
Some(
"for selection in selections.iter() {
let mut start = selection.start;
let mut end = selection.end;
let is_entire_line = selection.is_empty() || self.selections.line_mode;
if is_entire_line {
start = Point::new(start.row, 0);"
.to_string()
),
"Regular copying preserves all indentation selected",
);
cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
assert_eq!(
cx.read_from_clipboard()
.and_then(|item| item.text().as_deref().map(str::to_string)),
Some(
"for selection in selections.iter() {
let mut start = selection.start;
let mut end = selection.end;
let is_entire_line = selection.is_empty() || self.selections.line_mode;
if is_entire_line {
start = Point::new(start.row, 0);"
.to_string()
),
"Copying with stripping should strip all leading whitespaces"
);
cx.set_state(
r#" « for selection in selections.iter() {
let mut start = selection.start;
let mut end = selection.end;
let is_entire_line = selection.is_empty() || self.selections.line_mode;
if is_entire_line {
start = Point::new(start.row, 0);ˇ»
end = cmp::min(max_point, Point::new(end.row + 1, 0));
}
"#,
);
cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
assert_eq!(
cx.read_from_clipboard()
.and_then(|item| item.text().as_deref().map(str::to_string)),
Some(
" for selection in selections.iter() {
let mut start = selection.start;
let mut end = selection.end;
let is_entire_line = selection.is_empty() || self.selections.line_mode;
if is_entire_line {
start = Point::new(start.row, 0);"
.to_string()
),
"Regular copying preserves all indentation selected",
);
cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
assert_eq!(
cx.read_from_clipboard()
.and_then(|item| item.text().as_deref().map(str::to_string)),
Some(
"for selection in selections.iter() {
let mut start = selection.start;
let mut end = selection.end;
let is_entire_line = selection.is_empty() || self.selections.line_mode;
if is_entire_line {
start = Point::new(start.row, 0);"
.to_string()
),
"Copying with stripping should strip all leading whitespaces, even if some of it was selected"
);
cx.set_state(
r#" «ˇ for selection in selections.iter() {
let mut start = selection.start;
let mut end = selection.end;
let is_entire_line = selection.is_empty() || self.selections.line_mode;
if is_entire_line {
start = Point::new(start.row, 0);»
end = cmp::min(max_point, Point::new(end.row + 1, 0));
}
"#,
);
cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
assert_eq!(
cx.read_from_clipboard()
.and_then(|item| item.text().as_deref().map(str::to_string)),
Some(
" for selection in selections.iter() {
let mut start = selection.start;
let mut end = selection.end;
let is_entire_line = selection.is_empty() || self.selections.line_mode;
if is_entire_line {
start = Point::new(start.row, 0);"
.to_string()
),
"Regular copying for reverse selection works the same",
);
cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
assert_eq!(
cx.read_from_clipboard()
.and_then(|item| item.text().as_deref().map(str::to_string)),
Some(
"for selection in selections.iter() {
let mut start = selection.start;
let mut end = selection.end;
let is_entire_line = selection.is_empty() || self.selections.line_mode;
if is_entire_line {
start = Point::new(start.row, 0);"
.to_string()
),
"Copying with stripping for reverse selection works the same"
);
cx.set_state(
r#" for selection «in selections.iter() {
let mut start = selection.start;
let mut end = selection.end;
let is_entire_line = selection.is_empty() || self.selections.line_mode;
if is_entire_line {
start = Point::new(start.row, 0);ˇ»
end = cmp::min(max_point, Point::new(end.row + 1, 0));
}
"#,
);
cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
assert_eq!(
cx.read_from_clipboard()
.and_then(|item| item.text().as_deref().map(str::to_string)),
Some(
"in selections.iter() {
let mut start = selection.start;
let mut end = selection.end;
let is_entire_line = selection.is_empty() || self.selections.line_mode;
if is_entire_line {
start = Point::new(start.row, 0);"
.to_string()
),
"When selecting past the indent, the copying works as usual",
);
cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
assert_eq!(
cx.read_from_clipboard()
.and_then(|item| item.text().as_deref().map(str::to_string)),
Some(
"in selections.iter() {
let mut start = selection.start;
let mut end = selection.end;
let is_entire_line = selection.is_empty() || self.selections.line_mode;
if is_entire_line {
start = Point::new(start.row, 0);"
.to_string()
),
"When selecting past the indent, nothing is trimmed"
);
}
#[gpui::test]
async fn test_paste_multiline(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -9666,7 +9492,7 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
}
});
cx.simulate_keystroke("l");
cx.simulate_keystroke("s");
cx.executor().run_until_parked();
cx.condition(|editor, _| editor.context_menu_visible())
.await;
@@ -9675,7 +9501,7 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
{
assert_eq!(
completion_menu_entries(&menu),
&["last"],
&["second"],
"After showing word completions, further editing should filter them and not query the LSP"
);
} else {
@@ -13045,7 +12871,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestA
overrides: [(
"element".into(),
LanguageConfigOverride {
completion_query_characters: Override::Set(['-'].into_iter().collect()),
word_characters: Override::Set(['-'].into_iter().collect()),
..Default::default()
},
)]

View File

@@ -244,7 +244,6 @@ impl EditorElement {
register_action(editor, window, Editor::kill_ring_cut);
register_action(editor, window, Editor::kill_ring_yank);
register_action(editor, window, Editor::copy);
register_action(editor, window, Editor::copy_and_trim);
register_action(editor, window, Editor::paste);
register_action(editor, window, Editor::undo);
register_action(editor, window, Editor::redo);

View File

@@ -629,20 +629,18 @@ impl Item for Editor {
self.buffer()
.read(cx)
.as_singleton()
.and_then(|buffer| {
let buffer = buffer.read(cx);
let path = buffer.project_path(cx)?;
let buffer_id = buffer.remote_id();
.and_then(|buffer| buffer.read(cx).project_path(cx))
.and_then(|path| {
let project = self.project.as_ref()?.read(cx);
let entry = project.entry_for_path(&path, cx)?;
let (repo, repo_path) = project
.git_store()
let git_status = project
.worktree_for_id(path.worktree_id, cx)?
.read(cx)
.repository_and_path_for_buffer_id(buffer_id, cx)?;
let status = repo.read(cx).status_for_path(&repo_path)?.status;
.snapshot()
.status_for_file(path.path)?;
Some(entry_git_aware_label_color(
status.summary(),
git_status.summary(),
entry.is_ignored,
params.selected,
))

View File

@@ -137,9 +137,9 @@ pub fn deploy_context_menu(
menu
} else {
// Don't show the context menu if there isn't a project associated with this editor
let Some(project) = editor.project.clone() else {
if editor.project.is_none() {
return;
};
}
let display_map = editor.selections.display_map(cx);
let buffer = &editor.snapshot(window, cx).buffer_snapshot;
@@ -159,13 +159,10 @@ pub fn deploy_context_menu(
.all::<PointUtf16>(cx)
.into_iter()
.any(|s| !s.is_empty());
let has_git_repo = anchor.buffer_id.is_some_and(|buffer_id| {
project
.read(cx)
.git_store()
.read(cx)
.repository_and_path_for_buffer_id(buffer_id, cx)
.is_some()
let has_git_repo = editor.project.as_ref().map_or(false, |project| {
project.update(cx, |project, cx| {
project.get_first_worktree_root_repo(cx).is_some()
})
});
ui::ContextMenu::build(window, cx, |menu, _window, _cx| {

View File

@@ -264,7 +264,7 @@ impl EditorLspTestContext {
..Default::default()
},
block_comment: Some(("<!-- ".into(), " -->".into())),
completion_query_characters: ['-'].into_iter().collect(),
word_characters: ['-'].into_iter().collect(),
..Default::default()
},
Some(tree_sitter_html::LANGUAGE.into()),

View File

@@ -1,5 +1,14 @@
fn main() {
if cfg!(target_os = "macos") {
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7");
println!("cargo:rerun-if-env-changed=ZED_BUNDLE");
if std::env::var("ZED_BUNDLE").ok().as_deref() == Some("true") {
// Find WebRTC.framework in the Frameworks folder when running as part of an application bundle.
println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path/../Frameworks");
} else {
// Find WebRTC.framework as a sibling of the executable when running outside of an application bundle.
println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
}
}
}

View File

@@ -906,10 +906,7 @@ impl ExtensionStore {
.await
}
})
.await
.inspect_err(|error| {
util::log_err(error);
})?;
.await?;
let output_path = &extensions_dir.join(extension_id.as_ref());
if let Some(metadata) = fs.metadata(output_path).await? {

View File

@@ -612,7 +612,6 @@ impl ExtensionsPage {
self.buttons_for_entry(extension, &status, has_dev_extension, cx);
let version = extension.manifest.version.clone();
let repository_url = extension.manifest.repository.clone();
let authors = extension.manifest.authors.clone();
let installed_version = match status {
ExtensionStatus::Installed(installed_version) => Some(installed_version),
@@ -750,7 +749,6 @@ impl ExtensionsPage {
Some(Self::render_remote_extension_context_menu(
&this,
extension_id.clone(),
authors.clone(),
window,
cx,
))
@@ -763,7 +761,6 @@ impl ExtensionsPage {
fn render_remote_extension_context_menu(
this: &Entity<Self>,
extension_id: Arc<str>,
authors: Vec<String>,
window: &mut Window,
cx: &mut App,
) -> Entity<ContextMenu> {
@@ -785,12 +782,6 @@ impl ExtensionsPage {
cx.write_to_clipboard(ClipboardItem::new_string(extension_id.to_string()));
}
})
.entry("Copy Author Info", None, {
let authors = authors.clone();
move |_, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(authors.join(", ")));
}
})
});
context_menu

View File

@@ -15,12 +15,25 @@ path = "src/feedback.rs"
test-support = []
[dependencies]
anyhow.workspace = true
bitflags.workspace = true
client.workspace = true
db.workspace = true
editor.workspace = true
futures.workspace = true
gpui.workspace = true
http_client.workspace = true
human_bytes = "0.4.1"
language.workspace = true
log.workspace = true
menu.workspace = true
project.workspace = true
regex.workspace = true
release_channel.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
smol.workspace = true
sysinfo.workspace = true
ui.workspace = true
urlencoding.workspace = true

View File

@@ -11,16 +11,19 @@ actions!(
zed,
[
CopySystemSpecsIntoClipboard,
EmailZed,
FileBugReport,
OpenZedRepo,
RequestFeature,
OpenZedRepo
]
);
const ZED_REPO_URL: &str = "https://github.com/zed-industries/zed";
const fn zed_repo_url() -> &'static str {
"https://github.com/zed-industries/zed"
}
const REQUEST_FEATURE_URL: &str = "https://github.com/zed-industries/zed/discussions/new/choose";
fn request_feature_url() -> String {
"https://github.com/zed-industries/zed/discussions/new/choose".to_string()
}
fn file_bug_report_url(specs: &SystemSpecs) -> String {
format!(
@@ -35,18 +38,6 @@ fn file_bug_report_url(specs: &SystemSpecs) -> String {
)
}
fn email_zed_url(specs: &SystemSpecs) -> String {
format!(
concat!("mailto:hi@zed.dev", "?", "body={}"),
email_body(specs)
)
}
fn email_body(specs: &SystemSpecs) -> String {
let body = format!("\n\nSystem Information:\n\n{}", specs);
urlencoding::encode(&body).to_string()
}
pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, window, cx| {
let Some(window) = window else {
@@ -75,8 +66,14 @@ pub fn init(cx: &mut App) {
})
.detach();
})
.register_action(|_, _: &RequestFeature, _, cx| {
cx.open_url(REQUEST_FEATURE_URL);
.register_action(|_, _: &RequestFeature, window, cx| {
cx.spawn_in(window, async move |_, cx| {
cx.update(|_, cx| {
cx.open_url(&request_feature_url());
})
.log_err();
})
.detach();
})
.register_action(move |_, _: &FileBugReport, window, cx| {
let specs = SystemSpecs::new(window, cx);
@@ -89,19 +86,8 @@ pub fn init(cx: &mut App) {
})
.detach();
})
.register_action(move |_, _: &EmailZed, window, cx| {
let specs = SystemSpecs::new(window, cx);
cx.spawn_in(window, async move |_, cx| {
let specs = specs.await;
cx.update(|_, cx| {
cx.open_url(&email_zed_url(&specs));
})
.log_err();
})
.detach();
})
.register_action(move |_, _: &OpenZedRepo, _, cx| {
cx.open_url(ZED_REPO_URL);
cx.open_url(zed_repo_url());
});
})
.detach();

View File

@@ -1,37 +1,421 @@
use gpui::{App, Context, DismissEvent, EventEmitter, FocusHandle, Focusable, Render, Window};
use ui::{prelude::*, IconPosition};
use workspace::{ModalView, Workspace};
use std::{
ops::RangeInclusive,
sync::{Arc, LazyLock},
time::Duration,
};
use anyhow::{anyhow, bail};
use bitflags::bitflags;
use client::Client;
use db::kvp::KEY_VALUE_STORE;
use editor::{Editor, EditorEvent};
use futures::AsyncReadExt;
use gpui::{
div, rems, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
PromptLevel, Render, Task, Window,
};
use http_client::HttpClient;
use language::Buffer;
use project::Project;
use regex::Regex;
use serde_derive::Serialize;
use ui::{prelude::*, Button, ButtonStyle, IconPosition, Tooltip};
use util::ResultExt;
use workspace::{DismissDecision, ModalView, Workspace};
use zed_actions::feedback::GiveFeedback;
use crate::{EmailZed, FileBugReport, OpenZedRepo, RequestFeature};
use crate::{system_specs::SystemSpecs, OpenZedRepo};
// For UI testing purposes
const SEND_SUCCESS_IN_DEV_MODE: bool = true;
const SEND_TIME_IN_DEV_MODE: Duration = Duration::from_secs(2);
// Temporary, until tests are in place
#[cfg(debug_assertions)]
const DEV_MODE: bool = true;
#[cfg(not(debug_assertions))]
const DEV_MODE: bool = false;
const DATABASE_KEY_NAME: &str = "email_address";
static EMAIL_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b").unwrap());
const FEEDBACK_CHAR_LIMIT: RangeInclusive<i32> = 10..=5000;
const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
"Feedback failed to submit, see error log for details.";
#[derive(Serialize)]
struct FeedbackRequestBody<'a> {
feedback_text: &'a str,
email: Option<String>,
installation_id: Option<Arc<str>>,
metrics_id: Option<Arc<str>>,
system_specs: SystemSpecs,
is_staff: bool,
}
bitflags! {
#[derive(Debug, Clone, PartialEq)]
struct InvalidStateFlags: u8 {
const EmailAddress = 0b00000001;
const CharacterCount = 0b00000010;
}
}
#[derive(Debug, Clone, PartialEq)]
enum CannotSubmitReason {
InvalidState { flags: InvalidStateFlags },
AwaitingSubmission,
}
#[derive(Debug, Clone, PartialEq)]
enum SubmissionState {
CanSubmit,
CannotSubmit { reason: CannotSubmitReason },
}
pub struct FeedbackModal {
focus_handle: FocusHandle,
system_specs: SystemSpecs,
feedback_editor: Entity<Editor>,
email_address_editor: Entity<Editor>,
submission_state: Option<SubmissionState>,
dismiss_modal: bool,
character_count: i32,
}
impl Focusable for FeedbackModal {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.feedback_editor.focus_handle(cx)
}
}
impl EventEmitter<DismissEvent> for FeedbackModal {}
impl ModalView for FeedbackModal {}
impl ModalView for FeedbackModal {
fn on_before_dismiss(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> DismissDecision {
self.update_email_in_store(window, cx);
if self.dismiss_modal {
return DismissDecision::Dismiss(true);
}
let has_feedback = self.feedback_editor.read(cx).text_option(cx).is_some();
if !has_feedback {
return DismissDecision::Dismiss(true);
}
let answer = window.prompt(
PromptLevel::Info,
"Discard feedback?",
None,
&["Yes", "No"],
cx,
);
cx.spawn_in(window, async move |this, cx| {
if answer.await.ok() == Some(0) {
this.update(cx, |this, cx| {
this.dismiss_modal = true;
cx.emit(DismissEvent)
})
.log_err();
}
})
.detach();
DismissDecision::Pending
}
}
impl FeedbackModal {
pub fn register(workspace: &mut Workspace, _: &mut Window, cx: &mut Context<Workspace>) {
let _handle = cx.entity().downgrade();
workspace.register_action(move |workspace, _: &GiveFeedback, window, cx| {
workspace.toggle_modal(window, cx, move |_, cx| FeedbackModal::new(cx));
workspace
.with_local_workspace(window, cx, |workspace, window, cx| {
let markdown = workspace
.app_state()
.languages
.language_for_name("Markdown");
let project = workspace.project().clone();
let system_specs = SystemSpecs::new(window, cx);
cx.spawn_in(window, async move |workspace, cx| {
let markdown = markdown.await.log_err();
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer("", markdown, cx)
})?;
let system_specs = system_specs.await;
workspace.update_in(cx, |workspace, window, cx| {
workspace.toggle_modal(window, cx, move |window, cx| {
FeedbackModal::new(system_specs, project, buffer, window, cx)
});
})?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
})
.detach_and_log_err(cx);
});
}
pub fn new(cx: &mut Context<Self>) -> Self {
pub fn new(
system_specs: SystemSpecs,
project: Entity<Project>,
buffer: Entity<Buffer>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let email_address_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
editor.set_placeholder_text("Email address (optional)", cx);
if let Ok(Some(email_address)) = KEY_VALUE_STORE.read_kvp(DATABASE_KEY_NAME) {
editor.set_text(email_address, window, cx)
}
editor
});
let feedback_editor = cx.new(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(project.clone()), window, cx);
editor.set_placeholder_text(
"You can use markdown to organize your feedback with code and links.",
cx,
);
editor.set_show_gutter(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_show_edit_predictions(Some(false), window, cx);
editor.set_vertical_scroll_margin(5, cx);
editor.set_use_modal_editing(false);
editor.set_soft_wrap();
editor
});
cx.subscribe(&feedback_editor, |this, editor, event: &EditorEvent, cx| {
if matches!(event, EditorEvent::Edited { .. }) {
this.character_count = editor
.read(cx)
.buffer()
.read(cx)
.as_singleton()
.expect("Feedback editor is never a multi-buffer")
.read(cx)
.len() as i32;
cx.notify();
}
})
.detach();
Self {
focus_handle: cx.focus_handle(),
system_specs: system_specs.clone(),
feedback_editor,
email_address_editor,
submission_state: None,
dismiss_modal: false,
character_count: 0,
}
}
pub fn submit(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<anyhow::Result<()>> {
let feedback_text = self.feedback_editor.read(cx).text(cx).trim().to_string();
let email = self.email_address_editor.read(cx).text_option(cx);
let answer = window.prompt(
PromptLevel::Info,
"Ready to submit your feedback?",
None,
&["Yes, Submit!", "No"],
cx,
);
let client = Client::global(cx).clone();
let specs = self.system_specs.clone();
cx.spawn_in(window, async move |this, cx| {
let answer = answer.await.ok();
if answer == Some(0) {
this.update(cx, |this, cx| {
this.submission_state = Some(SubmissionState::CannotSubmit {
reason: CannotSubmitReason::AwaitingSubmission,
});
cx.notify();
})
.log_err();
let res =
FeedbackModal::submit_feedback(&feedback_text, email, client, specs).await;
match res {
Ok(_) => {
this.update(cx, |this, cx| {
this.dismiss_modal = true;
cx.notify();
cx.emit(DismissEvent)
})
.ok();
}
Err(error) => {
log::error!("{}", error);
this.update_in(cx, |this, window, cx| {
let prompt = window.prompt(
PromptLevel::Critical,
FEEDBACK_SUBMISSION_ERROR_TEXT,
None,
&["OK"],
cx,
);
cx.spawn_in(window, async move |_, _cx| {
prompt.await.ok();
})
.detach();
this.submission_state = Some(SubmissionState::CanSubmit);
cx.notify();
})
.log_err();
}
}
}
})
.detach();
Task::ready(Ok(()))
}
async fn submit_feedback(
feedback_text: &str,
email: Option<String>,
zed_client: Arc<Client>,
system_specs: SystemSpecs,
) -> anyhow::Result<()> {
if DEV_MODE {
smol::Timer::after(SEND_TIME_IN_DEV_MODE).await;
if SEND_SUCCESS_IN_DEV_MODE {
return Ok(());
} else {
return Err(anyhow!("Error submitting feedback"));
}
}
let telemetry = zed_client.telemetry();
let installation_id = telemetry.installation_id();
let metrics_id = telemetry.metrics_id();
let is_staff = telemetry.is_staff();
let http_client = zed_client.http_client();
let feedback_endpoint = http_client.build_url("/api/feedback");
let request = FeedbackRequestBody {
feedback_text,
email,
installation_id,
metrics_id,
system_specs,
is_staff: is_staff.unwrap_or(false),
};
let json_bytes = serde_json::to_vec(&request)?;
let request = http_client::http::Request::post(feedback_endpoint)
.header("content-type", "application/json")
.body(json_bytes.into())?;
let mut response = http_client.send(request).await?;
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
let response_status = response.status();
if !response_status.is_success() {
bail!("Feedback API failed with error: {}", response_status)
}
Ok(())
}
fn update_submission_state(&mut self, cx: &mut Context<Self>) {
if self.awaiting_submission() {
return;
}
let mut invalid_state_flags = InvalidStateFlags::empty();
let valid_email_address = match self.email_address_editor.read(cx).text_option(cx) {
Some(email_address) => EMAIL_REGEX.is_match(&email_address),
None => true,
};
if !valid_email_address {
invalid_state_flags |= InvalidStateFlags::EmailAddress;
}
if !FEEDBACK_CHAR_LIMIT.contains(&self.character_count) {
invalid_state_flags |= InvalidStateFlags::CharacterCount;
}
if invalid_state_flags.is_empty() {
self.submission_state = Some(SubmissionState::CanSubmit);
} else {
self.submission_state = Some(SubmissionState::CannotSubmit {
reason: CannotSubmitReason::InvalidState {
flags: invalid_state_flags,
},
});
}
}
fn update_email_in_store(&self, window: &mut Window, cx: &mut Context<Self>) {
let email = self.email_address_editor.read(cx).text_option(cx);
cx.spawn_in(window, async move |_, _| match email {
Some(email) => {
KEY_VALUE_STORE
.write_kvp(DATABASE_KEY_NAME.to_string(), email)
.await
.ok();
}
None => {
KEY_VALUE_STORE
.delete_kvp(DATABASE_KEY_NAME.to_string())
.await
.ok();
}
})
.detach();
}
fn valid_email_address(&self) -> bool {
!self.in_invalid_state(InvalidStateFlags::EmailAddress)
}
fn valid_character_count(&self) -> bool {
!self.in_invalid_state(InvalidStateFlags::CharacterCount)
}
fn in_invalid_state(&self, flag: InvalidStateFlags) -> bool {
match self.submission_state {
Some(SubmissionState::CannotSubmit {
reason: CannotSubmitReason::InvalidState { ref flags },
}) => flags.contains(flag),
_ => false,
}
}
fn awaiting_submission(&self) -> bool {
matches!(
self.submission_state,
Some(SubmissionState::CannotSubmit {
reason: CannotSubmitReason::AwaitingSubmission
})
)
}
fn can_submit(&self) -> bool {
matches!(self.submission_state, Some(SubmissionState::CanSubmit))
}
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent)
}
@@ -39,75 +423,118 @@ impl FeedbackModal {
impl Render for FeedbackModal {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
self.update_submission_state(cx);
let submit_button_text = if self.awaiting_submission() {
"Submitting..."
} else {
"Submit"
};
let open_zed_repo =
cx.listener(|_, _, window, cx| window.dispatch_action(Box::new(OpenZedRepo), cx));
v_flex()
.elevation_3(cx)
.key_context("GiveFeedback")
.on_action(cx.listener(Self::cancel))
.elevation_3(cx)
.w_96()
.h_auto()
.min_w(rems(40.))
.max_w(rems(96.))
.h(rems(32.))
.p_4()
.gap_2()
.child(Headline::new("Give Feedback"))
.child(
h_flex()
.w_full()
.justify_between()
.child(Headline::new("Give Feedback"))
Label::new(if self.character_count < *FEEDBACK_CHAR_LIMIT.start() {
format!(
"Feedback must be at least {} characters.",
FEEDBACK_CHAR_LIMIT.start()
)
} else {
format!(
"Characters: {}",
*FEEDBACK_CHAR_LIMIT.end() - self.character_count
)
})
.color(if self.valid_character_count() {
Color::Success
} else {
Color::Error
}),
)
.child(
div()
.flex_1()
.bg(cx.theme().colors().editor_background)
.p_2()
.border_1()
.rounded_sm()
.border_color(cx.theme().colors().border)
.child(self.feedback_editor.clone()),
)
.child(
v_flex()
.gap_1()
.child(
IconButton::new("close-btn", IconName::Close)
.icon_color(Color::Muted)
.on_click(cx.listener(move |_, _, window, cx| {
cx.spawn_in(window, async move |this, cx| {
this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
})
.detach();
})),
h_flex()
.bg(cx.theme().colors().editor_background)
.p_2()
.border_1()
.rounded_sm()
.border_color(if self.valid_email_address() {
cx.theme().colors().border
} else {
cx.theme().status().error_border
})
.child(self.email_address_editor.clone()),
)
.child(
Label::new("Provide an email address if you want us to be able to reply.")
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.child(Label::new("Thanks for using Zed! To share your experience with us, reach for the channel that's the most appropriate:"))
.child(
Button::new("file-a-bug-report", "File a Bug Report")
.full_width()
.icon(IconName::Debug)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(Box::new(FileBugReport), cx);
})),
)
.child(
Button::new("request-a-feature", "Request a Feature")
.full_width()
.icon(IconName::Sparkle)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(Box::new(RequestFeature), cx);
})),
)
.child(
Button::new("send-us_an-email", "Send an Email")
.full_width()
.icon(IconName::Envelope)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(Box::new(EmailZed), cx);
})),
)
.child(
Button::new("zed_repository", "GitHub Repository")
.full_width()
.icon(IconName::Github)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(open_zed_repo),
h_flex()
.justify_between()
.gap_1()
.child(
Button::new("zed_repository", "Zed Repository")
.style(ButtonStyle::Transparent)
.icon(IconName::ExternalLink)
.icon_position(IconPosition::End)
.icon_size(IconSize::Small)
.on_click(open_zed_repo),
)
.child(
h_flex()
.gap_1()
.child(
Button::new("cancel_feedback", "Cancel")
.style(ButtonStyle::Subtle)
.color(Color::Muted)
.on_click(cx.listener(move |_, _, window, cx| {
cx.spawn_in(window, async move |this, cx| {
this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
})
.detach();
})),
)
.child(
Button::new("submit_feedback", submit_button_text)
.color(Color::Accent)
.style(ButtonStyle::Filled)
.on_click(cx.listener(|this, _, window, cx| {
this.submit(window, cx).detach();
}))
.tooltip(move |_, cx| {
Tooltip::simple("Submit feedback to the Zed team.", cx)
})
.when(!self.can_submit(), |this| this.disabled(true)),
),
),
)
}
}
// TODO: Testing of various button states, dismissal prompts, etc. :)

View File

@@ -5,8 +5,8 @@ use futures::future::{self, BoxFuture};
use git::{
blame::Blame,
repository::{
AskPassSession, Branch, CommitDetails, GitRepository, GitRepositoryCheckpoint, PushOptions,
Remote, RepoPath, ResetMode,
AskPassSession, Branch, CommitDetails, GitRepository, PushOptions, Remote, RepoPath,
ResetMode,
},
status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus},
};
@@ -409,15 +409,11 @@ impl GitRepository for FakeGitRepository {
unimplemented!()
}
fn checkpoint(&self, _cx: AsyncApp) -> BoxFuture<Result<GitRepositoryCheckpoint>> {
fn checkpoint(&self, _cx: AsyncApp) -> BoxFuture<Result<git::Oid>> {
unimplemented!()
}
fn restore_checkpoint(
&self,
_checkpoint: GitRepositoryCheckpoint,
_cx: AsyncApp,
) -> BoxFuture<Result<()>> {
fn restore_checkpoint(&self, _oid: git::Oid, _cx: AsyncApp) -> BoxFuture<Result<()>> {
unimplemented!()
}
}

View File

@@ -290,14 +290,10 @@ pub trait GitRepository: Send + Sync {
fn diff(&self, diff: DiffType, cx: AsyncApp) -> BoxFuture<Result<String>>;
/// Creates a checkpoint for the repository.
fn checkpoint(&self, cx: AsyncApp) -> BoxFuture<Result<GitRepositoryCheckpoint>>;
fn checkpoint(&self, cx: AsyncApp) -> BoxFuture<Result<Oid>>;
/// Resets to a previously-created checkpoint.
fn restore_checkpoint(
&self,
checkpoint: GitRepositoryCheckpoint,
cx: AsyncApp,
) -> BoxFuture<Result<()>>;
fn restore_checkpoint(&self, oid: Oid, cx: AsyncApp) -> BoxFuture<Result<()>>;
}
pub enum DiffType {
@@ -341,12 +337,6 @@ impl RealGitRepository {
}
}
#[derive(Copy, Clone)]
pub struct GitRepositoryCheckpoint {
head_sha: Option<Oid>,
sha: Oid,
}
// https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
const GIT_MODE_SYMLINK: u32 = 0o120000;
@@ -1043,7 +1033,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn checkpoint(&self, cx: AsyncApp) -> BoxFuture<Result<GitRepositoryCheckpoint>> {
fn checkpoint(&self, cx: AsyncApp) -> BoxFuture<Result<Oid>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
let executor = cx.background_executor().clone();
@@ -1066,7 +1056,10 @@ impl GitRepository for RealGitRepository {
let output = new_smol_command(&git_binary_path)
.current_dir(&working_directory)
.env("GIT_INDEX_FILE", &index_file_path)
.envs(checkpoint_author_envs())
.env("GIT_AUTHOR_NAME", "Zed")
.env("GIT_AUTHOR_EMAIL", "hi@zed.dev")
.env("GIT_COMMITTER_NAME", "Zed")
.env("GIT_COMMITTER_EMAIL", "hi@zed.dev")
.args(args)
.output()
.await?;
@@ -1078,56 +1071,35 @@ impl GitRepository for RealGitRepository {
}
};
let head_sha = run_git_command(&["rev-parse", "HEAD"]).await.ok();
run_git_command(&["add", "--all"]).await?;
let tree = run_git_command(&["write-tree"]).await?;
let checkpoint_sha = if let Some(head_sha) = head_sha.as_deref() {
run_git_command(&["commit-tree", &tree, "-p", head_sha, "-m", "Checkpoint"]).await?
} else {
run_git_command(&["commit-tree", &tree, "-m", "Checkpoint"]).await?
};
let commit_sha = run_git_command(&["commit-tree", &tree, "-m", "Checkpoint"]).await?;
let ref_name = Uuid::new_v4().to_string();
run_git_command(&[
"update-ref",
&format!("refs/zed/{ref_name}"),
&checkpoint_sha,
])
.await?;
run_git_command(&["update-ref", &format!("refs/heads/{ref_name}"), &commit_sha])
.await?;
smol::fs::remove_file(index_file_path).await.ok();
delete_temp_index.abort();
Ok(GitRepositoryCheckpoint {
head_sha: if let Some(head_sha) = head_sha {
Some(head_sha.parse()?)
} else {
None
},
sha: checkpoint_sha.parse()?,
})
commit_sha.parse()
})
.boxed()
}
fn restore_checkpoint(
&self,
checkpoint: GitRepositoryCheckpoint,
cx: AsyncApp,
) -> BoxFuture<Result<()>> {
fn restore_checkpoint(&self, oid: Oid, cx: AsyncApp) -> BoxFuture<Result<()>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
cx.background_spawn(async move {
let working_directory = working_directory?;
let index_file_path = working_directory.join(".git/index.tmp");
let run_git_command = async |args: &[&str], use_temp_index: bool| {
let mut command = new_smol_command(&git_binary_path);
command.current_dir(&working_directory);
command.args(args);
if use_temp_index {
command.env("GIT_INDEX_FILE", &index_file_path);
}
let output = command.output().await?;
let run_git_command = async |args: &[&str]| {
let output = new_smol_command(&git_binary_path)
.current_dir(&working_directory)
.env("GIT_INDEX_FILE", &index_file_path)
.args(args)
.output()
.await?;
if output.status.success() {
anyhow::Ok(String::from_utf8(output.stdout)?.trim_end().to_string())
} else {
@@ -1136,26 +1108,9 @@ impl GitRepository for RealGitRepository {
}
};
run_git_command(
&[
"restore",
"--source",
&checkpoint.sha.to_string(),
"--worktree",
".",
],
false,
)
.await?;
run_git_command(&["read-tree", &checkpoint.sha.to_string()], true).await?;
run_git_command(&["clean", "-d", "--force"], true).await?;
if let Some(head_sha) = checkpoint.head_sha {
run_git_command(&["reset", "--mixed", &head_sha.to_string()], false).await?;
} else {
run_git_command(&["update-ref", "-d", "HEAD"], false).await?;
}
run_git_command(&["restore", "--source", &oid.to_string(), "--worktree", "."]).await?;
run_git_command(&["read-tree", &oid.to_string()]).await?;
run_git_command(&["clean", "-d", "--force"]).await?;
Ok(())
})
.boxed()
@@ -1395,111 +1350,14 @@ fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
}
}
fn checkpoint_author_envs() -> HashMap<String, String> {
HashMap::from_iter([
("GIT_AUTHOR_NAME".to_string(), "Zed".to_string()),
("GIT_AUTHOR_EMAIL".to_string(), "hi@zed.dev".to_string()),
("GIT_COMMITTER_NAME".to_string(), "Zed".to_string()),
("GIT_COMMITTER_EMAIL".to_string(), "hi@zed.dev".to_string()),
])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::status::FileStatus;
use gpui::TestAppContext;
#[gpui::test]
async fn test_checkpoint_basic(cx: &mut TestAppContext) {
cx.executor().allow_parking();
let repo_dir = tempfile::tempdir().unwrap();
git2::Repository::init(repo_dir.path()).unwrap();
let file_path = repo_dir.path().join("file");
smol::fs::write(&file_path, "initial").await.unwrap();
let repo = RealGitRepository::new(&repo_dir.path().join(".git"), None).unwrap();
repo.stage_paths(
vec![RepoPath::from_str("file")],
HashMap::default(),
cx.to_async(),
)
.await
.unwrap();
repo.commit(
"Initial commit".into(),
None,
checkpoint_author_envs(),
cx.to_async(),
)
.await
.unwrap();
smol::fs::write(&file_path, "modified before checkpoint")
.await
.unwrap();
smol::fs::write(repo_dir.path().join("new_file_before_checkpoint"), "1")
.await
.unwrap();
let sha_before_checkpoint = repo.head_sha().unwrap();
let checkpoint = repo.checkpoint(cx.to_async()).await.unwrap();
// Ensure the user can't see any branches after creating a checkpoint.
assert_eq!(repo.branches().await.unwrap().len(), 1);
smol::fs::write(&file_path, "modified after checkpoint")
.await
.unwrap();
repo.stage_paths(
vec![RepoPath::from_str("file")],
HashMap::default(),
cx.to_async(),
)
.await
.unwrap();
repo.commit(
"Commit after checkpoint".into(),
None,
checkpoint_author_envs(),
cx.to_async(),
)
.await
.unwrap();
smol::fs::remove_file(repo_dir.path().join("new_file_before_checkpoint"))
.await
.unwrap();
smol::fs::write(repo_dir.path().join("new_file_after_checkpoint"), "2")
.await
.unwrap();
repo.restore_checkpoint(checkpoint, cx.to_async())
.await
.unwrap();
assert_eq!(repo.head_sha().unwrap(), sha_before_checkpoint);
assert_eq!(
smol::fs::read_to_string(&file_path).await.unwrap(),
"modified before checkpoint"
);
assert_eq!(
smol::fs::read_to_string(repo_dir.path().join("new_file_before_checkpoint"))
.await
.unwrap(),
"1"
);
assert_eq!(
smol::fs::read_to_string(repo_dir.path().join("new_file_after_checkpoint"))
.await
.ok(),
None
);
}
use super::*;
#[gpui::test]
async fn test_checkpoint_empty_repo(cx: &mut TestAppContext) {
async fn test_checkpoint(cx: &mut TestAppContext) {
cx.executor().allow_parking();
let repo_dir = tempfile::tempdir().unwrap();
@@ -1511,9 +1369,6 @@ mod tests {
.unwrap();
let checkpoint_sha = repo.checkpoint(cx.to_async()).await.unwrap();
// Ensure the user can't see any branches after creating a checkpoint.
assert_eq!(repo.branches().await.unwrap().len(), 1);
smol::fs::write(repo_dir.path().join("foo"), "bar")
.await
.unwrap();
@@ -1537,88 +1392,6 @@ mod tests {
);
}
#[gpui::test]
async fn test_undoing_commit_via_checkpoint(cx: &mut TestAppContext) {
cx.executor().allow_parking();
let repo_dir = tempfile::tempdir().unwrap();
git2::Repository::init(repo_dir.path()).unwrap();
let file_path = repo_dir.path().join("file");
smol::fs::write(&file_path, "initial").await.unwrap();
let repo = RealGitRepository::new(&repo_dir.path().join(".git"), None).unwrap();
repo.stage_paths(
vec![RepoPath::from_str("file")],
HashMap::default(),
cx.to_async(),
)
.await
.unwrap();
repo.commit(
"Initial commit".into(),
None,
checkpoint_author_envs(),
cx.to_async(),
)
.await
.unwrap();
let initial_commit_sha = repo.head_sha().unwrap();
smol::fs::write(repo_dir.path().join("new_file1"), "content1")
.await
.unwrap();
smol::fs::write(repo_dir.path().join("new_file2"), "content2")
.await
.unwrap();
let checkpoint = repo.checkpoint(cx.to_async()).await.unwrap();
repo.stage_paths(
vec![
RepoPath::from_str("new_file1"),
RepoPath::from_str("new_file2"),
],
HashMap::default(),
cx.to_async(),
)
.await
.unwrap();
repo.commit(
"Commit new files".into(),
None,
checkpoint_author_envs(),
cx.to_async(),
)
.await
.unwrap();
repo.restore_checkpoint(checkpoint, cx.to_async())
.await
.unwrap();
assert_eq!(repo.head_sha().unwrap(), initial_commit_sha);
assert_eq!(
smol::fs::read_to_string(repo_dir.path().join("new_file1"))
.await
.unwrap(),
"content1"
);
assert_eq!(
smol::fs::read_to_string(repo_dir.path().join("new_file2"))
.await
.unwrap(),
"content2"
);
assert_eq!(
repo.status(&[]).unwrap().entries.as_ref(),
&[
(RepoPath::from_str("new_file1"), FileStatus::Untracked),
(RepoPath::from_str("new_file2"), FileStatus::Untracked)
]
);
}
#[test]
fn test_branches_parsing() {
// suppress "help: octal escapes are not supported, `\0` is always null"

View File

@@ -8,7 +8,7 @@ use gpui::{
SharedString, Styled, Subscription, Task, Window,
};
use picker::{Picker, PickerDelegate, PickerEditorPosition};
use project::git_store::Repository;
use project::git::Repository;
use std::sync::Arc;
use time::OffsetDateTime;
use time_format::format_local_timestamp;

View File

@@ -46,7 +46,7 @@ use panel::{
panel_icon_button, PanelHeader,
};
use project::{
git_store::{GitEvent, Repository},
git::{GitEvent, Repository},
Fs, Project, ProjectPath,
};
use serde::{Deserialize, Serialize};

View File

@@ -23,7 +23,7 @@ use gpui::{
use language::{Anchor, Buffer, Capability, OffsetRangeExt};
use multi_buffer::{MultiBuffer, PathKey};
use project::{
git_store::{GitEvent, GitStore},
git::{GitEvent, GitStore},
Project, ProjectPath,
};
use std::any::{Any, TypeId};

View File

@@ -4,7 +4,7 @@ use gpui::{
use itertools::Itertools;
use picker::{Picker, PickerDelegate};
use project::{
git_store::{GitStore, Repository},
git::{GitStore, Repository},
Project,
};
use std::sync::Arc;

View File

@@ -12,7 +12,7 @@ license = "Apache-2.0"
workspace = true
[features]
default = ["macos-blade", "http_client", "font-kit", "wayland", "x11"]
default = ["http_client", "font-kit", "wayland", "x11"]
test-support = [
"leak-detection",
"collections/test-support",
@@ -123,11 +123,10 @@ lyon = "1.0"
block = "0.1"
cocoa.workspace = true
core-foundation.workspace = true
core-foundation-sys.workspace = true
core-graphics = "0.24"
core-video.workspace = true
core-text = "21"
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5474cfad4b719a72ec8ed2cb7327b2b01fd10568", optional = true }
core-foundation-sys = "0.8"
core-graphics = "0.23"
core-text = "20.1"
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "40391b7", optional = true }
foreign-types = "0.5"
log.workspace = true
media.workspace = true
@@ -155,10 +154,9 @@ blade-macros = { workspace = true, optional = true }
blade-util = { workspace = true, optional = true }
bytemuck = { version = "1", optional = true }
cosmic-text = { git = "https://github.com/pop-os/cosmic-text", rev = "542b20c", optional = true }
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5474cfad4b719a72ec8ed2cb7327b2b01fd10568", features = [
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "40391b7", features = [
"source-fontconfig-dlopen",
], optional = true }
calloop = { version = "0.13.0" }
filedescriptor = { version = "0.8.2", optional = true }
open = { version = "5.2.0", optional = true }

View File

@@ -1590,10 +1590,10 @@ impl App {
.insert(entity_id, window_invalidators);
}
/// Returns the name for this [`App`].
/// Get the name for this App.
#[cfg(any(test, feature = "test-support", debug_assertions))]
pub fn get_name(&self) -> Option<&'static str> {
self.name
pub fn get_name(&self) -> &'static str {
self.name.as_ref().unwrap()
}
/// Returns `true` if the platform file picker supports selecting a mix of files and directories.

View File

@@ -3,7 +3,7 @@ use crate::{
Style, StyleRefinement, Styled, Window,
};
#[cfg(target_os = "macos")]
use core_video::pixel_buffer::CVPixelBuffer;
use media::core_video::CVImageBuffer;
use refineable::Refineable;
/// A source of a surface's content.
@@ -11,12 +11,12 @@ use refineable::Refineable;
pub enum SurfaceSource {
/// A macOS image buffer from CoreVideo
#[cfg(target_os = "macos")]
Surface(CVPixelBuffer),
Surface(CVImageBuffer),
}
#[cfg(target_os = "macos")]
impl From<CVPixelBuffer> for SurfaceSource {
fn from(value: CVPixelBuffer) -> Self {
impl From<CVImageBuffer> for SurfaceSource {
fn from(value: CVImageBuffer) -> Self {
SurfaceSource::Surface(value)
}
}
@@ -87,7 +87,7 @@ impl Element for Surface {
match &self.source {
#[cfg(target_os = "macos")]
SurfaceSource::Surface(surface) => {
let size = crate::size(surface.get_width().into(), surface.get_height().into());
let size = crate::size(surface.width().into(), surface.height().into());
let new_bounds = self.object_fit.get_bounds(bounds, size);
// TODO: Add support for corner_radii
window.paint_surface(new_bounds, surface.clone());

View File

@@ -725,8 +725,8 @@ impl BladeRenderer {
use std::ptr;
assert_eq!(
surface.image_buffer.get_pixel_format(),
core_video::pixel_buffer::kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
surface.image_buffer.pixel_format_type(),
media::core_video::kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
);
let y_texture = self
@@ -735,8 +735,8 @@ impl BladeRenderer {
surface.image_buffer.as_concrete_TypeRef(),
ptr::null(),
metal::MTLPixelFormat::R8Unorm,
surface.image_buffer.get_width_of_plane(0),
surface.image_buffer.get_height_of_plane(0),
surface.image_buffer.plane_width(0),
surface.image_buffer.plane_height(0),
0,
)
.unwrap();
@@ -746,8 +746,8 @@ impl BladeRenderer {
surface.image_buffer.as_concrete_TypeRef(),
ptr::null(),
metal::MTLPixelFormat::RG8Unorm,
surface.image_buffer.get_width_of_plane(1),
surface.image_buffer.get_height_of_plane(1),
surface.image_buffer.plane_width(1),
surface.image_buffer.plane_height(1),
1,
)
.unwrap();

View File

@@ -11,7 +11,7 @@ mod metal_atlas;
#[cfg(not(feature = "macos-blade"))]
pub mod metal_renderer;
use core_video::image_buffer::CVImageBuffer;
use media::core_video::CVImageBuffer;
#[cfg(not(feature = "macos-blade"))]
use metal_renderer as renderer;

View File

@@ -13,11 +13,8 @@ use cocoa::{
};
use collections::HashMap;
use core_foundation::base::TCFType;
use core_video::{
metal_texture::CVMetalTextureGetTexture, metal_texture_cache::CVMetalTextureCache,
pixel_buffer::kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
};
use foreign_types::{ForeignType, ForeignTypeRef};
use foreign_types::ForeignType;
use media::core_video::CVMetalTextureCache;
use metal::{CAMetalLayer, CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange};
use objc::{self, msg_send, sel, sel_impl};
use parking_lot::Mutex;
@@ -110,7 +107,7 @@ pub(crate) struct MetalRenderer {
#[allow(clippy::arc_with_non_send_sync)]
instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>,
sprite_atlas: Arc<MetalAtlas>,
core_video_texture_cache: core_video::metal_texture_cache::CVMetalTextureCache,
core_video_texture_cache: CVMetalTextureCache,
}
impl MetalRenderer {
@@ -238,7 +235,7 @@ impl MetalRenderer {
let command_queue = device.new_command_queue();
let sprite_atlas = Arc::new(MetalAtlas::new(device.clone(), PATH_SAMPLE_COUNT));
let core_video_texture_cache =
CVMetalTextureCache::new(None, device.clone(), None).unwrap();
unsafe { CVMetalTextureCache::new(device.as_ptr()).unwrap() };
Self {
device,
@@ -1057,37 +1054,39 @@ impl MetalRenderer {
for surface in surfaces {
let texture_size = size(
DevicePixels::from(surface.image_buffer.get_width() as i32),
DevicePixels::from(surface.image_buffer.get_height() as i32),
DevicePixels::from(surface.image_buffer.width() as i32),
DevicePixels::from(surface.image_buffer.height() as i32),
);
assert_eq!(
surface.image_buffer.get_pixel_format(),
kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
surface.image_buffer.pixel_format_type(),
media::core_video::kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
);
let y_texture = self
.core_video_texture_cache
.create_texture_from_image(
surface.image_buffer.as_concrete_TypeRef(),
None,
MTLPixelFormat::R8Unorm,
surface.image_buffer.get_width_of_plane(0),
surface.image_buffer.get_height_of_plane(0),
0,
)
.unwrap();
let cb_cr_texture = self
.core_video_texture_cache
.create_texture_from_image(
surface.image_buffer.as_concrete_TypeRef(),
None,
MTLPixelFormat::RG8Unorm,
surface.image_buffer.get_width_of_plane(1),
surface.image_buffer.get_height_of_plane(1),
1,
)
.unwrap();
let y_texture = unsafe {
self.core_video_texture_cache
.create_texture_from_image(
surface.image_buffer.as_concrete_TypeRef(),
ptr::null(),
MTLPixelFormat::R8Unorm,
surface.image_buffer.plane_width(0),
surface.image_buffer.plane_height(0),
0,
)
.unwrap()
};
let cb_cr_texture = unsafe {
self.core_video_texture_cache
.create_texture_from_image(
surface.image_buffer.as_concrete_TypeRef(),
ptr::null(),
MTLPixelFormat::RG8Unorm,
surface.image_buffer.plane_width(1),
surface.image_buffer.plane_height(1),
1,
)
.unwrap()
};
align_offset(instance_offset);
let next_offset = *instance_offset + mem::size_of::<Surface>();
@@ -1105,15 +1104,14 @@ impl MetalRenderer {
mem::size_of_val(&texture_size) as u64,
&texture_size as *const Size<DevicePixels> as *const _,
);
// let y_texture = y_texture.get_texture().unwrap().
command_encoder.set_fragment_texture(SurfaceInputIndex::YTexture as u64, unsafe {
let texture = CVMetalTextureGetTexture(y_texture.as_concrete_TypeRef());
Some(metal::TextureRef::from_ptr(texture as *mut _))
});
command_encoder.set_fragment_texture(SurfaceInputIndex::CbCrTexture as u64, unsafe {
let texture = CVMetalTextureGetTexture(cb_cr_texture.as_concrete_TypeRef());
Some(metal::TextureRef::from_ptr(texture as *mut _))
});
command_encoder.set_fragment_texture(
SurfaceInputIndex::YTexture as u64,
Some(y_texture.as_texture_ref()),
);
command_encoder.set_fragment_texture(
SurfaceInputIndex::CbCrTexture as u64,
Some(cb_cr_texture.as_texture_ref()),
);
unsafe {
let buffer_contents = (instance_buffer.metal_buffer.contents() as *mut u8)

View File

@@ -9,10 +9,6 @@ use cocoa::{
foundation::NSArray,
};
use core_foundation::base::TCFType;
use core_graphics::display::{
CGDirectDisplayID, CGDisplayCopyDisplayMode, CGDisplayModeGetPixelHeight,
CGDisplayModeGetPixelWidth, CGDisplayModeRelease,
};
use ctor::ctor;
use futures::channel::oneshot;
use media::core_media::{CMSampleBuffer, CMSampleBufferRef};
@@ -49,12 +45,8 @@ const SCStreamOutputTypeScreen: NSInteger = 0;
impl ScreenCaptureSource for MacScreenCaptureSource {
fn resolution(&self) -> Result<Size<Pixels>> {
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);
let width: i64 = msg_send![self.sc_display, width];
let height: i64 = msg_send![self.sc_display, height];
Ok(size(px(width as f32), px(height as f32)))
}
}
@@ -73,10 +65,6 @@ impl ScreenCaptureSource for MacScreenCaptureSource {
let excluded_windows = NSArray::array(nil);
let filter: id = msg_send![filter, initWithDisplay:self.sc_display excludingWindows:excluded_windows];
let configuration: id = msg_send![configuration, init];
let _: id = msg_send![configuration, setScalesToFit: true];
let _: id = msg_send![configuration, setPixelFormat: 0x42475241];
// let _: id = msg_send![configuration, setShowsCursor: false];
// let _: id = msg_send![configuration, setCaptureResolution: 3];
let delegate: id = msg_send![delegate, init];
let output: id = msg_send![output, init];
@@ -85,9 +73,6 @@ 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 stream: id = msg_send![stream, initWithFilter:filter configuration:configuration delegate:delegate];
let (mut tx, rx) = oneshot::channel();

View File

@@ -651,7 +651,7 @@ pub(crate) struct PaintSurface {
pub bounds: Bounds<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
#[cfg(target_os = "macos")]
pub image_buffer: core_video::pixel_buffer::CVPixelBuffer,
pub image_buffer: media::core_video::CVImageBuffer,
}
impl From<PaintSurface> for Primitive {

View File

@@ -17,11 +17,11 @@ use crate::{
};
use anyhow::{anyhow, Context as _, Result};
use collections::{FxHashMap, FxHashSet};
#[cfg(target_os = "macos")]
use core_video::pixel_buffer::CVPixelBuffer;
use derive_more::{Deref, DerefMut};
use futures::channel::oneshot;
use futures::FutureExt;
#[cfg(target_os = "macos")]
use media::core_video::CVImageBuffer;
use parking_lot::RwLock;
use raw_window_handle::{HandleError, HasWindowHandle};
use refineable::Refineable;
@@ -2654,7 +2654,7 @@ impl Window {
///
/// This method should only be called as part of the paint phase of element drawing.
#[cfg(target_os = "macos")]
pub fn paint_surface(&mut self, bounds: Bounds<Pixels>, image_buffer: CVPixelBuffer) {
pub fn paint_surface(&mut self, bounds: Bounds<Pixels>, image_buffer: CVImageBuffer) {
use crate::PaintSurface;
self.invalidator.debug_assert_paint();

View File

@@ -32,7 +32,7 @@ pub struct Tokio {}
impl Tokio {
/// Spawns the given future on Tokio's thread pool, and returns it via a GPUI task
/// Note that the Tokio task will be cancelled if the GPUI task is dropped
pub fn spawn<C, Fut, R>(cx: &C, f: Fut) -> C::Result<Task<Result<R, JoinError>>>
pub fn spawn<C, Fut, R>(cx: &mut C, f: Fut) -> C::Result<Task<Result<R, JoinError>>>
where
C: AppContext,
Fut: Future<Output = R> + Send + 'static,
@@ -52,7 +52,7 @@ impl Tokio {
})
}
pub fn handle(cx: &App) -> tokio::runtime::Handle {
pub fn handle(cx: &mut App) -> tokio::runtime::Handle {
GlobalTokio::global(cx).runtime.handle().clone()
}
}

View File

@@ -4727,27 +4727,23 @@ impl CharClassifier {
}
pub fn kind_with(&self, c: char, ignore_punctuation: bool) -> CharKind {
if c.is_alphanumeric() || c == '_' {
if c.is_whitespace() {
return CharKind::Whitespace;
} else if c.is_alphanumeric() || c == '_' {
return CharKind::Word;
}
if let Some(scope) = &self.scope {
let characters = if self.for_completion {
scope.completion_query_characters()
} else {
scope.word_characters()
};
if let Some(characters) = characters {
if let Some(characters) = scope.word_characters() {
if characters.contains(&c) {
if c == '-' && !self.for_completion && !ignore_punctuation {
return CharKind::Punctuation;
}
return CharKind::Word;
}
}
}
if c.is_whitespace() {
return CharKind::Whitespace;
}
if ignore_punctuation {
CharKind::Word
} else {

View File

@@ -700,9 +700,6 @@ pub struct LanguageConfig {
/// If configured, this language contains JSX style tags, and should support auto-closing of those tags.
#[serde(default)]
pub jsx_tag_auto_close: Option<JsxTagAutoCloseConfig>,
/// A list of characters that Zed should treat as word characters for completion queries.
#[serde(default)]
pub completion_query_characters: HashSet<char>,
}
#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
@@ -768,8 +765,6 @@ pub struct LanguageConfigOverride {
#[serde(default)]
pub word_characters: Override<HashSet<char>>,
#[serde(default)]
pub completion_query_characters: Override<HashSet<char>>,
#[serde(default)]
pub opt_into_language_servers: Vec<LanguageServerName>,
}
@@ -821,7 +816,6 @@ impl Default for LanguageConfig {
prettier_parser_name: None,
hidden: false,
jsx_tag_auto_close: None,
completion_query_characters: Default::default(),
}
}
}
@@ -1711,16 +1705,6 @@ impl LanguageScope {
)
}
/// Returns a list of language-specific characters that are considered part of
/// a completion query.
pub fn completion_query_characters(&self) -> Option<&HashSet<char>> {
Override::as_option(
self.config_override()
.map(|o| &o.completion_query_characters),
Some(&self.language.config.completion_query_characters),
)
}
/// Returns a list of bracket pairs for a given language with an additional
/// piece of information about whether the particular bracket pair is currently active for a given language.
pub fn brackets(&self) -> impl Iterator<Item = (&BracketPair, bool)> {

View File

@@ -56,20 +56,15 @@ impl LanguageModelSelector {
.max_height(Some(rems(20.).into()))
});
let subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
LanguageModelSelector {
picker,
update_matches_task: None,
_authenticate_all_providers_task: Self::authenticate_all_providers(cx),
_subscriptions: vec![
cx.subscribe_in(
&LanguageModelRegistry::global(cx),
window,
Self::handle_language_model_registry_event,
),
subscription,
],
_subscriptions: vec![cx.subscribe_in(
&LanguageModelRegistry::global(cx),
window,
Self::handle_language_model_registry_event,
)],
}
}

View File

@@ -9,6 +9,6 @@ brackets = [
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] },
{ start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
]
completion_query_characters = ["-"]
word_characters = ["-"]
block_comment = ["/* ", " */"]
prettier_parser_name = "css"

View File

@@ -32,5 +32,5 @@ block_comment = ["{/* ", " */}"]
opt_into_language_servers = ["emmet-language-server"]
[overrides.string]
completion_query_characters = ["-"]
word_characters = ["-"]
opt_into_language_servers = ["tailwindcss-language-server"]

View File

@@ -12,4 +12,4 @@ tab_size = 2
prettier_parser_name = "json"
[overrides.string]
completion_query_characters = [":", " "]
word_characters = [":"]

Some files were not shown because too many files have changed in this diff Show More