diff --git a/.config/hakari.toml b/.config/hakari.toml index 8ce0b77490..cbfb3cc1dc 100644 --- a/.config/hakari.toml +++ b/.config/hakari.toml @@ -26,7 +26,7 @@ third-party = [ # build of remote_server should not include scap / its x11 dependency { name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" }, # build of remote_server should not need to include on libalsa through rodio - { name = "rodio" }, + # { name = "rodio" }, ] [final-excludes] diff --git a/.github/ISSUE_TEMPLATE/10_bug_report.yml b/.github/ISSUE_TEMPLATE/10_bug_report.yml index 1bf6c80e40..e132eca1e5 100644 --- a/.github/ISSUE_TEMPLATE/10_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/10_bug_report.yml @@ -14,7 +14,7 @@ body: ### Description diff --git a/Cargo.lock b/Cargo.lock index 42649b137f..a06f51b937 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,26 +39,6 @@ dependencies = [ "workspace-hack", ] -[[package]] -name = "acp_tools" -version = "0.1.0" -dependencies = [ - "agent-client-protocol", - "collections", - "gpui", - "language", - "markdown", - "project", - "serde", - "serde_json", - "settings", - "theme", - "ui", - "util", - "workspace", - "workspace-hack", -] - [[package]] name = "action_log" version = "0.1.0" @@ -191,12 +171,11 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.31" +version = "0.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "289eb34ee17213dadcca47eedadd386a5e7678094095414e475965d1bcca2860" +checksum = "5f792e009ba59b137ee1db560bc37e567887ad4b5af6f32181d381fff690e2d4" dependencies = [ "anyhow", - "async-broadcast", "futures 0.3.31", "log", "parking_lot", @@ -285,10 +264,10 @@ name = "agent_servers" version = "0.1.0" dependencies = [ "acp_thread", - "acp_tools", "action_log", "agent-client-protocol", "agent_settings", + "agentic-coding-protocol", "anyhow", "client", "collections", @@ -403,7 +382,6 @@ dependencies = [ "parking_lot", "paths", "picker", - "postage", "pretty_assertions", "project", "prompt_store", @@ -443,6 +421,24 @@ dependencies = [ "zed_actions", ] +[[package]] +name = "agentic-coding-protocol" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e6ae951b36fa2f8d9dd6e1af6da2fcaba13d7c866cf6a9e65deda9dc6c5fe4" +dependencies = [ + "anyhow", + "chrono", + "derive_more 2.0.1", + "futures 0.3.31", + "log", + "parking_lot", + "schemars", + "semver", + "serde", + "serde_json", +] + [[package]] name = "ahash" version = "0.7.8" @@ -858,7 +854,7 @@ dependencies = [ "anyhow", "async-trait", "collections", - "derive_more", + "derive_more 0.99.19", "extension", "futures 0.3.31", "gpui", @@ -921,7 +917,7 @@ dependencies = [ "clock", "collections", "ctor", - "derive_more", + "derive_more 0.99.19", "gpui", "icons", "indoc", @@ -958,7 +954,7 @@ dependencies = [ "cloud_llm_client", "collections", "component", - "derive_more", + "derive_more 0.99.19", "diffy", "editor", "feature_flags", @@ -1385,6 +1381,8 @@ dependencies = [ "anyhow", "collections", "gpui", + "libwebrtc", + "parking_lot", "rodio", "schemars", "serde", @@ -2685,7 +2683,7 @@ dependencies = [ "cap-primitives", "cap-std", "io-lifetimes", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2714,7 +2712,7 @@ dependencies = [ "maybe-owned", "rustix 1.0.7", "rustix-linux-procfs", - "windows-sys 0.59.0", + "windows-sys 0.52.0", "winx", ] @@ -3071,7 +3069,7 @@ dependencies = [ "cocoa 0.26.0", "collections", "credentials_provider", - "derive_more", + "derive_more 0.99.19", "feature_flags", "fs", "futures 0.3.31", @@ -3503,7 +3501,7 @@ name = "command_palette_hooks" version = "0.1.0" dependencies = [ "collections", - "derive_more", + "derive_more 0.99.19", "gpui", "workspace-hack", ] @@ -4666,6 +4664,27 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "unicode-xid", +] + [[package]] name = "derive_refineable" version = "0.1.0" @@ -4686,6 +4705,7 @@ dependencies = [ "component", "ctor", "editor", + "futures 0.3.31", "gpui", "indoc", "language", @@ -5309,7 +5329,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -5696,7 +5716,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", "rustix 1.0.7", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6074,7 +6094,7 @@ checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" dependencies = [ "io-lifetimes", "rustix 1.0.7", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6401,7 +6421,7 @@ dependencies = [ "askpass", "async-trait", "collections", - "derive_more", + "derive_more 0.99.19", "futures 0.3.31", "git2", "gpui", @@ -7431,7 +7451,7 @@ dependencies = [ "core-video", "cosmic-text", "ctor", - "derive_more", + "derive_more 0.99.19", "embed-resource", "env_logger 0.11.8", "etagere", @@ -7956,7 +7976,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bytes 1.10.1", - "derive_more", + "derive_more 0.99.19", "futures 0.3.31", "http 1.3.1", "http-body 1.0.1", @@ -8468,7 +8488,6 @@ dependencies = [ "theme", "ui", "util", - "util_macros", "workspace", "workspace-hack", "zed_actions", @@ -8534,7 +8553,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]] @@ -8607,7 +8626,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi 0.5.0", "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -8689,7 +8708,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -9389,7 +9408,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]] @@ -9611,6 +9630,7 @@ dependencies = [ "core-video", "coreaudio-rs 0.12.1", "cpal", + "dasp_sample", "futures 0.3.31", "gpui", "gpui_tokio", @@ -13044,7 +13064,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -13521,7 +13541,6 @@ dependencies = [ "smol", "sysinfo", "telemetry_events", - "thiserror 2.0.12", "toml 0.8.20", "unindent", "util", @@ -13857,14 +13876,15 @@ dependencies = [ [[package]] name = "rodio" version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e40ecf59e742e03336be6a3d53755e789fd05a059fa22dfa0ed624722319e183" +source = "git+https://github.com/RustAudio/rodio?branch=microphone#cad73716a363a5ba92fcb73ec37a4b98a7d7de5f" dependencies = [ "cpal", "dasp_sample", "hound", "num-rational", + "rtrb", "symphonia", + "thiserror 2.0.12", "tracing", ] @@ -13939,6 +13959,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rtrb" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8388ea1a9e0ea807e442e8263a699e7edcb320ecbcd21b4fa8ff859acce3ba" + [[package]] name = "rules_library" version = "0.1.0" @@ -14080,7 +14106,7 @@ dependencies = [ "itoa", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -14093,7 +14119,7 @@ dependencies = [ "errno 0.3.11", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -14215,7 +14241,7 @@ dependencies = [ "security-framework 3.2.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -14361,10 +14387,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984" dependencies = [ + "chrono", "dyn-clone", "indexmap", "ref-cast", "schemars_derive", + "semver", "serde", "serde_json", ] @@ -15565,7 +15593,7 @@ dependencies = [ "cfg-if", "libc", "psm", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -15936,12 +15964,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" dependencies = [ "lazy_static", + "symphonia-bundle-flac", + "symphonia-bundle-mp3", + "symphonia-codec-aac", "symphonia-codec-pcm", + "symphonia-codec-vorbis", "symphonia-core", + "symphonia-format-isomp4", + "symphonia-format-ogg", "symphonia-format-riff", "symphonia-metadata", ] +[[package]] +name = "symphonia-bundle-flac" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdbf25b545ad0d3ee3e891ea643ad115aff4ca92f6aec472086b957a58522f70" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", +] + [[package]] name = "symphonia-codec-pcm" version = "0.5.4" @@ -15952,6 +16021,17 @@ dependencies = [ "symphonia-core", ] +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", +] + [[package]] name = "symphonia-core" version = "0.5.4" @@ -15965,6 +16045,31 @@ dependencies = [ "log", ] +[[package]] +name = "symphonia-format-isomp4" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abfdf178d697e50ce1e5d9b982ba1b94c47218e03ec35022d9f0e071a16dc844" +dependencies = [ + "encoding_rs", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + [[package]] name = "symphonia-format-riff" version = "0.5.4" @@ -15989,6 +16094,16 @@ dependencies = [ "symphonia-core", ] +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + [[package]] name = "syn" version = "1.0.109" @@ -16136,7 +16251,7 @@ dependencies = [ "fd-lock", "io-lifetimes", "rustix 0.38.44", - "windows-sys 0.59.0", + "windows-sys 0.52.0", "winx", ] @@ -16318,7 +16433,7 @@ dependencies = [ "getrandom 0.3.2", "once_cell", "rustix 1.0.7", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -16448,7 +16563,7 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", - "derive_more", + "derive_more 0.99.19", "fs", "futures 0.3.31", "gpui", @@ -18880,7 +18995,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]] @@ -19539,7 +19654,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" dependencies = [ "bitflags 2.9.0", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -19789,6 +19904,7 @@ dependencies = [ "any_vec", "anyhow", "async-recursion", + "bincode", "call", "client", "clock", @@ -19807,7 +19923,6 @@ dependencies = [ "node_runtime", "parking_lot", "postage", - "pretty_assertions", "project", "remote", "schemars", @@ -19916,6 +20031,7 @@ dependencies = [ "libsqlite3-sys", "linux-raw-sys 0.4.15", "linux-raw-sys 0.9.4", + "livekit-runtime", "log", "lyon", "lyon_path", @@ -19957,12 +20073,14 @@ dependencies = [ "regex-automata 0.4.9", "regex-syntax 0.8.5", "ring", + "rodio", "rust_decimal", "rustc-hash 1.1.0", "rustix 0.38.44", "rustix 1.0.7", "rustls 0.23.26", "rustls-webpki 0.103.1", + "schemars", "scopeguard", "sea-orm", "sea-query-binder", @@ -20398,7 +20516,6 @@ dependencies = [ name = "zed" version = "0.202.0" dependencies = [ - "acp_tools", "activity_indicator", "agent", "agent_servers", diff --git a/Cargo.toml b/Cargo.toml index 6ec243a9b9..debd2cdaf1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,6 @@ [workspace] resolver = "2" members = [ - "crates/acp_tools", "crates/acp_thread", "crates/action_log", "crates/activity_indicator", @@ -228,7 +227,6 @@ edition = "2024" # Workspace member crates # -acp_tools = { path = "crates/acp_tools" } acp_thread = { path = "crates/acp_thread" } action_log = { path = "crates/action_log" } agent = { path = "crates/agent" } @@ -363,7 +361,7 @@ remote_server = { path = "crates/remote_server" } repl = { path = "crates/repl" } reqwest_client = { path = "crates/reqwest_client" } rich_text = { path = "crates/rich_text" } -rodio = { version = "0.21.1", default-features = false } +rodio = { git = "https://github.com/RustAudio/rodio", branch = "microphone"} rope = { path = "crates/rope" } rpc = { path = "crates/rpc" } rules_library = { path = "crates/rules_library" } @@ -426,7 +424,8 @@ zlog_settings = { path = "crates/zlog_settings" } # External crates # -agent-client-protocol = "0.0.31" +agentic-coding-protocol = "0.0.10" +agent-client-protocol = "0.0.30" aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } any_vec = "0.14" diff --git a/GLOSSARY.md b/GLOSSARY.md new file mode 100644 index 0000000000..521fe53222 --- /dev/null +++ b/GLOSSARY.md @@ -0,0 +1,68 @@ +TODO dvdsk copy latest from notes + +These are some terms and structures frequently used throughout the zed codebase. This is a best effort list. PR's are very welcome. + +Questions: + +- Can we generate this list form doc comments throughout zed? + +## GPUI + +- `Context`: A wrapper around the App struct with specialized behavior for a specific Entity +- `App`: A singleton which holds the full application state including all the entities. +- `AsyncApp`: +- `Window`: +- `Task`: +- `Executor`: + - both background and foreground +- `Entity`: A strong, well-typed reference to a struct which is managed by gpui. Effectively a pointer into the Context. +- `Global`: A singleton type which has only one value +- `Event`: A datatype which can be send by an Entity to subscribers +- `Action`: An event that represents a user's keyboard input that can be handled by listerners + Example: `file finder: toggle` +- `Subscription`: An event handler that is used to react to the changes of state in the application. + 1. Emitted event handling + 2. Observing {new,release,on notify} of an entity +- `Focus`: The place where keystrokes are handled first +- `Element`: An type that can be rendered +- `AnyElement`: A type erased version of an Element +- `element expression`: An expression that builds an element tree, example: +``` +h_flex() + .id(text[i]) + .relative() + .when(selected, |this| { + this.child( + div() + .h_4() + .absolute() + etc etc +``` + +## Zed + +- `Workspace`: The root of the window +- `Buffer`: The in memory representation of a 'file' together with relevant data such as syntax trees, git status and diagnostics. +- `Picker`: A model showing a list of items. When an item is selected you can confirm the item and then something happens. +- `PickerDelegate`: A trait used to specialize behavior for a Picker +- `Modal`: todo! +- `Project`: todo! +- `Stores`: todo! + +## Collab + +- `Upstream client`: The zed client which has shared their workspace +- `Downstream client`: The zed client joining a shared workspace + +## Debugger + +- `DapStore`: Is an entity that manages debugger sessions +- `debugger::Session`: Is an entity that manages the lifecycle of a debug session and communication with DAPS +- `BreakpointStore`: Is an entity that manages breakpoints states in local and remote instances of Zed +- `DebugSession`: Manages a debug session's UI and running state +- `RunningState`: Directily manages all the views of a debug session +- `VariableList`: The variable and watch list view of a debug session +- `Console` +- `Terminal` +- `BreakpointList` +- ` diff --git a/Procfile.web b/Procfile.web deleted file mode 100644 index 8140555144..0000000000 --- a/Procfile.web +++ /dev/null @@ -1,2 +0,0 @@ -postgrest_llm: postgrest crates/collab/postgrest_llm.conf -website: cd ../zed.dev; npm run dev -- --port=3000 diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg index aba193930b..bca13f8d56 100644 --- a/assets/icons/copy.svg +++ b/assets/icons/copy.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/terminal_ghost.svg b/assets/icons/terminal_ghost.svg deleted file mode 100644 index 7d0d0e068e..0000000000 --- a/assets/icons/terminal_ghost.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/images/acp_grid.svg b/assets/images/acp_grid.svg deleted file mode 100644 index 8ebff8e1bc..0000000000 --- a/assets/images/acp_grid.svg +++ /dev/null @@ -1,1257 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/acp_logo.svg b/assets/images/acp_logo.svg deleted file mode 100644 index efaa46707b..0000000000 --- a/assets/images/acp_logo.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/images/acp_logo_serif.svg b/assets/images/acp_logo_serif.svg deleted file mode 100644 index 6bc359cf82..0000000000 --- a/assets/images/acp_logo_serif.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 3cca560c00..955e68f5a9 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -16,6 +16,7 @@ "up": "menu::SelectPrevious", "enter": "menu::Confirm", "ctrl-enter": "menu::SecondaryConfirm", + "ctrl-escape": "menu::Cancel", "ctrl-c": "menu::Cancel", "escape": "menu::Cancel", "alt-shift-enter": "menu::Restart", @@ -40,7 +41,7 @@ "shift-f11": "debugger::StepOut", "f11": "zed::ToggleFullScreen", "ctrl-alt-z": "edit_prediction::RateCompletions", - "ctrl-alt-shift-i": "edit_prediction::ToggleMenu", + "ctrl-shift-i": "edit_prediction::ToggleMenu", "ctrl-alt-l": "lsp_tool::ToggleMenu" } }, @@ -120,7 +121,7 @@ "alt-g m": "git::OpenModifiedFiles", "menu": "editor::OpenContextMenu", "shift-f10": "editor::OpenContextMenu", - "ctrl-alt-shift-e": "editor::ToggleEditPrediction", + "ctrl-shift-e": "editor::ToggleEditPrediction", "f9": "editor::ToggleBreakpoint", "shift-f9": "editor::EditLogBreakpoint" } @@ -855,7 +856,7 @@ "ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }], "ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }], "alt-ctrl-r": "project_panel::RevealInFileManager", - "ctrl-shift-enter": "workspace::OpenWithSystem", + "ctrl-shift-enter": "project_panel::OpenWithSystem", "alt-d": "project_panel::CompareMarkedFiles", "shift-find": "project_panel::NewSearchInDirectory", "ctrl-alt-shift-f": "project_panel::NewSearchInDirectory", @@ -1194,16 +1195,9 @@ "ctrl-1": "onboarding::ActivateBasicsPage", "ctrl-2": "onboarding::ActivateEditingPage", "ctrl-3": "onboarding::ActivateAISetupPage", - "ctrl-enter": "onboarding::Finish", - "alt-shift-l": "onboarding::SignIn", + "ctrl-escape": "onboarding::Finish", + "alt-tab": "onboarding::SignIn", "alt-shift-a": "onboarding::OpenAccount" } - }, - { - "context": "InvalidBuffer", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-enter": "workspace::OpenWithSystem" - } } ] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index e72f4174ff..8b18299a91 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -915,7 +915,7 @@ "cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }], "cmd-delete": ["project_panel::Delete", { "skip_prompt": false }], "alt-cmd-r": "project_panel::RevealInFileManager", - "ctrl-shift-enter": "workspace::OpenWithSystem", + "ctrl-shift-enter": "project_panel::OpenWithSystem", "alt-d": "project_panel::CompareMarkedFiles", "cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }], "cmd-alt-shift-f": "project_panel::NewSearchInDirectory", @@ -1301,12 +1301,5 @@ "alt-tab": "onboarding::SignIn", "alt-shift-a": "onboarding::OpenAccount" } - }, - { - "context": "InvalidBuffer", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-enter": "workspace::OpenWithSystem" - } } ] diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json deleted file mode 100644 index c7a6c3149c..0000000000 --- a/assets/keymaps/default-windows.json +++ /dev/null @@ -1,1260 +0,0 @@ -[ - // Standard Windows bindings - { - "use_key_equivalents": true, - "bindings": { - "home": "menu::SelectFirst", - "shift-pageup": "menu::SelectFirst", - "pageup": "menu::SelectFirst", - "end": "menu::SelectLast", - "shift-pagedown": "menu::SelectLast", - "pagedown": "menu::SelectLast", - "ctrl-n": "menu::SelectNext", - "tab": "menu::SelectNext", - "down": "menu::SelectNext", - "ctrl-p": "menu::SelectPrevious", - "shift-tab": "menu::SelectPrevious", - "up": "menu::SelectPrevious", - "enter": "menu::Confirm", - "ctrl-enter": "menu::SecondaryConfirm", - "ctrl-escape": "menu::Cancel", - "ctrl-c": "menu::Cancel", - "escape": "menu::Cancel", - "shift-alt-enter": "menu::Restart", - "alt-enter": ["picker::ConfirmInput", { "secondary": false }], - "ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }], - "ctrl-shift-w": "workspace::CloseWindow", - "shift-escape": "workspace::ToggleZoom", - "open": "workspace::Open", - "ctrl-o": "workspace::Open", - "ctrl-=": ["zed::IncreaseBufferFontSize", { "persist": false }], - "ctrl-shift-=": ["zed::IncreaseBufferFontSize", { "persist": false }], - "ctrl--": ["zed::DecreaseBufferFontSize", { "persist": false }], - "ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }], - "ctrl-,": "zed::OpenSettings", - "ctrl-q": "zed::Quit", - "f4": "debugger::Start", - "shift-f5": "debugger::Stop", - "ctrl-shift-f5": "debugger::RerunSession", - "f6": "debugger::Pause", - "f7": "debugger::StepOver", - "ctrl-f11": "debugger::StepInto", - "shift-f11": "debugger::StepOut", - "f11": "zed::ToggleFullScreen", - "ctrl-shift-i": "edit_prediction::ToggleMenu", - "shift-alt-l": "lsp_tool::ToggleMenu" - } - }, - { - "context": "Picker || menu", - "use_key_equivalents": true, - "bindings": { - "up": "menu::SelectPrevious", - "down": "menu::SelectNext" - } - }, - { - "context": "Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "editor::Cancel", - "shift-backspace": "editor::Backspace", - "backspace": "editor::Backspace", - "delete": "editor::Delete", - "tab": "editor::Tab", - "shift-tab": "editor::Backtab", - "ctrl-k": "editor::CutToEndOfLine", - "ctrl-k ctrl-q": "editor::Rewrap", - "ctrl-k q": "editor::Rewrap", - "ctrl-backspace": "editor::DeleteToPreviousWordStart", - "ctrl-delete": "editor::DeleteToNextWordEnd", - "cut": "editor::Cut", - "shift-delete": "editor::Cut", - "ctrl-x": "editor::Cut", - "copy": "editor::Copy", - "ctrl-insert": "editor::Copy", - "ctrl-c": "editor::Copy", - "paste": "editor::Paste", - "shift-insert": "editor::Paste", - "ctrl-v": "editor::Paste", - "undo": "editor::Undo", - "ctrl-z": "editor::Undo", - "redo": "editor::Redo", - "ctrl-y": "editor::Redo", - "ctrl-shift-z": "editor::Redo", - "up": "editor::MoveUp", - "ctrl-up": "editor::LineUp", - "ctrl-down": "editor::LineDown", - "pageup": "editor::MovePageUp", - "alt-pageup": "editor::PageUp", - "shift-pageup": "editor::SelectPageUp", - "home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }], - "down": "editor::MoveDown", - "pagedown": "editor::MovePageDown", - "alt-pagedown": "editor::PageDown", - "shift-pagedown": "editor::SelectPageDown", - "end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": true }], - "left": "editor::MoveLeft", - "right": "editor::MoveRight", - "ctrl-left": "editor::MoveToPreviousWordStart", - "ctrl-right": "editor::MoveToNextWordEnd", - "ctrl-home": "editor::MoveToBeginning", - "ctrl-end": "editor::MoveToEnd", - "shift-up": "editor::SelectUp", - "shift-down": "editor::SelectDown", - "shift-left": "editor::SelectLeft", - "shift-right": "editor::SelectRight", - "ctrl-shift-left": "editor::SelectToPreviousWordStart", - "ctrl-shift-right": "editor::SelectToNextWordEnd", - "ctrl-shift-home": "editor::SelectToBeginning", - "ctrl-shift-end": "editor::SelectToEnd", - "ctrl-a": "editor::SelectAll", - "ctrl-l": "editor::SelectLine", - "shift-alt-f": "editor::Format", - "shift-alt-o": "editor::OrganizeImports", - "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }], - "shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }], - "ctrl-alt-space": "editor::ShowCharacterPalette", - "ctrl-;": "editor::ToggleLineNumbers", - "ctrl-'": "editor::ToggleSelectedDiffHunks", - "ctrl-\"": "editor::ExpandAllDiffHunks", - "ctrl-i": "editor::ShowSignatureHelp", - "alt-g b": "git::Blame", - "alt-g m": "git::OpenModifiedFiles", - "menu": "editor::OpenContextMenu", - "shift-f10": "editor::OpenContextMenu", - "ctrl-shift-e": "editor::ToggleEditPrediction", - "f9": "editor::ToggleBreakpoint", - "shift-f9": "editor::EditLogBreakpoint" - } - }, - { - "context": "Editor && mode == full", - "use_key_equivalents": true, - "bindings": { - "shift-enter": "editor::Newline", - "enter": "editor::Newline", - "ctrl-enter": "editor::NewlineAbove", - "ctrl-shift-enter": "editor::NewlineBelow", - "ctrl-k ctrl-z": "editor::ToggleSoftWrap", - "ctrl-k z": "editor::ToggleSoftWrap", - "find": "buffer_search::Deploy", - "ctrl-f": "buffer_search::Deploy", - "ctrl-h": "buffer_search::DeployReplace", - "ctrl-shift-.": "assistant::QuoteSelection", - "ctrl-shift-,": "assistant::InsertIntoEditor", - "shift-alt-e": "editor::SelectEnclosingSymbol", - "ctrl-shift-backspace": "editor::GoToPreviousChange", - "ctrl-shift-alt-backspace": "editor::GoToNextChange", - "alt-enter": "editor::OpenSelectionsInMultibuffer" - } - }, - { - "context": "Editor && mode == full && edit_prediction", - "use_key_equivalents": true, - "bindings": { - "alt-]": "editor::NextEditPrediction", - "alt-[": "editor::PreviousEditPrediction" - } - }, - { - "context": "Editor && !edit_prediction", - "use_key_equivalents": true, - "bindings": { - "alt-\\": "editor::ShowEditPrediction" - } - }, - { - "context": "Editor && mode == auto_height", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "editor::Newline", - "shift-enter": "editor::Newline", - "ctrl-shift-enter": "editor::NewlineBelow" - } - }, - { - "context": "Markdown", - "use_key_equivalents": true, - "bindings": { - "copy": "markdown::Copy", - "ctrl-c": "markdown::Copy" - } - }, - { - "context": "Editor && jupyter && !ContextEditor", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-enter": "repl::Run", - "ctrl-alt-enter": "repl::RunInPlace" - } - }, - { - "context": "Editor && !agent_diff", - "use_key_equivalents": true, - "bindings": { - "ctrl-k ctrl-r": "git::Restore", - "alt-y": "git::StageAndNext", - "shift-alt-y": "git::UnstageAndNext" - } - }, - { - "context": "Editor && editor_agent_diff", - "use_key_equivalents": true, - "bindings": { - "ctrl-y": "agent::Keep", - "ctrl-n": "agent::Reject", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll", - "ctrl-shift-r": "agent::OpenAgentDiff" - } - }, - { - "context": "AgentDiff", - "use_key_equivalents": true, - "bindings": { - "ctrl-y": "agent::Keep", - "ctrl-n": "agent::Reject", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } - }, - { - "context": "ContextEditor > Editor", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "assistant::Assist", - "ctrl-s": "workspace::Save", - "save": "workspace::Save", - "ctrl-shift-,": "assistant::InsertIntoEditor", - "shift-enter": "assistant::Split", - "ctrl-r": "assistant::CycleMessageRole", - "enter": "assistant::ConfirmCommand", - "alt-enter": "editor::Newline", - "ctrl-k c": "assistant::CopyCode", - "ctrl-g": "search::SelectNextMatch", - "ctrl-shift-g": "search::SelectPreviousMatch", - "ctrl-k l": "agent::OpenRulesLibrary" - } - }, - { - "context": "AgentPanel", - "use_key_equivalents": true, - "bindings": { - "ctrl-n": "agent::NewThread", - "shift-alt-n": "agent::NewTextThread", - "ctrl-shift-h": "agent::OpenHistory", - "shift-alt-c": "agent::OpenSettings", - "shift-alt-p": "agent::OpenRulesLibrary", - "ctrl-i": "agent::ToggleProfileSelector", - "shift-alt-/": "agent::ToggleModelSelector", - "ctrl-shift-a": "agent::ToggleContextPicker", - "ctrl-shift-j": "agent::ToggleNavigationMenu", - "ctrl-shift-i": "agent::ToggleOptionsMenu", - // "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu", - "shift-alt-escape": "agent::ExpandMessageEditor", - "ctrl-shift-.": "assistant::QuoteSelection", - "shift-alt-e": "agent::RemoveAllContext", - "ctrl-shift-e": "project_panel::ToggleFocus", - "ctrl-shift-enter": "agent::ContinueThread", - "super-ctrl-b": "agent::ToggleBurnMode", - "alt-enter": "agent::ContinueWithBurnMode" - } - }, - { - "context": "AgentPanel > NavigationMenu", - "use_key_equivalents": true, - "bindings": { - "shift-backspace": "agent::DeleteRecentlyOpenThread" - } - }, - { - "context": "AgentPanel > Markdown", - "use_key_equivalents": true, - "bindings": { - "copy": "markdown::CopyAsMarkdown", - "ctrl-c": "markdown::CopyAsMarkdown" - } - }, - { - "context": "AgentPanel && prompt_editor", - "use_key_equivalents": true, - "bindings": { - "ctrl-n": "agent::NewTextThread", - "ctrl-alt-t": "agent::NewThread" - } - }, - { - "context": "AgentPanel && external_agent_thread", - "use_key_equivalents": true, - "bindings": { - "ctrl-n": "agent::NewExternalAgentThread", - "ctrl-alt-t": "agent::NewThread" - } - }, - { - "context": "MessageEditor && !Picker > Editor && !use_modifier_to_send", - "use_key_equivalents": true, - "bindings": { - "enter": "agent::Chat", - "ctrl-enter": "agent::ChatWithFollow", - "ctrl-i": "agent::ToggleProfileSelector", - "ctrl-shift-r": "agent::OpenAgentDiff", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } - }, - { - "context": "MessageEditor && !Picker > Editor && use_modifier_to_send", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "agent::Chat", - "enter": "editor::Newline", - "ctrl-i": "agent::ToggleProfileSelector", - "ctrl-shift-r": "agent::OpenAgentDiff", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } - }, - { - "context": "EditMessageEditor > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "enter": "menu::Confirm", - "alt-enter": "editor::Newline" - } - }, - { - "context": "AgentFeedbackMessageEditor > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "enter": "menu::Confirm", - "alt-enter": "editor::Newline" - } - }, - { - "context": "ContextStrip", - "use_key_equivalents": true, - "bindings": { - "up": "agent::FocusUp", - "right": "agent::FocusRight", - "left": "agent::FocusLeft", - "down": "agent::FocusDown", - "backspace": "agent::RemoveFocusedContext", - "enter": "agent::AcceptSuggestedContext" - } - }, - { - "context": "AcpThread > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "agent::Chat", - "ctrl-shift-r": "agent::OpenAgentDiff", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } - }, - { - "context": "ThreadHistory", - "use_key_equivalents": true, - "bindings": { - "backspace": "agent::RemoveSelectedThread" - } - }, - { - "context": "PromptLibrary", - "use_key_equivalents": true, - "bindings": { - "new": "rules_library::NewRule", - "ctrl-n": "rules_library::NewRule", - "ctrl-shift-s": "rules_library::ToggleDefaultRule" - } - }, - { - "context": "BufferSearchBar", - "use_key_equivalents": true, - "bindings": { - "escape": "buffer_search::Dismiss", - "tab": "buffer_search::FocusEditor", - "enter": "search::SelectNextMatch", - "shift-enter": "search::SelectPreviousMatch", - "alt-enter": "search::SelectAllMatches", - "find": "search::FocusSearch", - "ctrl-f": "search::FocusSearch", - "ctrl-h": "search::ToggleReplace", - "ctrl-l": "search::ToggleSelection" - } - }, - { - "context": "BufferSearchBar && in_replace > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "search::ReplaceNext", - "ctrl-enter": "search::ReplaceAll" - } - }, - { - "context": "BufferSearchBar && !in_replace > Editor", - "use_key_equivalents": true, - "bindings": { - "up": "search::PreviousHistoryQuery", - "down": "search::NextHistoryQuery" - } - }, - { - "context": "ProjectSearchBar", - "use_key_equivalents": true, - "bindings": { - "escape": "project_search::ToggleFocus", - "shift-find": "search::FocusSearch", - "ctrl-shift-f": "search::FocusSearch", - "ctrl-shift-h": "search::ToggleReplace", - "alt-r": "search::ToggleRegex" // vscode - } - }, - { - "context": "ProjectSearchBar > Editor", - "use_key_equivalents": true, - "bindings": { - "up": "search::PreviousHistoryQuery", - "down": "search::NextHistoryQuery" - } - }, - { - "context": "ProjectSearchBar && in_replace > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "search::ReplaceNext", - "ctrl-alt-enter": "search::ReplaceAll" - } - }, - { - "context": "ProjectSearchView", - "use_key_equivalents": true, - "bindings": { - "escape": "project_search::ToggleFocus", - "ctrl-shift-h": "search::ToggleReplace", - "alt-r": "search::ToggleRegex" // vscode - } - }, - { - "context": "Pane", - "use_key_equivalents": true, - "bindings": { - "alt-1": ["pane::ActivateItem", 0], - "alt-2": ["pane::ActivateItem", 1], - "alt-3": ["pane::ActivateItem", 2], - "alt-4": ["pane::ActivateItem", 3], - "alt-5": ["pane::ActivateItem", 4], - "alt-6": ["pane::ActivateItem", 5], - "alt-7": ["pane::ActivateItem", 6], - "alt-8": ["pane::ActivateItem", 7], - "alt-9": ["pane::ActivateItem", 8], - "alt-0": "pane::ActivateLastItem", - "ctrl-pageup": "pane::ActivatePreviousItem", - "ctrl-pagedown": "pane::ActivateNextItem", - "ctrl-shift-pageup": "pane::SwapItemLeft", - "ctrl-shift-pagedown": "pane::SwapItemRight", - "ctrl-f4": ["pane::CloseActiveItem", { "close_pinned": false }], - "ctrl-w": ["pane::CloseActiveItem", { "close_pinned": false }], - "ctrl-shift-alt-t": ["pane::CloseOtherItems", { "close_pinned": false }], - "ctrl-shift-alt-w": "workspace::CloseInactiveTabsAndPanes", - "ctrl-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }], - "ctrl-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }], - "ctrl-k u": ["pane::CloseCleanItems", { "close_pinned": false }], - "ctrl-k w": ["pane::CloseAllItems", { "close_pinned": false }], - "ctrl-k ctrl-w": "workspace::CloseAllItemsAndPanes", - "back": "pane::GoBack", - "alt--": "pane::GoBack", - "alt-=": "pane::GoForward", - "forward": "pane::GoForward", - "f3": "search::SelectNextMatch", - "shift-f3": "search::SelectPreviousMatch", - "shift-find": "project_search::ToggleFocus", - "ctrl-shift-f": "project_search::ToggleFocus", - "shift-alt-h": "search::ToggleReplace", - "alt-l": "search::ToggleSelection", - "alt-enter": "search::SelectAllMatches", - "alt-c": "search::ToggleCaseSensitive", - "alt-w": "search::ToggleWholeWord", - "alt-find": "project_search::ToggleFilters", - "alt-f": "project_search::ToggleFilters", - "alt-r": "search::ToggleRegex", - // "ctrl-shift-alt-x": "search::ToggleRegex", - "ctrl-k shift-enter": "pane::TogglePinTab" - } - }, - // Bindings from VS Code - { - "context": "Editor", - "use_key_equivalents": true, - "bindings": { - "ctrl-[": "editor::Outdent", - "ctrl-]": "editor::Indent", - "ctrl-shift-alt-up": "editor::AddSelectionAbove", // Insert Cursor Above - "ctrl-shift-alt-down": "editor::AddSelectionBelow", // Insert Cursor Below - "ctrl-shift-k": "editor::DeleteLine", - "alt-up": "editor::MoveLineUp", - "alt-down": "editor::MoveLineDown", - "shift-alt-up": "editor::DuplicateLineUp", - "shift-alt-down": "editor::DuplicateLineDown", - "shift-alt-right": "editor::SelectLargerSyntaxNode", // Expand Selection - "shift-alt-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection - "ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection - "ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word - "ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand - "ctrl-shift-down": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch - "ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch - "ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip - "ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch - "ctrl-k ctrl-i": "editor::Hover", - "ctrl-k ctrl-b": "editor::BlameHover", - "ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }], - "f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }], - "shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }], - "f2": "editor::Rename", - "f12": "editor::GoToDefinition", - "alt-f12": "editor::GoToDefinitionSplit", - "ctrl-shift-f10": "editor::GoToDefinitionSplit", - "ctrl-f12": "editor::GoToImplementation", - "shift-f12": "editor::GoToTypeDefinition", - "ctrl-alt-f12": "editor::GoToTypeDefinitionSplit", - "shift-alt-f12": "editor::FindAllReferences", - "ctrl-m": "editor::MoveToEnclosingBracket", // from jetbrains - "ctrl-shift-\\": "editor::MoveToEnclosingBracket", - "ctrl-shift-[": "editor::Fold", - "ctrl-shift-]": "editor::UnfoldLines", - "ctrl-k ctrl-l": "editor::ToggleFold", - "ctrl-k ctrl-[": "editor::FoldRecursive", - "ctrl-k ctrl-]": "editor::UnfoldRecursive", - "ctrl-k ctrl-1": ["editor::FoldAtLevel", 1], - "ctrl-k ctrl-2": ["editor::FoldAtLevel", 2], - "ctrl-k ctrl-3": ["editor::FoldAtLevel", 3], - "ctrl-k ctrl-4": ["editor::FoldAtLevel", 4], - "ctrl-k ctrl-5": ["editor::FoldAtLevel", 5], - "ctrl-k ctrl-6": ["editor::FoldAtLevel", 6], - "ctrl-k ctrl-7": ["editor::FoldAtLevel", 7], - "ctrl-k ctrl-8": ["editor::FoldAtLevel", 8], - "ctrl-k ctrl-9": ["editor::FoldAtLevel", 9], - "ctrl-k ctrl-0": "editor::FoldAll", - "ctrl-k ctrl-j": "editor::UnfoldAll", - "ctrl-space": "editor::ShowCompletions", - "ctrl-shift-space": "editor::ShowWordCompletions", - "ctrl-.": "editor::ToggleCodeActions", - "ctrl-k r": "editor::RevealInFileManager", - "ctrl-k p": "editor::CopyPath", - "ctrl-\\": "pane::SplitRight", - "ctrl-shift-alt-c": "editor::DisplayCursorNames", - "alt-.": "editor::GoToHunk", - "alt-,": "editor::GoToPreviousHunk" - } - }, - { - "context": "Editor && extension == md", - "use_key_equivalents": true, - "bindings": { - "ctrl-k v": "markdown::OpenPreviewToTheSide", - "ctrl-shift-v": "markdown::OpenPreview" - } - }, - { - "context": "Editor && extension == svg", - "use_key_equivalents": true, - "bindings": { - "ctrl-k v": "svg::OpenPreviewToTheSide", - "ctrl-shift-v": "svg::OpenPreview" - } - }, - { - "context": "Editor && mode == full", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-o": "outline::Toggle", - "ctrl-g": "go_to_line::Toggle" - } - }, - { - "context": "Workspace", - "use_key_equivalents": true, - "bindings": { - "alt-open": ["projects::OpenRecent", { "create_new_window": false }], - // Change the default action on `menu::Confirm` by setting the parameter - // "ctrl-alt-o": ["projects::OpenRecent", { "create_new_window": true }], - "ctrl-r": ["projects::OpenRecent", { "create_new_window": false }], - "shift-alt-open": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }], - // Change to open path modal for existing remote connection by setting the parameter - // "ctrl-shift-alt-o": "["projects::OpenRemote", { "from_existing_connection": true }]", - "ctrl-shift-alt-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }], - "shift-alt-b": "branches::OpenRecent", - "shift-alt-enter": "toast::RunAction", - "ctrl-shift-`": "workspace::NewTerminal", - "save": "workspace::Save", - "ctrl-s": "workspace::Save", - "ctrl-k ctrl-shift-s": "workspace::SaveWithoutFormat", - "shift-save": "workspace::SaveAs", - "ctrl-shift-s": "workspace::SaveAs", - "new": "workspace::NewFile", - "ctrl-n": "workspace::NewFile", - "shift-new": "workspace::NewWindow", - "ctrl-shift-n": "workspace::NewWindow", - "ctrl-`": "terminal_panel::ToggleFocus", - "f10": ["app_menu::OpenApplicationMenu", "Zed"], - "alt-1": ["workspace::ActivatePane", 0], - "alt-2": ["workspace::ActivatePane", 1], - "alt-3": ["workspace::ActivatePane", 2], - "alt-4": ["workspace::ActivatePane", 3], - "alt-5": ["workspace::ActivatePane", 4], - "alt-6": ["workspace::ActivatePane", 5], - "alt-7": ["workspace::ActivatePane", 6], - "alt-8": ["workspace::ActivatePane", 7], - "alt-9": ["workspace::ActivatePane", 8], - "ctrl-alt-b": "workspace::ToggleRightDock", - "ctrl-b": "workspace::ToggleLeftDock", - "ctrl-j": "workspace::ToggleBottomDock", - "ctrl-shift-y": "workspace::CloseAllDocks", - "alt-r": "workspace::ResetActiveDockSize", - // For 0px parameter, uses UI font size value. - "shift-alt--": ["workspace::DecreaseActiveDockSize", { "px": 0 }], - "shift-alt-=": ["workspace::IncreaseActiveDockSize", { "px": 0 }], - "shift-alt-0": "workspace::ResetOpenDocksSize", - "ctrl-shift-alt--": ["workspace::DecreaseOpenDocksSize", { "px": 0 }], - "ctrl-shift-alt-=": ["workspace::IncreaseOpenDocksSize", { "px": 0 }], - "shift-find": "pane::DeploySearch", - "ctrl-shift-f": "pane::DeploySearch", - "ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }], - "ctrl-shift-t": "pane::ReopenClosedItem", - "ctrl-k ctrl-s": "zed::OpenKeymapEditor", - "ctrl-k ctrl-t": "theme_selector::Toggle", - "ctrl-alt-super-p": "settings_profile_selector::Toggle", - "ctrl-t": "project_symbols::Toggle", - "ctrl-p": "file_finder::Toggle", - "ctrl-tab": "tab_switcher::Toggle", - "ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }], - "ctrl-e": "file_finder::Toggle", - "f1": "command_palette::Toggle", - "ctrl-shift-p": "command_palette::Toggle", - "ctrl-shift-m": "diagnostics::Deploy", - "ctrl-shift-e": "project_panel::ToggleFocus", - "ctrl-shift-b": "outline_panel::ToggleFocus", - "ctrl-shift-g": "git_panel::ToggleFocus", - "ctrl-shift-d": "debug_panel::ToggleFocus", - "ctrl-shift-/": "agent::ToggleFocus", - "alt-save": "workspace::SaveAll", - "ctrl-k s": "workspace::SaveAll", - "ctrl-k m": "language_selector::Toggle", - "escape": "workspace::Unfollow", - "ctrl-k ctrl-left": "workspace::ActivatePaneLeft", - "ctrl-k ctrl-right": "workspace::ActivatePaneRight", - "ctrl-k ctrl-up": "workspace::ActivatePaneUp", - "ctrl-k ctrl-down": "workspace::ActivatePaneDown", - "ctrl-k shift-left": "workspace::SwapPaneLeft", - "ctrl-k shift-right": "workspace::SwapPaneRight", - "ctrl-k shift-up": "workspace::SwapPaneUp", - "ctrl-k shift-down": "workspace::SwapPaneDown", - "ctrl-shift-x": "zed::Extensions", - "ctrl-shift-r": "task::Rerun", - "alt-t": "task::Rerun", - "shift-alt-t": "task::Spawn", - "shift-alt-r": ["task::Spawn", { "reveal_target": "center" }], - // also possible to spawn tasks by name: - // "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }] - // or by tag: - // "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }], - "f5": "debugger::Rerun", - "ctrl-f4": "workspace::CloseActiveDock", - "ctrl-w": "workspace::CloseActiveDock" - } - }, - { - "context": "Workspace && debugger_running", - "use_key_equivalents": true, - "bindings": { - "f5": "zed::NoAction" - } - }, - { - "context": "Workspace && debugger_stopped", - "use_key_equivalents": true, - "bindings": { - "f5": "debugger::Continue" - } - }, - { - "context": "ApplicationMenu", - "use_key_equivalents": true, - "bindings": { - "f10": "menu::Cancel", - "left": "app_menu::ActivateMenuLeft", - "right": "app_menu::ActivateMenuRight" - } - }, - // Bindings from Sublime Text - { - "context": "Editor", - "use_key_equivalents": true, - "bindings": { - "ctrl-u": "editor::UndoSelection", - "ctrl-shift-u": "editor::RedoSelection", - "ctrl-shift-j": "editor::JoinLines", - "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart", - "shift-alt-h": "editor::DeleteToPreviousSubwordStart", - "ctrl-alt-delete": "editor::DeleteToNextSubwordEnd", - "shift-alt-d": "editor::DeleteToNextSubwordEnd", - "ctrl-alt-left": "editor::MoveToPreviousSubwordStart", - "ctrl-alt-right": "editor::MoveToNextSubwordEnd", - "ctrl-shift-alt-left": "editor::SelectToPreviousSubwordStart", - "ctrl-shift-alt-right": "editor::SelectToNextSubwordEnd" - } - }, - // Bindings from Atom - { - "context": "Pane", - "use_key_equivalents": true, - "bindings": { - "ctrl-k up": "pane::SplitUp", - "ctrl-k down": "pane::SplitDown", - "ctrl-k left": "pane::SplitLeft", - "ctrl-k right": "pane::SplitRight" - } - }, - // Bindings that should be unified with bindings for more general actions - { - "context": "Editor && renaming", - "use_key_equivalents": true, - "bindings": { - "enter": "editor::ConfirmRename" - } - }, - { - "context": "Editor && showing_completions", - "use_key_equivalents": true, - "bindings": { - "enter": "editor::ConfirmCompletion", - "shift-enter": "editor::ConfirmCompletionReplace", - "tab": "editor::ComposeCompletion" - } - }, - // Bindings for accepting edit predictions - // - // alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This is - // because alt-tab may not be available, as it is often used for window switching. - { - "context": "Editor && edit_prediction", - "use_key_equivalents": true, - "bindings": { - "alt-tab": "editor::AcceptEditPrediction", - "alt-l": "editor::AcceptEditPrediction", - "tab": "editor::AcceptEditPrediction", - "alt-right": "editor::AcceptPartialEditPrediction" - } - }, - { - "context": "Editor && edit_prediction_conflict", - "use_key_equivalents": true, - "bindings": { - "alt-tab": "editor::AcceptEditPrediction", - "alt-l": "editor::AcceptEditPrediction", - "alt-right": "editor::AcceptPartialEditPrediction" - } - }, - { - "context": "Editor && showing_code_actions", - "use_key_equivalents": true, - "bindings": { - "enter": "editor::ConfirmCodeAction" - } - }, - { - "context": "Editor && (showing_code_actions || showing_completions)", - "use_key_equivalents": true, - "bindings": { - "ctrl-p": "editor::ContextMenuPrevious", - "up": "editor::ContextMenuPrevious", - "ctrl-n": "editor::ContextMenuNext", - "down": "editor::ContextMenuNext", - "pageup": "editor::ContextMenuFirst", - "pagedown": "editor::ContextMenuLast" - } - }, - { - "context": "Editor && showing_signature_help && !showing_completions", - "use_key_equivalents": true, - "bindings": { - "up": "editor::SignatureHelpPrevious", - "down": "editor::SignatureHelpNext" - } - }, - // Custom bindings - { - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-alt-f": "workspace::FollowNextCollaborator", - // Only available in debug builds: opens an element inspector for development. - "shift-alt-i": "dev::ToggleInspector" - } - }, - { - "context": "!Terminal", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-c": "collab_panel::ToggleFocus" - } - }, - { - "context": "!ContextEditor > Editor && mode == full", - "use_key_equivalents": true, - "bindings": { - "alt-enter": "editor::OpenExcerpts", - "shift-enter": "editor::ExpandExcerpts", - "ctrl-alt-enter": "editor::OpenExcerptsSplit", - "ctrl-shift-e": "pane::RevealInProjectPanel", - "ctrl-f8": "editor::GoToHunk", - "ctrl-shift-f8": "editor::GoToPreviousHunk", - "ctrl-enter": "assistant::InlineAssist", - "ctrl-shift-;": "editor::ToggleInlayHints" - } - }, - { - "context": "PromptEditor", - "use_key_equivalents": true, - "bindings": { - "ctrl-[": "agent::CyclePreviousInlineAssist", - "ctrl-]": "agent::CycleNextInlineAssist", - "shift-alt-e": "agent::RemoveAllContext" - } - }, - { - "context": "Prompt", - "use_key_equivalents": true, - "bindings": { - "left": "menu::SelectPrevious", - "right": "menu::SelectNext", - "h": "menu::SelectPrevious", - "l": "menu::SelectNext" - } - }, - { - "context": "ProjectSearchBar && !in_replace", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "project_search::SearchInNew" - } - }, - { - "context": "OutlinePanel && not_editing", - "use_key_equivalents": true, - "bindings": { - "left": "outline_panel::CollapseSelectedEntry", - "right": "outline_panel::ExpandSelectedEntry", - "alt-copy": "outline_panel::CopyPath", - "shift-alt-c": "outline_panel::CopyPath", - "shift-alt-copy": "workspace::CopyRelativePath", - "ctrl-shift-alt-c": "workspace::CopyRelativePath", - "ctrl-alt-r": "outline_panel::RevealInFileManager", - "space": "outline_panel::OpenSelectedEntry", - "shift-down": "menu::SelectNext", - "shift-up": "menu::SelectPrevious", - "alt-enter": "editor::OpenExcerpts", - "ctrl-alt-enter": "editor::OpenExcerptsSplit" - } - }, - { - "context": "ProjectPanel", - "use_key_equivalents": true, - "bindings": { - "left": "project_panel::CollapseSelectedEntry", - "right": "project_panel::ExpandSelectedEntry", - "new": "project_panel::NewFile", - "ctrl-n": "project_panel::NewFile", - "alt-new": "project_panel::NewDirectory", - "alt-n": "project_panel::NewDirectory", - "cut": "project_panel::Cut", - "ctrl-x": "project_panel::Cut", - "copy": "project_panel::Copy", - "ctrl-insert": "project_panel::Copy", - "ctrl-c": "project_panel::Copy", - "paste": "project_panel::Paste", - "shift-insert": "project_panel::Paste", - "ctrl-v": "project_panel::Paste", - "alt-copy": "project_panel::CopyPath", - "shift-alt-c": "project_panel::CopyPath", - "shift-alt-copy": "workspace::CopyRelativePath", - "ctrl-k ctrl-shift-c": "workspace::CopyRelativePath", - "enter": "project_panel::Rename", - "f2": "project_panel::Rename", - "backspace": ["project_panel::Trash", { "skip_prompt": false }], - "delete": ["project_panel::Trash", { "skip_prompt": false }], - "shift-delete": ["project_panel::Delete", { "skip_prompt": false }], - "ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }], - "ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }], - "ctrl-alt-r": "project_panel::RevealInFileManager", - "ctrl-shift-enter": "project_panel::OpenWithSystem", - "alt-d": "project_panel::CompareMarkedFiles", - "shift-find": "project_panel::NewSearchInDirectory", - "ctrl-k ctrl-shift-f": "project_panel::NewSearchInDirectory", - "shift-down": "menu::SelectNext", - "shift-up": "menu::SelectPrevious", - "escape": "menu::Cancel" - } - }, - { - "context": "ProjectPanel && not_editing", - "use_key_equivalents": true, - "bindings": { - "space": "project_panel::Open" - } - }, - { - "context": "GitPanel && ChangesList", - "use_key_equivalents": true, - "bindings": { - "up": "menu::SelectPrevious", - "down": "menu::SelectNext", - "enter": "menu::Confirm", - "alt-y": "git::StageFile", - "shift-alt-y": "git::UnstageFile", - "space": "git::ToggleStaged", - "shift-space": "git::StageRange", - "tab": "git_panel::FocusEditor", - "shift-tab": "git_panel::FocusEditor", - "escape": "git_panel::ToggleFocus", - "alt-enter": "menu::SecondaryConfirm", - "delete": ["git::RestoreFile", { "skip_prompt": false }], - "backspace": ["git::RestoreFile", { "skip_prompt": false }], - "shift-delete": ["git::RestoreFile", { "skip_prompt": false }], - "ctrl-backspace": ["git::RestoreFile", { "skip_prompt": false }], - "ctrl-delete": ["git::RestoreFile", { "skip_prompt": false }] - } - }, - { - "context": "GitPanel && CommitEditor", - "use_key_equivalents": true, - "bindings": { - "escape": "git::Cancel" - } - }, - { - "context": "GitCommit > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "enter": "editor::Newline", - "ctrl-enter": "git::Commit", - "ctrl-shift-enter": "git::Amend", - "alt-l": "git::GenerateCommitMessage" - } - }, - { - "context": "GitPanel", - "use_key_equivalents": true, - "bindings": { - "ctrl-g ctrl-g": "git::Fetch", - "ctrl-g up": "git::Push", - "ctrl-g down": "git::Pull", - "ctrl-g shift-up": "git::ForcePush", - "ctrl-g d": "git::Diff", - "ctrl-g backspace": "git::RestoreTrackedFiles", - "ctrl-g shift-backspace": "git::TrashUntrackedFiles", - "ctrl-space": "git::StageAll", - "ctrl-shift-space": "git::UnstageAll", - "ctrl-enter": "git::Commit", - "ctrl-shift-enter": "git::Amend" - } - }, - { - "context": "GitDiff > Editor", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "git::Commit", - "ctrl-shift-enter": "git::Amend", - "ctrl-space": "git::StageAll", - "ctrl-shift-space": "git::UnstageAll" - } - }, - { - "context": "AskPass > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "menu::Confirm" - } - }, - { - "context": "CommitEditor > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "git_panel::FocusChanges", - "tab": "git_panel::FocusChanges", - "shift-tab": "git_panel::FocusChanges", - "enter": "editor::Newline", - "ctrl-enter": "git::Commit", - "ctrl-shift-enter": "git::Amend", - "alt-up": "git_panel::FocusChanges", - "alt-l": "git::GenerateCommitMessage" - } - }, - { - "context": "DebugPanel", - "use_key_equivalents": true, - "bindings": { - "ctrl-t": "debugger::ToggleThreadPicker", - "ctrl-i": "debugger::ToggleSessionPicker", - "shift-alt-escape": "debugger::ToggleExpandItem" - } - }, - { - "context": "VariableList", - "use_key_equivalents": true, - "bindings": { - "left": "variable_list::CollapseSelectedEntry", - "right": "variable_list::ExpandSelectedEntry", - "enter": "variable_list::EditVariable", - "ctrl-c": "variable_list::CopyVariableValue", - "ctrl-alt-c": "variable_list::CopyVariableName", - "delete": "variable_list::RemoveWatch", - "backspace": "variable_list::RemoveWatch", - "alt-enter": "variable_list::AddWatch" - } - }, - { - "context": "BreakpointList", - "use_key_equivalents": true, - "bindings": { - "space": "debugger::ToggleEnableBreakpoint", - "backspace": "debugger::UnsetBreakpoint", - "left": "debugger::PreviousBreakpointProperty", - "right": "debugger::NextBreakpointProperty" - } - }, - { - "context": "CollabPanel && not_editing", - "use_key_equivalents": true, - "bindings": { - "ctrl-backspace": "collab_panel::Remove", - "space": "menu::Confirm" - } - }, - { - "context": "CollabPanel", - "use_key_equivalents": true, - "bindings": { - "alt-up": "collab_panel::MoveChannelUp", - "alt-down": "collab_panel::MoveChannelDown" - } - }, - { - "context": "(CollabPanel && editing) > Editor", - "use_key_equivalents": true, - "bindings": { - "space": "collab_panel::InsertSpace" - } - }, - { - "context": "ChannelModal", - "use_key_equivalents": true, - "bindings": { - "tab": "channel_modal::ToggleMode" - } - }, - { - "context": "Picker > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "up": "menu::SelectPrevious", - "down": "menu::SelectNext", - "tab": "picker::ConfirmCompletion", - "alt-enter": ["picker::ConfirmInput", { "secondary": false }] - } - }, - { - "context": "ChannelModal > Picker > Editor", - "use_key_equivalents": true, - "bindings": { - "tab": "channel_modal::ToggleMode" - } - }, - { - "context": "FileFinder || (FileFinder > Picker > Editor)", - "use_key_equivalents": true, - "bindings": { - "ctrl-p": "file_finder::Toggle", - "ctrl-shift-a": "file_finder::ToggleSplitMenu", - "ctrl-shift-i": "file_finder::ToggleFilterMenu" - } - }, - { - "context": "FileFinder || (FileFinder > Picker > Editor) || (FileFinder > Picker > menu)", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-p": "file_finder::SelectPrevious", - "ctrl-j": "pane::SplitDown", - "ctrl-k": "pane::SplitUp", - "ctrl-h": "pane::SplitLeft", - "ctrl-l": "pane::SplitRight" - } - }, - { - "context": "TabSwitcher", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-tab": "menu::SelectPrevious", - "ctrl-up": "menu::SelectPrevious", - "ctrl-down": "menu::SelectNext", - "ctrl-backspace": "tab_switcher::CloseSelectedItem" - } - }, - { - "context": "Terminal", - "use_key_equivalents": true, - "bindings": { - "ctrl-alt-space": "terminal::ShowCharacterPalette", - "copy": "terminal::Copy", - "ctrl-insert": "terminal::Copy", - "ctrl-shift-c": "terminal::Copy", - "paste": "terminal::Paste", - "shift-insert": "terminal::Paste", - "ctrl-shift-v": "terminal::Paste", - "ctrl-enter": "assistant::InlineAssist", - "alt-b": ["terminal::SendText", "\u001bb"], - "alt-f": ["terminal::SendText", "\u001bf"], - "alt-.": ["terminal::SendText", "\u001b."], - "ctrl-delete": ["terminal::SendText", "\u001bd"], - // Overrides for conflicting keybindings - "ctrl-b": ["terminal::SendKeystroke", "ctrl-b"], - "ctrl-c": ["terminal::SendKeystroke", "ctrl-c"], - "ctrl-e": ["terminal::SendKeystroke", "ctrl-e"], - "ctrl-o": ["terminal::SendKeystroke", "ctrl-o"], - "ctrl-w": ["terminal::SendKeystroke", "ctrl-w"], - "ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"], - "ctrl-shift-a": "editor::SelectAll", - "find": "buffer_search::Deploy", - "ctrl-shift-f": "buffer_search::Deploy", - "ctrl-shift-l": "terminal::Clear", - "ctrl-shift-w": "pane::CloseActiveItem", - "up": ["terminal::SendKeystroke", "up"], - "pageup": ["terminal::SendKeystroke", "pageup"], - "down": ["terminal::SendKeystroke", "down"], - "pagedown": ["terminal::SendKeystroke", "pagedown"], - "escape": ["terminal::SendKeystroke", "escape"], - "enter": ["terminal::SendKeystroke", "enter"], - "shift-pageup": "terminal::ScrollPageUp", - "shift-pagedown": "terminal::ScrollPageDown", - "shift-up": "terminal::ScrollLineUp", - "shift-down": "terminal::ScrollLineDown", - "shift-home": "terminal::ScrollToTop", - "shift-end": "terminal::ScrollToBottom", - "ctrl-shift-space": "terminal::ToggleViMode", - "ctrl-shift-r": "terminal::RerunTask", - "ctrl-alt-r": "terminal::RerunTask", - "alt-t": "terminal::RerunTask" - } - }, - { - "context": "ZedPredictModal", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel" - } - }, - { - "context": "ConfigureContextServerModal > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "enter": "editor::Newline", - "ctrl-enter": "menu::Confirm" - } - }, - { - "context": "OnboardingAiConfigurationModal", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel" - } - }, - { - "context": "Diagnostics", - "use_key_equivalents": true, - "bindings": { - "ctrl-r": "diagnostics::ToggleDiagnosticsRefresh" - } - }, - { - "context": "DebugConsole > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "menu::Confirm", - "alt-enter": "console::WatchExpression" - } - }, - { - "context": "RunModal", - "use_key_equivalents": true, - "bindings": { - "ctrl-tab": "pane::ActivateNextItem", - "ctrl-shift-tab": "pane::ActivatePreviousItem" - } - }, - { - "context": "MarkdownPreview", - "use_key_equivalents": true, - "bindings": { - "pageup": "markdown::MovePageUp", - "pagedown": "markdown::MovePageDown" - } - }, - { - "context": "KeymapEditor", - "use_key_equivalents": true, - "bindings": { - "ctrl-f": "search::FocusSearch", - "alt-find": "keymap_editor::ToggleKeystrokeSearch", - "alt-f": "keymap_editor::ToggleKeystrokeSearch", - "alt-c": "keymap_editor::ToggleConflictFilter", - "enter": "keymap_editor::EditBinding", - "alt-enter": "keymap_editor::CreateBinding", - "ctrl-c": "keymap_editor::CopyAction", - "ctrl-shift-c": "keymap_editor::CopyContext", - "ctrl-t": "keymap_editor::ShowMatchingKeybinds" - } - }, - { - "context": "KeystrokeInput", - "use_key_equivalents": true, - "bindings": { - "enter": "keystroke_input::StartRecording", - "escape escape escape": "keystroke_input::StopRecording", - "delete": "keystroke_input::ClearKeystrokes" - } - }, - { - "context": "KeybindEditorModal", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "menu::Confirm", - "escape": "menu::Cancel" - } - }, - { - "context": "KeybindEditorModal > Editor", - "use_key_equivalents": true, - "bindings": { - "up": "menu::SelectPrevious", - "down": "menu::SelectNext" - } - }, - { - "context": "Onboarding", - "use_key_equivalents": true, - "bindings": { - "ctrl-1": "onboarding::ActivateBasicsPage", - "ctrl-2": "onboarding::ActivateEditingPage", - "ctrl-3": "onboarding::ActivateAISetupPage", - "ctrl-escape": "onboarding::Finish", - "alt-tab": "onboarding::SignIn", - "shift-alt-a": "onboarding::OpenAccount" - } - } -] diff --git a/assets/keymaps/linux/emacs.json b/assets/keymaps/linux/emacs.json index 62910e297b..0ff3796f03 100755 --- a/assets/keymaps/linux/emacs.json +++ b/assets/keymaps/linux/emacs.json @@ -38,7 +38,6 @@ "alt-;": ["editor::ToggleComments", { "advance_downwards": false }], "ctrl-x ctrl-;": "editor::ToggleComments", "alt-.": "editor::GoToDefinition", // xref-find-definitions - "alt-?": "editor::FindAllReferences", // xref-find-references "alt-,": "pane::GoBack", // xref-pop-marker-stack "ctrl-x h": "editor::SelectAll", // mark-whole-buffer "ctrl-d": "editor::Delete", // delete-char diff --git a/assets/keymaps/macos/emacs.json b/assets/keymaps/macos/emacs.json index 62910e297b..0ff3796f03 100755 --- a/assets/keymaps/macos/emacs.json +++ b/assets/keymaps/macos/emacs.json @@ -38,7 +38,6 @@ "alt-;": ["editor::ToggleComments", { "advance_downwards": false }], "ctrl-x ctrl-;": "editor::ToggleComments", "alt-.": "editor::GoToDefinition", // xref-find-definitions - "alt-?": "editor::FindAllReferences", // xref-find-references "alt-,": "pane::GoBack", // xref-pop-marker-stack "ctrl-x h": "editor::SelectAll", // mark-whole-buffer "ctrl-d": "editor::Delete", // delete-char diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 67add61bd3..be6d34a134 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -428,13 +428,11 @@ "g h": "vim::StartOfLine", "g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s" "g e": "vim::EndOfDocument", - "g .": "vim::HelixGotoLastModification", // go to last modification "g r": "editor::FindAllReferences", // zed specific "g t": "vim::WindowTop", "g c": "vim::WindowMiddle", "g b": "vim::WindowBottom", - "shift-r": "editor::Paste", "x": "editor::SelectLine", "shift-x": "editor::SelectLine", "%": "editor::SelectAll", @@ -821,7 +819,7 @@ "v": "project_panel::OpenPermanent", "p": "project_panel::Open", "x": "project_panel::RevealInFileManager", - "s": "workspace::OpenWithSystem", + "s": "project_panel::OpenWithSystem", "z d": "project_panel::CompareMarkedFiles", "] c": "project_panel::SelectNextGitEntry", "[ c": "project_panel::SelectPrevGitEntry", diff --git a/assets/settings/default.json b/assets/settings/default.json index 804198090f..014b483250 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -162,12 +162,6 @@ // 2. Always quit the application // "on_last_window_closed": "quit_app", "on_last_window_closed": "platform_default", - // Whether to show padding for zoomed panels. - // When enabled, zoomed center panels (e.g. code editor) will have padding all around, - // while zoomed bottom/left/right panels will have padding to the top/right/left (respectively). - // - // Default: true - "zoomed_padding": true, // Whether to use the system provided dialogs for Open and Save As. // When set to false, Zed will use the built-in keyboard-first pickers. "use_system_path_prompts": true, @@ -653,8 +647,6 @@ // "never" "show": "always" }, - // Whether to enable drag-and-drop operations in the project panel. - "drag_and_drop": true, // Whether to hide the root entry when only one folder is open in the window. "hide_root": false }, @@ -1141,6 +1133,11 @@ // The minimum severity of the diagnostics to show inline. // Inherits editor's diagnostics' max severity settings when `null`. "max_severity": null + }, + "cargo": { + // When enabled, Zed disables rust-analyzer's check on save and starts to query + // Cargo diagnostics separately. + "fetch_cargo_diagnostics": false } }, // Files or globs of files that will be excluded by Zed entirely. They will be skipped during file @@ -1637,9 +1634,6 @@ "allowed": true } }, - "Kotlin": { - "language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."] - }, "LaTeX": { "formatter": "language_server", "language_servers": ["texlab", "..."], diff --git a/assets/settings/initial_tasks.json b/assets/settings/initial_tasks.json index 5cead67b6d..a79c550671 100644 --- a/assets/settings/initial_tasks.json +++ b/assets/settings/initial_tasks.json @@ -43,8 +43,8 @@ // "args": ["--login"] // } // } - "shell": "system" + "shell": "system", // Represents the tags for inline runnable indicators, or spawning multiple tasks at once. - // "tags": [] + "tags": [] } ] diff --git a/assets/themes/ayu/ayu.json b/assets/themes/ayu/ayu.json index 0ffbb9f61e..f9f8720729 100644 --- a/assets/themes/ayu/ayu.json +++ b/assets/themes/ayu/ayu.json @@ -93,7 +93,7 @@ "terminal.ansi.bright_cyan": "#4c806fff", "terminal.ansi.dim_cyan": "#cbf2e4ff", "terminal.ansi.white": "#bfbdb6ff", - "terminal.ansi.bright_white": "#fafafaff", + "terminal.ansi.bright_white": "#bfbdb6ff", "terminal.ansi.dim_white": "#787876ff", "link_text.hover": "#5ac1feff", "conflict": "#feb454ff", @@ -479,7 +479,7 @@ "terminal.ansi.bright_cyan": "#ace0cbff", "terminal.ansi.dim_cyan": "#2a5f4aff", "terminal.ansi.white": "#fcfcfcff", - "terminal.ansi.bright_white": "#ffffffff", + "terminal.ansi.bright_white": "#fcfcfcff", "terminal.ansi.dim_white": "#bcbec0ff", "link_text.hover": "#3b9ee5ff", "conflict": "#f1ad49ff", @@ -865,7 +865,7 @@ "terminal.ansi.bright_cyan": "#4c806fff", "terminal.ansi.dim_cyan": "#cbf2e4ff", "terminal.ansi.white": "#cccac2ff", - "terminal.ansi.bright_white": "#fafafaff", + "terminal.ansi.bright_white": "#cccac2ff", "terminal.ansi.dim_white": "#898a8aff", "link_text.hover": "#72cffeff", "conflict": "#fecf72ff", diff --git a/assets/themes/gruvbox/gruvbox.json b/assets/themes/gruvbox/gruvbox.json index f0f0358b76..459825c733 100644 --- a/assets/themes/gruvbox/gruvbox.json +++ b/assets/themes/gruvbox/gruvbox.json @@ -94,7 +94,7 @@ "terminal.ansi.bright_cyan": "#45603eff", "terminal.ansi.dim_cyan": "#c7dfbdff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#ffffffff", + "terminal.ansi.bright_white": "#fbf1c7ff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#83a598ff", "version_control.added": "#b7bb26ff", @@ -494,7 +494,7 @@ "terminal.ansi.bright_cyan": "#45603eff", "terminal.ansi.dim_cyan": "#c7dfbdff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#ffffffff", + "terminal.ansi.bright_white": "#fbf1c7ff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#83a598ff", "version_control.added": "#b7bb26ff", @@ -894,7 +894,7 @@ "terminal.ansi.bright_cyan": "#45603eff", "terminal.ansi.dim_cyan": "#c7dfbdff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#ffffffff", + "terminal.ansi.bright_white": "#fbf1c7ff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#83a598ff", "version_control.added": "#b7bb26ff", @@ -1294,7 +1294,7 @@ "terminal.ansi.bright_cyan": "#9fbca8ff", "terminal.ansi.dim_cyan": "#253e2eff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#ffffffff", + "terminal.ansi.bright_white": "#fbf1c7ff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#0b6678ff", "version_control.added": "#797410ff", @@ -1694,7 +1694,7 @@ "terminal.ansi.bright_cyan": "#9fbca8ff", "terminal.ansi.dim_cyan": "#253e2eff", "terminal.ansi.white": "#f9f5d7ff", - "terminal.ansi.bright_white": "#ffffffff", + "terminal.ansi.bright_white": "#f9f5d7ff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#0b6678ff", "version_control.added": "#797410ff", @@ -2094,7 +2094,7 @@ "terminal.ansi.bright_cyan": "#9fbca8ff", "terminal.ansi.dim_cyan": "#253e2eff", "terminal.ansi.white": "#f2e5bcff", - "terminal.ansi.bright_white": "#ffffffff", + "terminal.ansi.bright_white": "#f2e5bcff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#0b6678ff", "version_control.added": "#797410ff", diff --git a/assets/themes/one/one.json b/assets/themes/one/one.json index 33f6d3c622..23ebbcc67e 100644 --- a/assets/themes/one/one.json +++ b/assets/themes/one/one.json @@ -93,7 +93,7 @@ "terminal.ansi.bright_cyan": "#3a565bff", "terminal.ansi.dim_cyan": "#b9d9dfff", "terminal.ansi.white": "#dce0e5ff", - "terminal.ansi.bright_white": "#fafafaff", + "terminal.ansi.bright_white": "#dce0e5ff", "terminal.ansi.dim_white": "#575d65ff", "link_text.hover": "#74ade8ff", "version_control.added": "#27a657ff", @@ -468,7 +468,7 @@ "terminal.bright_foreground": "#242529ff", "terminal.dim_foreground": "#fafafaff", "terminal.ansi.black": "#242529ff", - "terminal.ansi.bright_black": "#747579ff", + "terminal.ansi.bright_black": "#242529ff", "terminal.ansi.dim_black": "#97979aff", "terminal.ansi.red": "#d36151ff", "terminal.ansi.bright_red": "#f0b0a4ff", @@ -489,7 +489,7 @@ "terminal.ansi.bright_cyan": "#a3bedaff", "terminal.ansi.dim_cyan": "#254058ff", "terminal.ansi.white": "#fafafaff", - "terminal.ansi.bright_white": "#ffffffff", + "terminal.ansi.bright_white": "#fafafaff", "terminal.ansi.dim_white": "#aaaaaaff", "link_text.hover": "#5c78e2ff", "version_control.added": "#27a657ff", diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 4ded647a74..c748f22275 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -183,15 +183,16 @@ impl ToolCall { language_registry: Arc, cx: &mut App, ) -> Self { - let title = if let Some((first_line, _)) = tool_call.title.split_once("\n") { - first_line.to_owned() + "…" - } else { - tool_call.title - }; Self { id: tool_call.id, - label: cx - .new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)), + label: cx.new(|cx| { + Markdown::new( + tool_call.title.into(), + Some(language_registry.clone()), + None, + cx, + ) + }), kind: tool_call.kind, content: tool_call .content @@ -232,11 +233,7 @@ impl ToolCall { if let Some(title) = title { self.label.update(cx, |label, cx| { - if let Some((first_line, _)) = title.split_once("\n") { - label.replace(first_line.to_owned() + "…", cx) - } else { - label.replace(title, cx); - } + label.replace(title, cx); }); } @@ -512,7 +509,7 @@ impl ContentBlock { "`Image`".into() } - pub fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str { + fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str { match self { ContentBlock::Empty => "", ContentBlock::Markdown { markdown } => markdown.read(cx).source(), @@ -759,8 +756,6 @@ pub struct AcpThread { connection: Rc, session_id: acp::SessionId, token_usage: Option, - prompt_capabilities: acp::PromptCapabilities, - _observe_prompt_capabilities: Task>, } #[derive(Debug)] @@ -775,12 +770,11 @@ pub enum AcpThreadEvent { Stopped, Error, LoadError(LoadError), - PromptCapabilitiesUpdated, } impl EventEmitter for AcpThread {} -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq)] pub enum ThreadStatus { Idle, WaitingForToolConfirmation, @@ -827,20 +821,7 @@ impl AcpThread { project: Entity, action_log: Entity, session_id: acp::SessionId, - mut prompt_capabilities_rx: watch::Receiver, - cx: &mut Context, ) -> Self { - let prompt_capabilities = *prompt_capabilities_rx.borrow(); - let task = cx.spawn::<_, anyhow::Result<()>>(async move |this, cx| { - loop { - let caps = prompt_capabilities_rx.recv().await?; - this.update(cx, |this, cx| { - this.prompt_capabilities = caps; - cx.emit(AcpThreadEvent::PromptCapabilitiesUpdated); - })?; - } - }); - Self { action_log, shared_buffers: Default::default(), @@ -852,15 +833,9 @@ impl AcpThread { connection, session_id, token_usage: None, - prompt_capabilities, - _observe_prompt_capabilities: task, } } - pub fn prompt_capabilities(&self) -> acp::PromptCapabilities { - self.prompt_capabilities - } - pub fn connection(&self) -> &Rc { &self.connection } @@ -1398,10 +1373,6 @@ impl AcpThread { }) } - pub fn can_resume(&self, cx: &App) -> bool { - self.connection.resume(&self.session_id, cx).is_some() - } - pub fn resume(&mut self, cx: &mut Context) -> BoxFuture<'static, Result<()>> { self.run_turn(cx, async move |this, cx| { this.update(cx, |this, cx| { @@ -2624,19 +2595,13 @@ mod tests { .into(), ); let action_log = cx.new(|_| ActionLog::new(project.clone())); - let thread = cx.new(|cx| { + let thread = cx.new(|_cx| { AcpThread::new( "Test", self.clone(), project, action_log, session_id.clone(), - watch::Receiver::constant(acp::PromptCapabilities { - image: true, - audio: true, - embedded_context: true, - }), - cx, ) }); self.sessions.lock().insert(session_id, thread.downgrade()); @@ -2670,6 +2635,14 @@ mod tests { } } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + } + } + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { let sessions = self.sessions.lock(); let thread = sessions.get(session_id).unwrap().clone(); @@ -2686,7 +2659,7 @@ mod tests { fn truncate( &self, session_id: &acp::SessionId, - _cx: &App, + _cx: &mut App, ) -> Option> { Some(Rc::new(FakeAgentSessionEditor { _session_id: session_id.clone(), diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index af229b7545..91e46dbac1 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -38,10 +38,12 @@ pub trait AgentConnection { cx: &mut App, ) -> Task>; + fn prompt_capabilities(&self) -> acp::PromptCapabilities; + fn resume( &self, _session_id: &acp::SessionId, - _cx: &App, + _cx: &mut App, ) -> Option> { None } @@ -51,7 +53,7 @@ pub trait AgentConnection { fn truncate( &self, _session_id: &acp::SessionId, - _cx: &App, + _cx: &mut App, ) -> Option> { None } @@ -59,7 +61,7 @@ pub trait AgentConnection { fn set_title( &self, _session_id: &acp::SessionId, - _cx: &App, + _cx: &mut App, ) -> Option> { None } @@ -327,19 +329,13 @@ mod test_support { ) -> Task>> { let session_id = acp::SessionId(self.sessions.lock().len().to_string().into()); let action_log = cx.new(|_| ActionLog::new(project.clone())); - let thread = cx.new(|cx| { + let thread = cx.new(|_cx| { AcpThread::new( "Test", self.clone(), project, action_log, session_id.clone(), - watch::Receiver::constant(acp::PromptCapabilities { - image: true, - audio: true, - embedded_context: true, - }), - cx, ) }); self.sessions.lock().insert( @@ -352,6 +348,14 @@ mod test_support { Task::ready(Ok(thread)) } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + } + } + fn authenticate( &self, _method_id: acp::AuthMethodId, @@ -435,7 +439,7 @@ mod test_support { fn truncate( &self, _session_id: &agent_client_protocol::SessionId, - _cx: &App, + _cx: &mut App, ) -> Option> { Some(Rc::new(StubAgentSessionEditor)) } diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index 6fa0887e22..a1e713cffa 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -5,7 +5,7 @@ use prompt_store::{PromptId, UserPromptId}; use serde::{Deserialize, Serialize}; use std::{ fmt, - ops::RangeInclusive, + ops::Range, path::{Path, PathBuf}, str::FromStr, }; @@ -17,14 +17,13 @@ pub enum MentionUri { File { abs_path: PathBuf, }, - PastedImage, Directory { abs_path: PathBuf, }, Symbol { - abs_path: PathBuf, + path: PathBuf, name: String, - line_range: RangeInclusive, + line_range: Range, }, Thread { id: acp::SessionId, @@ -39,9 +38,8 @@ pub enum MentionUri { name: String, }, Selection { - #[serde(default, skip_serializing_if = "Option::is_none")] - abs_path: Option, - line_range: RangeInclusive, + path: PathBuf, + line_range: Range, }, Fetch { url: Url, @@ -50,44 +48,36 @@ pub enum MentionUri { impl MentionUri { pub fn parse(input: &str) -> Result { - fn parse_line_range(fragment: &str) -> Result> { - let range = fragment - .strip_prefix("L") - .context("Line range must start with \"L\"")?; - let (start, end) = range - .split_once(":") - .context("Line range must use colon as separator")?; - let range = start - .parse::() - .context("Parsing line range start")? - .checked_sub(1) - .context("Line numbers should be 1-based")? - ..=end - .parse::() - .context("Parsing line range end")? - .checked_sub(1) - .context("Line numbers should be 1-based")?; - Ok(range) - } - let url = url::Url::parse(input)?; let path = url.path(); match url.scheme() { "file" => { let path = url.to_file_path().ok().context("Extracting file path")?; if let Some(fragment) = url.fragment() { - let line_range = parse_line_range(fragment)?; + let range = fragment + .strip_prefix("L") + .context("Line range must start with \"L\"")?; + let (start, end) = range + .split_once(":") + .context("Line range must use colon as separator")?; + let line_range = start + .parse::() + .context("Parsing line range start")? + .checked_sub(1) + .context("Line numbers should be 1-based")? + ..end + .parse::() + .context("Parsing line range end")? + .checked_sub(1) + .context("Line numbers should be 1-based")?; if let Some(name) = single_query_param(&url, "symbol")? { Ok(Self::Symbol { name, - abs_path: path, + path, line_range, }) } else { - Ok(Self::Selection { - abs_path: Some(path), - line_range, - }) + Ok(Self::Selection { path, line_range }) } } else if input.ends_with("/") { Ok(Self::Directory { abs_path: path }) @@ -115,17 +105,6 @@ impl MentionUri { id: rule_id.into(), name, }) - } else if path.starts_with("/agent/pasted-image") { - Ok(Self::PastedImage) - } else if path.starts_with("/agent/untitled-buffer") { - let fragment = url - .fragment() - .context("Missing fragment for untitled buffer selection")?; - let line_range = parse_line_range(fragment)?; - Ok(Self::Selection { - abs_path: None, - line_range, - }) } else { bail!("invalid zed url: {:?}", input); } @@ -142,16 +121,13 @@ impl MentionUri { .unwrap_or_default() .to_string_lossy() .into_owned(), - MentionUri::PastedImage => "Image".to_string(), MentionUri::Symbol { name, .. } => name.clone(), MentionUri::Thread { name, .. } => name.clone(), MentionUri::TextThread { name, .. } => name.clone(), MentionUri::Rule { name, .. } => name.clone(), MentionUri::Selection { - abs_path: path, - line_range, - .. - } => selection_name(path.as_deref(), line_range), + path, line_range, .. + } => selection_name(path, line_range), MentionUri::Fetch { url } => url.to_string(), } } @@ -161,7 +137,6 @@ impl MentionUri { MentionUri::File { abs_path } => { FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into()) } - MentionUri::PastedImage => IconName::Image.path().into(), MentionUri::Directory { .. } => FileIcons::get_folder_icon(false, cx) .unwrap_or_else(|| IconName::Folder.path().into()), MentionUri::Symbol { .. } => IconName::Code.path().into(), @@ -182,40 +157,29 @@ impl MentionUri { MentionUri::File { abs_path } => { Url::from_file_path(abs_path).expect("mention path should be absolute") } - MentionUri::PastedImage => Url::parse("zed:///agent/pasted-image").unwrap(), MentionUri::Directory { abs_path } => { Url::from_directory_path(abs_path).expect("mention path should be absolute") } MentionUri::Symbol { - abs_path, + path, name, line_range, } => { - let mut url = - Url::from_file_path(abs_path).expect("mention path should be absolute"); + let mut url = Url::from_file_path(path).expect("mention path should be absolute"); url.query_pairs_mut().append_pair("symbol", name); url.set_fragment(Some(&format!( "L{}:{}", - line_range.start() + 1, - line_range.end() + 1 + line_range.start + 1, + line_range.end + 1 ))); url } - MentionUri::Selection { - abs_path: path, - line_range, - } => { - let mut url = if let Some(path) = path { - Url::from_file_path(path).expect("mention path should be absolute") - } else { - let mut url = Url::parse("zed:///").unwrap(); - url.set_path("/agent/untitled-buffer"); - url - }; + MentionUri::Selection { path, line_range } => { + let mut url = Url::from_file_path(path).expect("mention path should be absolute"); url.set_fragment(Some(&format!( "L{}:{}", - line_range.start() + 1, - line_range.end() + 1 + line_range.start + 1, + line_range.end + 1 ))); url } @@ -227,10 +191,7 @@ impl MentionUri { } MentionUri::TextThread { path, name } => { let mut url = Url::parse("zed:///").unwrap(); - url.set_path(&format!( - "/agent/text-thread/{}", - path.to_string_lossy().trim_start_matches('/') - )); + url.set_path(&format!("/agent/text-thread/{}", path.to_string_lossy())); url.query_pairs_mut().append_pair("name", name); url } @@ -276,14 +237,12 @@ fn single_query_param(url: &Url, name: &'static str) -> Result> { } } -pub fn selection_name(path: Option<&Path>, line_range: &RangeInclusive) -> String { +pub fn selection_name(path: &Path, line_range: &Range) -> String { format!( "{} ({}:{})", - path.and_then(|path| path.file_name()) - .unwrap_or("Untitled".as_ref()) - .display(), - *line_range.start() + 1, - *line_range.end() + 1 + path.file_name().unwrap_or_default().display(), + line_range.start + 1, + line_range.end + 1 ) } @@ -343,14 +302,14 @@ mod tests { let parsed = MentionUri::parse(symbol_uri).unwrap(); match &parsed { MentionUri::Symbol { - abs_path: path, + path, name, line_range, } => { assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs")); assert_eq!(name, "MySymbol"); - assert_eq!(line_range.start(), &9); - assert_eq!(line_range.end(), &19); + assert_eq!(line_range.start, 9); + assert_eq!(line_range.end, 19); } _ => panic!("Expected Symbol variant"), } @@ -362,39 +321,16 @@ mod tests { let selection_uri = uri!("file:///path/to/file.rs#L5:15"); let parsed = MentionUri::parse(selection_uri).unwrap(); match &parsed { - MentionUri::Selection { - abs_path: path, - line_range, - } => { - assert_eq!( - path.as_ref().unwrap().to_str().unwrap(), - path!("/path/to/file.rs") - ); - assert_eq!(line_range.start(), &4); - assert_eq!(line_range.end(), &14); + MentionUri::Selection { path, line_range } => { + assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs")); + assert_eq!(line_range.start, 4); + assert_eq!(line_range.end, 14); } _ => panic!("Expected Selection variant"), } assert_eq!(parsed.to_uri().to_string(), selection_uri); } - #[test] - fn test_parse_untitled_selection_uri() { - let selection_uri = uri!("zed:///agent/untitled-buffer#L1:10"); - let parsed = MentionUri::parse(selection_uri).unwrap(); - match &parsed { - MentionUri::Selection { - abs_path: None, - line_range, - } => { - assert_eq!(line_range.start(), &0); - assert_eq!(line_range.end(), &9); - } - _ => panic!("Expected Selection variant without path"), - } - assert_eq!(parsed.to_uri().to_string(), selection_uri); - } - #[test] fn test_parse_thread_uri() { let thread_uri = "zed:///agent/thread/session123?name=Thread+name"; diff --git a/crates/acp_tools/Cargo.toml b/crates/acp_tools/Cargo.toml deleted file mode 100644 index 7a6d8c21a0..0000000000 --- a/crates/acp_tools/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "acp_tools" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - - -[lints] -workspace = true - -[lib] -path = "src/acp_tools.rs" -doctest = false - -[dependencies] -agent-client-protocol.workspace = true -collections.workspace = true -gpui.workspace = true -language.workspace= true -markdown.workspace = true -project.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -workspace-hack.workspace = true -workspace.workspace = true diff --git a/crates/acp_tools/LICENSE-GPL b/crates/acp_tools/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/acp_tools/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/acp_tools/src/acp_tools.rs b/crates/acp_tools/src/acp_tools.rs deleted file mode 100644 index e20a040e9d..0000000000 --- a/crates/acp_tools/src/acp_tools.rs +++ /dev/null @@ -1,494 +0,0 @@ -use std::{ - cell::RefCell, - collections::HashSet, - fmt::Display, - rc::{Rc, Weak}, - sync::Arc, -}; - -use agent_client_protocol as acp; -use collections::HashMap; -use gpui::{ - App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, ListState, - StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list, prelude::*, -}; -use language::LanguageRegistry; -use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle}; -use project::Project; -use settings::Settings; -use theme::ThemeSettings; -use ui::prelude::*; -use util::ResultExt as _; -use workspace::{Item, Workspace}; - -actions!(dev, [OpenAcpLogs]); - -pub fn init(cx: &mut App) { - cx.observe_new( - |workspace: &mut Workspace, _window, _cx: &mut Context| { - workspace.register_action(|workspace, _: &OpenAcpLogs, window, cx| { - let acp_tools = - Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx))); - workspace.add_item_to_active_pane(acp_tools, None, true, window, cx); - }); - }, - ) - .detach(); -} - -struct GlobalAcpConnectionRegistry(Entity); - -impl Global for GlobalAcpConnectionRegistry {} - -#[derive(Default)] -pub struct AcpConnectionRegistry { - active_connection: RefCell>, -} - -struct ActiveConnection { - server_name: SharedString, - connection: Weak, -} - -impl AcpConnectionRegistry { - pub fn default_global(cx: &mut App) -> Entity { - if cx.has_global::() { - cx.global::().0.clone() - } else { - let registry = cx.new(|_cx| AcpConnectionRegistry::default()); - cx.set_global(GlobalAcpConnectionRegistry(registry.clone())); - registry - } - } - - pub fn set_active_connection( - &self, - server_name: impl Into, - connection: &Rc, - cx: &mut Context, - ) { - self.active_connection.replace(Some(ActiveConnection { - server_name: server_name.into(), - connection: Rc::downgrade(connection), - })); - cx.notify(); - } -} - -struct AcpTools { - project: Entity, - focus_handle: FocusHandle, - expanded: HashSet, - watched_connection: Option, - connection_registry: Entity, - _subscription: Subscription, -} - -struct WatchedConnection { - server_name: SharedString, - messages: Vec, - list_state: ListState, - connection: Weak, - incoming_request_methods: HashMap>, - outgoing_request_methods: HashMap>, - _task: Task<()>, -} - -impl AcpTools { - fn new(project: Entity, cx: &mut Context) -> Self { - let connection_registry = AcpConnectionRegistry::default_global(cx); - - let subscription = cx.observe(&connection_registry, |this, _, cx| { - this.update_connection(cx); - cx.notify(); - }); - - let mut this = Self { - project, - focus_handle: cx.focus_handle(), - expanded: HashSet::default(), - watched_connection: None, - connection_registry, - _subscription: subscription, - }; - this.update_connection(cx); - this - } - - fn update_connection(&mut self, cx: &mut Context) { - let active_connection = self.connection_registry.read(cx).active_connection.borrow(); - let Some(active_connection) = active_connection.as_ref() else { - return; - }; - - if let Some(watched_connection) = self.watched_connection.as_ref() { - if Weak::ptr_eq( - &watched_connection.connection, - &active_connection.connection, - ) { - return; - } - } - - if let Some(connection) = active_connection.connection.upgrade() { - let mut receiver = connection.subscribe(); - let task = cx.spawn(async move |this, cx| { - while let Ok(message) = receiver.recv().await { - this.update(cx, |this, cx| { - this.push_stream_message(message, cx); - }) - .ok(); - } - }); - - self.watched_connection = Some(WatchedConnection { - server_name: active_connection.server_name.clone(), - messages: vec![], - list_state: ListState::new(0, ListAlignment::Bottom, px(2048.)), - connection: active_connection.connection.clone(), - incoming_request_methods: HashMap::default(), - outgoing_request_methods: HashMap::default(), - _task: task, - }); - } - } - - fn push_stream_message(&mut self, stream_message: acp::StreamMessage, cx: &mut Context) { - let Some(connection) = self.watched_connection.as_mut() else { - return; - }; - let language_registry = self.project.read(cx).languages().clone(); - let index = connection.messages.len(); - - let (request_id, method, message_type, params) = match stream_message.message { - acp::StreamMessageContent::Request { id, method, params } => { - let method_map = match stream_message.direction { - acp::StreamMessageDirection::Incoming => { - &mut connection.incoming_request_methods - } - acp::StreamMessageDirection::Outgoing => { - &mut connection.outgoing_request_methods - } - }; - - method_map.insert(id, method.clone()); - (Some(id), method.into(), MessageType::Request, Ok(params)) - } - acp::StreamMessageContent::Response { id, result } => { - let method_map = match stream_message.direction { - acp::StreamMessageDirection::Incoming => { - &mut connection.outgoing_request_methods - } - acp::StreamMessageDirection::Outgoing => { - &mut connection.incoming_request_methods - } - }; - - if let Some(method) = method_map.remove(&id) { - (Some(id), method.into(), MessageType::Response, result) - } else { - ( - Some(id), - "[unrecognized response]".into(), - MessageType::Response, - result, - ) - } - } - acp::StreamMessageContent::Notification { method, params } => { - (None, method.into(), MessageType::Notification, Ok(params)) - } - }; - - let message = WatchedConnectionMessage { - name: method, - message_type, - request_id, - direction: stream_message.direction, - collapsed_params_md: match params.as_ref() { - Ok(params) => params - .as_ref() - .map(|params| collapsed_params_md(params, &language_registry, cx)), - Err(err) => { - if let Ok(err) = &serde_json::to_value(err) { - Some(collapsed_params_md(&err, &language_registry, cx)) - } else { - None - } - } - }, - - expanded_params_md: None, - params, - }; - - connection.messages.push(message); - connection.list_state.splice(index..index, 1); - cx.notify(); - } - - fn render_message( - &mut self, - index: usize, - window: &mut Window, - cx: &mut Context, - ) -> AnyElement { - let Some(connection) = self.watched_connection.as_ref() else { - return Empty.into_any(); - }; - - let Some(message) = connection.messages.get(index) else { - return Empty.into_any(); - }; - - let base_size = TextSize::Editor.rems(cx); - - let theme_settings = ThemeSettings::get_global(cx); - let text_style = window.text_style(); - - let colors = cx.theme().colors(); - let expanded = self.expanded.contains(&index); - - v_flex() - .w_full() - .px_4() - .py_3() - .border_color(colors.border) - .border_b_1() - .gap_2() - .items_start() - .font_buffer(cx) - .text_size(base_size) - .id(index) - .group("message") - .hover(|this| this.bg(colors.element_background.opacity(0.5))) - .on_click(cx.listener(move |this, _, _, cx| { - if this.expanded.contains(&index) { - this.expanded.remove(&index); - } else { - this.expanded.insert(index); - let Some(connection) = &mut this.watched_connection else { - return; - }; - let Some(message) = connection.messages.get_mut(index) else { - return; - }; - message.expanded(this.project.read(cx).languages().clone(), cx); - connection.list_state.scroll_to_reveal_item(index); - } - cx.notify() - })) - .child( - h_flex() - .w_full() - .gap_2() - .items_center() - .flex_shrink_0() - .child(match message.direction { - acp::StreamMessageDirection::Incoming => { - ui::Icon::new(ui::IconName::ArrowDown).color(Color::Error) - } - acp::StreamMessageDirection::Outgoing => { - ui::Icon::new(ui::IconName::ArrowUp).color(Color::Success) - } - }) - .child( - Label::new(message.name.clone()) - .buffer_font(cx) - .color(Color::Muted), - ) - .child(div().flex_1()) - .child( - div() - .child(ui::Chip::new(message.message_type.to_string())) - .visible_on_hover("message"), - ) - .children( - message - .request_id - .map(|req_id| div().child(ui::Chip::new(req_id.to_string()))), - ), - ) - // I'm aware using markdown is a hack. Trying to get something working for the demo. - // Will clean up soon! - .when_some( - if expanded { - message.expanded_params_md.clone() - } else { - message.collapsed_params_md.clone() - }, - |this, params| { - this.child( - div().pl_6().w_full().child( - MarkdownElement::new( - params, - MarkdownStyle { - base_text_style: text_style, - selection_background_color: colors.element_selection_background, - syntax: cx.theme().syntax().clone(), - code_block_overflow_x_scroll: true, - code_block: StyleRefinement { - text: Some(TextStyleRefinement { - font_family: Some( - theme_settings.buffer_font.family.clone(), - ), - font_size: Some((base_size * 0.8).into()), - ..Default::default() - }), - ..Default::default() - }, - ..Default::default() - }, - ) - .code_block_renderer( - CodeBlockRenderer::Default { - copy_button: false, - copy_button_on_hover: expanded, - border: false, - }, - ), - ), - ) - }, - ) - .into_any() - } -} - -struct WatchedConnectionMessage { - name: SharedString, - request_id: Option, - direction: acp::StreamMessageDirection, - message_type: MessageType, - params: Result, acp::Error>, - collapsed_params_md: Option>, - expanded_params_md: Option>, -} - -impl WatchedConnectionMessage { - fn expanded(&mut self, language_registry: Arc, cx: &mut App) { - let params_md = match &self.params { - Ok(Some(params)) => Some(expanded_params_md(params, &language_registry, cx)), - Err(err) => { - if let Some(err) = &serde_json::to_value(err).log_err() { - Some(expanded_params_md(&err, &language_registry, cx)) - } else { - None - } - } - _ => None, - }; - self.expanded_params_md = params_md; - } -} - -fn collapsed_params_md( - params: &serde_json::Value, - language_registry: &Arc, - cx: &mut App, -) -> Entity { - let params_json = serde_json::to_string(params).unwrap_or_default(); - let mut spaced_out_json = String::with_capacity(params_json.len() + params_json.len() / 4); - - for ch in params_json.chars() { - match ch { - '{' => spaced_out_json.push_str("{ "), - '}' => spaced_out_json.push_str(" }"), - ':' => spaced_out_json.push_str(": "), - ',' => spaced_out_json.push_str(", "), - c => spaced_out_json.push(c), - } - } - - let params_md = format!("```json\n{}\n```", spaced_out_json); - cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx)) -} - -fn expanded_params_md( - params: &serde_json::Value, - language_registry: &Arc, - cx: &mut App, -) -> Entity { - let params_json = serde_json::to_string_pretty(params).unwrap_or_default(); - let params_md = format!("```json\n{}\n```", params_json); - cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx)) -} - -enum MessageType { - Request, - Response, - Notification, -} - -impl Display for MessageType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - MessageType::Request => write!(f, "Request"), - MessageType::Response => write!(f, "Response"), - MessageType::Notification => write!(f, "Notification"), - } - } -} - -enum AcpToolsEvent {} - -impl EventEmitter for AcpTools {} - -impl Item for AcpTools { - type Event = AcpToolsEvent; - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString { - format!( - "ACP: {}", - self.watched_connection - .as_ref() - .map_or("Disconnected", |connection| &connection.server_name) - ) - .into() - } - - fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { - Some(ui::Icon::new(IconName::Thread)) - } -} - -impl Focusable for AcpTools { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for AcpTools { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() - .track_focus(&self.focus_handle) - .size_full() - .bg(cx.theme().colors().editor_background) - .child(match self.watched_connection.as_ref() { - Some(connection) => { - if connection.messages.is_empty() { - h_flex() - .size_full() - .justify_center() - .items_center() - .child("No messages recorded yet") - .into_any() - } else { - list( - connection.list_state.clone(), - cx.processor(Self::render_message), - ) - .with_sizing_behavior(gpui::ListSizingBehavior::Auto) - .flex_grow() - .into_any() - } - } - None => h_flex() - .size_full() - .justify_center() - .items_center() - .child("No active connection") - .into_any(), - }) - } -} diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 7b70fde56a..899e360ab0 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -664,7 +664,7 @@ impl Thread { } pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option { - if self.configured_model.is_none() { + if self.configured_model.is_none() || self.messages.is_empty() { self.configured_model = LanguageModelRegistry::read_global(cx).default_model(); } self.configured_model.clone() @@ -2097,7 +2097,7 @@ impl Thread { } pub fn summarize(&mut self, cx: &mut Context) { - let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else { + let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model(cx) else { println!("No thread summary model"); return; }; @@ -2416,7 +2416,7 @@ impl Thread { } let Some(ConfiguredModel { model, provider }) = - LanguageModelRegistry::read_global(cx).thread_summary_model() + LanguageModelRegistry::read_global(cx).thread_summary_model(cx) else { return; }; @@ -5410,13 +5410,10 @@ fn main() {{ }), cx, ); - registry.set_thread_summary_model( - Some(ConfiguredModel { - provider, - model: model.clone(), - }), - cx, - ); + registry.set_thread_summary_model(Some(ConfiguredModel { + provider, + model: model.clone(), + })); }) }); diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index cba2457566..45e551dbdf 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -893,19 +893,8 @@ impl ThreadsDatabase { let needs_migration_from_heed = mdb_path.exists(); - let connection = if *ZED_STATELESS { + let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) { Connection::open_memory(Some("THREAD_FALLBACK_DB")) - } else if cfg!(any(feature = "test-support", test)) { - // rust stores the name of the test on the current thread. - // We use this to automatically create a database that will - // be shared within the test (for the test_retrieve_old_thread) - // but not with concurrent tests. - let thread = std::thread::current(); - let test_name = thread.name(); - Connection::open_memory(Some(&format!( - "THREAD_FALLBACK_{}", - test_name.unwrap_or_default() - ))) } else { Connection::open_file(&sqlite_path.to_string_lossy()) }; diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 6fa36d33d5..4eaf87e218 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -180,7 +180,7 @@ impl NativeAgent { fs: Arc, cx: &mut AsyncApp, ) -> Result> { - log::debug!("Creating new NativeAgent"); + log::info!("Creating new NativeAgent"); let project_context = cx .update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))? @@ -228,7 +228,7 @@ impl NativeAgent { ) -> Entity { let connection = Rc::new(NativeAgentConnection(cx.entity())); let registry = LanguageModelRegistry::read_global(cx); - let summarization_model = registry.thread_summary_model().map(|c| c.model); + let summarization_model = registry.thread_summary_model(cx).map(|c| c.model); thread_handle.update(cx, |thread, cx| { thread.set_summarization_model(summarization_model, cx); @@ -240,16 +240,13 @@ impl NativeAgent { let title = thread.title(); let project = thread.project.clone(); let action_log = thread.action_log.clone(); - let prompt_capabilities_rx = thread.prompt_capabilities_rx.clone(); - let acp_thread = cx.new(|cx| { + let acp_thread = cx.new(|_cx| { acp_thread::AcpThread::new( title, connection, project.clone(), action_log.clone(), session_id.clone(), - prompt_capabilities_rx, - cx, ) }); let subscriptions = vec![ @@ -524,7 +521,7 @@ impl NativeAgent { let registry = LanguageModelRegistry::read_global(cx); let default_model = registry.default_model().map(|m| m.model); - let summarization_model = registry.thread_summary_model().map(|m| m.model); + let summarization_model = registry.thread_summary_model(cx).map(|m| m.model); for session in self.sessions.values_mut() { session.thread.update(cx, |thread, cx| { @@ -759,7 +756,7 @@ impl NativeAgentConnection { } } - log::debug!("Response stream completed"); + log::info!("Response stream completed"); anyhow::Ok(acp::PromptResponse { stop_reason: acp::StopReason::EndTurn, }) @@ -784,7 +781,7 @@ impl AgentModelSelector for NativeAgentConnection { model_id: acp_thread::AgentModelId, cx: &mut App, ) -> Task> { - log::debug!("Setting model for session {}: {}", session_id, model_id); + log::info!("Setting model for session {}: {}", session_id, model_id); let Some(thread) = self .0 .read(cx) @@ -855,7 +852,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { cx: &mut App, ) -> Task>> { let agent = self.0.clone(); - log::debug!("Creating new thread for project at: {:?}", cwd); + log::info!("Creating new thread for project at: {:?}", cwd); cx.spawn(async move |cx| { log::debug!("Starting thread creation in async context"); @@ -920,7 +917,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { .into_iter() .map(Into::into) .collect::>(); - log::debug!("Converted prompt to message: {} chars", content.len()); + log::info!("Converted prompt to message: {} chars", content.len()); log::debug!("Message id: {:?}", id); log::debug!("Message content: {:?}", content); @@ -928,10 +925,18 @@ impl acp_thread::AgentConnection for NativeAgentConnection { }) } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: true, + audio: false, + embedded_context: true, + } + } + fn resume( &self, session_id: &acp::SessionId, - _cx: &App, + _cx: &mut App, ) -> Option> { Some(Rc::new(NativeAgentSessionResume { connection: self.clone(), @@ -951,9 +956,9 @@ impl acp_thread::AgentConnection for NativeAgentConnection { fn truncate( &self, session_id: &agent_client_protocol::SessionId, - cx: &App, + cx: &mut App, ) -> Option> { - self.0.read_with(cx, |agent, _cx| { + self.0.update(cx, |agent, _cx| { agent.sessions.get(session_id).map(|session| { Rc::new(NativeAgentSessionEditor { thread: session.thread.clone(), @@ -966,7 +971,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { fn set_title( &self, session_id: &acp::SessionId, - _cx: &App, + _cx: &mut App, ) -> Option> { Some(Rc::new(NativeAgentSessionSetTitle { connection: self.clone(), diff --git a/crates/agent2/src/db.rs b/crates/agent2/src/db.rs index e7d31c0c7a..1b88955a24 100644 --- a/crates/agent2/src/db.rs +++ b/crates/agent2/src/db.rs @@ -266,19 +266,8 @@ impl ThreadsDatabase { } pub fn new(executor: BackgroundExecutor) -> Result { - let connection = if *ZED_STATELESS { + let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) { Connection::open_memory(Some("THREAD_FALLBACK_DB")) - } else if cfg!(any(feature = "test-support", test)) { - // rust stores the name of the test on the current thread. - // We use this to automatically create a database that will - // be shared within the test (for the test_retrieve_old_thread) - // but not with concurrent tests. - let thread = std::thread::current(); - let test_name = thread.name(); - Connection::open_memory(Some(&format!( - "THREAD_FALLBACK_{}", - test_name.unwrap_or_default() - ))) } else { let threads_dir = paths::data_dir().join("threads"); std::fs::create_dir_all(&threads_dir)?; diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index 9ff98ccd18..ac5aa95c04 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -3,7 +3,7 @@ use std::{any::Any, path::Path, rc::Rc, sync::Arc}; use agent_servers::AgentServer; use anyhow::Result; use fs::Fs; -use gpui::{App, Entity, SharedString, Task}; +use gpui::{App, Entity, Task}; use project::Project; use prompt_store::PromptStore; @@ -22,20 +22,16 @@ impl NativeAgentServer { } impl AgentServer for NativeAgentServer { - fn telemetry_id(&self) -> &'static str { - "zed" + fn name(&self) -> &'static str { + "Native Agent" } - fn name(&self) -> SharedString { - "Zed Agent".into() + fn empty_state_headline(&self) -> &'static str { + "Welcome to the Agent Panel" } - fn empty_state_headline(&self) -> SharedString { - self.name() - } - - fn empty_state_message(&self) -> SharedString { - "".into() + fn empty_state_message(&self) -> &'static str { + "" } fn logo(&self) -> ui::IconName { @@ -48,7 +44,7 @@ impl AgentServer for NativeAgentServer { project: &Entity, cx: &mut App, ) -> Task>> { - log::debug!( + log::info!( "NativeAgentServer::connect called for path: {:?}", _root_dir ); @@ -67,7 +63,7 @@ impl AgentServer for NativeAgentServer { // Create the connection wrapper let connection = NativeAgentConnection(agent); - log::debug!("NativeAgentServer connection established successfully"); + log::info!("NativeAgentServer connection established successfully"); Ok(Rc::new(connection) as Rc) }) diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index fbeee46a48..60b3198081 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -4,8 +4,6 @@ use agent_client_protocol::{self as acp}; use agent_settings::AgentProfileId; use anyhow::Result; use client::{Client, UserStore}; -use cloud_llm_client::CompletionIntent; -use collections::IndexMap; use context_server::{ContextServer, ContextServerCommand, ContextServerId}; use fs::{FakeFs, Fs}; use futures::{ @@ -72,7 +70,6 @@ async fn test_echo(cx: &mut TestAppContext) { } #[gpui::test] -#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows async fn test_thinking(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); @@ -472,7 +469,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { tool_name: ToolRequiringPermission::name().into(), is_error: true, content: "Permission to run tool denied by user".into(), - output: Some("Permission to run tool denied by user".into()) + output: None }) ] ); @@ -675,6 +672,15 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) { "} ) }); + + // Ensure we error if calling resume when tool use limit was *not* reached. + let error = thread + .update(cx, |thread, cx| thread.resume(cx)) + .unwrap_err(); + assert_eq!( + error.to_string(), + "can only resume after tool use limit is reached" + ) } #[gpui::test] @@ -1348,7 +1354,6 @@ async fn test_cancellation(cx: &mut TestAppContext) { } #[gpui::test] -#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); @@ -1687,7 +1692,6 @@ async fn test_truncate_second_message(cx: &mut TestAppContext) { } #[gpui::test] -#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows async fn test_title_generation(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); @@ -1733,81 +1737,6 @@ async fn test_title_generation(cx: &mut TestAppContext) { thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world")); } -#[gpui::test] -async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) { - let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - let _events = thread - .update(cx, |thread, cx| { - thread.add_tool(ToolRequiringPermission); - thread.add_tool(EchoTool); - thread.send(UserMessageId::new(), ["Hey!"], cx) - }) - .unwrap(); - cx.run_until_parked(); - - let permission_tool_use = LanguageModelToolUse { - id: "tool_id_1".into(), - name: ToolRequiringPermission::name().into(), - raw_input: "{}".into(), - input: json!({}), - is_input_complete: true, - }; - let echo_tool_use = LanguageModelToolUse { - id: "tool_id_2".into(), - name: EchoTool::name().into(), - raw_input: json!({"text": "test"}).to_string(), - input: json!({"text": "test"}), - is_input_complete: true, - }; - fake_model.send_last_completion_stream_text_chunk("Hi!"); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( - permission_tool_use, - )); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( - echo_tool_use.clone(), - )); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - - // Ensure pending tools are skipped when building a request. - let request = thread - .read_with(cx, |thread, cx| { - thread.build_completion_request(CompletionIntent::EditFile, cx) - }) - .unwrap(); - assert_eq!( - request.messages[1..], - vec![ - LanguageModelRequestMessage { - role: Role::User, - content: vec!["Hey!".into()], - cache: true - }, - LanguageModelRequestMessage { - role: Role::Assistant, - content: vec![ - MessageContent::Text("Hi!".into()), - MessageContent::ToolUse(echo_tool_use.clone()) - ], - cache: false - }, - LanguageModelRequestMessage { - role: Role::User, - content: vec![MessageContent::ToolResult(LanguageModelToolResult { - tool_use_id: echo_tool_use.id.clone(), - tool_name: echo_tool_use.name, - is_error: false, - content: "test".into(), - output: Some("test".into()) - })], - cache: false - }, - ], - ); -} - #[gpui::test] async fn test_agent_connection(cx: &mut TestAppContext) { cx.update(settings::init); @@ -1822,11 +1751,11 @@ async fn test_agent_connection(cx: &mut TestAppContext) { let clock = Arc::new(clock::FakeSystemClock::new()); let client = Client::new(clock, http_client, cx); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); + Project::init_settings(cx); + agent_settings::init(cx); language_model::init(client.clone(), cx); language_models::init(user_store, client.clone(), cx); - Project::init_settings(cx); LanguageModelRegistry::test(cx); - agent_settings::init(cx); }); cx.executor().forbid_parking(); @@ -2100,7 +2029,6 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) { .unwrap(); cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Hey,"); fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded { provider: LanguageModelProviderName::new("Anthropic"), retry_after: Some(Duration::from_secs(3)), @@ -2110,9 +2038,8 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) { cx.executor().advance_clock(Duration::from_secs(3)); cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("there!"); + fake_model.send_last_completion_stream_text_chunk("Hey!"); fake_model.end_last_completion_stream(); - cx.run_until_parked(); let mut retry_events = Vec::new(); while let Some(Ok(event)) = events.next().await { @@ -2140,94 +2067,12 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) { ## Assistant - Hey, - - [resume] - - ## Assistant - - there! + Hey! "} ) }); } -#[gpui::test] -async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) { - let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - let events = thread - .update(cx, |thread, cx| { - thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx); - thread.add_tool(EchoTool); - thread.send(UserMessageId::new(), ["Call the echo tool!"], cx) - }) - .unwrap(); - cx.run_until_parked(); - - let tool_use_1 = LanguageModelToolUse { - id: "tool_1".into(), - name: EchoTool::name().into(), - raw_input: json!({"text": "test"}).to_string(), - input: json!({"text": "test"}), - is_input_complete: true, - }; - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( - tool_use_1.clone(), - )); - fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded { - provider: LanguageModelProviderName::new("Anthropic"), - retry_after: Some(Duration::from_secs(3)), - }); - fake_model.end_last_completion_stream(); - - cx.executor().advance_clock(Duration::from_secs(3)); - let completion = fake_model.pending_completions().pop().unwrap(); - assert_eq!( - completion.messages[1..], - vec![ - LanguageModelRequestMessage { - role: Role::User, - content: vec!["Call the echo tool!".into()], - cache: false - }, - LanguageModelRequestMessage { - role: Role::Assistant, - content: vec![language_model::MessageContent::ToolUse(tool_use_1.clone())], - cache: false - }, - LanguageModelRequestMessage { - role: Role::User, - content: vec![language_model::MessageContent::ToolResult( - LanguageModelToolResult { - tool_use_id: tool_use_1.id.clone(), - tool_name: tool_use_1.name.clone(), - is_error: false, - content: "test".into(), - output: Some("test".into()) - } - )], - cache: true - }, - ] - ); - - fake_model.send_last_completion_stream_text_chunk("Done"); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - events.collect::>().await; - thread.read_with(cx, |thread, _cx| { - assert_eq!( - thread.last_message(), - Some(Message::Agent(AgentMessage { - content: vec![AgentMessageContent::Text("Done".into())], - tool_results: IndexMap::default() - })) - ); - }) -} - #[gpui::test] async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) { let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 97ea1caf1d..c89e5875f9 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -45,15 +45,14 @@ use schemars::{JsonSchema, Schema}; use serde::{Deserialize, Serialize}; use settings::{Settings, update_settings_file}; use smol::stream::StreamExt; -use std::fmt::Write; use std::{ collections::BTreeMap, - ops::RangeInclusive, path::Path, sync::Arc, time::{Duration, Instant}, }; -use util::{ResultExt, debug_panic, markdown::MarkdownCodeBlock}; +use std::{fmt::Write, ops::Range}; +use util::{ResultExt, markdown::MarkdownCodeBlock}; use uuid::Uuid; const TOOL_CANCELED_MESSAGE: &str = "Tool canceled by user"; @@ -123,7 +122,7 @@ impl Message { match self { Message::User(message) => message.to_markdown(), Message::Agent(message) => message.to_markdown(), - Message::Resume => "[resume]\n".into(), + Message::Resume => "[resumed after tool use limit was reached]".into(), } } @@ -188,7 +187,6 @@ impl UserMessage { const OPEN_FILES_TAG: &str = ""; const OPEN_DIRECTORIES_TAG: &str = ""; const OPEN_SYMBOLS_TAG: &str = ""; - const OPEN_SELECTIONS_TAG: &str = ""; const OPEN_THREADS_TAG: &str = ""; const OPEN_FETCH_TAG: &str = ""; const OPEN_RULES_TAG: &str = @@ -197,7 +195,6 @@ impl UserMessage { let mut file_context = OPEN_FILES_TAG.to_string(); let mut directory_context = OPEN_DIRECTORIES_TAG.to_string(); let mut symbol_context = OPEN_SYMBOLS_TAG.to_string(); - let mut selection_context = OPEN_SELECTIONS_TAG.to_string(); let mut thread_context = OPEN_THREADS_TAG.to_string(); let mut fetch_context = OPEN_FETCH_TAG.to_string(); let mut rules_context = OPEN_RULES_TAG.to_string(); @@ -214,7 +211,7 @@ impl UserMessage { match uri { MentionUri::File { abs_path } => { write!( - &mut file_context, + &mut symbol_context, "\n{}", MarkdownCodeBlock { tag: &codeblock_tag(abs_path, None), @@ -223,19 +220,17 @@ impl UserMessage { ) .ok(); } - MentionUri::PastedImage => { - debug_panic!("pasted image URI should not be used in mention content") - } MentionUri::Directory { .. } => { write!(&mut directory_context, "\n{}\n", content).ok(); } MentionUri::Symbol { - abs_path: path, - line_range, - .. + path, line_range, .. + } + | MentionUri::Selection { + path, line_range, .. } => { write!( - &mut symbol_context, + &mut rules_context, "\n{}", MarkdownCodeBlock { tag: &codeblock_tag(path, Some(line_range)), @@ -244,24 +239,6 @@ impl UserMessage { ) .ok(); } - MentionUri::Selection { - abs_path: path, - line_range, - .. - } => { - write!( - &mut selection_context, - "\n{}", - MarkdownCodeBlock { - tag: &codeblock_tag( - path.as_deref().unwrap_or("Untitled".as_ref()), - Some(line_range) - ), - text: content - } - ) - .ok(); - } MentionUri::Thread { .. } => { write!(&mut thread_context, "\n{}\n", content).ok(); } @@ -314,13 +291,6 @@ impl UserMessage { .push(language_model::MessageContent::Text(symbol_context)); } - if selection_context.len() > OPEN_SELECTIONS_TAG.len() { - selection_context.push_str("\n"); - message - .content - .push(language_model::MessageContent::Text(selection_context)); - } - if thread_context.len() > OPEN_THREADS_TAG.len() { thread_context.push_str("\n"); message @@ -356,7 +326,7 @@ impl UserMessage { } } -fn codeblock_tag(full_path: &Path, line_range: Option<&RangeInclusive>) -> String { +fn codeblock_tag(full_path: &Path, line_range: Option<&Range>) -> String { let mut result = String::new(); if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) { @@ -366,10 +336,10 @@ fn codeblock_tag(full_path: &Path, line_range: Option<&RangeInclusive>) -> let _ = write!(result, "{}", full_path.display()); if let Some(range) = line_range { - if range.start() == range.end() { - let _ = write!(result, ":{}", range.start() + 1); + if range.start == range.end { + let _ = write!(result, ":{}", range.start + 1); } else { - let _ = write!(result, ":{}-{}", range.start() + 1, range.end() + 1); + let _ = write!(result, ":{}-{}", range.start + 1, range.end + 1); } } @@ -448,33 +418,24 @@ impl AgentMessage { cache: false, }; for chunk in &self.content { - match chunk { + let chunk = match chunk { AgentMessageContent::Text(text) => { - assistant_message - .content - .push(language_model::MessageContent::Text(text.clone())); + language_model::MessageContent::Text(text.clone()) } AgentMessageContent::Thinking { text, signature } => { - assistant_message - .content - .push(language_model::MessageContent::Thinking { - text: text.clone(), - signature: signature.clone(), - }); - } - AgentMessageContent::RedactedThinking(value) => { - assistant_message.content.push( - language_model::MessageContent::RedactedThinking(value.clone()), - ); - } - AgentMessageContent::ToolUse(tool_use) => { - if self.tool_results.contains_key(&tool_use.id) { - assistant_message - .content - .push(language_model::MessageContent::ToolUse(tool_use.clone())); + language_model::MessageContent::Thinking { + text: text.clone(), + signature: signature.clone(), } } + AgentMessageContent::RedactedThinking(value) => { + language_model::MessageContent::RedactedThinking(value.clone()) + } + AgentMessageContent::ToolUse(value) => { + language_model::MessageContent::ToolUse(value.clone()) + } }; + assistant_message.content.push(chunk); } let mut user_message = LanguageModelRequestMessage { @@ -575,22 +536,11 @@ pub struct Thread { templates: Arc, model: Option>, summarization_model: Option>, - prompt_capabilities_tx: watch::Sender, - pub(crate) prompt_capabilities_rx: watch::Receiver, pub(crate) project: Entity, pub(crate) action_log: Entity, } impl Thread { - fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities { - let image = model.map_or(true, |model| model.supports_images()); - acp::PromptCapabilities { - image, - audio: false, - embedded_context: true, - } - } - pub fn new( project: Entity, project_context: Entity, @@ -601,8 +551,6 @@ impl Thread { ) -> Self { let profile_id = AgentSettings::get_global(cx).default_profile.clone(); let action_log = cx.new(|_cx| ActionLog::new(project.clone())); - let (prompt_capabilities_tx, prompt_capabilities_rx) = - watch::channel(Self::prompt_capabilities(model.as_deref())); Self { id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()), prompt_id: PromptId::new(), @@ -630,8 +578,6 @@ impl Thread { templates, model, summarization_model: None, - prompt_capabilities_tx, - prompt_capabilities_rx, project, action_log, } @@ -732,17 +678,7 @@ impl Thread { stream.update_tool_call_fields( &tool_use.id, acp::ToolCallUpdateFields { - status: Some( - tool_result - .as_ref() - .map_or(acp::ToolCallStatus::Failed, |result| { - if result.is_error { - acp::ToolCallStatus::Failed - } else { - acp::ToolCallStatus::Completed - } - }), - ), + status: Some(acp::ToolCallStatus::Completed), raw_output: output, ..Default::default() }, @@ -775,8 +711,6 @@ impl Thread { .or_else(|| registry.default_model()) .map(|model| model.model) }); - let (prompt_capabilities_tx, prompt_capabilities_rx) = - watch::channel(Self::prompt_capabilities(model.as_deref())); Self { id, @@ -806,8 +740,6 @@ impl Thread { project, action_log, updated_at: db_thread.updated_at, - prompt_capabilities_tx, - prompt_capabilities_rx, } } @@ -975,12 +907,10 @@ impl Thread { pub fn set_model(&mut self, model: Arc, cx: &mut Context) { let old_usage = self.latest_token_usage(); self.model = Some(model); - let new_caps = Self::prompt_capabilities(self.model.as_deref()); let new_usage = self.latest_token_usage(); if old_usage != new_usage { cx.emit(TokenUsageUpdated(new_usage)); } - self.prompt_capabilities_tx.send(new_caps).log_err(); cx.notify() } @@ -1116,10 +1046,15 @@ impl Thread { &mut self, cx: &mut Context, ) -> Result>> { + anyhow::ensure!( + self.tool_use_limit_reached, + "can only resume after tool use limit is reached" + ); + self.messages.push(Message::Resume); cx.notify(); - log::debug!("Total messages in thread: {}", self.messages.len()); + log::info!("Total messages in thread: {}", self.messages.len()); self.run_turn(cx) } @@ -1137,7 +1072,7 @@ impl Thread { { let model = self.model().context("No language model configured")?; - log::info!("Thread::send called with model: {}", model.name().0); + log::info!("Thread::send called with model: {:?}", model.name()); self.advance_prompt_id(); let content = content.into_iter().map(Into::into).collect::>(); @@ -1147,7 +1082,7 @@ impl Thread { .push(Message::User(UserMessage { id, content })); cx.notify(); - log::debug!("Total messages in thread: {}", self.messages.len()); + log::info!("Total messages in thread: {}", self.messages.len()); self.run_turn(cx) } @@ -1171,14 +1106,44 @@ impl Thread { event_stream: event_stream.clone(), tools: self.enabled_tools(profile, &model, cx), _task: cx.spawn(async move |this, cx| { - log::debug!("Starting agent turn execution"); + log::info!("Starting agent turn execution"); - let turn_result = Self::run_turn_internal(&this, model, &event_stream, cx).await; + let turn_result: Result<()> = async { + let mut intent = CompletionIntent::UserPrompt; + loop { + Self::stream_completion(&this, &model, intent, &event_stream, cx).await?; + + let mut end_turn = true; + this.update(cx, |this, cx| { + // Generate title if needed. + if this.title.is_none() && this.pending_title_generation.is_none() { + this.generate_title(cx); + } + + // End the turn if the model didn't use tools. + let message = this.pending_message.as_ref(); + end_turn = + message.map_or(true, |message| message.tool_results.is_empty()); + this.flush_pending_message(cx); + })?; + + if this.read_with(cx, |this, _| this.tool_use_limit_reached)? { + log::info!("Tool use limit reached, completing turn"); + return Err(language_model::ToolUseLimitReachedError.into()); + } else if end_turn { + log::info!("No tool uses found, completing turn"); + return Ok(()); + } else { + intent = CompletionIntent::ToolResults; + } + } + } + .await; _ = this.update(cx, |this, cx| this.flush_pending_message(cx)); match turn_result { Ok(()) => { - log::debug!("Turn execution completed"); + log::info!("Turn execution completed"); event_stream.send_stop(acp::StopReason::EndTurn); } Err(error) => { @@ -1204,18 +1169,20 @@ impl Thread { Ok(events_rx) } - async fn run_turn_internal( + async fn stream_completion( this: &WeakEntity, - model: Arc, + model: &Arc, + completion_intent: CompletionIntent, event_stream: &ThreadEventStream, cx: &mut AsyncApp, ) -> Result<()> { - let mut attempt = 0; - let mut intent = CompletionIntent::UserPrompt; - loop { - let request = - this.update(cx, |this, cx| this.build_completion_request(intent, cx))??; + log::debug!("Stream completion started successfully"); + let request = this.update(cx, |this, cx| { + this.build_completion_request(completion_intent, cx) + })??; + let mut attempt = None; + 'retry: loop { telemetry::event!( "Agent Thread Completion", thread_id = this.read_with(cx, |this, _| this.id.to_string())?, @@ -1225,31 +1192,75 @@ impl Thread { attempt ); - log::debug!("Calling model.stream_completion, attempt {}", attempt); + log::info!( + "Calling model.stream_completion, attempt {}", + attempt.unwrap_or(0) + ); let mut events = model - .stream_completion(request, cx) + .stream_completion(request.clone(), cx) .await .map_err(|error| anyhow!(error))?; let mut tool_results = FuturesUnordered::new(); - let mut error = None; + while let Some(event) = events.next().await { - log::trace!("Received completion event: {:?}", event); match event { Ok(event) => { + log::trace!("Received completion event: {:?}", event); tool_results.extend(this.update(cx, |this, cx| { - this.handle_completion_event(event, event_stream, cx) + this.handle_streamed_completion_event(event, event_stream, cx) })??); } - Err(err) => { - error = Some(err); - break; + Err(error) => { + let completion_mode = + this.read_with(cx, |thread, _cx| thread.completion_mode())?; + if completion_mode == CompletionMode::Normal { + return Err(anyhow!(error))?; + } + + let Some(strategy) = Self::retry_strategy_for(&error) else { + return Err(anyhow!(error))?; + }; + + let max_attempts = match &strategy { + RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts, + RetryStrategy::Fixed { max_attempts, .. } => *max_attempts, + }; + + let attempt = attempt.get_or_insert(0u8); + + *attempt += 1; + + let attempt = *attempt; + if attempt > max_attempts { + return Err(anyhow!(error))?; + } + + let delay = match &strategy { + RetryStrategy::ExponentialBackoff { initial_delay, .. } => { + let delay_secs = + initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32); + Duration::from_secs(delay_secs) + } + RetryStrategy::Fixed { delay, .. } => *delay, + }; + log::debug!("Retry attempt {attempt} with delay {delay:?}"); + + event_stream.send_retry(acp_thread::RetryStatus { + last_error: error.to_string().into(), + attempt: attempt as usize, + max_attempts: max_attempts as usize, + started_at: Instant::now(), + duration: delay, + }); + + cx.background_executor().timer(delay).await; + continue 'retry; } } } - let end_turn = tool_results.is_empty(); while let Some(tool_result) = tool_results.next().await { - log::debug!("Tool finished {:?}", tool_result); + log::info!("Tool finished {:?}", tool_result); event_stream.update_tool_call_fields( &tool_result.tool_use_id, @@ -1270,83 +1281,31 @@ impl Thread { })?; } - this.update(cx, |this, cx| { - this.flush_pending_message(cx); - if this.title.is_none() && this.pending_title_generation.is_none() { - this.generate_title(cx); - } - })?; - - if let Some(error) = error { - attempt += 1; - let retry = - this.update(cx, |this, _| this.handle_completion_error(error, attempt))??; - let timer = cx.background_executor().timer(retry.duration); - event_stream.send_retry(retry); - timer.await; - this.update(cx, |this, _cx| { - if let Some(Message::Agent(message)) = this.messages.last() { - if message.tool_results.is_empty() { - intent = CompletionIntent::UserPrompt; - this.messages.push(Message::Resume); - } - } - })?; - } else if this.read_with(cx, |this, _| this.tool_use_limit_reached)? { - return Err(language_model::ToolUseLimitReachedError.into()); - } else if end_turn { - return Ok(()); - } else { - intent = CompletionIntent::ToolResults; - attempt = 0; - } + return Ok(()); } } - fn handle_completion_error( - &mut self, - error: LanguageModelCompletionError, - attempt: u8, - ) -> Result { - if self.completion_mode == CompletionMode::Normal { - return Err(anyhow!(error)); + pub fn build_system_message(&self, cx: &App) -> LanguageModelRequestMessage { + log::debug!("Building system message"); + let prompt = SystemPromptTemplate { + project: self.project_context.read(cx), + available_tools: self.tools.keys().cloned().collect(), } - - let Some(strategy) = Self::retry_strategy_for(&error) else { - return Err(anyhow!(error)); - }; - - let max_attempts = match &strategy { - RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts, - RetryStrategy::Fixed { max_attempts, .. } => *max_attempts, - }; - - if attempt > max_attempts { - return Err(anyhow!(error)); + .render(&self.templates) + .context("failed to build system prompt") + .expect("Invalid template"); + log::debug!("System message built"); + LanguageModelRequestMessage { + role: Role::System, + content: vec![prompt.into()], + cache: true, } - - let delay = match &strategy { - RetryStrategy::ExponentialBackoff { initial_delay, .. } => { - let delay_secs = initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32); - Duration::from_secs(delay_secs) - } - RetryStrategy::Fixed { delay, .. } => *delay, - }; - log::debug!("Retry attempt {attempt} with delay {delay:?}"); - - Ok(acp_thread::RetryStatus { - last_error: error.to_string().into(), - attempt: attempt as usize, - max_attempts: max_attempts as usize, - started_at: Instant::now(), - duration: delay, - }) } /// A helper method that's called on every streamed completion event. /// Returns an optional tool result task, which the main agentic loop will /// send back to the model when it resolves. - fn handle_completion_event( + fn handle_streamed_completion_event( &mut self, event: LanguageModelCompletionEvent, event_stream: &ThreadEventStream, @@ -1541,7 +1500,7 @@ impl Thread { }); let supports_images = self.model().is_some_and(|model| model.supports_images()); let tool_result = tool.run(tool_use.input, tool_event_stream, cx); - log::debug!("Running tool {}", tool_use.name); + log::info!("Running tool {}", tool_use.name); Some(cx.foreground_executor().spawn(async move { let tool_result = tool_result.await.and_then(|output| { if let LanguageModelToolResultContent::Image(_) = &output.llm_output @@ -1567,7 +1526,7 @@ impl Thread { tool_name: tool_use.name, is_error: true, content: LanguageModelToolResultContent::Text(Arc::from(error.to_string())), - output: Some(error.to_string().into()), + output: None, }, } })) @@ -1653,7 +1612,7 @@ impl Thread { summary.extend(lines.next()); } - log::debug!("Setting summary: {}", summary); + log::info!("Setting summary: {}", summary); let summary = SharedString::from(summary); this.update(cx, |this, cx| { @@ -1670,7 +1629,7 @@ impl Thread { return; }; - log::debug!( + log::info!( "Generating title with model: {:?}", self.summarization_model.as_ref().map(|model| model.name()) ); @@ -1756,10 +1715,6 @@ impl Thread { return; }; - if message.content.is_empty() { - return; - } - for content in &message.content { let AgentMessageContent::ToolUse(tool_use) = content else { continue; @@ -1788,7 +1743,7 @@ impl Thread { pub(crate) fn build_completion_request( &self, completion_intent: CompletionIntent, - cx: &App, + cx: &mut App, ) -> Result { let model = self.model().context("No language model configured")?; let tools = if let Some(turn) = self.running_turn.as_ref() { @@ -1812,8 +1767,8 @@ impl Thread { log::debug!("Completion mode: {:?}", self.completion_mode); let messages = self.build_request_messages(cx); - log::debug!("Request will include {} messages", messages.len()); - log::debug!("Request includes {} tools", tools.len()); + log::info!("Request will include {} messages", messages.len()); + log::info!("Request includes {} tools", tools.len()); let request = LanguageModelRequest { thread_id: Some(self.id.to_string()), @@ -1909,31 +1864,23 @@ impl Thread { "Building request messages from {} thread messages", self.messages.len() ); - - let system_prompt = SystemPromptTemplate { - project: self.project_context.read(cx), - available_tools: self.tools.keys().cloned().collect(), - } - .render(&self.templates) - .context("failed to build system prompt") - .expect("Invalid template"); - let mut messages = vec![LanguageModelRequestMessage { - role: Role::System, - content: vec![system_prompt.into()], - cache: false, - }]; + let mut messages = vec![self.build_system_message(cx)]; for message in &self.messages { messages.extend(message.to_request()); } - if let Some(last_message) = messages.last_mut() { - last_message.cache = true; - } - if let Some(message) = self.pending_message.as_ref() { messages.extend(message.to_request()); } + if let Some(last_user_message) = messages + .iter_mut() + .rev() + .find(|message| message.role == Role::User) + { + last_user_message.cache = true; + } + messages } @@ -2469,30 +2416,6 @@ impl ToolCallEventStreamReceiver { } } - pub async fn expect_update_fields(&mut self) -> acp::ToolCallUpdateFields { - let event = self.0.next().await; - if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields( - update, - )))) = event - { - update.fields - } else { - panic!("Expected update fields but got: {:?}", event); - } - } - - pub async fn expect_diff(&mut self) -> Entity { - let event = self.0.next().await; - if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateDiff( - update, - )))) = event - { - update.diff - } else { - panic!("Expected diff but got: {:?}", event); - } - } - pub async fn expect_terminal(&mut self) -> Entity { let event = self.0.next().await; if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateTerminal( diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index f86bfd25f7..5a68d0c70a 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -273,13 +273,6 @@ impl AgentTool for EditFileTool { let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?; event_stream.update_diff(diff.clone()); - let _finalize_diff = util::defer({ - let diff = diff.downgrade(); - let mut cx = cx.clone(); - move || { - diff.update(&mut cx, |diff, cx| diff.finalize(cx)).ok(); - } - }); let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; let old_text = cx @@ -396,6 +389,8 @@ impl AgentTool for EditFileTool { }) .await; + diff.update(cx, |diff, cx| diff.finalize(cx)).ok(); + let input_path = input.path.display(); if unified_diff.is_empty() { anyhow::ensure!( @@ -1550,100 +1545,6 @@ mod tests { ); } - #[gpui::test] - async fn test_diff_finalization(cx: &mut TestAppContext) { - init_test(cx); - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree("/", json!({"main.rs": ""})).await; - - let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await; - let languages = project.read_with(cx, |project, _cx| project.languages().clone()); - let context_server_registry = - cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry.clone(), - Templates::new(), - Some(model.clone()), - cx, - ) - }); - - // Ensure the diff is finalized after the edit completes. - { - let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone())); - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let edit = cx.update(|cx| { - tool.run( - EditFileToolInput { - display_description: "Edit file".into(), - path: path!("/main.rs").into(), - mode: EditFileMode::Edit, - }, - stream_tx, - cx, - ) - }); - stream_rx.expect_update_fields().await; - let diff = stream_rx.expect_diff().await; - diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_)))); - cx.run_until_parked(); - model.end_last_completion_stream(); - edit.await.unwrap(); - diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_)))); - } - - // Ensure the diff is finalized if an error occurs while editing. - { - model.forbid_requests(); - let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone())); - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let edit = cx.update(|cx| { - tool.run( - EditFileToolInput { - display_description: "Edit file".into(), - path: path!("/main.rs").into(), - mode: EditFileMode::Edit, - }, - stream_tx, - cx, - ) - }); - stream_rx.expect_update_fields().await; - let diff = stream_rx.expect_diff().await; - diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_)))); - edit.await.unwrap_err(); - diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_)))); - model.allow_requests(); - } - - // Ensure the diff is finalized if the tool call gets dropped. - { - let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone())); - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let edit = cx.update(|cx| { - tool.run( - EditFileToolInput { - display_description: "Edit file".into(), - path: path!("/main.rs").into(), - mode: EditFileMode::Edit, - }, - stream_tx, - cx, - ) - }); - stream_rx.expect_update_fields().await; - let diff = stream_rx.expect_diff().await; - diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_)))); - drop(edit); - cx.run_until_parked(); - diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_)))); - } - } - fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); diff --git a/crates/agent2/src/tools/fetch_tool.rs b/crates/agent2/src/tools/fetch_tool.rs index dd97271a79..0313c4e4c2 100644 --- a/crates/agent2/src/tools/fetch_tool.rs +++ b/crates/agent2/src/tools/fetch_tool.rs @@ -136,17 +136,12 @@ impl AgentTool for FetchTool { fn run( self: Arc, input: Self::Input, - event_stream: ToolCallEventStream, + _event_stream: ToolCallEventStream, cx: &mut App, ) -> Task> { - let authorize = event_stream.authorize(input.url.clone(), cx); - let text = cx.background_spawn({ let http_client = self.http_client.clone(); - async move { - authorize.await?; - Self::build_message(http_client, &input.url).await - } + async move { Self::build_message(http_client, &input.url).await } }); cx.foreground_executor().spawn(async move { diff --git a/crates/agent2/src/tools/find_path_tool.rs b/crates/agent2/src/tools/find_path_tool.rs index 384bd56e77..5b35c40f85 100644 --- a/crates/agent2/src/tools/find_path_tool.rs +++ b/crates/agent2/src/tools/find_path_tool.rs @@ -165,17 +165,16 @@ fn search_paths(glob: &str, project: Entity, cx: &mut App) -> Task) -> SharedString { - input - .ok() - .as_ref() - .and_then(|input| Path::new(&input.path).file_name()) - .map(|file_name| file_name.to_string_lossy().to_string().into()) - .unwrap_or_default() + if let Ok(input) = input { + let path = &input.path; + match (input.start_line, input.end_line) { + (Some(start), Some(end)) => { + format!( + "[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))", + path, start, end, path, start, end + ) + } + (Some(start), None) => { + format!( + "[Read file `{}` (from line {})](@selection:{}:({}-{}))", + path, start, path, start, start + ) + } + _ => format!("[Read file `{}`](@file:{})", path, path), + } + .into() + } else { + "Read file".into() + } } fn run( @@ -244,19 +258,6 @@ impl AgentTool for ReadFileTool { }]), ..Default::default() }); - if let Ok(LanguageModelToolResultContent::Text(text)) = &result { - let markdown = MarkdownCodeBlock { - tag: &input.path, - text, - } - .to_string(); - event_stream.update_fields(ToolCallUpdateFields { - content: Some(vec![acp::ToolCallContent::Content { - content: markdown.into(), - }]), - ..Default::default() - }) - } } })?; diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index 9f90f3a78a..60dd796463 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -17,11 +17,11 @@ path = "src/agent_servers.rs" doctest = false [dependencies] -acp_tools.workspace = true acp_thread.workspace = true action_log.workspace = true agent-client-protocol.workspace = true agent_settings.workspace = true +agentic-coding-protocol.workspace = true anyhow.workspace = true client = { workspace = true, optional = true } collections.workspace = true diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index b4e897374a..1cfb1fcabf 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -1,414 +1,34 @@ +use std::{path::Path, rc::Rc}; + use crate::AgentServerCommand; use acp_thread::AgentConnection; -use acp_tools::AcpConnectionRegistry; -use action_log::ActionLog; -use agent_client_protocol::{self as acp, Agent as _, ErrorCode}; -use anyhow::anyhow; -use collections::HashMap; -use futures::AsyncBufReadExt as _; -use futures::channel::oneshot; -use futures::io::BufReader; -use project::Project; -use serde::Deserialize; -use std::{any::Any, cell::RefCell}; -use std::{path::Path, rc::Rc}; +use anyhow::Result; +use gpui::AsyncApp; use thiserror::Error; -use anyhow::{Context as _, Result}; -use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntity}; - -use acp_thread::{AcpThread, AuthRequired, LoadError}; +mod v0; +mod v1; #[derive(Debug, Error)] #[error("Unsupported version")] pub struct UnsupportedVersion; -pub struct AcpConnection { - server_name: SharedString, - connection: Rc, - sessions: Rc>>, - auth_methods: Vec, - prompt_capabilities: acp::PromptCapabilities, - _io_task: Task>, -} - -pub struct AcpSession { - thread: WeakEntity, - suppress_abort_err: bool, -} - pub async fn connect( - server_name: SharedString, + server_name: &'static str, command: AgentServerCommand, root_dir: &Path, cx: &mut AsyncApp, ) -> Result> { - let conn = AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await?; - Ok(Rc::new(conn) as _) -} + let conn = v1::AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await; -const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1; - -impl AcpConnection { - pub async fn stdio( - server_name: SharedString, - command: AgentServerCommand, - root_dir: &Path, - cx: &mut AsyncApp, - ) -> Result { - let mut child = util::command::new_smol_command(&command.path) - .args(command.args.iter().map(|arg| arg.as_str())) - .envs(command.env.iter().flatten()) - .current_dir(root_dir) - .stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .kill_on_drop(true) - .spawn()?; - - let stdout = child.stdout.take().context("Failed to take stdout")?; - let stdin = child.stdin.take().context("Failed to take stdin")?; - let stderr = child.stderr.take().context("Failed to take stderr")?; - log::trace!("Spawned (pid: {})", child.id()); - - let sessions = Rc::new(RefCell::new(HashMap::default())); - - let client = ClientDelegate { - sessions: sessions.clone(), - cx: cx.clone(), - }; - let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, { - let foreground_executor = cx.foreground_executor().clone(); - move |fut| { - foreground_executor.spawn(fut).detach(); - } - }); - - let io_task = cx.background_spawn(io_task); - - cx.background_spawn(async move { - let mut stderr = BufReader::new(stderr); - let mut line = String::new(); - while let Ok(n) = stderr.read_line(&mut line).await - && n > 0 - { - log::warn!("agent stderr: {}", &line); - line.clear(); - } - }) - .detach(); - - cx.spawn({ - let sessions = sessions.clone(); - async move |cx| { - let status = child.status().await?; - - for session in sessions.borrow().values() { - session - .thread - .update(cx, |thread, cx| { - thread.emit_load_error(LoadError::Exited { status }, cx) - }) - .ok(); - } - - anyhow::Ok(()) - } - }) - .detach(); - - let connection = Rc::new(connection); - - cx.update(|cx| { - AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| { - registry.set_active_connection(server_name.clone(), &connection, cx) - }); - })?; - - let response = connection - .initialize(acp::InitializeRequest { - protocol_version: acp::VERSION, - client_capabilities: acp::ClientCapabilities { - fs: acp::FileSystemCapability { - read_text_file: true, - write_text_file: true, - }, - }, - }) - .await?; - - if response.protocol_version < MINIMUM_SUPPORTED_VERSION { - return Err(UnsupportedVersion.into()); + match conn { + Ok(conn) => Ok(Rc::new(conn) as _), + Err(err) if err.is::() => { + // Consider re-using initialize response and subprocess when adding another version here + let conn: Rc = + Rc::new(v0::AcpConnection::stdio(server_name, command, root_dir, cx).await?); + Ok(conn) } - - Ok(Self { - auth_methods: response.auth_methods, - connection, - server_name, - sessions, - prompt_capabilities: response.agent_capabilities.prompt_capabilities, - _io_task: io_task, - }) - } -} - -impl AgentConnection for AcpConnection { - fn new_thread( - self: Rc, - project: Entity, - cwd: &Path, - cx: &mut App, - ) -> Task>> { - let conn = self.connection.clone(); - let sessions = self.sessions.clone(); - let cwd = cwd.to_path_buf(); - let context_server_store = project.read(cx).context_server_store().read(cx); - let mcp_servers = context_server_store - .configured_server_ids() - .iter() - .filter_map(|id| { - let configuration = context_server_store.configuration_for_server(id)?; - let command = configuration.command(); - Some(acp::McpServer { - name: id.0.to_string(), - command: command.path.clone(), - args: command.args.clone(), - env: if let Some(env) = command.env.as_ref() { - env.iter() - .map(|(name, value)| acp::EnvVariable { - name: name.clone(), - value: value.clone(), - }) - .collect() - } else { - vec![] - }, - }) - }) - .collect(); - - cx.spawn(async move |cx| { - let response = conn - .new_session(acp::NewSessionRequest { mcp_servers, cwd }) - .await - .map_err(|err| { - if err.code == acp::ErrorCode::AUTH_REQUIRED.code { - let mut error = AuthRequired::new(); - - if err.message != acp::ErrorCode::AUTH_REQUIRED.message { - error = error.with_description(err.message); - } - - anyhow!(error) - } else { - anyhow!(err) - } - })?; - - let session_id = response.session_id; - let action_log = cx.new(|_| ActionLog::new(project.clone()))?; - let thread = cx.new(|cx| { - AcpThread::new( - self.server_name.clone(), - self.clone(), - project, - action_log, - session_id.clone(), - // ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically. - watch::Receiver::constant(self.prompt_capabilities), - cx, - ) - })?; - - let session = AcpSession { - thread: thread.downgrade(), - suppress_abort_err: false, - }; - sessions.borrow_mut().insert(session_id, session); - - Ok(thread) - }) - } - - fn auth_methods(&self) -> &[acp::AuthMethod] { - &self.auth_methods - } - - fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task> { - let conn = self.connection.clone(); - cx.foreground_executor().spawn(async move { - let result = conn - .authenticate(acp::AuthenticateRequest { - method_id: method_id.clone(), - }) - .await?; - - Ok(result) - }) - } - - fn prompt( - &self, - _id: Option, - params: acp::PromptRequest, - cx: &mut App, - ) -> Task> { - let conn = self.connection.clone(); - let sessions = self.sessions.clone(); - let session_id = params.session_id.clone(); - cx.foreground_executor().spawn(async move { - let result = conn.prompt(params).await; - - let mut suppress_abort_err = false; - - if let Some(session) = sessions.borrow_mut().get_mut(&session_id) { - suppress_abort_err = session.suppress_abort_err; - session.suppress_abort_err = false; - } - - match result { - Ok(response) => Ok(response), - Err(err) => { - if err.code != ErrorCode::INTERNAL_ERROR.code { - anyhow::bail!(err) - } - - let Some(data) = &err.data else { - anyhow::bail!(err) - }; - - // Temporary workaround until the following PR is generally available: - // https://github.com/google-gemini/gemini-cli/pull/6656 - - #[derive(Deserialize)] - #[serde(deny_unknown_fields)] - struct ErrorDetails { - details: Box, - } - - match serde_json::from_value(data.clone()) { - Ok(ErrorDetails { details }) => { - if suppress_abort_err - && (details.contains("This operation was aborted") - || details.contains("The user aborted a request")) - { - Ok(acp::PromptResponse { - stop_reason: acp::StopReason::Cancelled, - }) - } else { - Err(anyhow!(details)) - } - } - Err(_) => Err(anyhow!(err)), - } - } - } - }) - } - - fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { - if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) { - session.suppress_abort_err = true; - } - let conn = self.connection.clone(); - let params = acp::CancelNotification { - session_id: session_id.clone(), - }; - cx.foreground_executor() - .spawn(async move { conn.cancel(params).await }) - .detach(); - } - - fn into_any(self: Rc) -> Rc { - self - } -} - -struct ClientDelegate { - sessions: Rc>>, - cx: AsyncApp, -} - -impl acp::Client for ClientDelegate { - async fn request_permission( - &self, - arguments: acp::RequestPermissionRequest, - ) -> Result { - let cx = &mut self.cx.clone(); - let rx = self - .sessions - .borrow() - .get(&arguments.session_id) - .context("Failed to get session")? - .thread - .update(cx, |thread, cx| { - thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx) - })?; - - let result = rx?.await; - - let outcome = match result { - Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option }, - Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled, - }; - - Ok(acp::RequestPermissionResponse { outcome }) - } - - async fn write_text_file( - &self, - arguments: acp::WriteTextFileRequest, - ) -> Result<(), acp::Error> { - let cx = &mut self.cx.clone(); - let task = self - .sessions - .borrow() - .get(&arguments.session_id) - .context("Failed to get session")? - .thread - .update(cx, |thread, cx| { - thread.write_text_file(arguments.path, arguments.content, cx) - })?; - - task.await?; - - Ok(()) - } - - async fn read_text_file( - &self, - arguments: acp::ReadTextFileRequest, - ) -> Result { - let cx = &mut self.cx.clone(); - let task = self - .sessions - .borrow() - .get(&arguments.session_id) - .context("Failed to get session")? - .thread - .update(cx, |thread, cx| { - thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx) - })?; - - let content = task.await?; - - Ok(acp::ReadTextFileResponse { content }) - } - - async fn session_notification( - &self, - notification: acp::SessionNotification, - ) -> Result<(), acp::Error> { - let cx = &mut self.cx.clone(); - let sessions = self.sessions.borrow(); - let session = sessions - .get(¬ification.session_id) - .context("Failed to get session")?; - - session.thread.update(cx, |thread, cx| { - thread.handle_session_update(notification.update, cx) - })??; - - Ok(()) + Err(err) => Err(err), } } diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs new file mode 100644 index 0000000000..be96048929 --- /dev/null +++ b/crates/agent_servers/src/acp/v0.rs @@ -0,0 +1,524 @@ +// Translates old acp agents into the new schema +use action_log::ActionLog; +use agent_client_protocol as acp; +use agentic_coding_protocol::{self as acp_old, AgentRequest as _}; +use anyhow::{Context as _, Result, anyhow}; +use futures::channel::oneshot; +use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity}; +use project::Project; +use std::{any::Any, cell::RefCell, path::Path, rc::Rc}; +use ui::App; +use util::ResultExt as _; + +use crate::AgentServerCommand; +use acp_thread::{AcpThread, AgentConnection, AuthRequired}; + +#[derive(Clone)] +struct OldAcpClientDelegate { + thread: Rc>>, + cx: AsyncApp, + next_tool_call_id: Rc>, + // sent_buffer_versions: HashMap, HashMap>, +} + +impl OldAcpClientDelegate { + fn new(thread: Rc>>, cx: AsyncApp) -> Self { + Self { + thread, + cx, + next_tool_call_id: Rc::new(RefCell::new(0)), + } + } +} + +impl acp_old::Client for OldAcpClientDelegate { + async fn stream_assistant_message_chunk( + &self, + params: acp_old::StreamAssistantMessageChunkParams, + ) -> Result<(), acp_old::Error> { + let cx = &mut self.cx.clone(); + + cx.update(|cx| { + self.thread + .borrow() + .update(cx, |thread, cx| match params.chunk { + acp_old::AssistantMessageChunk::Text { text } => { + thread.push_assistant_content_block(text.into(), false, cx) + } + acp_old::AssistantMessageChunk::Thought { thought } => { + thread.push_assistant_content_block(thought.into(), true, cx) + } + }) + .log_err(); + })?; + + Ok(()) + } + + async fn request_tool_call_confirmation( + &self, + request: acp_old::RequestToolCallConfirmationParams, + ) -> Result { + let cx = &mut self.cx.clone(); + + let old_acp_id = *self.next_tool_call_id.borrow() + 1; + self.next_tool_call_id.replace(old_acp_id); + + let tool_call = into_new_tool_call( + acp::ToolCallId(old_acp_id.to_string().into()), + request.tool_call, + ); + + let mut options = match request.confirmation { + acp_old::ToolCallConfirmation::Edit { .. } => vec![( + acp_old::ToolCallConfirmationOutcome::AlwaysAllow, + acp::PermissionOptionKind::AllowAlways, + "Always Allow Edits".to_string(), + )], + acp_old::ToolCallConfirmation::Execute { root_command, .. } => vec![( + acp_old::ToolCallConfirmationOutcome::AlwaysAllow, + acp::PermissionOptionKind::AllowAlways, + format!("Always Allow {}", root_command), + )], + acp_old::ToolCallConfirmation::Mcp { + server_name, + tool_name, + .. + } => vec![ + ( + acp_old::ToolCallConfirmationOutcome::AlwaysAllowMcpServer, + acp::PermissionOptionKind::AllowAlways, + format!("Always Allow {}", server_name), + ), + ( + acp_old::ToolCallConfirmationOutcome::AlwaysAllowTool, + acp::PermissionOptionKind::AllowAlways, + format!("Always Allow {}", tool_name), + ), + ], + acp_old::ToolCallConfirmation::Fetch { .. } => vec![( + acp_old::ToolCallConfirmationOutcome::AlwaysAllow, + acp::PermissionOptionKind::AllowAlways, + "Always Allow".to_string(), + )], + acp_old::ToolCallConfirmation::Other { .. } => vec![( + acp_old::ToolCallConfirmationOutcome::AlwaysAllow, + acp::PermissionOptionKind::AllowAlways, + "Always Allow".to_string(), + )], + }; + + options.extend([ + ( + acp_old::ToolCallConfirmationOutcome::Allow, + acp::PermissionOptionKind::AllowOnce, + "Allow".to_string(), + ), + ( + acp_old::ToolCallConfirmationOutcome::Reject, + acp::PermissionOptionKind::RejectOnce, + "Reject".to_string(), + ), + ]); + + let mut outcomes = Vec::with_capacity(options.len()); + let mut acp_options = Vec::with_capacity(options.len()); + + for (index, (outcome, kind, label)) in options.into_iter().enumerate() { + outcomes.push(outcome); + acp_options.push(acp::PermissionOption { + id: acp::PermissionOptionId(index.to_string().into()), + name: label, + kind, + }) + } + + let response = cx + .update(|cx| { + self.thread.borrow().update(cx, |thread, cx| { + thread.request_tool_call_authorization(tool_call.into(), acp_options, cx) + }) + })?? + .context("Failed to update thread")? + .await; + + let outcome = match response { + Ok(option_id) => outcomes[option_id.0.parse::().unwrap_or(0)], + Err(oneshot::Canceled) => acp_old::ToolCallConfirmationOutcome::Cancel, + }; + + Ok(acp_old::RequestToolCallConfirmationResponse { + id: acp_old::ToolCallId(old_acp_id), + outcome, + }) + } + + async fn push_tool_call( + &self, + request: acp_old::PushToolCallParams, + ) -> Result { + let cx = &mut self.cx.clone(); + + let old_acp_id = *self.next_tool_call_id.borrow() + 1; + self.next_tool_call_id.replace(old_acp_id); + + cx.update(|cx| { + self.thread.borrow().update(cx, |thread, cx| { + thread.upsert_tool_call( + into_new_tool_call(acp::ToolCallId(old_acp_id.to_string().into()), request), + cx, + ) + }) + })?? + .context("Failed to update thread")?; + + Ok(acp_old::PushToolCallResponse { + id: acp_old::ToolCallId(old_acp_id), + }) + } + + async fn update_tool_call( + &self, + request: acp_old::UpdateToolCallParams, + ) -> Result<(), acp_old::Error> { + let cx = &mut self.cx.clone(); + + cx.update(|cx| { + self.thread.borrow().update(cx, |thread, cx| { + thread.update_tool_call( + acp::ToolCallUpdate { + id: acp::ToolCallId(request.tool_call_id.0.to_string().into()), + fields: acp::ToolCallUpdateFields { + status: Some(into_new_tool_call_status(request.status)), + content: Some( + request + .content + .into_iter() + .map(into_new_tool_call_content) + .collect::>(), + ), + ..Default::default() + }, + }, + cx, + ) + }) + })? + .context("Failed to update thread")??; + + Ok(()) + } + + async fn update_plan(&self, request: acp_old::UpdatePlanParams) -> Result<(), acp_old::Error> { + let cx = &mut self.cx.clone(); + + cx.update(|cx| { + self.thread.borrow().update(cx, |thread, cx| { + thread.update_plan( + acp::Plan { + entries: request + .entries + .into_iter() + .map(into_new_plan_entry) + .collect(), + }, + cx, + ) + }) + })? + .context("Failed to update thread")?; + + Ok(()) + } + + async fn read_text_file( + &self, + acp_old::ReadTextFileParams { path, line, limit }: acp_old::ReadTextFileParams, + ) -> Result { + let content = self + .cx + .update(|cx| { + self.thread.borrow().update(cx, |thread, cx| { + thread.read_text_file(path, line, limit, false, cx) + }) + })? + .context("Failed to update thread")? + .await?; + Ok(acp_old::ReadTextFileResponse { content }) + } + + async fn write_text_file( + &self, + acp_old::WriteTextFileParams { path, content }: acp_old::WriteTextFileParams, + ) -> Result<(), acp_old::Error> { + self.cx + .update(|cx| { + self.thread + .borrow() + .update(cx, |thread, cx| thread.write_text_file(path, content, cx)) + })? + .context("Failed to update thread")? + .await?; + + Ok(()) + } +} + +fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall { + acp::ToolCall { + id, + title: request.label, + kind: acp_kind_from_old_icon(request.icon), + status: acp::ToolCallStatus::InProgress, + content: request + .content + .into_iter() + .map(into_new_tool_call_content) + .collect(), + locations: request + .locations + .into_iter() + .map(into_new_tool_call_location) + .collect(), + raw_input: None, + raw_output: None, + } +} + +fn acp_kind_from_old_icon(icon: acp_old::Icon) -> acp::ToolKind { + match icon { + acp_old::Icon::FileSearch => acp::ToolKind::Search, + acp_old::Icon::Folder => acp::ToolKind::Search, + acp_old::Icon::Globe => acp::ToolKind::Search, + acp_old::Icon::Hammer => acp::ToolKind::Other, + acp_old::Icon::LightBulb => acp::ToolKind::Think, + acp_old::Icon::Pencil => acp::ToolKind::Edit, + acp_old::Icon::Regex => acp::ToolKind::Search, + acp_old::Icon::Terminal => acp::ToolKind::Execute, + } +} + +fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallStatus { + match status { + acp_old::ToolCallStatus::Running => acp::ToolCallStatus::InProgress, + acp_old::ToolCallStatus::Finished => acp::ToolCallStatus::Completed, + acp_old::ToolCallStatus::Error => acp::ToolCallStatus::Failed, + } +} + +fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent { + match content { + acp_old::ToolCallContent::Markdown { markdown } => markdown.into(), + acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff { + diff: into_new_diff(diff), + }, + } +} + +fn into_new_diff(diff: acp_old::Diff) -> acp::Diff { + acp::Diff { + path: diff.path, + old_text: diff.old_text, + new_text: diff.new_text, + } +} + +fn into_new_tool_call_location(location: acp_old::ToolCallLocation) -> acp::ToolCallLocation { + acp::ToolCallLocation { + path: location.path, + line: location.line, + } +} + +fn into_new_plan_entry(entry: acp_old::PlanEntry) -> acp::PlanEntry { + acp::PlanEntry { + content: entry.content, + priority: into_new_plan_priority(entry.priority), + status: into_new_plan_status(entry.status), + } +} + +fn into_new_plan_priority(priority: acp_old::PlanEntryPriority) -> acp::PlanEntryPriority { + match priority { + acp_old::PlanEntryPriority::Low => acp::PlanEntryPriority::Low, + acp_old::PlanEntryPriority::Medium => acp::PlanEntryPriority::Medium, + acp_old::PlanEntryPriority::High => acp::PlanEntryPriority::High, + } +} + +fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatus { + match status { + acp_old::PlanEntryStatus::Pending => acp::PlanEntryStatus::Pending, + acp_old::PlanEntryStatus::InProgress => acp::PlanEntryStatus::InProgress, + acp_old::PlanEntryStatus::Completed => acp::PlanEntryStatus::Completed, + } +} + +pub struct AcpConnection { + pub name: &'static str, + pub connection: acp_old::AgentConnection, + pub _child_status: Task>, + pub current_thread: Rc>>, +} + +impl AcpConnection { + pub fn stdio( + name: &'static str, + command: AgentServerCommand, + root_dir: &Path, + cx: &mut AsyncApp, + ) -> Task> { + let root_dir = root_dir.to_path_buf(); + + cx.spawn(async move |cx| { + let mut child = util::command::new_smol_command(&command.path) + .args(command.args.iter()) + .current_dir(root_dir) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .kill_on_drop(true) + .spawn()?; + + let stdin = child.stdin.take().unwrap(); + let stdout = child.stdout.take().unwrap(); + log::trace!("Spawned (pid: {})", child.id()); + + let foreground_executor = cx.foreground_executor().clone(); + + let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid())); + + let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent( + OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()), + stdin, + stdout, + move |fut| foreground_executor.spawn(fut).detach(), + ); + + let io_task = cx.background_spawn(async move { + io_fut.await.log_err(); + }); + + let child_status = cx.background_spawn(async move { + let result = match child.status().await { + Err(e) => Err(anyhow!(e)), + Ok(result) if result.success() => Ok(()), + Ok(result) => Err(anyhow!(result)), + }; + drop(io_task); + result + }); + + Ok(Self { + name, + connection, + _child_status: child_status, + current_thread: thread_rc, + }) + }) + } +} + +impl AgentConnection for AcpConnection { + fn new_thread( + self: Rc, + project: Entity, + _cwd: &Path, + cx: &mut App, + ) -> Task>> { + let task = self.connection.request_any( + acp_old::InitializeParams { + protocol_version: acp_old::ProtocolVersion::latest(), + } + .into_any(), + ); + let current_thread = self.current_thread.clone(); + cx.spawn(async move |cx| { + let result = task.await?; + let result = acp_old::InitializeParams::response_from_any(result)?; + + if !result.is_authenticated { + anyhow::bail!(AuthRequired::new()) + } + + cx.update(|cx| { + let thread = cx.new(|cx| { + let session_id = acp::SessionId("acp-old-no-id".into()); + let action_log = cx.new(|_| ActionLog::new(project.clone())); + AcpThread::new(self.name, self.clone(), project, action_log, session_id) + }); + current_thread.replace(thread.downgrade()); + thread + }) + }) + } + + fn auth_methods(&self) -> &[acp::AuthMethod] { + &[] + } + + fn authenticate(&self, _method_id: acp::AuthMethodId, cx: &mut App) -> Task> { + let task = self + .connection + .request_any(acp_old::AuthenticateParams.into_any()); + cx.foreground_executor().spawn(async move { + task.await?; + Ok(()) + }) + } + + fn prompt( + &self, + _id: Option, + params: acp::PromptRequest, + cx: &mut App, + ) -> Task> { + let chunks = params + .prompt + .into_iter() + .filter_map(|block| match block { + acp::ContentBlock::Text(text) => { + Some(acp_old::UserMessageChunk::Text { text: text.text }) + } + acp::ContentBlock::ResourceLink(link) => Some(acp_old::UserMessageChunk::Path { + path: link.uri.into(), + }), + _ => None, + }) + .collect(); + + let task = self + .connection + .request_any(acp_old::SendUserMessageParams { chunks }.into_any()); + cx.foreground_executor().spawn(async move { + task.await?; + anyhow::Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) + }) + } + + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: false, + audio: false, + embedded_context: false, + } + } + + fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) { + let task = self + .connection + .request_any(acp_old::CancelSendMessageParams.into_any()); + cx.foreground_executor() + .spawn(async move { + task.await?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx) + } + + fn into_any(self: Rc) -> Rc { + self + } +} diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs new file mode 100644 index 0000000000..29f389547d --- /dev/null +++ b/crates/agent_servers/src/acp/v1.rs @@ -0,0 +1,367 @@ +use action_log::ActionLog; +use agent_client_protocol::{self as acp, Agent as _, ErrorCode}; +use anyhow::anyhow; +use collections::HashMap; +use futures::AsyncBufReadExt as _; +use futures::channel::oneshot; +use futures::io::BufReader; +use project::Project; +use serde::Deserialize; +use std::path::Path; +use std::rc::Rc; +use std::{any::Any, cell::RefCell}; + +use anyhow::{Context as _, Result}; +use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity}; + +use crate::{AgentServerCommand, acp::UnsupportedVersion}; +use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError}; + +pub struct AcpConnection { + server_name: &'static str, + connection: Rc, + sessions: Rc>>, + auth_methods: Vec, + prompt_capabilities: acp::PromptCapabilities, + _io_task: Task>, +} + +pub struct AcpSession { + thread: WeakEntity, + suppress_abort_err: bool, +} + +const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1; + +impl AcpConnection { + pub async fn stdio( + server_name: &'static str, + command: AgentServerCommand, + root_dir: &Path, + cx: &mut AsyncApp, + ) -> Result { + let mut child = util::command::new_smol_command(&command.path) + .args(command.args.iter().map(|arg| arg.as_str())) + .envs(command.env.iter().flatten()) + .current_dir(root_dir) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .kill_on_drop(true) + .spawn()?; + + let stdout = child.stdout.take().context("Failed to take stdout")?; + let stdin = child.stdin.take().context("Failed to take stdin")?; + let stderr = child.stderr.take().context("Failed to take stderr")?; + log::trace!("Spawned (pid: {})", child.id()); + + let sessions = Rc::new(RefCell::new(HashMap::default())); + + let client = ClientDelegate { + sessions: sessions.clone(), + cx: cx.clone(), + }; + let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, { + let foreground_executor = cx.foreground_executor().clone(); + move |fut| { + foreground_executor.spawn(fut).detach(); + } + }); + + let io_task = cx.background_spawn(io_task); + + cx.background_spawn(async move { + let mut stderr = BufReader::new(stderr); + let mut line = String::new(); + while let Ok(n) = stderr.read_line(&mut line).await + && n > 0 + { + log::warn!("agent stderr: {}", &line); + line.clear(); + } + }) + .detach(); + + cx.spawn({ + let sessions = sessions.clone(); + async move |cx| { + let status = child.status().await?; + + for session in sessions.borrow().values() { + session + .thread + .update(cx, |thread, cx| { + thread.emit_load_error(LoadError::Exited { status }, cx) + }) + .ok(); + } + + anyhow::Ok(()) + } + }) + .detach(); + + let response = connection + .initialize(acp::InitializeRequest { + protocol_version: acp::VERSION, + client_capabilities: acp::ClientCapabilities { + fs: acp::FileSystemCapability { + read_text_file: true, + write_text_file: true, + }, + }, + }) + .await?; + + if response.protocol_version < MINIMUM_SUPPORTED_VERSION { + return Err(UnsupportedVersion.into()); + } + + Ok(Self { + auth_methods: response.auth_methods, + connection: connection.into(), + server_name, + sessions, + prompt_capabilities: response.agent_capabilities.prompt_capabilities, + _io_task: io_task, + }) + } +} + +impl AgentConnection for AcpConnection { + fn new_thread( + self: Rc, + project: Entity, + cwd: &Path, + cx: &mut App, + ) -> Task>> { + let conn = self.connection.clone(); + let sessions = self.sessions.clone(); + let cwd = cwd.to_path_buf(); + cx.spawn(async move |cx| { + let response = conn + .new_session(acp::NewSessionRequest { + mcp_servers: vec![], + cwd, + }) + .await + .map_err(|err| { + if err.code == acp::ErrorCode::AUTH_REQUIRED.code { + let mut error = AuthRequired::new(); + + if err.message != acp::ErrorCode::AUTH_REQUIRED.message { + error = error.with_description(err.message); + } + + anyhow!(error) + } else { + anyhow!(err) + } + })?; + + let session_id = response.session_id; + let action_log = cx.new(|_| ActionLog::new(project.clone()))?; + let thread = cx.new(|_cx| { + AcpThread::new( + self.server_name, + self.clone(), + project, + action_log, + session_id.clone(), + ) + })?; + + let session = AcpSession { + thread: thread.downgrade(), + suppress_abort_err: false, + }; + sessions.borrow_mut().insert(session_id, session); + + Ok(thread) + }) + } + + fn auth_methods(&self) -> &[acp::AuthMethod] { + &self.auth_methods + } + + fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task> { + let conn = self.connection.clone(); + cx.foreground_executor().spawn(async move { + let result = conn + .authenticate(acp::AuthenticateRequest { + method_id: method_id.clone(), + }) + .await?; + + Ok(result) + }) + } + + fn prompt( + &self, + _id: Option, + params: acp::PromptRequest, + cx: &mut App, + ) -> Task> { + let conn = self.connection.clone(); + let sessions = self.sessions.clone(); + let session_id = params.session_id.clone(); + cx.foreground_executor().spawn(async move { + let result = conn.prompt(params).await; + + let mut suppress_abort_err = false; + + if let Some(session) = sessions.borrow_mut().get_mut(&session_id) { + suppress_abort_err = session.suppress_abort_err; + session.suppress_abort_err = false; + } + + match result { + Ok(response) => Ok(response), + Err(err) => { + if err.code != ErrorCode::INTERNAL_ERROR.code { + anyhow::bail!(err) + } + + let Some(data) = &err.data else { + anyhow::bail!(err) + }; + + // Temporary workaround until the following PR is generally available: + // https://github.com/google-gemini/gemini-cli/pull/6656 + + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct ErrorDetails { + details: Box, + } + + match serde_json::from_value(data.clone()) { + Ok(ErrorDetails { details }) => { + if suppress_abort_err && details.contains("This operation was aborted") + { + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::Cancelled, + }) + } else { + Err(anyhow!(details)) + } + } + Err(_) => Err(anyhow!(err)), + } + } + } + }) + } + + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + self.prompt_capabilities + } + + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { + if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) { + session.suppress_abort_err = true; + } + let conn = self.connection.clone(); + let params = acp::CancelNotification { + session_id: session_id.clone(), + }; + cx.foreground_executor() + .spawn(async move { conn.cancel(params).await }) + .detach(); + } + + fn into_any(self: Rc) -> Rc { + self + } +} + +struct ClientDelegate { + sessions: Rc>>, + cx: AsyncApp, +} + +impl acp::Client for ClientDelegate { + async fn request_permission( + &self, + arguments: acp::RequestPermissionRequest, + ) -> Result { + let cx = &mut self.cx.clone(); + let rx = self + .sessions + .borrow() + .get(&arguments.session_id) + .context("Failed to get session")? + .thread + .update(cx, |thread, cx| { + thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx) + })?; + + let result = rx?.await; + + let outcome = match result { + Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option }, + Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled, + }; + + Ok(acp::RequestPermissionResponse { outcome }) + } + + async fn write_text_file( + &self, + arguments: acp::WriteTextFileRequest, + ) -> Result<(), acp::Error> { + let cx = &mut self.cx.clone(); + let task = self + .sessions + .borrow() + .get(&arguments.session_id) + .context("Failed to get session")? + .thread + .update(cx, |thread, cx| { + thread.write_text_file(arguments.path, arguments.content, cx) + })?; + + task.await?; + + Ok(()) + } + + async fn read_text_file( + &self, + arguments: acp::ReadTextFileRequest, + ) -> Result { + let cx = &mut self.cx.clone(); + let task = self + .sessions + .borrow() + .get(&arguments.session_id) + .context("Failed to get session")? + .thread + .update(cx, |thread, cx| { + thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx) + })?; + + let content = task.await?; + + Ok(acp::ReadTextFileResponse { content }) + } + + async fn session_notification( + &self, + notification: acp::SessionNotification, + ) -> Result<(), acp::Error> { + let cx = &mut self.cx.clone(); + let sessions = self.sessions.borrow(); + let session = sessions + .get(¬ification.session_id) + .context("Failed to get session")?; + + session.thread.update(cx, |thread, cx| { + thread.handle_session_update(notification.update, cx) + })??; + + Ok(()) + } +} diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index 7c7e124ca7..2f5ec478ae 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -1,6 +1,5 @@ mod acp; mod claude; -mod custom; mod gemini; mod settings; @@ -8,7 +7,6 @@ mod settings; pub mod e2e_tests; pub use claude::*; -pub use custom::*; pub use gemini::*; pub use settings::*; @@ -33,10 +31,9 @@ pub fn init(cx: &mut App) { pub trait AgentServer: Send { fn logo(&self) -> ui::IconName; - fn name(&self) -> SharedString; - fn empty_state_headline(&self) -> SharedString; - fn empty_state_message(&self) -> SharedString; - fn telemetry_id(&self) -> &'static str; + fn name(&self) -> &'static str; + fn empty_state_headline(&self) -> &'static str; + fn empty_state_message(&self) -> &'static str; fn connect( &self, @@ -98,7 +95,7 @@ pub struct AgentServerCommand { } impl AgentServerCommand { - pub async fn resolve( + pub(crate) async fn resolve( path_bin_name: &'static str, extra_args: &[&'static str], fallback_path: Option<&Path>, diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 250e564526..ef666974f1 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -30,7 +30,7 @@ use futures::{ io::BufReader, select_biased, }; -use gpui::{App, AppContext, AsyncApp, Entity, SharedString, Task, WeakEntity}; +use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity}; use serde::{Deserialize, Serialize}; use util::{ResultExt, debug_panic}; @@ -43,20 +43,16 @@ use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri pub struct ClaudeCode; impl AgentServer for ClaudeCode { - fn telemetry_id(&self) -> &'static str { - "claude-code" + fn name(&self) -> &'static str { + "Claude Code" } - fn name(&self) -> SharedString { - "Claude Code".into() - } - - fn empty_state_headline(&self) -> SharedString { + fn empty_state_headline(&self) -> &'static str { self.name() } - fn empty_state_message(&self) -> SharedString { - "How can I help you today?".into() + fn empty_state_message(&self) -> &'static str { + "How can I help you today?" } fn logo(&self) -> ui::IconName { @@ -253,19 +249,13 @@ impl AgentConnection for ClaudeAgentConnection { }); let action_log = cx.new(|_| ActionLog::new(project.clone()))?; - let thread = cx.new(|cx| { + let thread = cx.new(|_cx| { AcpThread::new( "Claude Code", self.clone(), project, action_log, session_id.clone(), - watch::Receiver::constant(acp::PromptCapabilities { - image: true, - audio: false, - embedded_context: true, - }), - cx, ) })?; @@ -329,6 +319,14 @@ impl AgentConnection for ClaudeAgentConnection { cx.foreground_executor().spawn(async move { end_rx.await? }) } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: true, + audio: false, + embedded_context: true, + } + } + fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) { let sessions = self.sessions.borrow(); let Some(session) = sessions.get(session_id) else { diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs deleted file mode 100644 index 72823026d7..0000000000 --- a/crates/agent_servers/src/custom.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::{AgentServerCommand, AgentServerSettings}; -use acp_thread::AgentConnection; -use anyhow::Result; -use gpui::{App, Entity, SharedString, Task}; -use project::Project; -use std::{path::Path, rc::Rc}; -use ui::IconName; - -/// A generic agent server implementation for custom user-defined agents -pub struct CustomAgentServer { - name: SharedString, - command: AgentServerCommand, -} - -impl CustomAgentServer { - pub fn new(name: SharedString, settings: &AgentServerSettings) -> Self { - Self { - name, - command: settings.command.clone(), - } - } -} - -impl crate::AgentServer for CustomAgentServer { - fn telemetry_id(&self) -> &'static str { - "custom" - } - - fn name(&self) -> SharedString { - self.name.clone() - } - - fn logo(&self) -> IconName { - IconName::Terminal - } - - fn empty_state_headline(&self) -> SharedString { - "No conversations yet".into() - } - - fn empty_state_message(&self) -> SharedString { - format!("Start a conversation with {}", self.name).into() - } - - fn connect( - &self, - root_dir: &Path, - _project: &Entity, - cx: &mut App, - ) -> Task>> { - let server_name = self.name(); - let command = self.command.clone(); - let root_dir = root_dir.to_path_buf(); - - cx.spawn(async move |mut cx| { - crate::acp::connect(server_name, command, &root_dir, &mut cx).await - }) - } - - fn into_any(self: Rc) -> Rc { - self - } -} diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index 42264b4b4f..c271079071 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -1,15 +1,17 @@ -use crate::AgentServer; -use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus}; -use agent_client_protocol as acp; -use futures::{FutureExt, StreamExt, channel::mpsc, select}; -use gpui::{AppContext, Entity, TestAppContext}; -use indoc::indoc; -use project::{FakeFs, Project}; use std::{ path::{Path, PathBuf}, sync::Arc, time::Duration, }; + +use crate::AgentServer; +use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus}; +use agent_client_protocol as acp; + +use futures::{FutureExt, StreamExt, channel::mpsc, select}; +use gpui::{AppContext, Entity, TestAppContext}; +use indoc::indoc; +use project::{FakeFs, Project}; use util::path; pub async fn test_basic(server: F, cx: &mut TestAppContext) @@ -477,7 +479,6 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc { gemini: Some(crate::AgentServerSettings { command: crate::gemini::tests::local_command(), }), - custom: collections::HashMap::default(), }, cx, ); diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 5d6a70fa64..29120fff6e 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -4,10 +4,11 @@ use std::{any::Any, path::Path}; use crate::{AgentServer, AgentServerCommand}; use acp_thread::{AgentConnection, LoadError}; use anyhow::Result; -use gpui::{App, Entity, SharedString, Task}; +use gpui::{Entity, Task}; use language_models::provider::google::GoogleLanguageModelProvider; use project::Project; use settings::SettingsStore; +use ui::App; use crate::AllAgentServersSettings; @@ -17,20 +18,16 @@ pub struct Gemini; const ACP_ARG: &str = "--experimental-acp"; impl AgentServer for Gemini { - fn telemetry_id(&self) -> &'static str { - "gemini-cli" + fn name(&self) -> &'static str { + "Gemini CLI" } - fn name(&self) -> SharedString { - "Gemini CLI".into() - } - - fn empty_state_headline(&self) -> SharedString { + fn empty_state_headline(&self) -> &'static str { self.name() } - fn empty_state_message(&self) -> SharedString { - "Ask questions, edit files, run commands".into() + fn empty_state_message(&self) -> &'static str { + "Ask questions, edit files, run commands" } fn logo(&self) -> ui::IconName { @@ -57,7 +54,7 @@ impl AgentServer for Gemini { return Err(LoadError::NotInstalled { error_message: "Failed to find Gemini CLI binary".into(), install_message: "Install Gemini CLI".into(), - install_command: Self::install_command().into(), + install_command: "npm install -g @google/gemini-cli@preview".into() }.into()); }; @@ -92,7 +89,7 @@ impl AgentServer for Gemini { current_version ).into(), upgrade_message: "Upgrade Gemini CLI to latest".into(), - upgrade_command: Self::upgrade_command().into(), + upgrade_command: "npm install -g @google/gemini-cli@preview".into(), }.into()) } } @@ -105,20 +102,6 @@ impl AgentServer for Gemini { } } -impl Gemini { - pub fn binary_name() -> &'static str { - "gemini" - } - - pub fn install_command() -> &'static str { - "npm install -g @google/gemini-cli@preview" - } - - pub fn upgrade_command() -> &'static str { - "npm install -g @google/gemini-cli@preview" - } -} - #[cfg(test)] pub(crate) mod tests { use super::*; diff --git a/crates/agent_servers/src/settings.rs b/crates/agent_servers/src/settings.rs index 96ac6e3cbe..645674b5f1 100644 --- a/crates/agent_servers/src/settings.rs +++ b/crates/agent_servers/src/settings.rs @@ -1,7 +1,6 @@ use crate::AgentServerCommand; use anyhow::Result; -use collections::HashMap; -use gpui::{App, SharedString}; +use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; @@ -14,13 +13,9 @@ pub fn init(cx: &mut App) { pub struct AllAgentServersSettings { pub gemini: Option, pub claude: Option, - - /// Custom agent servers configured by the user - #[serde(flatten)] - pub custom: HashMap, } -#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)] +#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)] pub struct AgentServerSettings { #[serde(flatten)] pub command: AgentServerCommand, @@ -34,26 +29,13 @@ impl settings::Settings for AllAgentServersSettings { fn load(sources: SettingsSources, _: &mut App) -> Result { let mut settings = AllAgentServersSettings::default(); - for AllAgentServersSettings { - gemini, - claude, - custom, - } in sources.defaults_and_customizations() - { + for AllAgentServersSettings { gemini, claude } in sources.defaults_and_customizations() { if gemini.is_some() { settings.gemini = gemini.clone(); } if claude.is_some() { settings.claude = claude.clone(); } - - // Merge custom agents - for (name, config) in custom { - // Skip built-in agent names to avoid conflicts - if name != "gemini" && name != "claude" { - settings.custom.insert(name.clone(), config.clone()); - } - } } Ok(settings) diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 6b0979ee69..43e3b25124 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -67,7 +67,6 @@ ordered-float.workspace = true parking_lot.workspace = true paths.workspace = true picker.workspace = true -postage.workspace = true project.workspace = true prompt_store.workspace = true proto.workspace = true diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 5b40967069..22a9ea6773 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -247,9 +247,9 @@ impl ContextPickerCompletionProvider { let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?; let uri = MentionUri::Symbol { - abs_path, + path: abs_path, name: symbol.name.clone(), - line_range: symbol.range.start.0.row..=symbol.range.end.0.row, + line_range: symbol.range.start.0.row..symbol.range.end.0.row, }; let new_text = format!("{} ", uri.as_link()); let new_text_len = new_text.len(); diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index becf6953fd..0e4080d689 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -6,7 +6,7 @@ use agent2::HistoryStore; use collections::HashMap; use editor::{Editor, EditorMode, MinimapVisibility}; use gpui::{ - AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, ScrollHandle, + AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, TextStyleRefinement, WeakEntity, Window, }; use language::language_settings::SoftWrap; @@ -154,22 +154,10 @@ impl EntryViewState { }); } } - AgentThreadEntry::AssistantMessage(message) => { - let entry = if let Some(Entry::AssistantMessage(entry)) = - self.entries.get_mut(index) - { - entry - } else { - self.set_entry( - index, - Entry::AssistantMessage(AssistantMessageEntry::default()), - ); - let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else { - unreachable!() - }; - entry - }; - entry.sync(message); + AgentThreadEntry::AssistantMessage(_) => { + if index == self.entries.len() { + self.entries.push(Entry::empty()) + } } }; } @@ -189,7 +177,7 @@ impl EntryViewState { pub fn settings_changed(&mut self, cx: &mut App) { for entry in self.entries.iter() { match entry { - Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {} + Entry::UserMessage { .. } => {} Entry::Content(response_views) => { for view in response_views.values() { if let Ok(diff_editor) = view.clone().downcast::() { @@ -220,29 +208,9 @@ pub enum ViewEvent { MessageEditorEvent(Entity, MessageEditorEvent), } -#[derive(Default, Debug)] -pub struct AssistantMessageEntry { - scroll_handles_by_chunk_index: HashMap, -} - -impl AssistantMessageEntry { - pub fn scroll_handle_for_chunk(&self, ix: usize) -> Option { - self.scroll_handles_by_chunk_index.get(&ix).cloned() - } - - pub fn sync(&mut self, message: &acp_thread::AssistantMessage) { - if let Some(acp_thread::AssistantMessageChunk::Thought { .. }) = message.chunks.last() { - let ix = message.chunks.len() - 1; - let handle = self.scroll_handles_by_chunk_index.entry(ix).or_default(); - handle.scroll_to_bottom(); - } - } -} - #[derive(Debug)] pub enum Entry { UserMessage(Entity), - AssistantMessage(AssistantMessageEntry), Content(HashMap), } @@ -250,7 +218,7 @@ impl Entry { pub fn message_editor(&self) -> Option<&Entity> { match self { Self::UserMessage(editor) => Some(editor), - Self::AssistantMessage(_) | Self::Content(_) => None, + Entry::Content(_) => None, } } @@ -271,16 +239,6 @@ impl Entry { .map(|entity| entity.downcast::().unwrap()) } - pub fn scroll_handle_for_assistant_message_chunk( - &self, - chunk_ix: usize, - ) -> Option { - match self { - Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix), - Self::UserMessage(_) | Self::Content(_) => None, - } - } - fn content_map(&self) -> Option<&HashMap> { match self { Self::Content(map) => Some(map), @@ -296,7 +254,7 @@ impl Entry { pub fn has_content(&self) -> bool { match self { Self::Content(map) => !map.is_empty(), - Self::UserMessage(_) | Self::AssistantMessage(_) => false, + Self::UserMessage(_) => false, } } } diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 12ae893c31..7d73ebeb19 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -6,7 +6,7 @@ use acp_thread::{MentionUri, selection_name}; use agent_client_protocol as acp; use agent_servers::AgentServer; use agent2::HistoryStore; -use anyhow::{Result, anyhow}; +use anyhow::{Context as _, Result, anyhow}; use assistant_slash_commands::codeblock_fence_for_path; use collections::{HashMap, HashSet}; use editor::{ @@ -17,26 +17,25 @@ use editor::{ display_map::{Crease, CreaseId, FoldId}, }; use futures::{ - FutureExt as _, - future::{Shared, join_all}, + FutureExt as _, TryFutureExt as _, + future::{Shared, join_all, try_join_all}, }; use gpui::{ - Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Entity, EntityId, - EventEmitter, FocusHandle, Focusable, HighlightStyle, Image, ImageFormat, Img, KeyContext, - Subscription, Task, TextStyle, UnderlineStyle, WeakEntity, pulsating_between, + AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, + HighlightStyle, Image, ImageFormat, Img, KeyContext, Subscription, Task, TextStyle, + UnderlineStyle, WeakEntity, }; use language::{Buffer, Language}; use language_model::LanguageModelImage; -use postage::stream::Stream as _; use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree}; -use prompt_store::{PromptId, PromptStore}; +use prompt_store::PromptStore; use rope::Point; use settings::Settings; use std::{ cell::Cell, ffi::OsStr, fmt::Write, - ops::{Range, RangeInclusive}, + ops::Range, path::{Path, PathBuf}, rc::Rc, sync::Arc, @@ -45,13 +44,17 @@ use std::{ use text::{OffsetRangeExt, ToOffset as _}; use theme::ThemeSettings; use ui::{ - ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Element as _, - FluentBuilder as _, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label, - LabelCommon, LabelSize, ParentElement, Render, SelectableButton, SharedString, Styled, - TextSize, TintColor, Toggleable, Window, div, h_flex, px, + ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Icon, IconName, + IconSize, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement, + Render, SelectableButton, SharedString, Styled, TextSize, TintColor, Toggleable, Window, div, + h_flex, px, +}; +use url::Url; +use util::ResultExt; +use workspace::{ + Toast, Workspace, + notifications::{NotificationId, NotifyResultExt as _}, }; -use util::{ResultExt, debug_panic}; -use workspace::{Workspace, notifications::NotifyResultExt as _}; use zed_actions::agent::Chat; const PARSE_SLASH_COMMAND_DEBOUNCE: Duration = Duration::from_millis(50); @@ -74,7 +77,6 @@ pub enum MessageEditorEvent { Send, Cancel, Focus, - LostFocus, } impl EventEmitter for MessageEditor {} @@ -132,14 +134,10 @@ impl MessageEditor { editor }); - cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| { + cx.on_focus(&editor.focus_handle(cx), window, |_, _, cx| { cx.emit(MessageEditorEvent::Focus) }) .detach(); - cx.on_focus_out(&editor.focus_handle(cx), window, |_, _, _, cx| { - cx.emit(MessageEditorEvent::LostFocus) - }) - .detach(); let mut subscriptions = Vec::new(); subscriptions.push(cx.subscribe_in(&editor, window, { @@ -221,9 +219,9 @@ impl MessageEditor { pub fn mentions(&self) -> HashSet { self.mention_set - .mentions + .uri_by_crease_id .values() - .map(|(uri, _)| uri.clone()) + .cloned() .collect() } @@ -248,172 +246,132 @@ impl MessageEditor { else { return Task::ready(()); }; - let end_anchor = snapshot - .buffer_snapshot - .anchor_before(start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1); - let crease = if let MentionUri::File { abs_path } = &mention_uri - && let Some(extension) = abs_path.extension() - && let Some(extension) = extension.to_str() - && Img::extensions().contains(&extension) - && !extension.contains("svg") - { - let Some(project_path) = self - .project - .read(cx) - .project_path_for_absolute_path(&abs_path, cx) - else { - log::error!("project path not found"); - return Task::ready(()); - }; - let image = self - .project - .update(cx, |project, cx| project.open_image(project_path, cx)); - let image = cx - .spawn(async move |_, cx| { - let image = image.await.map_err(|e| e.to_string())?; - let image = image - .update(cx, |image, _| image.image.clone()) - .map_err(|e| e.to_string())?; - Ok(image) - }) - .shared(); - insert_crease_for_mention( - *excerpt_id, - start, - content_len, - mention_uri.name().into(), - IconName::Image.path().into(), - Some(image), - self.editor.clone(), - window, - cx, - ) - } else { - insert_crease_for_mention( - *excerpt_id, - start, - content_len, - crease_text, - mention_uri.icon_path(cx), - None, - self.editor.clone(), - window, - cx, - ) - }; - let Some((crease_id, tx)) = crease else { + if let MentionUri::File { abs_path, .. } = &mention_uri { + let extension = abs_path + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default(); + + if Img::extensions().contains(&extension) && !extension.contains("svg") { + if !self.prompt_capabilities.get().image { + struct ImagesNotAllowed; + + let end_anchor = snapshot.buffer_snapshot.anchor_before( + start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1, + ); + + self.editor.update(cx, |editor, cx| { + // Remove mention + editor.edit([((start_anchor..end_anchor), "")], cx); + }); + + self.workspace + .update(cx, |workspace, cx| { + workspace.show_toast( + Toast::new( + NotificationId::unique::(), + "This agent does not support images yet", + ) + .autohide(), + cx, + ); + }) + .ok(); + return Task::ready(()); + } + + let project = self.project.clone(); + let Some(project_path) = project + .read(cx) + .project_path_for_absolute_path(abs_path, cx) + else { + return Task::ready(()); + }; + let image = cx + .spawn(async move |_, cx| { + let image = project + .update(cx, |project, cx| project.open_image(project_path, cx)) + .map_err(|e| e.to_string())? + .await + .map_err(|e| e.to_string())?; + image + .read_with(cx, |image, _cx| image.image.clone()) + .map_err(|e| e.to_string()) + }) + .shared(); + let Some(crease_id) = insert_crease_for_image( + *excerpt_id, + start, + content_len, + Some(abs_path.as_path().into()), + image.clone(), + self.editor.clone(), + window, + cx, + ) else { + return Task::ready(()); + }; + return self.confirm_mention_for_image( + crease_id, + start_anchor, + Some(abs_path.clone()), + image, + window, + cx, + ); + } + } + + let Some(crease_id) = crate::context_picker::insert_crease_for_mention( + *excerpt_id, + start, + content_len, + crease_text, + mention_uri.icon_path(cx), + self.editor.clone(), + window, + cx, + ) else { return Task::ready(()); }; - let task = match mention_uri.clone() { - MentionUri::Fetch { url } => self.confirm_mention_for_fetch(url, cx), - MentionUri::Directory { abs_path } => self.confirm_mention_for_directory(abs_path, cx), - MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx), - MentionUri::TextThread { path, .. } => self.confirm_mention_for_text_thread(path, cx), - MentionUri::File { abs_path } => self.confirm_mention_for_file(abs_path, cx), - MentionUri::Symbol { - abs_path, - line_range, - .. - } => self.confirm_mention_for_symbol(abs_path, line_range, cx), - MentionUri::Rule { id, .. } => self.confirm_mention_for_rule(id, cx), - MentionUri::PastedImage => { - debug_panic!("pasted image URI should not be included in completions"); - Task::ready(Err(anyhow!( - "pasted imaged URI should not be included in completions" - ))) + match mention_uri { + MentionUri::Fetch { url } => { + self.confirm_mention_for_fetch(crease_id, start_anchor, url, window, cx) } - MentionUri::Selection { .. } => { - // Handled elsewhere - debug_panic!("unexpected selection URI"); - Task::ready(Err(anyhow!("unexpected selection URI"))) + MentionUri::Directory { abs_path } => { + self.confirm_mention_for_directory(crease_id, start_anchor, abs_path, window, cx) } - }; - let task = cx - .spawn(async move |_, _| task.await.map_err(|e| e.to_string())) - .shared(); - self.mention_set - .mentions - .insert(crease_id, (mention_uri, task.clone())); - - // Notify the user if we failed to load the mentioned context - cx.spawn_in(window, async move |this, cx| { - let result = task.await.notify_async_err(cx); - drop(tx); - if result.is_none() { - this.update(cx, |this, cx| { - this.editor.update(cx, |editor, cx| { - // Remove mention - editor.edit([(start_anchor..end_anchor, "")], cx); - }); - this.mention_set.mentions.remove(&crease_id); - }) - .ok(); + MentionUri::Thread { id, name } => { + self.confirm_mention_for_thread(crease_id, start_anchor, id, name, window, cx) } - }) - } - - fn confirm_mention_for_file( - &mut self, - abs_path: PathBuf, - cx: &mut Context, - ) -> Task> { - let Some(project_path) = self - .project - .read(cx) - .project_path_for_absolute_path(&abs_path, cx) - else { - return Task::ready(Err(anyhow!("project path not found"))); - }; - let extension = abs_path - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default(); - - if Img::extensions().contains(&extension) && !extension.contains("svg") { - if !self.prompt_capabilities.get().image { - return Task::ready(Err(anyhow!("This model does not support images yet"))); + MentionUri::TextThread { path, name } => self.confirm_mention_for_text_thread( + crease_id, + start_anchor, + path, + name, + window, + cx, + ), + MentionUri::File { .. } + | MentionUri::Symbol { .. } + | MentionUri::Rule { .. } + | MentionUri::Selection { .. } => { + self.mention_set.insert_uri(crease_id, mention_uri.clone()); + Task::ready(()) } - let task = self - .project - .update(cx, |project, cx| project.open_image(project_path, cx)); - return cx.spawn(async move |_, cx| { - let image = task.await?; - let image = image.update(cx, |image, _| image.image.clone())?; - let format = image.format; - let image = cx - .update(|cx| LanguageModelImage::from_image(image, cx))? - .await; - if let Some(image) = image { - Ok(Mention::Image(MentionImage { - data: image.source, - format, - })) - } else { - Err(anyhow!("Failed to convert image")) - } - }); } - - let buffer = self - .project - .update(cx, |project, cx| project.open_buffer(project_path, cx)); - cx.spawn(async move |_, cx| { - let buffer = buffer.await?; - let mention = buffer.update(cx, |buffer, cx| Mention::Text { - content: buffer.text(), - tracked_buffers: vec![cx.entity()], - })?; - anyhow::Ok(mention) - }) } fn confirm_mention_for_directory( &mut self, + crease_id: CreaseId, + anchor: Anchor, abs_path: PathBuf, + window: &mut Window, cx: &mut Context, - ) -> Task> { + ) -> Task<()> { fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<(Arc, PathBuf)> { let mut files = Vec::new(); @@ -428,21 +386,24 @@ impl MessageEditor { files } + let uri = MentionUri::Directory { + abs_path: abs_path.clone(), + }; let Some(project_path) = self .project .read(cx) .project_path_for_absolute_path(&abs_path, cx) else { - return Task::ready(Err(anyhow!("project path not found"))); + return Task::ready(()); }; let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else { - return Task::ready(Err(anyhow!("project entry not found"))); + return Task::ready(()); }; let Some(worktree) = self.project.read(cx).worktree_for_entry(entry.id, cx) else { - return Task::ready(Err(anyhow!("worktree not found"))); + return Task::ready(()); }; let project = self.project.clone(); - cx.spawn(async move |_, cx| { + let task = cx.spawn(async move |_, cx| { let directory_path = entry.path.clone(); let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?; @@ -492,83 +453,89 @@ impl MessageEditor { ((rel_path, full_path, rope), buffer) }) .unzip(); - Mention::Text { - content: render_directory_contents(contents), - tracked_buffers, - } + (render_directory_contents(contents), tracked_buffers) }) .await; anyhow::Ok(contents) + }); + let task = cx + .spawn(async move |_, _| task.await.map_err(|e| e.to_string())) + .shared(); + + self.mention_set + .directories + .insert(abs_path.clone(), task.clone()); + + let editor = self.editor.clone(); + cx.spawn_in(window, async move |this, cx| { + if task.await.notify_async_err(cx).is_some() { + this.update(cx, |this, _| { + this.mention_set.insert_uri(crease_id, uri); + }) + .ok(); + } else { + editor + .update(cx, |editor, cx| { + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting(vec![anchor..anchor], true, cx); + }); + editor.remove_creases([crease_id], cx); + }) + .ok(); + this.update(cx, |this, _cx| { + this.mention_set.directories.remove(&abs_path); + }) + .ok(); + } }) } fn confirm_mention_for_fetch( &mut self, + crease_id: CreaseId, + anchor: Anchor, url: url::Url, + window: &mut Window, cx: &mut Context, - ) -> Task> { - let http_client = match self + ) -> Task<()> { + let Some(http_client) = self .workspace - .update(cx, |workspace, _| workspace.client().http_client()) - { - Ok(http_client) => http_client, - Err(e) => return Task::ready(Err(e)), - }; - cx.background_executor().spawn(async move { - let content = fetch_url_content(http_client, url.to_string()).await?; - Ok(Mention::Text { - content, - tracked_buffers: Vec::new(), - }) - }) - } - - fn confirm_mention_for_symbol( - &mut self, - abs_path: PathBuf, - line_range: RangeInclusive, - cx: &mut Context, - ) -> Task> { - let Some(project_path) = self - .project - .read(cx) - .project_path_for_absolute_path(&abs_path, cx) + .update(cx, |workspace, _cx| workspace.client().http_client()) + .ok() else { - return Task::ready(Err(anyhow!("project path not found"))); + return Task::ready(()); }; - let buffer = self - .project - .update(cx, |project, cx| project.open_buffer(project_path, cx)); - cx.spawn(async move |_, cx| { - let buffer = buffer.await?; - let mention = buffer.update(cx, |buffer, cx| { - let start = Point::new(*line_range.start(), 0).min(buffer.max_point()); - let end = Point::new(*line_range.end() + 1, 0).min(buffer.max_point()); - let content = buffer.text_for_range(start..end).collect(); - Mention::Text { - content, - tracked_buffers: vec![cx.entity()], - } - })?; - anyhow::Ok(mention) - }) - } - fn confirm_mention_for_rule( - &mut self, - id: PromptId, - cx: &mut Context, - ) -> Task> { - let Some(prompt_store) = self.prompt_store.clone() else { - return Task::ready(Err(anyhow!("missing prompt store"))); - }; - let prompt = prompt_store.read(cx).load(id, cx); - cx.spawn(async move |_, _| { - let prompt = prompt.await?; - Ok(Mention::Text { - content: prompt, - tracked_buffers: Vec::new(), + let url_string = url.to_string(); + let fetch = cx + .background_executor() + .spawn(async move { + fetch_url_content(http_client, url_string) + .map_err(|e| e.to_string()) + .await }) + .shared(); + self.mention_set + .add_fetch_result(url.clone(), fetch.clone()); + + cx.spawn_in(window, async move |this, cx| { + let fetch = fetch.await.notify_async_err(cx); + this.update(cx, |this, cx| { + if fetch.is_some() { + this.mention_set + .insert_uri(crease_id, MentionUri::Fetch { url }); + } else { + // Remove crease if we failed to fetch + this.editor.update(cx, |editor, cx| { + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting(vec![anchor..anchor], true, cx); + }); + editor.remove_creases([crease_id], cx); + }); + this.mention_set.fetch_results.remove(&url); + } + }) + .ok(); }) } @@ -593,24 +560,24 @@ impl MessageEditor { let range = snapshot.anchor_after(offset + range_to_fold.start) ..snapshot.anchor_after(offset + range_to_fold.end); - let abs_path = buffer - .read(cx) - .project_path(cx) - .and_then(|project_path| self.project.read(cx).absolute_path(&project_path, cx)); + // TODO support selections from buffers with no path + let Some(project_path) = buffer.read(cx).project_path(cx) else { + continue; + }; + let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else { + continue; + }; let snapshot = buffer.read(cx).snapshot(); - let text = snapshot - .text_for_range(selection_range.clone()) - .collect::(); let point_range = selection_range.to_point(&snapshot); - let line_range = point_range.start.row..=point_range.end.row; + let line_range = point_range.start.row..point_range.end.row; let uri = MentionUri::Selection { - abs_path: abs_path.clone(), + path: abs_path.clone(), line_range: line_range.clone(), }; let crease = crate::context_picker::crease_for_mention( - selection_name(abs_path.as_deref(), &line_range).into(), + selection_name(&abs_path, &line_range).into(), uri.icon_path(cx), range, self.editor.downgrade(), @@ -622,69 +589,132 @@ impl MessageEditor { crease_ids.first().copied().unwrap() }); - self.mention_set.mentions.insert( - crease_id, - ( - uri, - Task::ready(Ok(Mention::Text { - content: text, - tracked_buffers: vec![buffer], - })) - .shared(), - ), - ); + self.mention_set.insert_uri(crease_id, uri); } } fn confirm_mention_for_thread( &mut self, + crease_id: CreaseId, + anchor: Anchor, id: acp::SessionId, + name: String, + window: &mut Window, cx: &mut Context, - ) -> Task> { + ) -> Task<()> { + let uri = MentionUri::Thread { + id: id.clone(), + name, + }; let server = Rc::new(agent2::NativeAgentServer::new( self.project.read(cx).fs().clone(), self.history_store.clone(), )); let connection = server.connect(Path::new(""), &self.project, cx); - cx.spawn(async move |_, cx| { - let agent = connection.await?; - let agent = agent.downcast::().unwrap(); - let summary = agent - .0 - .update(cx, |agent, cx| agent.thread_summary(id, cx))? - .await?; - anyhow::Ok(Mention::Text { - content: summary.to_string(), - tracked_buffers: Vec::new(), - }) + let load_summary = cx.spawn({ + let id = id.clone(); + async move |_, cx| { + let agent = connection.await?; + let agent = agent.downcast::().unwrap(); + let summary = agent + .0 + .update(cx, |agent, cx| agent.thread_summary(id, cx))? + .await?; + anyhow::Ok(summary) + } + }); + let task = cx + .spawn(async move |_, _| load_summary.await.map_err(|e| format!("{e}"))) + .shared(); + + self.mention_set.insert_thread(id.clone(), task.clone()); + self.mention_set.insert_uri(crease_id, uri); + + let editor = self.editor.clone(); + cx.spawn_in(window, async move |this, cx| { + if task.await.notify_async_err(cx).is_none() { + editor + .update(cx, |editor, cx| { + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting(vec![anchor..anchor], true, cx); + }); + editor.remove_creases([crease_id], cx); + }) + .ok(); + this.update(cx, |this, _| { + this.mention_set.thread_summaries.remove(&id); + this.mention_set.uri_by_crease_id.remove(&crease_id); + }) + .ok(); + } }) } fn confirm_mention_for_text_thread( &mut self, + crease_id: CreaseId, + anchor: Anchor, path: PathBuf, + name: String, + window: &mut Window, cx: &mut Context, - ) -> Task> { + ) -> Task<()> { + let uri = MentionUri::TextThread { + path: path.clone(), + name, + }; let context = self.history_store.update(cx, |text_thread_store, cx| { text_thread_store.load_text_thread(path.as_path().into(), cx) }); - cx.spawn(async move |_, cx| { - let context = context.await?; - let xml = context.update(cx, |context, cx| context.to_xml(cx))?; - Ok(Mention::Text { - content: xml, - tracked_buffers: Vec::new(), + let task = cx + .spawn(async move |_, cx| { + let context = context.await.map_err(|e| e.to_string())?; + let xml = context + .update(cx, |context, cx| context.to_xml(cx)) + .map_err(|e| e.to_string())?; + Ok(xml) }) + .shared(); + + self.mention_set + .insert_text_thread(path.clone(), task.clone()); + + let editor = self.editor.clone(); + cx.spawn_in(window, async move |this, cx| { + if task.await.notify_async_err(cx).is_some() { + this.update(cx, |this, _| { + this.mention_set.insert_uri(crease_id, uri); + }) + .ok(); + } else { + editor + .update(cx, |editor, cx| { + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting(vec![anchor..anchor], true, cx); + }); + editor.remove_creases([crease_id], cx); + }) + .ok(); + this.update(cx, |this, _| { + this.mention_set.text_thread_summaries.remove(&path); + }) + .ok(); + } }) } pub fn contents( &self, + window: &mut Window, cx: &mut Context, ) -> Task, Vec>)>> { - let contents = self - .mention_set - .contents(&self.prompt_capabilities.get(), cx); + let contents = self.mention_set.contents( + &self.project, + self.prompt_store.as_ref(), + &self.prompt_capabilities.get(), + window, + cx, + ); let editor = self.editor.clone(); let prevent_slash_commands = self.prevent_slash_commands; @@ -699,7 +729,7 @@ impl MessageEditor { editor.display_map.update(cx, |map, cx| { let snapshot = map.snapshot(cx); for (crease_id, crease) in snapshot.crease_snapshot.creases() { - let Some((uri, mention)) = contents.get(&crease_id) else { + let Some(mention) = contents.get(&crease_id) else { continue; }; @@ -717,6 +747,7 @@ impl MessageEditor { } let chunk = match mention { Mention::Text { + uri, content, tracked_buffers, } => { @@ -733,25 +764,17 @@ impl MessageEditor { }) } Mention::Image(mention_image) => { - let uri = match uri { - MentionUri::File { .. } => Some(uri.to_uri().to_string()), - MentionUri::PastedImage => None, - other => { - debug_panic!( - "unexpected mention uri for image: {:?}", - other - ); - None - } - }; acp::ContentBlock::Image(acp::ImageContent { annotations: None, data: mention_image.data.to_string(), mime_type: mention_image.format.mime_type().into(), - uri, + uri: mention_image + .abs_path + .as_ref() + .map(|path| format!("file://{}", path.display())), }) } - Mention::UriOnly => { + Mention::UriOnly(uri) => { acp::ContentBlock::ResourceLink(acp::ResourceLink { name: uri.name(), uri: uri.to_uri().to_string(), @@ -790,13 +813,7 @@ impl MessageEditor { pub fn clear(&mut self, window: &mut Window, cx: &mut Context) { self.editor.update(cx, |editor, cx| { editor.clear(window, cx); - editor.remove_creases( - self.mention_set - .mentions - .drain() - .map(|(crease_id, _)| crease_id), - cx, - ) + editor.remove_creases(self.mention_set.drain(), cx) }); } @@ -836,7 +853,7 @@ impl MessageEditor { } cx.stop_propagation(); - let replacement_text = MentionUri::PastedImage.as_link().to_string(); + let replacement_text = "image"; for image in images { let (excerpt_id, text_anchor, multibuffer_anchor) = self.editor.update(cx, |message_editor, cx| { @@ -859,64 +876,24 @@ impl MessageEditor { }); let content_len = replacement_text.len(); - let Some(start_anchor) = multibuffer_anchor else { - continue; + let Some(anchor) = multibuffer_anchor else { + return; }; - let end_anchor = self.editor.update(cx, |editor, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len) - }); - let image = Arc::new(image); - let Some((crease_id, tx)) = insert_crease_for_mention( + let task = Task::ready(Ok(Arc::new(image))).shared(); + let Some(crease_id) = insert_crease_for_image( excerpt_id, text_anchor, content_len, - MentionUri::PastedImage.name().into(), - IconName::Image.path().into(), - Some(Task::ready(Ok(image.clone())).shared()), + None.clone(), + task.clone(), self.editor.clone(), window, cx, ) else { - continue; + return; }; - let task = cx - .spawn_in(window, { - async move |_, cx| { - let format = image.format; - let image = cx - .update(|_, cx| LanguageModelImage::from_image(image, cx)) - .map_err(|e| e.to_string())? - .await; - drop(tx); - if let Some(image) = image { - Ok(Mention::Image(MentionImage { - data: image.source, - format, - })) - } else { - Err("Failed to convert image".into()) - } - } - }) - .shared(); - - self.mention_set - .mentions - .insert(crease_id, (MentionUri::PastedImage, task.clone())); - - cx.spawn_in(window, async move |this, cx| { - if task.await.notify_async_err(cx).is_none() { - this.update(cx, |this, cx| { - this.editor.update(cx, |editor, cx| { - editor.edit([(start_anchor..end_anchor, "")], cx); - }); - this.mention_set.mentions.remove(&crease_id); - }) - .ok(); - } - }) - .detach(); + self.confirm_mention_for_image(crease_id, anchor, None, task, window, cx) + .detach(); } } @@ -1018,6 +995,67 @@ impl MessageEditor { }) } + fn confirm_mention_for_image( + &mut self, + crease_id: CreaseId, + anchor: Anchor, + abs_path: Option, + image: Shared, String>>>, + window: &mut Window, + cx: &mut Context, + ) -> Task<()> { + let editor = self.editor.clone(); + let task = cx + .spawn_in(window, { + let abs_path = abs_path.clone(); + async move |_, cx| { + let image = image.await?; + let format = image.format; + let image = cx + .update(|_, cx| LanguageModelImage::from_image(image, cx)) + .map_err(|e| e.to_string())? + .await; + if let Some(image) = image { + Ok(MentionImage { + abs_path, + data: image.source, + format, + }) + } else { + Err("Failed to convert image".into()) + } + } + }) + .shared(); + + self.mention_set.insert_image(crease_id, task.clone()); + + cx.spawn_in(window, async move |this, cx| { + if task.await.notify_async_err(cx).is_some() { + if let Some(abs_path) = abs_path.clone() { + this.update(cx, |this, _cx| { + this.mention_set + .insert_uri(crease_id, MentionUri::File { abs_path }); + }) + .ok(); + } + } else { + editor + .update(cx, |editor, cx| { + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting(vec![anchor..anchor], true, cx); + }); + editor.remove_creases([crease_id], cx); + }) + .ok(); + this.update(cx, |this, _cx| { + this.mention_set.images.remove(&crease_id); + }) + .ok(); + } + }) + } + pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context) { self.editor.update(cx, |editor, cx| { editor.set_mode(mode); @@ -1035,6 +1073,7 @@ impl MessageEditor { let mut text = String::new(); let mut mentions = Vec::new(); + let mut images = Vec::new(); for chunk in message { match chunk { @@ -1045,58 +1084,26 @@ impl MessageEditor { resource: acp::EmbeddedResourceResource::TextResourceContents(resource), .. }) => { - let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() else { - continue; - }; - let start = text.len(); - write!(&mut text, "{}", mention_uri.as_link()).ok(); - let end = text.len(); - mentions.push(( - start..end, - mention_uri, - Mention::Text { - content: resource.text, - tracked_buffers: Vec::new(), - }, - )); + if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() { + let start = text.len(); + write!(&mut text, "{}", mention_uri.as_link()).ok(); + let end = text.len(); + mentions.push((start..end, mention_uri, resource.text)); + } } acp::ContentBlock::ResourceLink(resource) => { if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() { let start = text.len(); write!(&mut text, "{}", mention_uri.as_link()).ok(); let end = text.len(); - mentions.push((start..end, mention_uri, Mention::UriOnly)); + mentions.push((start..end, mention_uri, resource.uri)); } } - acp::ContentBlock::Image(acp::ImageContent { - uri, - data, - mime_type, - annotations: _, - }) => { - let mention_uri = if let Some(uri) = uri { - MentionUri::parse(&uri) - } else { - Ok(MentionUri::PastedImage) - }; - let Some(mention_uri) = mention_uri.log_err() else { - continue; - }; - let Some(format) = ImageFormat::from_mime_type(&mime_type) else { - log::error!("failed to parse MIME type for image: {mime_type:?}"); - continue; - }; + acp::ContentBlock::Image(content) => { let start = text.len(); - write!(&mut text, "{}", mention_uri.as_link()).ok(); + text.push_str("image"); let end = text.len(); - mentions.push(( - start..end, - mention_uri, - Mention::Image(MentionImage { - data: data.into(), - format, - }), - )); + images.push((start..end, content)); } acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => {} } @@ -1107,27 +1114,88 @@ impl MessageEditor { editor.buffer().read(cx).snapshot(cx) }); - for (range, mention_uri, mention) in mentions { + for (range, mention_uri, text) in mentions { let anchor = snapshot.anchor_before(range.start); - let Some((crease_id, tx)) = insert_crease_for_mention( + let crease_id = crate::context_picker::insert_crease_for_mention( anchor.excerpt_id, anchor.text_anchor, range.end - range.start, mention_uri.name().into(), mention_uri.icon_path(cx), - None, self.editor.clone(), window, cx, - ) else { + ); + + if let Some(crease_id) = crease_id { + self.mention_set.insert_uri(crease_id, mention_uri.clone()); + } + + match mention_uri { + MentionUri::Thread { id, .. } => { + self.mention_set + .insert_thread(id, Task::ready(Ok(text.into())).shared()); + } + MentionUri::TextThread { path, .. } => { + self.mention_set + .insert_text_thread(path, Task::ready(Ok(text)).shared()); + } + MentionUri::Fetch { url } => { + self.mention_set + .add_fetch_result(url, Task::ready(Ok(text)).shared()); + } + MentionUri::Directory { abs_path } => { + let task = Task::ready(Ok((text, Vec::new()))).shared(); + self.mention_set.directories.insert(abs_path, task); + } + MentionUri::File { .. } + | MentionUri::Symbol { .. } + | MentionUri::Rule { .. } + | MentionUri::Selection { .. } => {} + } + } + for (range, content) in images { + let Some(format) = ImageFormat::from_mime_type(&content.mime_type) else { continue; }; - drop(tx); + let anchor = snapshot.anchor_before(range.start); + let abs_path = content + .uri + .as_ref() + .and_then(|uri| uri.strip_prefix("file://").map(|s| Path::new(s).into())); - self.mention_set.mentions.insert( - crease_id, - (mention_uri.clone(), Task::ready(Ok(mention)).shared()), + let name = content + .uri + .as_ref() + .and_then(|uri| { + uri.strip_prefix("file://") + .and_then(|path| Path::new(path).file_name()) + }) + .map(|name| name.to_string_lossy().to_string()) + .unwrap_or("Image".to_owned()); + let crease_id = crate::context_picker::insert_crease_for_mention( + anchor.excerpt_id, + anchor.text_anchor, + range.end - range.start, + name.into(), + IconName::Image.path().into(), + self.editor.clone(), + window, + cx, ); + let data: SharedString = content.data.to_string().into(); + + if let Some(crease_id) = crease_id { + self.mention_set.insert_image( + crease_id, + Task::ready(Ok(MentionImage { + abs_path, + data, + format, + })) + .shared(), + ); + } } cx.notify(); } @@ -1174,16 +1242,17 @@ impl MessageEditor { }) } - pub fn text(&self, cx: &App) -> String { - self.editor.read(cx).text(cx) - } - #[cfg(test)] pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context) { self.editor.update(cx, |editor, cx| { editor.set_text(text, window, cx); }); } + + #[cfg(test)] + pub fn text(&self, cx: &App) -> String { + self.editor.read(cx).text(cx) + } } fn render_directory_contents(entries: Vec<(Arc, PathBuf, String)>) -> String { @@ -1240,21 +1309,23 @@ impl Render for MessageEditor { } } -pub(crate) fn insert_crease_for_mention( +pub(crate) fn insert_crease_for_image( excerpt_id: ExcerptId, anchor: text::Anchor, content_len: usize, - crease_label: SharedString, - crease_icon: SharedString, - // abs_path: Option>, - image: Option, String>>>>, + abs_path: Option>, + image: Shared, String>>>, editor: Entity, window: &mut Window, cx: &mut App, -) -> Option<(CreaseId, postage::barrier::Sender)> { - let (tx, rx) = postage::barrier::channel(); +) -> Option { + let crease_label = abs_path + .as_ref() + .and_then(|path| path.file_name()) + .map(|name| name.to_string_lossy().to_string().into()) + .unwrap_or(SharedString::from("Image")); - let crease_id = editor.update(cx, |editor, cx| { + editor.update(cx, |editor, cx| { let snapshot = editor.buffer().read(cx).snapshot(cx); let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?; @@ -1263,15 +1334,7 @@ pub(crate) fn insert_crease_for_mention( let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len); let placeholder = FoldPlaceholder { - render: render_fold_icon_button( - crease_label, - crease_icon, - start..end, - rx, - image, - cx.weak_entity(), - cx, - ), + render: render_image_fold_icon_button(crease_label, image, cx.weak_entity()), merge_adjacent: false, ..Default::default() }; @@ -1288,112 +1351,63 @@ pub(crate) fn insert_crease_for_mention( editor.fold_creases(vec![crease], false, window, cx); Some(ids[0]) - })?; - - Some((crease_id, tx)) + }) } -fn render_fold_icon_button( +fn render_image_fold_icon_button( label: SharedString, - icon: SharedString, - range: Range, - mut loading_finished: postage::barrier::Receiver, - image_task: Option, String>>>>, + image_task: Shared, String>>>, editor: WeakEntity, - cx: &mut App, ) -> Arc, &mut App) -> AnyElement> { - let loading = cx.new(|cx| { - let loading = cx.spawn(async move |this, cx| { - loading_finished.recv().await; - this.update(cx, |this: &mut LoadingContext, cx| { - this.loading = None; - cx.notify(); - }) - .ok(); - }); - LoadingContext { - id: cx.entity_id(), - label, - icon, - range, - editor, - loading: Some(loading), - image: image_task.clone(), - } - }); - Arc::new(move |_fold_id, _fold_range, _cx| loading.clone().into_any_element()) -} + Arc::new({ + move |fold_id, fold_range, cx| { + let is_in_text_selection = editor + .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx)) + .unwrap_or_default(); -struct LoadingContext { - id: EntityId, - label: SharedString, - icon: SharedString, - range: Range, - editor: WeakEntity, - loading: Option>, - image: Option, String>>>>, -} - -impl Render for LoadingContext { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let is_in_text_selection = self - .editor - .update(cx, |editor, cx| editor.is_range_selected(&self.range, cx)) - .unwrap_or_default(); - ButtonLike::new(("loading-context", self.id)) - .style(ButtonStyle::Filled) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .toggle_state(is_in_text_selection) - .when_some(self.image.clone(), |el, image_task| { - el.hoverable_tooltip(move |_, cx| { - let image = image_task.peek().cloned().transpose().ok().flatten(); + ButtonLike::new(fold_id) + .style(ButtonStyle::Filled) + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + .toggle_state(is_in_text_selection) + .child( + h_flex() + .gap_1() + .child( + Icon::new(IconName::Image) + .size(IconSize::XSmall) + .color(Color::Muted), + ) + .child( + Label::new(label.clone()) + .size(LabelSize::Small) + .buffer_font(cx) + .single_line(), + ), + ) + .hoverable_tooltip({ let image_task = image_task.clone(); - cx.new::(|cx| ImageHover { - image, - _task: cx.spawn(async move |this, cx| { - if let Ok(image) = image_task.clone().await { - this.update(cx, |this, cx| { - if this.image.replace(image).is_none() { - cx.notify(); - } - }) - .ok(); - } - }), - }) - .into() + move |_, cx| { + let image = image_task.peek().cloned().transpose().ok().flatten(); + let image_task = image_task.clone(); + cx.new::(|cx| ImageHover { + image, + _task: cx.spawn(async move |this, cx| { + if let Ok(image) = image_task.clone().await { + this.update(cx, |this, cx| { + if this.image.replace(image).is_none() { + cx.notify(); + } + }) + .ok(); + } + }), + }) + .into() + } }) - }) - .child( - h_flex() - .gap_1() - .child( - Icon::from_path(self.icon.clone()) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - .child( - Label::new(self.label.clone()) - .size(LabelSize::Small) - .buffer_font(cx) - .single_line(), - ) - .map(|el| { - if self.loading.is_some() { - el.with_animation( - "loading-context-crease", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between(0.4, 0.8)), - |label, delta| label.opacity(delta), - ) - .into_any() - } else { - el.into_any() - } - }), - ) - } + .into_any_element() + } + }) } struct ImageHover { @@ -1411,60 +1425,289 @@ impl Render for ImageHover { } } -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq)] pub enum Mention { Text { + uri: MentionUri, content: String, tracked_buffers: Vec>, }, Image(MentionImage), - UriOnly, + UriOnly(MentionUri), } #[derive(Clone, Debug, Eq, PartialEq)] pub struct MentionImage { + pub abs_path: Option, pub data: SharedString, pub format: ImageFormat, } #[derive(Default)] pub struct MentionSet { - mentions: HashMap>>)>, + uri_by_crease_id: HashMap, + fetch_results: HashMap>>>, + images: HashMap>>>, + thread_summaries: HashMap>>>, + text_thread_summaries: HashMap>>>, + directories: HashMap>), String>>>>, } impl MentionSet { - fn contents( + pub fn insert_uri(&mut self, crease_id: CreaseId, uri: MentionUri) { + self.uri_by_crease_id.insert(crease_id, uri); + } + + pub fn add_fetch_result(&mut self, url: Url, content: Shared>>) { + self.fetch_results.insert(url, content); + } + + pub fn insert_image( + &mut self, + crease_id: CreaseId, + task: Shared>>, + ) { + self.images.insert(crease_id, task); + } + + fn insert_thread( + &mut self, + id: acp::SessionId, + task: Shared>>, + ) { + self.thread_summaries.insert(id, task); + } + + fn insert_text_thread(&mut self, path: PathBuf, task: Shared>>) { + self.text_thread_summaries.insert(path, task); + } + + pub fn contents( &self, + project: &Entity, + prompt_store: Option<&Entity>, prompt_capabilities: &acp::PromptCapabilities, + _window: &mut Window, cx: &mut App, - ) -> Task>> { + ) -> Task>> { if !prompt_capabilities.embedded_context { let mentions = self - .mentions + .uri_by_crease_id .iter() - .map(|(crease_id, (uri, _))| (*crease_id, (uri.clone(), Mention::UriOnly))) + .map(|(crease_id, uri)| (*crease_id, Mention::UriOnly(uri.clone()))) .collect(); return Task::ready(Ok(mentions)); } - let mentions = self.mentions.clone(); - cx.spawn(async move |_cx| { - let mut contents = HashMap::default(); - for (crease_id, (mention_uri, task)) in mentions { - contents.insert( - crease_id, - (mention_uri, task.await.map_err(|e| anyhow!("{e}"))?), - ); + let mut processed_image_creases = HashSet::default(); + + let mut contents = self + .uri_by_crease_id + .iter() + .map(|(&crease_id, uri)| { + match uri { + MentionUri::File { abs_path, .. } => { + let uri = uri.clone(); + let abs_path = abs_path.to_path_buf(); + + if let Some(task) = self.images.get(&crease_id).cloned() { + processed_image_creases.insert(crease_id); + return cx.spawn(async move |_| { + let image = task.await.map_err(|e| anyhow!("{e}"))?; + anyhow::Ok((crease_id, Mention::Image(image))) + }); + } + + let buffer_task = project.update(cx, |project, cx| { + let path = project + .find_project_path(abs_path, cx) + .context("Failed to find project path")?; + anyhow::Ok(project.open_buffer(path, cx)) + }); + cx.spawn(async move |cx| { + let buffer = buffer_task?.await?; + let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; + + anyhow::Ok(( + crease_id, + Mention::Text { + uri, + content, + tracked_buffers: vec![buffer], + }, + )) + }) + } + MentionUri::Directory { abs_path } => { + let Some(content) = self.directories.get(abs_path).cloned() else { + return Task::ready(Err(anyhow!("missing directory load task"))); + }; + let uri = uri.clone(); + cx.spawn(async move |_| { + let (content, tracked_buffers) = + content.await.map_err(|e| anyhow::anyhow!("{e}"))?; + Ok(( + crease_id, + Mention::Text { + uri, + content, + tracked_buffers, + }, + )) + }) + } + MentionUri::Symbol { + path, line_range, .. + } + | MentionUri::Selection { + path, line_range, .. + } => { + let uri = uri.clone(); + let path_buf = path.clone(); + let line_range = line_range.clone(); + + let buffer_task = project.update(cx, |project, cx| { + let path = project + .find_project_path(&path_buf, cx) + .context("Failed to find project path")?; + anyhow::Ok(project.open_buffer(path, cx)) + }); + + cx.spawn(async move |cx| { + let buffer = buffer_task?.await?; + let content = buffer.read_with(cx, |buffer, _cx| { + buffer + .text_for_range( + Point::new(line_range.start, 0) + ..Point::new( + line_range.end, + buffer.line_len(line_range.end), + ), + ) + .collect() + })?; + + anyhow::Ok(( + crease_id, + Mention::Text { + uri, + content, + tracked_buffers: vec![buffer], + }, + )) + }) + } + MentionUri::Thread { id, .. } => { + let Some(content) = self.thread_summaries.get(id).cloned() else { + return Task::ready(Err(anyhow!("missing thread summary"))); + }; + let uri = uri.clone(); + cx.spawn(async move |_| { + Ok(( + crease_id, + Mention::Text { + uri, + content: content + .await + .map_err(|e| anyhow::anyhow!("{e}"))? + .to_string(), + tracked_buffers: Vec::new(), + }, + )) + }) + } + MentionUri::TextThread { path, .. } => { + let Some(content) = self.text_thread_summaries.get(path).cloned() else { + return Task::ready(Err(anyhow!("missing text thread summary"))); + }; + let uri = uri.clone(); + cx.spawn(async move |_| { + Ok(( + crease_id, + Mention::Text { + uri, + content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, + tracked_buffers: Vec::new(), + }, + )) + }) + } + MentionUri::Rule { id: prompt_id, .. } => { + let Some(prompt_store) = prompt_store else { + return Task::ready(Err(anyhow!("missing prompt store"))); + }; + let text_task = prompt_store.read(cx).load(*prompt_id, cx); + let uri = uri.clone(); + cx.spawn(async move |_| { + // TODO: report load errors instead of just logging + let text = text_task.await?; + anyhow::Ok(( + crease_id, + Mention::Text { + uri, + content: text, + tracked_buffers: Vec::new(), + }, + )) + }) + } + MentionUri::Fetch { url } => { + let Some(content) = self.fetch_results.get(url).cloned() else { + return Task::ready(Err(anyhow!("missing fetch result"))); + }; + let uri = uri.clone(); + cx.spawn(async move |_| { + Ok(( + crease_id, + Mention::Text { + uri, + content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, + tracked_buffers: Vec::new(), + }, + )) + }) + } + } + }) + .collect::>(); + + // Handle images that didn't have a mention URI (because they were added by the paste handler). + contents.extend(self.images.iter().filter_map(|(crease_id, image)| { + if processed_image_creases.contains(crease_id) { + return None; } - Ok(contents) + let crease_id = *crease_id; + let image = image.clone(); + Some(cx.spawn(async move |_| { + Ok(( + crease_id, + Mention::Image(image.await.map_err(|e| anyhow::anyhow!("{e}"))?), + )) + })) + })); + + cx.spawn(async move |_cx| { + let contents = try_join_all(contents).await?.into_iter().collect(); + anyhow::Ok(contents) }) } - fn remove_invalid(&mut self, snapshot: EditorSnapshot) { + pub fn drain(&mut self) -> impl Iterator { + self.fetch_results.clear(); + self.thread_summaries.clear(); + self.text_thread_summaries.clear(); + self.directories.clear(); + self.uri_by_crease_id + .drain() + .map(|(id, _)| id) + .chain(self.images.drain().map(|(id, _)| id)) + } + + pub fn remove_invalid(&mut self, snapshot: EditorSnapshot) { for (crease_id, crease) in snapshot.crease_snapshot.creases() { if !crease.range().start.is_valid(&snapshot.buffer_snapshot) { - self.mentions.remove(&crease_id); + self.uri_by_crease_id.remove(&crease_id); } } } @@ -1726,7 +1969,9 @@ mod tests { }); let (content, _) = message_editor - .update(cx, |message_editor, cx| message_editor.contents(cx)) + .update_in(cx, |message_editor, window, cx| { + message_editor.contents(window, cx) + }) .await .unwrap(); @@ -1793,8 +2038,7 @@ mod tests { "six.txt": "6", "seven.txt": "7", "eight.txt": "8", - }, - "x.png": "", + } }), ) .await; @@ -1978,10 +2222,14 @@ mod tests { }; let contents = message_editor - .update(&mut cx, |message_editor, cx| { - message_editor - .mention_set() - .contents(&all_prompt_capabilities, cx) + .update_in(&mut cx, |message_editor, window, cx| { + message_editor.mention_set().contents( + &project, + None, + &all_prompt_capabilities, + window, + cx, + ) }) .await .unwrap() @@ -1989,7 +2237,7 @@ mod tests { .collect::>(); { - let [(uri, Mention::Text { content, .. })] = contents.as_slice() else { + let [Mention::Text { content, uri, .. }] = contents.as_slice() else { panic!("Unexpected mentions"); }; pretty_assertions::assert_eq!(content, "1"); @@ -1997,10 +2245,14 @@ mod tests { } let contents = message_editor - .update(&mut cx, |message_editor, cx| { - message_editor - .mention_set() - .contents(&acp::PromptCapabilities::default(), cx) + .update_in(&mut cx, |message_editor, window, cx| { + message_editor.mention_set().contents( + &project, + None, + &acp::PromptCapabilities::default(), + window, + cx, + ) }) .await .unwrap() @@ -2008,7 +2260,7 @@ mod tests { .collect::>(); { - let [(uri, Mention::UriOnly)] = contents.as_slice() else { + let [Mention::UriOnly(uri)] = contents.as_slice() else { panic!("Unexpected mentions"); }; pretty_assertions::assert_eq!(uri, &url_one.parse::().unwrap()); @@ -2048,10 +2300,14 @@ mod tests { cx.run_until_parked(); let contents = message_editor - .update(&mut cx, |message_editor, cx| { - message_editor - .mention_set() - .contents(&all_prompt_capabilities, cx) + .update_in(&mut cx, |message_editor, window, cx| { + message_editor.mention_set().contents( + &project, + None, + &all_prompt_capabilities, + window, + cx, + ) }) .await .unwrap() @@ -2061,7 +2317,7 @@ mod tests { let url_eight = uri!("file:///dir/b/eight.txt"); { - let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else { + let [_, Mention::Text { content, uri, .. }] = contents.as_slice() else { panic!("Unexpected mentions"); }; pretty_assertions::assert_eq!(content, "8"); @@ -2158,10 +2414,14 @@ mod tests { }); let contents = message_editor - .update(&mut cx, |message_editor, cx| { - message_editor - .mention_set() - .contents(&all_prompt_capabilities, cx) + .update_in(&mut cx, |message_editor, window, cx| { + message_editor.mention_set().contents( + &project, + None, + &all_prompt_capabilities, + window, + cx, + ) }) .await .unwrap() @@ -2169,7 +2429,7 @@ mod tests { .collect::>(); { - let [_, _, (uri, Mention::Text { content, .. })] = contents.as_slice() else { + let [_, _, Mention::Text { content, uri, .. }] = contents.as_slice() else { panic!("Unexpected mentions"); }; pretty_assertions::assert_eq!(content, "1"); @@ -2184,85 +2444,11 @@ mod tests { cx.run_until_parked(); editor.read_with(&cx, |editor, cx| { - assert_eq!( - editor.text(cx), - format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ") - ); - }); - - // Try to mention an "image" file that will fail to load - cx.simulate_input("@file x.png"); - - editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.text(cx), - format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png") - ); - assert!(editor.has_visible_completions_menu()); - assert_eq!(current_completion_labels(editor), &["x.png dir/"]); - }); - - editor.update_in(&mut cx, |editor, window, cx| { - editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); - }); - - // Getting the message contents fails - message_editor - .update(&mut cx, |message_editor, cx| { - message_editor - .mention_set() - .contents(&all_prompt_capabilities, cx) - }) - .await - .expect_err("Should fail to load x.png"); - - cx.run_until_parked(); - - // Mention was removed - editor.read_with(&cx, |editor, cx| { - assert_eq!( - editor.text(cx), - format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ") - ); - }); - - // Once more - cx.simulate_input("@file x.png"); - - editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.text(cx), - format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png") - ); - assert!(editor.has_visible_completions_menu()); - assert_eq!(current_completion_labels(editor), &["x.png dir/"]); - }); - - editor.update_in(&mut cx, |editor, window, cx| { - editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); - }); - - // This time don't immediately get the contents, just let the confirmed completion settle - cx.run_until_parked(); - - // Mention was removed - editor.read_with(&cx, |editor, cx| { - assert_eq!( - editor.text(cx), - format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ") - ); - }); - - // Now getting the contents succeeds, because the invalid mention was removed - let contents = message_editor - .update(&mut cx, |message_editor, cx| { - message_editor - .mention_set() - .contents(&all_prompt_capabilities, cx) - }) - .await - .unwrap(); - assert_eq!(contents.len(), 3); + assert_eq!( + editor.text(cx), + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ") + ); + }); } fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec> { diff --git a/crates/agent_ui/src/acp/thread_history.rs b/crates/agent_ui/src/acp/thread_history.rs index a49dae25b3..5d852f0ddc 100644 --- a/crates/agent_ui/src/acp/thread_history.rs +++ b/crates/agent_ui/src/acp/thread_history.rs @@ -462,7 +462,7 @@ impl AcpThreadHistory { cx.notify(); })) - .end_slot::(if hovered { + .end_slot::(if hovered || selected { Some( IconButton::new("delete", IconName::Trash) .shape(IconButtonShape::Square) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index c68c3a3e93..d27dee1fe6 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -20,11 +20,11 @@ use file_icons::FileIcons; use fs::Fs; use gpui::{ Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem, - CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, - ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, - Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, - Window, WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, - point, prelude::*, pulsating_between, + EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, + ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, + Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, + WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, point, + prelude::*, pulsating_between, }; use language::Buffer; @@ -35,7 +35,6 @@ use prompt_store::{PromptId, PromptStore}; use rope::Point; use settings::{Settings as _, SettingsStore}; use std::cell::Cell; -use std::path::Path; use std::sync::Arc; use std::time::Instant; use std::{collections::BTreeMap, rc::Rc, time::Duration}; @@ -43,7 +42,7 @@ use text::Anchor; use theme::ThemeSettings; use ui::{ Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle, - Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*, + Scrollbar, ScrollbarState, SpinnerLabel, Tooltip, prelude::*, }; use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use workspace::{CollaboratorId, Workspace}; @@ -66,6 +65,7 @@ use crate::{ KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector, }; +const RESPONSE_PADDING_X: Pixels = px(19.); pub const MIN_EDITOR_LINES: usize = 4; pub const MAX_EDITOR_LINES: usize = 8; @@ -258,7 +258,6 @@ pub struct AcpThreadView { hovered_recent_history_item: Option, entry_view_state: Entity, message_editor: Entity, - focus_handle: FocusHandle, model_selector: Option>, profile_selector: Option>, notifications: Vec>, @@ -274,11 +273,8 @@ pub struct AcpThreadView { edits_expanded: bool, plan_expanded: bool, editor_expanded: bool, - should_be_following: bool, editing_message: Option, prompt_capabilities: Rc>, - is_loading_contents: bool, - install_command_markdown: Entity, _cancel_task: Option>, _subscriptions: [Subscription; 3], } @@ -316,13 +312,6 @@ impl AcpThreadView { ) -> Self { let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default())); let prevent_slash_commands = agent.clone().downcast::().is_some(); - - let placeholder = if agent.name() == "Zed Agent" { - format!("Message the {} — @ to include context", agent.name()) - } else { - format!("Message {} — @ to include context", agent.name()) - }; - let message_editor = cx.new(|cx| { let mut editor = MessageEditor::new( workspace.clone(), @@ -330,7 +319,7 @@ impl AcpThreadView { history_store.clone(), prompt_store.clone(), prompt_capabilities.clone(), - placeholder, + "Message the agent — @ to include context", prevent_slash_commands, editor::EditorMode::AutoHeight { min_lines: MIN_EDITOR_LINES, @@ -387,15 +376,11 @@ impl AcpThreadView { edits_expanded: false, plan_expanded: false, editor_expanded: false, - should_be_following: false, history_store, hovered_recent_history_item: None, prompt_capabilities, - is_loading_contents: false, - install_command_markdown: cx.new(|cx| Markdown::new("".into(), None, None, cx)), _subscriptions: subscriptions, _cancel_task: None, - focus_handle: cx.focus_handle(), } } @@ -419,12 +404,8 @@ impl AcpThreadView { let connection = match connect_task.await { Ok(connection) => connection, Err(err) => { - this.update_in(cx, |this, window, cx| { - if err.downcast_ref::().is_some() { - this.handle_load_error(err, window, cx); - } else { - this.handle_thread_error(err, cx); - } + this.update(cx, |this, cx| { + this.handle_load_error(err, cx); cx.notify(); }) .log_err(); @@ -476,7 +457,7 @@ impl AcpThreadView { let action_log = thread.read(cx).action_log().clone(); this.prompt_capabilities - .set(thread.read(cx).prompt_capabilities()); + .set(connection.prompt_capabilities()); let count = thread.read(cx).entries().len(); this.list_state.splice(0..0, count); @@ -541,7 +522,6 @@ impl AcpThreadView { title_editor, _subscriptions: subscriptions, }; - this.message_editor.focus_handle(cx).focus(window); this.profile_selector = this.as_native_thread(cx).map(|thread| { cx.new(|cx| { @@ -557,7 +537,7 @@ impl AcpThreadView { cx.notify(); } Err(err) => { - this.handle_load_error(err, window, cx); + this.handle_load_error(err, cx); } }; }) @@ -604,7 +584,7 @@ impl AcpThreadView { let view = registry.read(cx).provider(&provider_id).map(|provider| { provider.configuration_view( - language_model::ConfigurationViewTargetAgent::Other(agent_name.clone()), + language_model::ConfigurationViewTargetAgent::Other(agent_name), window, cx, ) @@ -626,28 +606,17 @@ impl AcpThreadView { .map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))), _subscription: subscription, }; - if this.message_editor.focus_handle(cx).is_focused(window) { - this.focus_handle.focus(window) - } cx.notify(); }) .ok(); } - fn handle_load_error( - &mut self, - err: anyhow::Error, - window: &mut Window, - cx: &mut Context, - ) { + fn handle_load_error(&mut self, err: anyhow::Error, cx: &mut Context) { if let Some(load_err) = err.downcast_ref::() { self.thread_state = ThreadState::LoadError(load_err.clone()); } else { self.thread_state = ThreadState::LoadError(LoadError::Other(err.to_string().into())) } - if self.message_editor.focus_handle(cx).is_focused(window) { - self.focus_handle.focus(window) - } cx.notify(); } @@ -664,16 +633,12 @@ impl AcpThreadView { } } - pub fn title(&self) -> SharedString { + pub fn title(&self, cx: &App) -> SharedString { match &self.thread_state { - ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(), + ThreadState::Ready { thread, .. } => thread.read(cx).title(), ThreadState::Loading { .. } => "Loading…".into(), - ThreadState::LoadError(error) => match error { - LoadError::NotInstalled { .. } => format!("Install {}", self.agent.name()).into(), - LoadError::Unsupported { .. } => format!("Upgrade {}", self.agent.name()).into(), - LoadError::Exited { .. } => format!("{} Exited", self.agent.name()).into(), - LoadError::Other(_) => format!("Error Loading {}", self.agent.name()).into(), - }, + ThreadState::LoadError(_) => "Failed to load".into(), + ThreadState::Unauthenticated { .. } => "Authentication Required".into(), } } @@ -771,7 +736,6 @@ impl AcpThreadView { MessageEditorEvent::Focus => { self.cancel_editing(&Default::default(), window, cx); } - MessageEditorEvent::LostFocus => {} } } @@ -803,18 +767,6 @@ impl AcpThreadView { cx.notify(); } } - ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::LostFocus) => { - if let Some(thread) = self.thread() - && let Some(AgentThreadEntry::UserMessage(user_message)) = - thread.read(cx).entries().get(event.entry_index) - && user_message.id.is_some() - { - if editor.read(cx).text(cx).as_str() == user_message.content.to_markdown(cx) { - self.editing_message = None; - cx.notify(); - } - } - } ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => { self.regenerate(event.entry_index, editor, window, cx); } @@ -829,9 +781,6 @@ impl AcpThreadView { let Some(thread) = self.thread() else { return; }; - if !thread.read(cx).can_resume(cx) { - return; - } let task = thread.update(cx, |thread, cx| thread.resume(cx)); cx.spawn(async move |this, cx| { @@ -848,11 +797,6 @@ impl AcpThreadView { fn send(&mut self, window: &mut Window, cx: &mut Context) { let Some(thread) = self.thread() else { return }; - - if self.is_loading_contents { - return; - } - self.history_store.update(cx, |history, cx| { history.push_recently_opened_entry( HistoryEntryId::AcpThread(thread.read(cx).session_id().clone()), @@ -867,7 +811,7 @@ impl AcpThreadView { let contents = self .message_editor - .update(cx, |message_editor, cx| message_editor.contents(cx)); + .update(cx, |message_editor, cx| message_editor.contents(window, cx)); self.send_impl(contents, window, cx) } @@ -880,7 +824,7 @@ impl AcpThreadView { let contents = self .message_editor - .update(cx, |message_editor, cx| message_editor.contents(cx)); + .update(cx, |message_editor, cx| message_editor.contents(window, cx)); cx.spawn_in(window, async move |this, cx| { cancelled.await; @@ -899,8 +843,6 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context, ) { - let agent_telemetry_id = self.agent.telemetry_id(); - self.thread_error.take(); self.editing_message.take(); self.thread_feedback.clear(); @@ -908,22 +850,6 @@ impl AcpThreadView { let Some(thread) = self.thread().cloned() else { return; }; - if self.should_be_following { - self.workspace - .update(cx, |workspace, cx| { - workspace.follow(CollaboratorId::Agent, window, cx); - }) - .ok(); - } - - self.is_loading_contents = true; - let guard = cx.new(|_| ()); - cx.observe_release(&guard, |this, _guard, cx| { - this.is_loading_contents = false; - cx.notify(); - }) - .detach(); - let task = cx.spawn_in(window, async move |this, cx| { let (contents, tracked_buffers) = contents.await?; @@ -944,10 +870,6 @@ impl AcpThreadView { action_log.buffer_read(buffer, cx) } }); - drop(guard); - - telemetry::event!("Agent Message Sent", agent = agent_telemetry_id); - thread.send(contents, cx) })?; send.await @@ -959,16 +881,6 @@ impl AcpThreadView { this.handle_thread_error(err, cx); }) .ok(); - } else { - this.update(cx, |this, cx| { - this.should_be_following = this - .workspace - .update(cx, |workspace, _| { - workspace.is_being_followed(CollaboratorId::Agent) - }) - .unwrap_or_default(); - }) - .ok(); } }) .detach(); @@ -1012,24 +924,20 @@ impl AcpThreadView { let Some(thread) = self.thread().cloned() else { return; }; - if self.is_loading_contents { - return; - } - let Some(user_message_id) = thread.update(cx, |thread, _| { - thread.entries().get(entry_ix)?.user_message()?.id.clone() + let Some(rewind) = thread.update(cx, |thread, cx| { + let user_message_id = thread.entries().get(entry_ix)?.user_message()?.id.clone()?; + Some(thread.rewind(user_message_id, cx)) }) else { return; }; - let contents = message_editor.update(cx, |message_editor, cx| message_editor.contents(cx)); + let contents = + message_editor.update(cx, |message_editor, cx| message_editor.contents(window, cx)); - let task = cx.spawn(async move |_, cx| { - let contents = contents.await?; - thread - .update(cx, |thread, cx| thread.rewind(user_message_id, cx))? - .await?; - Ok(contents) + let task = cx.foreground_executor().spawn(async move { + rewind.await?; + contents.await }); self.send_impl(task, window, cx); } @@ -1161,9 +1069,6 @@ impl AcpThreadView { AcpThreadEvent::LoadError(error) => { self.thread_retry_status.take(); self.thread_state = ThreadState::LoadError(error.clone()); - if self.message_editor.focus_handle(cx).is_focused(window) { - self.focus_handle.focus(window) - } } AcpThreadEvent::TitleUpdated => { let title = thread.read(cx).title(); @@ -1175,10 +1080,6 @@ impl AcpThreadView { }); } } - AcpThreadEvent::PromptCapabilitiesUpdated => { - self.prompt_capabilities - .set(thread.read(cx).prompt_capabilities()); - } AcpThreadEvent::TokenUsageUpdated => {} } cx.notify(); @@ -1258,44 +1159,30 @@ impl AcpThreadView { pending_auth_method.replace(method.clone()); let authenticate = connection.authenticate(method, cx); cx.notify(); - self.auth_task = - Some(cx.spawn_in(window, { - let project = self.project.clone(); - let agent = self.agent.clone(); - async move |this, cx| { - let result = authenticate.await; + self.auth_task = Some(cx.spawn_in(window, { + let project = self.project.clone(); + let agent = self.agent.clone(); + async move |this, cx| { + let result = authenticate.await; - match &result { - Ok(_) => telemetry::event!( - "Authenticate Agent Succeeded", - agent = agent.telemetry_id() - ), - Err(_) => { - telemetry::event!( - "Authenticate Agent Failed", - agent = agent.telemetry_id(), - ) - } + this.update_in(cx, |this, window, cx| { + if let Err(err) = result { + this.handle_thread_error(err, cx); + } else { + this.thread_state = Self::initial_state( + agent, + None, + this.workspace.clone(), + project.clone(), + window, + cx, + ) } - - this.update_in(cx, |this, window, cx| { - if let Err(err) = result { - this.handle_thread_error(err, cx); - } else { - this.thread_state = Self::initial_state( - agent, - None, - this.workspace.clone(), - project.clone(), - window, - cx, - ) - } - this.auth_task.take() - }) - .ok(); - } - })); + this.auth_task.take() + }) + .ok(); + } + })); } fn authorize_tool_call( @@ -1303,7 +1190,6 @@ impl AcpThreadView { tool_call_id: acp::ToolCallId, option_id: acp::PermissionOptionId, option_kind: acp::PermissionOptionKind, - window: &mut Window, cx: &mut Context, ) { let Some(thread) = self.thread() else { @@ -1312,13 +1198,6 @@ impl AcpThreadView { thread.update(cx, |thread, cx| { thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx); }); - if self.should_be_following { - self.workspace - .update(cx, |workspace, cx| { - workspace.follow(CollaboratorId::Agent, window, cx); - }) - .ok(); - } cx.notify(); } @@ -1340,10 +1219,6 @@ impl AcpThreadView { window: &mut Window, cx: &Context, ) -> AnyElement { - let is_generating = self - .thread() - .is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle); - let primary = match &entry { AgentThreadEntry::UserMessage(message) => { let Some(editor) = self @@ -1366,24 +1241,11 @@ impl AcpThreadView { None }; - let has_checkpoint_button = message - .checkpoint - .as_ref() - .is_some_and(|checkpoint| checkpoint.show); - let agent_name = self.agent.name(); v_flex() .id(("user_message", entry_ix)) - .map(|this| { - if entry_ix == 0 && !has_checkpoint_button && rules_item.is_none() { - this.pt_4() - } else if rules_item.is_some() { - this.pt_3() - } else { - this.pt_2() - } - }) + .pt_2() .pb_4() .px_2() .gap_1p5() @@ -1392,7 +1254,6 @@ impl AcpThreadView { .children(message.id.clone().and_then(|message_id| { message.checkpoint.as_ref()?.show.then(|| { h_flex() - .px_3() .gap_2() .child(Divider::horizontal()) .child( @@ -1452,34 +1313,25 @@ impl AcpThreadView { base_container .child( IconButton::new("cancel", IconName::Close) - .disabled(self.is_loading_contents) .icon_color(Color::Error) .icon_size(IconSize::XSmall) .on_click(cx.listener(Self::cancel_editing)) ) .child( - if self.is_loading_contents { - div() - .id("loading-edited-message-content") - .tooltip(Tooltip::text("Loading Added Context…")) - .child(loading_contents_spinner(IconSize::XSmall)) - .into_any_element() - } else { - IconButton::new("regenerate", IconName::Return) - .icon_color(Color::Muted) - .icon_size(IconSize::XSmall) - .tooltip(Tooltip::text( - "Editing will restart the thread from this point." - )) - .on_click(cx.listener({ - let editor = editor.clone(); - move |this, _, window, cx| { - this.regenerate( - entry_ix, &editor, window, cx, - ); - } - })).into_any_element() - } + IconButton::new("regenerate", IconName::Return) + .icon_color(Color::Muted) + .icon_size(IconSize::XSmall) + .tooltip(Tooltip::text( + "Editing will restart the thread from this point." + )) + .on_click(cx.listener({ + let editor = editor.clone(); + move |this, _, window, cx| { + this.regenerate( + entry_ix, &editor, window, cx, + ); + } + })), ) ) } else { @@ -1492,7 +1344,7 @@ impl AcpThreadView { .icon_color(Color::Muted) .style(ButtonStyle::Transparent) .tooltip(move |_window, cx| { - cx.new(|_| UnavailableEditingTooltip::new(agent_name.clone())) + cx.new(|_| UnavailableEditingTooltip::new(agent_name.into())) .into() }) ) @@ -1503,20 +1355,6 @@ impl AcpThreadView { .into_any() } AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => { - let is_last = entry_ix + 1 == total_entries; - let pending_thinking_chunk_ix = if is_generating && is_last { - chunks - .iter() - .enumerate() - .next_back() - .filter(|(_, segment)| { - matches!(segment, AssistantMessageChunk::Thought { .. }) - }) - .map(|(index, _)| index) - } else { - None - }; - let style = default_markdown_style(false, false, window, cx); let message_body = v_flex() .w_full() @@ -1535,7 +1373,6 @@ impl AcpThreadView { entry_ix, chunk_ix, md.clone(), - Some(chunk_ix) == pending_thinking_chunk_ix, window, cx, ) @@ -1549,7 +1386,7 @@ impl AcpThreadView { v_flex() .px_5() .py_1() - .when(is_last, |this| this.pb_4()) + .when(entry_ix + 1 == total_entries, |this| this.pb_4()) .w_full() .text_ui(cx) .child(message_body) @@ -1558,7 +1395,7 @@ impl AcpThreadView { AgentThreadEntry::ToolCall(tool_call) => { let has_terminals = tool_call.terminals().next().is_some(); - div().w_full().map(|this| { + div().w_full().py_1().px_5().map(|this| { if has_terminals { this.children(tool_call.terminals().map(|terminal| { self.render_terminal_tool_call( @@ -1577,14 +1414,17 @@ impl AcpThreadView { return primary; }; - let primary = if entry_ix == total_entries - 1 { + let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating); + let primary = if entry_ix == total_entries - 1 && !is_generating { v_flex() .w_full() .child(primary) - .child(self.render_thread_controls(&thread, cx)) + .child(self.render_thread_controls(cx)) .when_some( self.thread_feedback.comments_editor.clone(), - |this, editor| this.child(Self::render_feedback_feedback_editor(editor, cx)), + |this, editor| { + this.child(Self::render_feedback_feedback_editor(editor, window, cx)) + }, ) .into_any_element() } else { @@ -1634,90 +1474,64 @@ impl AcpThreadView { entry_ix: usize, chunk_ix: usize, chunk: Entity, - pending: bool, window: &Window, cx: &Context, ) -> AnyElement { let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix)); let card_header_id = SharedString::from("inner-card-header"); - let key = (entry_ix, chunk_ix); - let is_open = self.expanded_thinking_blocks.contains(&key); - let editor_bg = cx.theme().colors().editor_background; - let gradient_overlay = div() - .rounded_b_lg() - .h_full() - .absolute() - .w_full() - .bottom_0() - .left_0() - .bg(linear_gradient( - 180., - linear_color_stop(editor_bg, 1.), - linear_color_stop(editor_bg.opacity(0.2), 0.), - )); - - let scroll_handle = self - .entry_view_state - .read(cx) - .entry(entry_ix) - .and_then(|entry| entry.scroll_handle_for_assistant_message_chunk(chunk_ix)); v_flex() - .rounded_md() - .border_1() - .border_color(self.tool_card_border_color(cx)) .child( h_flex() .id(header_id) .group(&card_header_id) .relative() .w_full() - .py_0p5() - .px_1p5() - .rounded_t_md() - .bg(self.tool_card_header_bg(cx)) - .justify_between() - .border_b_1() - .border_color(self.tool_card_border_color(cx)) + .gap_1p5() .child( h_flex() - .h(window.line_height()) - .gap_1p5() - .child( - Icon::new(IconName::ToolThink) - .size(IconSize::Small) - .color(Color::Muted), - ) + .size_4() + .justify_center() .child( div() - .text_size(self.tool_name_font_size()) - .text_color(cx.theme().colors().text_muted) - .map(|this| { - if pending { - this.child("Thinking") - } else { - this.child("Thought Process") - } - }), + .group_hover(&card_header_id, |s| s.invisible().w_0()) + .child( + Icon::new(IconName::ToolThink) + .size(IconSize::Small) + .color(Color::Muted), + ), + ) + .child( + h_flex() + .absolute() + .inset_0() + .invisible() + .justify_center() + .group_hover(&card_header_id, |s| s.visible()) + .child( + Disclosure::new(("expand", entry_ix), is_open) + .opened_icon(IconName::ChevronUp) + .closed_icon(IconName::ChevronRight) + .on_click(cx.listener({ + move |this, _event, _window, cx| { + if is_open { + this.expanded_thinking_blocks.remove(&key); + } else { + this.expanded_thinking_blocks.insert(key); + } + cx.notify(); + } + })), + ), ), ) .child( - Disclosure::new(("expand", entry_ix), is_open) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown) - .visible_on_hover(&card_header_id) - .on_click(cx.listener({ - move |this, _event, _window, cx| { - if is_open { - this.expanded_thinking_blocks.remove(&key); - } else { - this.expanded_thinking_blocks.insert(key); - } - cx.notify(); - } - })), + div() + .text_size(self.tool_name_font_size()) + .text_color(cx.theme().colors().text_muted) + .child("Thinking"), ) .on_click(cx.listener({ move |this, _event, _window, cx| { @@ -1730,40 +1544,34 @@ impl AcpThreadView { } })), ) - .child( - div() - .relative() - .bg(editor_bg) - .rounded_b_lg() - .child( - div() - .id(("thinking-content", chunk_ix)) - .when_some(scroll_handle, |this, scroll_handle| { - this.track_scroll(&scroll_handle) - }) - .p_2() - .when(!is_open, |this| this.max_h_20()) - .text_ui_sm(cx) - .overflow_hidden() - .child(self.render_markdown( - chunk, - default_markdown_style(false, false, window, cx), - )), - ) - .when(!is_open && pending, |this| this.child(gradient_overlay)), - ) + .when(is_open, |this| { + this.child( + div() + .relative() + .mt_1p5() + .ml(px(7.)) + .pl_4() + .border_l_1() + .border_color(self.tool_card_border_color(cx)) + .text_ui_sm(cx) + .child(self.render_markdown( + chunk, + default_markdown_style(false, false, window, cx), + )), + ) + }) .into_any_element() } - fn render_tool_call( + fn render_tool_call_icon( &self, + group_name: SharedString, entry_ix: usize, + is_collapsible: bool, + is_open: bool, tool_call: &ToolCall, - window: &Window, cx: &Context, ) -> Div { - let card_header_id = SharedString::from("inner-tool-call-header"); - let tool_icon = if tool_call.kind == acp::ToolKind::Edit && tool_call.locations.len() == 1 { FileIcons::get_icon(&tool_call.locations[0].path, cx) @@ -1771,7 +1579,7 @@ impl AcpThreadView { .unwrap_or(Icon::new(IconName::ToolPencil)) } else { Icon::new(match tool_call.kind { - acp::ToolKind::Read => IconName::ToolSearch, + acp::ToolKind::Read => IconName::ToolRead, acp::ToolKind::Edit => IconName::ToolPencil, acp::ToolKind::Delete => IconName::ToolDeleteFile, acp::ToolKind::Move => IconName::ArrowRightLeft, @@ -1785,12 +1593,93 @@ impl AcpThreadView { .size(IconSize::Small) .color(Color::Muted); - let failed_or_canceled = match &tool_call.status { - ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true, - _ => false, + let base_container = h_flex().flex_shrink_0().size_4().justify_center(); + + if is_collapsible { + base_container + .child( + div() + .group_hover(&group_name, |s| s.invisible().w_0()) + .child(tool_icon), + ) + .child( + h_flex() + .absolute() + .inset_0() + .invisible() + .justify_center() + .group_hover(&group_name, |s| s.visible()) + .child( + Disclosure::new(("expand", entry_ix), is_open) + .opened_icon(IconName::ChevronUp) + .closed_icon(IconName::ChevronRight) + .on_click(cx.listener({ + let id = tool_call.id.clone(); + move |this: &mut Self, _, _, cx: &mut Context| { + if is_open { + this.expanded_tool_calls.remove(&id); + } else { + this.expanded_tool_calls.insert(id.clone()); + } + cx.notify(); + } + })), + ), + ) + } else { + base_container.child(tool_icon) + } + } + + fn render_tool_call( + &self, + entry_ix: usize, + tool_call: &ToolCall, + window: &Window, + cx: &Context, + ) -> Div { + let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix)); + let card_header_id = SharedString::from("inner-tool-call-header"); + + let status_icon = match &tool_call.status { + ToolCallStatus::Pending + | ToolCallStatus::WaitingForConfirmation { .. } + | ToolCallStatus::Completed => None, + ToolCallStatus::InProgress => Some( + div() + .absolute() + .right_2() + .child( + Icon::new(IconName::ArrowCircle) + .color(Color::Muted) + .size(IconSize::Small) + .with_animation( + "running", + Animation::new(Duration::from_secs(3)).repeat(), + |icon, delta| { + icon.transform(Transformation::rotate(percentage(delta))) + }, + ), + ) + .into_any(), + ), + ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => Some( + div() + .absolute() + .right_2() + .child( + Icon::new(IconName::Close) + .color(Color::Error) + .size(IconSize::Small), + ) + .into_any_element(), + ), }; - let has_location = tool_call.locations.len() == 1; + let failed_tool_call = matches!( + tool_call.status, + ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed + ); let needs_confirmation = matches!( tool_call.status, ToolCallStatus::WaitingForConfirmation { .. } @@ -1803,31 +1692,23 @@ impl AcpThreadView { let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id); - let gradient_overlay = { + let gradient_overlay = |color: Hsla| { div() .absolute() .top_0() .right_0() - .w_12() + .w_16() .h_full() - .map(|this| { - if use_card_layout { - this.bg(linear_gradient( - 90., - linear_color_stop(self.tool_card_header_bg(cx), 1.), - linear_color_stop(self.tool_card_header_bg(cx).opacity(0.2), 0.), - )) - } else { - this.bg(linear_gradient( - 90., - linear_color_stop(cx.theme().colors().panel_background, 1.), - linear_color_stop( - cx.theme().colors().panel_background.opacity(0.2), - 0., - ), - )) - } - }) + .bg(linear_gradient( + 90., + linear_color_stop(color, 1.), + linear_color_stop(color.opacity(0.2), 0.), + )) + }; + let gradient_color = if use_card_layout { + self.tool_card_header_bg(cx) + } else { + cx.theme().colors().panel_background }; let tool_output_display = if is_open { @@ -1878,58 +1759,47 @@ impl AcpThreadView { }; v_flex() - .map(|this| { - if use_card_layout { - this.my_2() - .rounded_md() - .border_1() - .border_color(self.tool_card_border_color(cx)) - .bg(cx.theme().colors().editor_background) - .overflow_hidden() - } else { - this.my_1() - } + .when(use_card_layout, |this| { + this.rounded_md() + .border_1() + .border_color(self.tool_card_border_color(cx)) + .bg(cx.theme().colors().editor_background) + .overflow_hidden() }) - .map(|this| { - if has_location && !use_card_layout { - this.ml_4() - } else { - this.ml_5() - } - }) - .mr_5() .child( h_flex() - .group(&card_header_id) + .id(header_id) .relative() .w_full() + .max_w_full() .gap_1() - .justify_between() .when(use_card_layout, |this| { - this.p_0p5() + this.pl_1p5() + .pr_1() + .py_0p5() .rounded_t_md() - .bg(self.tool_card_header_bg(cx)) - .when(is_open && !failed_or_canceled, |this| { + .when(is_open && !failed_tool_call, |this| { this.border_b_1() .border_color(self.tool_card_border_color(cx)) }) + .bg(self.tool_card_header_bg(cx)) }) .child( h_flex() + .group(&card_header_id) .relative() .w_full() - .h(window.line_height()) + .h(window.line_height() - px(2.)) .text_size(self.tool_name_font_size()) - .gap_1p5() - .when(has_location || use_card_layout, |this| this.px_1()) - .when(has_location, |this| { - this.cursor(CursorStyle::PointingHand) - .rounded_sm() - .hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.5))) - }) - .overflow_hidden() - .child(tool_icon) - .child(if has_location { + .child(self.render_tool_call_icon( + card_header_id, + entry_ix, + is_collapsible, + is_open, + tool_call, + cx, + )) + .child(if tool_call.locations.len() == 1 { let name = tool_call.locations[0] .path .file_name() @@ -1940,6 +1810,13 @@ impl AcpThreadView { h_flex() .id(("open-tool-call-location", entry_ix)) .w_full() + .max_w_full() + .px_1p5() + .rounded_sm() + .overflow_x_scroll() + .hover(|label| { + label.bg(cx.theme().colors().element_hover.opacity(0.5)) + }) .map(|this| { if use_card_layout { this.text_color(cx.theme().colors().text) @@ -1955,48 +1832,32 @@ impl AcpThreadView { .into_any_element() } else { h_flex() + .id("non-card-label-container") + .relative() .w_full() - .child(self.render_markdown( + .max_w_full() + .ml_1p5() + .overflow_hidden() + .child(h_flex().pr_8().child(self.render_markdown( tool_call.label.clone(), default_markdown_style(false, true, window, cx), - )) - .into_any() - }) - .when(!has_location, |this| this.child(gradient_overlay)), - ) - .when(is_collapsible || failed_or_canceled, |this| { - this.child( - h_flex() - .px_1() - .gap_px() - .when(is_collapsible, |this| { - this.child( - Disclosure::new(("expand", entry_ix), is_open) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown) - .visible_on_hover(&card_header_id) - .on_click(cx.listener({ - let id = tool_call.id.clone(); - move |this: &mut Self, _, _, cx: &mut Context| { - if is_open { - this.expanded_tool_calls.remove(&id); - } else { - this.expanded_tool_calls.insert(id.clone()); - } - cx.notify(); + ))) + .child(gradient_overlay(gradient_color)) + .on_click(cx.listener({ + let id = tool_call.id.clone(); + move |this: &mut Self, _, _, cx: &mut Context| { + if is_open { + this.expanded_tool_calls.remove(&id); + } else { + this.expanded_tool_calls.insert(id.clone()); } - })), - ) - }) - .when(failed_or_canceled, |this| { - this.child( - Icon::new(IconName::Close) - .color(Color::Error) - .size(IconSize::Small), - ) - }), - ) - }), + cx.notify(); + } + })) + .into_any() + }), + ) + .children(status_icon), ) .children(tool_output_display) } @@ -2037,7 +1898,7 @@ impl AcpThreadView { v_flex() .mt_1p5() - .ml(rems(0.4)) + .ml(px(7.)) .px_3p5() .gap_2() .border_l_1() @@ -2066,27 +1927,9 @@ impl AcpThreadView { cx: &Context, ) -> AnyElement { let uri: SharedString = resource_link.uri.clone().into(); - let is_file = resource_link.uri.strip_prefix("file://"); - let label: SharedString = if let Some(abs_path) = is_file { - if let Some(project_path) = self - .project - .read(cx) - .project_path_for_absolute_path(&Path::new(abs_path), cx) - && let Some(worktree) = self - .project - .read(cx) - .worktree_for_id(project_path.worktree_id, cx) - { - worktree - .read(cx) - .full_path(&project_path.path) - .to_string_lossy() - .to_string() - .into() - } else { - abs_path.to_string().into() - } + let label: SharedString = if let Some(path) = resource_link.uri.strip_prefix("file://") { + path.to_string().into() } else { uri.clone() }; @@ -2094,7 +1937,7 @@ impl AcpThreadView { let button_id = SharedString::from(format!("item-{}", uri)); div() - .ml(rems(0.4)) + .ml(px(7.)) .pl_2p5() .border_l_1() .border_color(self.tool_card_border_color(cx)) @@ -2103,12 +1946,10 @@ impl AcpThreadView { Button::new(button_id, label) .label_size(LabelSize::Small) .color(Color::Muted) + .icon(IconName::ArrowUpRight) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) .truncate(true) - .when(is_file.is_none(), |this| { - this.icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - }) .on_click(cx.listener({ let workspace = self.workspace.clone(); move |_, _, window, cx: &mut Context| { @@ -2167,12 +2008,11 @@ impl AcpThreadView { let tool_call_id = tool_call_id.clone(); let option_id = option.id.clone(); let option_kind = option.kind; - move |this, _, window, cx| { + move |this, _, _, cx| { this.authorize_tool_call( tool_call_id.clone(), option_id.clone(), option_kind, - window, cx, ); } @@ -2282,12 +2122,6 @@ impl AcpThreadView { started_at.elapsed() }; - let header_id = - SharedString::from(format!("terminal-tool-header-{}", terminal.entity_id())); - let header_group = SharedString::from(format!( - "terminal-tool-header-group-{}", - terminal.entity_id() - )); let header_bg = cx .theme() .colors() @@ -2303,7 +2137,10 @@ impl AcpThreadView { let is_expanded = self.expanded_tool_calls.contains(&tool_call.id); let header = h_flex() - .id(header_id) + .id(SharedString::from(format!( + "terminal-tool-header-{}", + terminal.entity_id() + ))) .flex_none() .gap_1() .justify_between() @@ -2367,6 +2204,23 @@ impl AcpThreadView { ), ) }) + .when(tool_failed || command_failed, |header| { + header.child( + div() + .id(("terminal-tool-error-code-indicator", terminal.entity_id())) + .child( + Icon::new(IconName::Close) + .size(IconSize::Small) + .color(Color::Error), + ) + .when_some(output.and_then(|o| o.exit_status), |this, status| { + this.tooltip(Tooltip::text(format!( + "Exited with code {}", + status.code().unwrap_or(-1), + ))) + }), + ) + }) .when(truncated_output, |header| { let tooltip = if let Some(output) = output { if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES { @@ -2419,7 +2273,6 @@ impl AcpThreadView { ) .opened_icon(IconName::ChevronUp) .closed_icon(IconName::ChevronDown) - .visible_on_hover(&header_group) .on_click(cx.listener({ let id = tool_call.id.clone(); move |this, _event, _window, _cx| { @@ -2430,24 +2283,7 @@ impl AcpThreadView { } } })), - ) - .when(tool_failed || command_failed, |header| { - header.child( - div() - .id(("terminal-tool-error-code-indicator", terminal.entity_id())) - .child( - Icon::new(IconName::Close) - .size(IconSize::Small) - .color(Color::Error), - ) - .when_some(output.and_then(|o| o.exit_status), |this, status| { - this.tooltip(Tooltip::text(format!( - "Exited with code {}", - status.code().unwrap_or(-1), - ))) - }), - ) - }); + ); let terminal_view = self .entry_view_state @@ -2457,8 +2293,7 @@ impl AcpThreadView { let show_output = is_expanded && terminal_view.is_some(); v_flex() - .my_2() - .mx_5() + .mb_2() .border_1() .when(tool_failed || command_failed, |card| card.border_dashed()) .border_color(border_color) @@ -2466,10 +2301,9 @@ impl AcpThreadView { .overflow_hidden() .child( v_flex() - .group(&header_group) .py_1p5() - .pr_1p5() .pl_2() + .pr_1p5() .gap_0p5() .bg(header_bg) .text_xs() @@ -2504,6 +2338,33 @@ impl AcpThreadView { .into_any() } + fn render_agent_logo(&self) -> AnyElement { + Icon::new(self.agent.logo()) + .color(Color::Muted) + .size(IconSize::XLarge) + .into_any_element() + } + + fn render_error_agent_logo(&self) -> AnyElement { + let logo = Icon::new(self.agent.logo()) + .color(Color::Muted) + .size(IconSize::XLarge) + .into_any_element(); + + h_flex() + .relative() + .justify_center() + .child(div().opacity(0.3).child(logo)) + .child( + h_flex() + .absolute() + .right_1() + .bottom_0() + .child(Icon::new(IconName::XCircleFilled).color(Color::Error)), + ) + .into_any_element() + } + fn render_rules_item(&self, cx: &Context) -> Option { let project_context = self .as_native_thread(cx)? @@ -2556,6 +2417,7 @@ impl AcpThreadView { Some( h_flex() .px_2p5() + .pb_1() .child( Icon::new(IconName::Attach) .size(IconSize::XSmall) @@ -2571,7 +2433,8 @@ impl AcpThreadView { Label::new(user_rules_text) .size(LabelSize::XSmall) .color(Color::Muted) - .truncate(), + .truncate() + .buffer_font(cx), ) .hover(|s| s.bg(cx.theme().colors().element_hover)) .tooltip(Tooltip::text("View User Rules")) @@ -2585,13 +2448,7 @@ impl AcpThreadView { }), ) }) - .when(has_both, |this| { - this.child( - Label::new("•") - .size(LabelSize::XSmall) - .color(Color::Disabled), - ) - }) + .when(has_both, |this| this.child(Divider::vertical())) .when_some(rules_file_text, |parent, rules_file_text| { parent.child( h_flex() @@ -2600,7 +2457,8 @@ impl AcpThreadView { .child( Label::new(rules_file_text) .size(LabelSize::XSmall) - .color(Color::Muted), + .color(Color::Muted) + .buffer_font(cx), ) .hover(|s| s.bg(cx.theme().colors().element_hover)) .tooltip(Tooltip::text("View Project Rules")) @@ -2635,7 +2493,8 @@ impl AcpThreadView { ) } - fn render_recent_history(&self, window: &mut Window, cx: &mut Context) -> AnyElement { + fn render_empty_state(&self, window: &mut Window, cx: &mut Context) -> AnyElement { + let loading = matches!(&self.thread_state, ThreadState::Loading { .. }); let render_history = self .agent .clone() @@ -2647,6 +2506,38 @@ impl AcpThreadView { v_flex() .size_full() + .when(!render_history, |this| { + this.child( + v_flex() + .size_full() + .items_center() + .justify_center() + .child(if loading { + h_flex() + .justify_center() + .child(self.render_agent_logo()) + .with_animation( + "pulsating_icon", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.4, 1.0)), + |icon, delta| icon.opacity(delta), + ) + .into_any() + } else { + self.render_agent_logo().into_any_element() + }) + .child(h_flex().mt_4().mb_2().justify_center().child(if loading { + div() + .child(LoadingLabel::new("").size(LabelSize::Large)) + .into_any_element() + } else { + Headline::new(self.agent.empty_state_headline()) + .size(HeadlineSize::Medium) + .into_any_element() + })), + ) + }) .when(render_history, |this| { let recent_history: Vec<_> = self.history_store.update(cx, |history_store, _| { history_store.entries().take(3).collect() @@ -2721,276 +2612,240 @@ impl AcpThreadView { window: &mut Window, cx: &Context, ) -> Div { - let show_description = - configuration_view.is_none() && description.is_none() && pending_auth_method.is_none(); - - v_flex().flex_1().size_full().justify_end().child( - v_flex() - .p_2() - .pr_3() - .w_full() - .gap_1() - .border_t_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().status().warning.opacity(0.04)) - .child( - h_flex() - .gap_1p5() - .child( - Icon::new(IconName::Warning) - .color(Color::Warning) - .size(IconSize::Small), - ) - .child(Label::new("Authentication Required").size(LabelSize::Small)), - ) - .children(description.map(|desc| { - div().text_ui(cx).child(self.render_markdown( - desc.clone(), - default_markdown_style(false, false, window, cx), - )) - })) - .children( - configuration_view - .cloned() - .map(|view| div().w_full().child(view)), - ) - .when( - show_description, - |el| { - el.child( - Label::new(format!( - "You are not currently authenticated with {}. Please choose one of the following options:", - self.agent.name() - )) - .size(LabelSize::Small) - .color(Color::Muted) - .mb_1() - .ml_5(), - ) - }, - ) - .when_some(pending_auth_method, |el, _| { - el.child( - h_flex() - .py_4() - .w_full() - .justify_center() - .gap_1() - .child( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Small) - .color(Color::Muted) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage( - delta, - ))) - }, - ) - .into_any_element(), - ) - .child(Label::new("Authenticating…").size(LabelSize::Small)), - ) - }) - .when(!connection.auth_methods().is_empty(), |this| { - this.child( - h_flex() - .justify_end() - .flex_wrap() - .gap_1() - .when(!show_description, |this| { - this.border_t_1() - .mt_1() - .pt_2() - .border_color(cx.theme().colors().border.opacity(0.8)) - }) - .children( - connection - .auth_methods() - .iter() - .enumerate() - .rev() - .map(|(ix, method)| { - Button::new( - SharedString::from(method.id.0.clone()), - method.name.clone(), - ) - .when(ix == 0, |el| { - el.style(ButtonStyle::Tinted(ui::TintColor::Warning)) - }) - .label_size(LabelSize::Small) - .on_click({ - let method_id = method.id.clone(); - cx.listener(move |this, _, window, cx| { - telemetry::event!( - "Authenticate Agent Started", - agent = this.agent.telemetry_id(), - method = method_id - ); - - this.authenticate(method_id.clone(), window, cx) - }) - }) - }), - ), - ) - }) - - ) - } - - fn render_load_error( - &self, - e: &LoadError, - window: &mut Window, - cx: &mut Context, - ) -> AnyElement { - let (message, action_slot): (SharedString, _) = match e { - LoadError::NotInstalled { - error_message: _, - install_message: _, - install_command, - } => { - return self.render_not_installed(install_command.clone(), false, window, cx); - } - LoadError::Unsupported { - error_message: _, - upgrade_message: _, - upgrade_command, - } => { - return self.render_not_installed(upgrade_command.clone(), true, window, cx); - } - LoadError::Exited { .. } => ("Server exited with status {status}".into(), None), - LoadError::Other(msg) => ( - msg.into(), - Some(self.create_copy_button(msg.to_string()).into_any_element()), - ), - }; - - Callout::new() - .severity(Severity::Error) - .icon(IconName::XCircleFilled) - .title("Failed to Launch") - .description(message) - .actions_slot(div().children(action_slot)) - .into_any_element() - } - - fn install_agent(&self, install_command: String, window: &mut Window, cx: &mut Context) { - telemetry::event!("Agent Install CLI", agent = self.agent.telemetry_id()); - let task = self - .workspace - .update(cx, |workspace, cx| { - let project = workspace.project().read(cx); - let cwd = project.first_project_directory(cx); - let shell = project.terminal_settings(&cwd, cx).shell.clone(); - let spawn_in_terminal = task::SpawnInTerminal { - id: task::TaskId(install_command.clone()), - full_label: install_command.clone(), - label: install_command.clone(), - command: Some(install_command.clone()), - args: Vec::new(), - command_label: install_command.clone(), - cwd, - env: Default::default(), - use_new_terminal: true, - allow_concurrent_runs: true, - reveal: Default::default(), - reveal_target: Default::default(), - hide: Default::default(), - shell, - show_summary: true, - show_command: true, - show_rerun: false, - }; - workspace.spawn_in_terminal(spawn_in_terminal, window, cx) - }) - .ok(); - let Some(task) = task else { return }; - cx.spawn_in(window, async move |this, cx| { - if let Some(Ok(_)) = task.await { - this.update_in(cx, |this, window, cx| { - this.reset(window, cx); - }) - .ok(); - } - }) - .detach() - } - - fn render_not_installed( - &self, - install_command: String, - is_upgrade: bool, - window: &mut Window, - cx: &mut Context, - ) -> AnyElement { - self.install_command_markdown.update(cx, |markdown, cx| { - if !markdown.source().contains(&install_command) { - markdown.replace(format!("```\n{}\n```", install_command), cx); - } - }); - - let (heading_label, description_label, button_label, or_label) = if is_upgrade { - ( - "Upgrade Gemini CLI in Zed", - "Get access to the latest version with support for Zed.", - "Upgrade Gemini CLI", - "Or, to upgrade it manually:", - ) - } else { - ( - "Get Started with Gemini CLI in Zed", - "Use Google's new coding agent directly in Zed.", - "Install Gemini CLI", - "Or, to install it manually:", - ) - }; - v_flex() - .w_full() - .p_3p5() - .gap_2p5() - .border_t_1() - .border_color(cx.theme().colors().border) - .bg(linear_gradient( - 180., - linear_color_stop(cx.theme().colors().editor_background.opacity(0.4), 4.), - linear_color_stop(cx.theme().status().info_background.opacity(0.), 0.), - )) + .p_2() + .gap_2() + .flex_1() + .items_center() + .justify_center() .child( - v_flex().gap_0p5().child(Label::new(heading_label)).child( - Label::new(description_label) - .size(LabelSize::Small) - .color(Color::Muted), - ), + v_flex() + .items_center() + .justify_center() + .child(self.render_error_agent_logo()) + .child( + h_flex().mt_4().mb_1().justify_center().child( + Headline::new("Authentication Required").size(HeadlineSize::Medium), + ), + ) + .into_any(), ) + .children(description.map(|desc| { + div().text_ui(cx).text_center().child(self.render_markdown( + desc.clone(), + default_markdown_style(false, false, window, cx), + )) + })) + .children( + configuration_view + .cloned() + .map(|view| div().px_4().w_full().max_w_128().child(view)), + ) + .when( + configuration_view.is_none() + && description.is_none() + && pending_auth_method.is_none(), + |el| { + el.child( + div() + .text_ui(cx) + .text_center() + .px_4() + .w_full() + .max_w_128() + .child(Label::new("Authentication required")), + ) + }, + ) + .when_some(pending_auth_method, |el, _| { + let spinner_icon = div() + .px_0p5() + .id("generating") + .tooltip(Tooltip::text("Generating Changes…")) + .child( + Icon::new(IconName::ArrowCircle) + .size(IconSize::Small) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| { + icon.transform(Transformation::rotate(percentage(delta))) + }, + ) + .into_any_element(), + ) + .into_any(); + el.child( + h_flex() + .text_ui(cx) + .text_center() + .justify_center() + .gap_2() + .px_4() + .w_full() + .max_w_128() + .child(Label::new("Authenticating...")) + .child(spinner_icon), + ) + }) .child( - Button::new("install_gemini", button_label) - .full_width() - .size(ButtonSize::Medium) - .style(ButtonStyle::Tinted(TintColor::Accent)) - .label_size(LabelSize::Small) - .icon(IconName::TerminalGhost) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) + h_flex() + .mt_1p5() + .gap_1() + .flex_wrap() + .justify_center() + .children(connection.auth_methods().iter().enumerate().rev().map( + |(ix, method)| { + Button::new( + SharedString::from(method.id.0.clone()), + method.name.clone(), + ) + .style(ButtonStyle::Outlined) + .when(ix == 0, |el| { + el.style(ButtonStyle::Tinted(ui::TintColor::Accent)) + }) + .size(ButtonSize::Medium) + .label_size(LabelSize::Small) + .on_click({ + let method_id = method.id.clone(); + cx.listener(move |this, _, window, cx| { + this.authenticate(method_id.clone(), window, cx) + }) + }) + }, + )), + ) + } + + fn render_load_error(&self, e: &LoadError, cx: &Context) -> AnyElement { + let mut container = v_flex() + .items_center() + .justify_center() + .child(self.render_error_agent_logo()) + .child( + v_flex() + .mt_4() + .mb_2() + .gap_0p5() + .text_center() + .items_center() + .child(Headline::new("Failed to launch").size(HeadlineSize::Medium)) + .child( + Label::new(e.to_string()) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ); + + if let LoadError::Unsupported { + upgrade_message, + upgrade_command, + .. + } = &e + { + let upgrade_message = upgrade_message.clone(); + let upgrade_command = upgrade_command.clone(); + container = container.child( + Button::new("upgrade", upgrade_message) + .tooltip(Tooltip::text(upgrade_command.clone())) .on_click(cx.listener(move |this, _, window, cx| { - this.install_agent(install_command.clone(), window, cx) + let task = this + .workspace + .update(cx, |workspace, cx| { + let project = workspace.project().read(cx); + let cwd = project.first_project_directory(cx); + let shell = project.terminal_settings(&cwd, cx).shell.clone(); + let spawn_in_terminal = task::SpawnInTerminal { + id: task::TaskId("upgrade".to_string()), + full_label: upgrade_command.clone(), + label: upgrade_command.clone(), + command: Some(upgrade_command.clone()), + args: Vec::new(), + command_label: upgrade_command.clone(), + cwd, + env: Default::default(), + use_new_terminal: true, + allow_concurrent_runs: true, + reveal: Default::default(), + reveal_target: Default::default(), + hide: Default::default(), + shell, + show_summary: true, + show_command: true, + show_rerun: false, + }; + workspace.spawn_in_terminal(spawn_in_terminal, window, cx) + }) + .ok(); + let Some(task) = task else { return }; + cx.spawn_in(window, async move |this, cx| { + if let Some(Ok(_)) = task.await { + this.update_in(cx, |this, window, cx| { + this.reset(window, cx); + }) + .ok(); + } + }) + .detach() })), - ) - .child( - Label::new(or_label) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child(MarkdownElement::new( - self.install_command_markdown.clone(), - default_markdown_style(false, false, window, cx), - )) - .into_any_element() + ); + } else if let LoadError::NotInstalled { + install_message, + install_command, + .. + } = e + { + let install_message = install_message.clone(); + let install_command = install_command.clone(); + container = container.child( + Button::new("install", install_message) + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .size(ButtonSize::Medium) + .tooltip(Tooltip::text(install_command.clone())) + .on_click(cx.listener(move |this, _, window, cx| { + let task = this + .workspace + .update(cx, |workspace, cx| { + let project = workspace.project().read(cx); + let cwd = project.first_project_directory(cx); + let shell = project.terminal_settings(&cwd, cx).shell.clone(); + let spawn_in_terminal = task::SpawnInTerminal { + id: task::TaskId("install".to_string()), + full_label: install_command.clone(), + label: install_command.clone(), + command: Some(install_command.clone()), + args: Vec::new(), + command_label: install_command.clone(), + cwd, + env: Default::default(), + use_new_terminal: true, + allow_concurrent_runs: true, + reveal: Default::default(), + reveal_target: Default::default(), + hide: Default::default(), + shell, + show_summary: true, + show_command: true, + show_rerun: false, + }; + workspace.spawn_in_terminal(spawn_in_terminal, window, cx) + }) + .ok(); + let Some(task) = task else { return }; + cx.spawn_in(window, async move |this, cx| { + if let Some(Ok(_)) = task.await { + this.update_in(cx, |this, window, cx| { + this.reset(window, cx); + }) + .ok(); + } + }) + .detach() + })), + ); + } + + container.into_any() } fn render_activity_bar( @@ -3203,13 +3058,13 @@ impl AcpThreadView { h_flex() .p_1() .justify_between() - .flex_wrap() .when(expanded, |this| { this.border_b_1().border_color(cx.theme().colors().border) }) .child( h_flex() .id("edits-container") + .w_full() .gap_1() .child(Disclosure::new("edits-disclosure", expanded)) .map(|this| { @@ -3481,19 +3336,6 @@ impl AcpThreadView { (IconName::Maximize, "Expand Message Editor") }; - let backdrop = div() - .size_full() - .absolute() - .inset_0() - .bg(cx.theme().colors().panel_background) - .opacity(0.8) - .block_mouse_except_scroll(); - - let enable_editor = match self.thread_state { - ThreadState::Loading { .. } | ThreadState::Ready { .. } => true, - ThreadState::Unauthenticated { .. } | ThreadState::LoadError(..) => false, - }; - v_flex() .on_action(cx.listener(Self::expand_message_editor)) .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| { @@ -3569,7 +3411,6 @@ impl AcpThreadView { .child(self.render_send_button(cx)), ), ) - .when(!enable_editor, |this| this.child(backdrop)) .into_any() } @@ -3716,14 +3557,7 @@ impl AcpThreadView { .thread() .is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle); - if self.is_loading_contents { - div() - .id("loading-message-content") - .px_1() - .tooltip(Tooltip::text("Loading Added Context…")) - .child(loading_contents_spinner(IconSize::default())) - .into_any_element() - } else if is_generating && is_editor_empty { + if is_generating && is_editor_empty { IconButton::new("stop-generation", IconName::Stop) .icon_color(Color::Error) .style(ButtonStyle::Tinted(ui::TintColor::Error)) @@ -3758,53 +3592,13 @@ impl AcpThreadView { } } - fn is_following(&self, cx: &App) -> bool { - match self.thread().map(|thread| thread.read(cx).status()) { - Some(ThreadStatus::Generating) => self - .workspace - .read_with(cx, |workspace, _| { - workspace.is_being_followed(CollaboratorId::Agent) - }) - .unwrap_or(false), - _ => self.should_be_following, - } - } - - fn toggle_following(&mut self, window: &mut Window, cx: &mut Context) { - let following = self.is_following(cx); - - self.should_be_following = !following; - if self.thread().map(|thread| thread.read(cx).status()) == Some(ThreadStatus::Generating) { - self.workspace - .update(cx, |workspace, cx| { - if following { - workspace.unfollow(CollaboratorId::Agent, window, cx); - } else { - workspace.follow(CollaboratorId::Agent, window, cx); - } - }) - .ok(); - } - - telemetry::event!("Follow Agent Selected", following = !following); - } - fn render_follow_toggle(&self, cx: &mut Context) -> impl IntoElement { - let following = self.is_following(cx); - - let tooltip_label = if following { - if self.agent.name() == "Zed Agent" { - format!("Stop Following the {}", self.agent.name()) - } else { - format!("Stop Following {}", self.agent.name()) - } - } else { - if self.agent.name() == "Zed Agent" { - format!("Follow the {}", self.agent.name()) - } else { - format!("Follow {}", self.agent.name()) - } - }; + let following = self + .workspace + .read_with(cx, |workspace, _| { + workspace.is_being_followed(CollaboratorId::Agent) + }) + .unwrap_or(false); IconButton::new("follow-agent", IconName::Crosshair) .icon_size(IconSize::Small) @@ -3813,10 +3607,10 @@ impl AcpThreadView { .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor))) .tooltip(move |window, cx| { if following { - Tooltip::for_action(tooltip_label.clone(), &Follow, window, cx) + Tooltip::for_action("Stop Following Agent", &Follow, window, cx) } else { Tooltip::with_meta( - tooltip_label.clone(), + "Follow Agent", Some(&Follow), "Track the agent's location as it reads and edits files.", window, @@ -3825,7 +3619,15 @@ impl AcpThreadView { } }) .on_click(cx.listener(move |this, _, window, cx| { - this.toggle_following(window, cx); + this.workspace + .update(cx, |workspace, cx| { + if following { + workspace.unfollow(CollaboratorId::Agent, window, cx); + } else { + workspace.follow(CollaboratorId::Agent, window, cx); + } + }) + .ok(); })) } @@ -3861,7 +3663,6 @@ impl AcpThreadView { .open_path(path, None, true, window, cx) .detach_and_log_err(cx); } - MentionUri::PastedImage => {} MentionUri::Directory { abs_path } => { let project = workspace.project(); let Some(entry) = project.update(cx, |project, cx| { @@ -3876,14 +3677,9 @@ impl AcpThreadView { }); } MentionUri::Symbol { - abs_path: path, - line_range, - .. + path, line_range, .. } - | MentionUri::Selection { - abs_path: Some(path), - line_range, - } => { + | MentionUri::Selection { path, line_range } => { let project = workspace.project(); let Some((path, _)) = project.update(cx, |project, cx| { let path = project.find_project_path(path, cx)?; @@ -3899,8 +3695,8 @@ impl AcpThreadView { let Some(editor) = item.await?.downcast::() else { return Ok(()); }; - let range = Point::new(*line_range.start(), 0) - ..Point::new(*line_range.start(), 0); + let range = + Point::new(line_range.start, 0)..Point::new(line_range.start, 0); editor .update_in(cx, |editor, window, cx| { editor.change_selections( @@ -3915,7 +3711,6 @@ impl AcpThreadView { }) .detach_and_log_err(cx); } - MentionUri::Selection { abs_path: None, .. } => {} MentionUri::Thread { id, name } => { if let Some(panel) = workspace.panel::(cx) { panel.update(cx, |panel, cx| { @@ -4118,8 +3913,7 @@ impl AcpThreadView { return; } - // TODO: Change this once we have title summarization for external agents. - let title = self.agent.name(); + let title = self.title(cx); match AgentSettings::get_global(cx).notify_when_agent_waiting { NotifyWhenAgentWaiting::PrimaryScreen => { @@ -4237,21 +4031,7 @@ impl AcpThreadView { } } - fn render_thread_controls( - &self, - thread: &Entity, - cx: &Context, - ) -> impl IntoElement { - let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating); - if is_generating { - return h_flex().id("thread-controls-container").child( - div() - .py_2() - .px_5() - .child(SpinnerLabel::new().size(LabelSize::Small)), - ); - } - + fn render_thread_controls(&self, cx: &Context) -> impl IntoElement { let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::Small) @@ -4277,10 +4057,12 @@ impl AcpThreadView { .id("thread-controls-container") .group("thread-controls-container") .w_full() - .py_2() - .px_5() + .mr_1() + .pt_1() + .pb_2() + .px(RESPONSE_PADDING_X) .gap_px() - .opacity(0.6) + .opacity(0.4) .hover(|style| style.opacity(1.)) .flex_wrap() .justify_end(); @@ -4291,57 +4073,68 @@ impl AcpThreadView { .is_some_and(|thread| thread.read(cx).connection().telemetry().is_some()) { let feedback = self.thread_feedback.feedback; - - container = container - .child( - div().visible_on_hover("thread-controls-container").child( - Label::new(match feedback { + container = container.child( + div().visible_on_hover("thread-controls-container").child( + Label::new( + match feedback { Some(ThreadFeedback::Positive) => "Thanks for your feedback!", - Some(ThreadFeedback::Negative) => { - "We appreciate your feedback and will use it to improve." - } - None => { - "Rating the thread sends all of your current conversation to the Zed team." - } - }) - .color(Color::Muted) - .size(LabelSize::XSmall) - .truncate(), - ), - ) - .child( - IconButton::new("feedback-thumbs-up", IconName::ThumbsUp) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(match feedback { - Some(ThreadFeedback::Positive) => Color::Accent, - _ => Color::Ignored, - }) - .tooltip(Tooltip::text("Helpful Response")) - .on_click(cx.listener(move |this, _, window, cx| { - this.handle_feedback_click(ThreadFeedback::Positive, window, cx); - })), - ) - .child( - IconButton::new("feedback-thumbs-down", IconName::ThumbsDown) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(match feedback { - Some(ThreadFeedback::Negative) => Color::Accent, - _ => Color::Ignored, - }) - .tooltip(Tooltip::text("Not Helpful")) - .on_click(cx.listener(move |this, _, window, cx| { - this.handle_feedback_click(ThreadFeedback::Negative, window, cx); - })), - ); + Some(ThreadFeedback::Negative) => "We appreciate your feedback and will use it to improve.", + None => "Rating the thread sends all of your current conversation to the Zed team.", + } + ) + .color(Color::Muted) + .size(LabelSize::XSmall) + .truncate(), + ), + ).child( + h_flex() + .child( + IconButton::new("feedback-thumbs-up", IconName::ThumbsUp) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) + .icon_color(match feedback { + Some(ThreadFeedback::Positive) => Color::Accent, + _ => Color::Ignored, + }) + .tooltip(Tooltip::text("Helpful Response")) + .on_click(cx.listener(move |this, _, window, cx| { + this.handle_feedback_click( + ThreadFeedback::Positive, + window, + cx, + ); + })), + ) + .child( + IconButton::new("feedback-thumbs-down", IconName::ThumbsDown) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) + .icon_color(match feedback { + Some(ThreadFeedback::Negative) => Color::Accent, + _ => Color::Ignored, + }) + .tooltip(Tooltip::text("Not Helpful")) + .on_click(cx.listener(move |this, _, window, cx| { + this.handle_feedback_click( + ThreadFeedback::Negative, + window, + cx, + ); + })), + ) + ) } container.child(open_as_markdown).child(scroll_to_top) } - fn render_feedback_feedback_editor(editor: Entity, cx: &Context) -> Div { - h_flex() + fn render_feedback_feedback_editor( + editor: Entity, + window: &mut Window, + cx: &Context, + ) -> Div { + let focus_handle = editor.focus_handle(cx); + v_flex() .key_context("AgentFeedbackMessageEditor") .on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| { this.thread_feedback.dismiss_comments(); @@ -4350,31 +4143,43 @@ impl AcpThreadView { .on_action(cx.listener(move |this, _: &menu::Confirm, _window, cx| { this.submit_feedback_message(cx); })) - .p_2() .mb_2() - .mx_5() - .gap_1() + .mx_4() + .p_2() .rounded_md() .border_1() .border_color(cx.theme().colors().border) .bg(cx.theme().colors().editor_background) - .child(div().w_full().child(editor)) + .child(editor) .child( h_flex() + .gap_1() + .justify_end() .child( - IconButton::new("dismiss-feedback-message", IconName::Close) - .icon_color(Color::Error) - .icon_size(IconSize::XSmall) - .shape(ui::IconButtonShape::Square) + Button::new("dismiss-feedback-message", "Cancel") + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in(&menu::Cancel, &focus_handle, window, cx) + .map(|kb| kb.size(rems_from_px(10.))), + ) .on_click(cx.listener(move |this, _, _window, cx| { this.thread_feedback.dismiss_comments(); cx.notify(); })), ) .child( - IconButton::new("submit-feedback-message", IconName::Return) - .icon_size(IconSize::XSmall) - .shape(ui::IconButtonShape::Square) + Button::new("submit-feedback-message", "Share Feedback") + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in( + &menu::Confirm, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(10.))), + ) .on_click(cx.listener(move |this, _, _window, cx| { this.submit_feedback_message(cx); })), @@ -4615,53 +4420,11 @@ impl AcpThreadView { } fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout { - let can_resume = self - .thread() - .map_or(false, |thread| thread.read(cx).can_resume(cx)); - - let can_enable_burn_mode = self.as_native_thread(cx).map_or(false, |thread| { - let thread = thread.read(cx); - let supports_burn_mode = thread - .model() - .map_or(false, |model| model.supports_burn_mode()); - supports_burn_mode && thread.completion_mode() == CompletionMode::Normal - }); - Callout::new() .severity(Severity::Error) .title("Error") - .icon(IconName::XCircle) .description(error.clone()) - .actions_slot( - h_flex() - .gap_0p5() - .when(can_resume && can_enable_burn_mode, |this| { - this.child( - Button::new("enable-burn-mode-and-retry", "Enable Burn Mode and Retry") - .icon(IconName::ZedBurnMode) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - this.toggle_burn_mode(&ToggleBurnMode, window, cx); - this.resume_chat(cx); - })), - ) - }) - .when(can_resume, |this| { - this.child( - Button::new("retry", "Retry") - .icon(IconName::RotateCw) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, _window, cx| { - this.resume_chat(cx); - })), - ) - }) - .child(self.create_copy_button(error.to_string())), - ) + .actions_slot(self.create_copy_button(error.to_string())) .dismiss_action(self.dismiss_error_button(cx)) } @@ -4671,7 +4434,6 @@ impl AcpThreadView { Callout::new() .severity(Severity::Error) - .icon(IconName::XCircle) .title("Free Usage Exceeded") .description(ERROR_MESSAGE) .actions_slot( @@ -4691,7 +4453,6 @@ impl AcpThreadView { Callout::new() .severity(Severity::Error) .title("Authentication Required") - .icon(IconName::XCircle) .description(error.clone()) .actions_slot( h_flex() @@ -4717,7 +4478,6 @@ impl AcpThreadView { Callout::new() .severity(Severity::Error) .title("Model Prompt Limit Reached") - .icon(IconName::XCircle) .description(error_message) .actions_slot( h_flex() @@ -4849,24 +4609,6 @@ impl AcpThreadView { })) } - pub(crate) fn reauthenticate(&mut self, window: &mut Window, cx: &mut Context) { - let agent = self.agent.clone(); - let ThreadState::Ready { thread, .. } = &self.thread_state else { - return; - }; - - let connection = thread.read(cx).connection().clone(); - let err = AuthRequired { - description: None, - provider_id: None, - }; - self.clear_thread_error(cx); - let this = cx.weak_entity(); - window.defer(cx, |window, cx| { - Self::handle_auth_required(this, err, agent, connection, window, cx); - }) - } - fn upgrade_button(&self, cx: &mut Context) -> impl IntoElement { Button::new("upgrade", "Upgrade") .label_size(LabelSize::Small) @@ -4904,28 +4646,9 @@ impl AcpThreadView { } } -fn loading_contents_spinner(size: IconSize) -> AnyElement { - Icon::new(IconName::LoadCircle) - .size(size) - .color(Color::Accent) - .with_animation( - "load_context_circle", - Animation::new(Duration::from_secs(3)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ) - .into_any_element() -} - impl Focusable for AcpThreadView { fn focus_handle(&self, cx: &App) -> FocusHandle { - match self.thread_state { - ThreadState::Loading { .. } | ThreadState::Ready { .. } => { - self.message_editor.focus_handle(cx) - } - ThreadState::LoadError(_) | ThreadState::Unauthenticated { .. } => { - self.focus_handle.clone() - } - } + self.message_editor.focus_handle(cx) } } @@ -4941,7 +4664,6 @@ impl Render for AcpThreadView { .on_action(cx.listener(Self::toggle_burn_mode)) .on_action(cx.listener(Self::keep_all)) .on_action(cx.listener(Self::reject_all)) - .track_focus(&self.focus_handle) .bg(cx.theme().colors().panel_background) .child(match &self.thread_state { ThreadState::Unauthenticated { @@ -4958,39 +4680,54 @@ impl Render for AcpThreadView { window, cx, ), - ThreadState::Loading { .. } => v_flex() - .flex_1() - .child(self.render_recent_history(window, cx)), + ThreadState::Loading { .. } => { + v_flex().flex_1().child(self.render_empty_state(window, cx)) + } ThreadState::LoadError(e) => v_flex() + .p_2() .flex_1() - .size_full() .items_center() - .justify_end() - .child(self.render_load_error(e, window, cx)), - ThreadState::Ready { .. } => v_flex().flex_1().map(|this| { - if has_messages { - this.child( - list( - self.list_state.clone(), - cx.processor(|this, index: usize, window, cx| { - let Some((entry, len)) = this.thread().and_then(|thread| { - let entries = &thread.read(cx).entries(); - Some((entries.get(index)?, entries.len())) - }) else { - return Empty.into_any(); - }; - this.render_entry(index, len, entry, window, cx) - }), + .justify_center() + .child(self.render_load_error(e, cx)), + ThreadState::Ready { thread, .. } => { + let thread_clone = thread.clone(); + + v_flex().flex_1().map(|this| { + if has_messages { + this.child( + list( + self.list_state.clone(), + cx.processor(|this, index: usize, window, cx| { + let Some((entry, len)) = this.thread().and_then(|thread| { + let entries = &thread.read(cx).entries(); + Some((entries.get(index)?, entries.len())) + }) else { + return Empty.into_any(); + }; + this.render_entry(index, len, entry, window, cx) + }), + ) + .with_sizing_behavior(gpui::ListSizingBehavior::Auto) + .flex_grow() + .into_any(), ) - .with_sizing_behavior(gpui::ListSizingBehavior::Auto) - .flex_grow() - .into_any(), - ) - .child(self.render_vertical_scrollbar(cx)) - } else { - this.child(self.render_recent_history(window, cx)) - } - }), + .child(self.render_vertical_scrollbar(cx)) + .children( + match thread_clone.read(cx).status() { + ThreadStatus::Idle + | ThreadStatus::WaitingForToolConfirmation => None, + ThreadStatus::Generating => div() + .py_2() + .px(rems_from_px(22.)) + .child(SpinnerLabel::new().size(LabelSize::Small)) + .into(), + }, + ) + } else { + this.child(self.render_empty_state(window, cx)) + } + }) + } }) // The activity bar is intentionally rendered outside of the ThreadState::Ready match // above so that the scrollbar doesn't render behind it. The current setup allows @@ -5407,24 +5144,20 @@ pub(crate) mod tests { where C: 'static + AgentConnection + Send + Clone, { - fn telemetry_id(&self) -> &'static str { - "test" - } - fn logo(&self) -> ui::IconName { ui::IconName::Ai } - fn name(&self) -> SharedString { - "Test".into() + fn name(&self) -> &'static str { + "Test" } - fn empty_state_headline(&self) -> SharedString { - "Test".into() + fn empty_state_headline(&self) -> &'static str { + "Test" } - fn empty_state_message(&self) -> SharedString { - "Test".into() + fn empty_state_message(&self) -> &'static str { + "Test" } fn connect( @@ -5459,12 +5192,6 @@ pub(crate) mod tests { project, action_log, SessionId("test".into()), - watch::Receiver::constant(acp::PromptCapabilities { - image: true, - audio: true, - embedded_context: true, - }), - cx, ) }))) } @@ -5473,6 +5200,14 @@ pub(crate) mod tests { &[] } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + } + } + fn authenticate( &self, _method_id: acp::AuthMethodId, diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index e0cecad6e2..2cad913295 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -1595,6 +1595,11 @@ impl ActiveThread { return; }; + if model.provider.must_accept_terms(cx) { + cx.notify(); + return; + } + let edited_text = state.editor.read(cx).text(cx); let creases = state.editor.update(cx, extract_message_creases); diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 224f49cc3e..00e48efdac 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -3,23 +3,19 @@ mod configure_context_server_modal; mod manage_profiles_modal; mod tool_picker; -use std::{ops::Range, sync::Arc, time::Duration}; +use std::{sync::Arc, time::Duration}; -use agent_servers::{AgentServerCommand, AgentServerSettings, AllAgentServersSettings, Gemini}; use agent_settings::AgentSettings; -use anyhow::Result; use assistant_tool::{ToolSource, ToolWorkingSet}; use cloud_llm_client::Plan; use collections::HashMap; use context_server::ContextServerId; -use editor::{Editor, SelectionEffects, scroll::Autoscroll}; use extension::ExtensionManifest; use extension_host::ExtensionStore; use fs::Fs; use gpui::{ - Action, Animation, AnimationExt as _, AnyView, App, AsyncWindowContext, Corner, Entity, - EventEmitter, FocusHandle, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, - WeakEntity, percentage, + Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle, + Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage, }; use language::LanguageRegistry; use language_model::{ @@ -27,24 +23,23 @@ use language_model::{ }; use notifications::status_toast::{StatusToast, ToastIcon}; use project::{ - Project, context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore}, project_settings::{ContextServerSettings, ProjectSettings}, }; -use settings::{Settings, SettingsStore, update_settings_file}; +use settings::{Settings, update_settings_file}; use ui::{ Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu, Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*, }; use util::ResultExt as _; -use workspace::{Workspace, create_and_open_local_file}; +use workspace::Workspace; use zed_actions::ExtensionCategoryFilter; pub(crate) use configure_context_server_modal::ConfigureContextServerModal; pub(crate) use manage_profiles_modal::ManageProfilesModal; use crate::{ - AddContextServer, ExternalAgent, NewExternalAgentThread, + AddContextServer, agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider}, }; @@ -52,7 +47,6 @@ pub struct AgentConfiguration { fs: Arc, language_registry: Arc, workspace: WeakEntity, - project: WeakEntity, focus_handle: FocusHandle, configuration_views_by_provider: HashMap, context_server_store: Entity, @@ -62,8 +56,6 @@ pub struct AgentConfiguration { _registry_subscription: Subscription, scroll_handle: ScrollHandle, scrollbar_state: ScrollbarState, - gemini_is_installed: bool, - _check_for_gemini: Task<()>, } impl AgentConfiguration { @@ -73,7 +65,6 @@ impl AgentConfiguration { tools: Entity, language_registry: Arc, workspace: WeakEntity, - project: WeakEntity, window: &mut Window, cx: &mut Context, ) -> Self { @@ -98,34 +89,33 @@ impl AgentConfiguration { cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify()) .detach(); - cx.observe_global_in::(window, |this, _, cx| { - this.check_for_gemini(cx); - cx.notify(); - }) - .detach(); let scroll_handle = ScrollHandle::new(); let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); + let mut expanded_provider_configurations = HashMap::default(); + if LanguageModelRegistry::read_global(cx) + .provider(&ZED_CLOUD_PROVIDER_ID) + .is_some_and(|cloud_provider| cloud_provider.must_accept_terms(cx)) + { + expanded_provider_configurations.insert(ZED_CLOUD_PROVIDER_ID, true); + } + let mut this = Self { fs, language_registry, workspace, - project, focus_handle, configuration_views_by_provider: HashMap::default(), context_server_store, expanded_context_server_tools: HashMap::default(), - expanded_provider_configurations: HashMap::default(), + expanded_provider_configurations, tools, _registry_subscription: registry_subscription, scroll_handle, scrollbar_state, - gemini_is_installed: false, - _check_for_gemini: Task::ready(()), }; this.build_provider_configuration_views(window, cx); - this.check_for_gemini(cx); this } @@ -155,34 +145,6 @@ impl AgentConfiguration { self.configuration_views_by_provider .insert(provider.id(), configuration_view); } - - fn check_for_gemini(&mut self, cx: &mut Context) { - let project = self.project.clone(); - let settings = AllAgentServersSettings::get_global(cx).clone(); - self._check_for_gemini = cx.spawn({ - async move |this, cx| { - let Some(project) = project.upgrade() else { - return; - }; - let gemini_is_installed = AgentServerCommand::resolve( - Gemini::binary_name(), - &[], - // TODO expose fallback path from the Gemini/CC types so we don't have to hardcode it again here - None, - settings.gemini, - &project, - cx, - ) - .await - .is_some(); - this.update(cx, |this, cx| { - this.gemini_is_installed = gemini_is_installed; - cx.notify(); - }) - .ok(); - } - }); - } } impl Focusable for AgentConfiguration { @@ -257,6 +219,7 @@ impl AgentConfiguration { .child( h_flex() .id(provider_id_string.clone()) + .cursor_pointer() .px_2() .py_0p5() .w_full() @@ -276,7 +239,10 @@ impl AgentConfiguration { h_flex() .w_full() .gap_1() - .child(Label::new(provider_name.clone())) + .child( + Label::new(provider_name.clone()) + .size(LabelSize::Large), + ) .map(|this| { if is_zed_provider && is_signed_in { this.child( @@ -321,7 +287,7 @@ impl AgentConfiguration { "Start New Thread", ) .icon_position(IconPosition::Start) - .icon(IconName::Thread) + .icon(IconName::Plus) .icon_size(IconSize::Small) .icon_color(Color::Muted) .label_size(LabelSize::Small) @@ -420,7 +386,7 @@ impl AgentConfiguration { ), ) .child( - Label::new("Add at least one provider to use AI-powered features with Zed's native agent.") + Label::new("Add at least one provider to use AI-powered features.") .color(Color::Muted), ), ), @@ -561,14 +527,6 @@ impl AgentConfiguration { } } - fn card_item_bg_color(&self, cx: &mut Context) -> Hsla { - cx.theme().colors().background.opacity(0.25) - } - - fn card_item_border_color(&self, cx: &mut Context) -> Hsla { - cx.theme().colors().border.opacity(0.6) - } - fn render_context_servers_section( &mut self, window: &mut Window, @@ -586,12 +544,7 @@ impl AgentConfiguration { v_flex() .gap_0p5() .child(Headline::new("Model Context Protocol (MCP) Servers")) - .child( - Label::new( - "All context servers connected through the Model Context Protocol.", - ) - .color(Color::Muted), - ), + .child(Label::new("Connect to context servers through the Model Context Protocol, either using Zed extensions or directly.").color(Color::Muted)), ) .children( context_server_ids.into_iter().map(|context_server_id| { @@ -601,7 +554,7 @@ impl AgentConfiguration { .child( h_flex() .justify_between() - .gap_1p5() + .gap_2() .child( h_flex().w_full().child( Button::new("add-context-server", "Add Custom Server") @@ -692,6 +645,8 @@ impl AgentConfiguration { .map_or([].as_slice(), |tools| tools.as_slice()); let tool_count = tools.len(); + let border_color = cx.theme().colors().border.opacity(0.6); + let (source_icon, source_tooltip) = if is_from_extension { ( IconName::ZedMcpExtension, @@ -834,8 +789,8 @@ impl AgentConfiguration { .id(item_id.clone()) .border_1() .rounded_md() - .border_color(self.card_item_border_color(cx)) - .bg(self.card_item_bg_color(cx)) + .border_color(border_color) + .bg(cx.theme().colors().background.opacity(0.2)) .overflow_hidden() .child( h_flex() @@ -843,11 +798,7 @@ impl AgentConfiguration { .justify_between() .when( error.is_some() || are_tools_expanded && tool_count >= 1, - |element| { - element - .border_b_1() - .border_color(self.card_item_border_color(cx)) - }, + |element| element.border_b_1().border_color(border_color), ) .child( h_flex() @@ -1029,195 +980,6 @@ impl AgentConfiguration { )) }) } - - fn render_agent_servers_section(&mut self, cx: &mut Context) -> impl IntoElement { - let settings = AllAgentServersSettings::get_global(cx).clone(); - let user_defined_agents = settings - .custom - .iter() - .map(|(name, settings)| { - self.render_agent_server( - IconName::Ai, - name.clone(), - ExternalAgent::Custom { - name: name.clone(), - settings: settings.clone(), - }, - None, - cx, - ) - .into_any_element() - }) - .collect::>(); - - v_flex() - .border_b_1() - .border_color(cx.theme().colors().border) - .child( - v_flex() - .p(DynamicSpacing::Base16.rems(cx)) - .pr(DynamicSpacing::Base20.rems(cx)) - .gap_2() - .child( - v_flex() - .gap_0p5() - .child( - h_flex() - .w_full() - .gap_2() - .justify_between() - .child(Headline::new("External Agents")) - .child( - Button::new("add-agent", "Add Agent") - .icon_position(IconPosition::Start) - .icon(IconName::Plus) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .label_size(LabelSize::Small) - .on_click( - move |_, window, cx| { - if let Some(workspace) = window.root().flatten() { - let workspace = workspace.downgrade(); - window - .spawn(cx, async |cx| { - open_new_agent_servers_entry_in_settings_editor( - workspace, - cx, - ).await - }) - .detach_and_log_err(cx); - } - } - ), - ) - ) - .child( - Label::new( - "Bring the agent of your choice to Zed via our new Agent Client Protocol.", - ) - .color(Color::Muted), - ), - ) - .child(self.render_agent_server( - IconName::AiGemini, - "Gemini CLI", - ExternalAgent::Gemini, - (!self.gemini_is_installed).then_some(Gemini::install_command().into()), - cx, - )) - // TODO add CC - .children(user_defined_agents), - ) - } - - fn render_agent_server( - &self, - icon: IconName, - name: impl Into, - agent: ExternalAgent, - install_command: Option, - cx: &mut Context, - ) -> impl IntoElement { - let name = name.into(); - h_flex() - .p_1() - .pl_2() - .gap_1p5() - .justify_between() - .border_1() - .rounded_md() - .border_color(self.card_item_border_color(cx)) - .bg(self.card_item_bg_color(cx)) - .overflow_hidden() - .child( - h_flex() - .gap_1p5() - .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted)) - .child(Label::new(name.clone())), - ) - .map(|this| { - if let Some(install_command) = install_command { - this.child( - Button::new( - SharedString::from(format!("install_external_agent-{name}")), - "Install Agent", - ) - .label_size(LabelSize::Small) - .icon(IconName::Plus) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .tooltip(Tooltip::text(install_command.clone())) - .on_click(cx.listener( - move |this, _, window, cx| { - let Some(project) = this.project.upgrade() else { - return; - }; - let Some(workspace) = this.workspace.upgrade() else { - return; - }; - let cwd = project.read(cx).first_project_directory(cx); - let shell = - project.read(cx).terminal_settings(&cwd, cx).shell.clone(); - let spawn_in_terminal = task::SpawnInTerminal { - id: task::TaskId(install_command.to_string()), - full_label: install_command.to_string(), - label: install_command.to_string(), - command: Some(install_command.to_string()), - args: Vec::new(), - command_label: install_command.to_string(), - cwd, - env: Default::default(), - use_new_terminal: true, - allow_concurrent_runs: true, - reveal: Default::default(), - reveal_target: Default::default(), - hide: Default::default(), - shell, - show_summary: true, - show_command: true, - show_rerun: false, - }; - let task = workspace.update(cx, |workspace, cx| { - workspace.spawn_in_terminal(spawn_in_terminal, window, cx) - }); - cx.spawn(async move |this, cx| { - task.await; - this.update(cx, |this, cx| { - this.check_for_gemini(cx); - }) - .ok(); - }) - .detach(); - }, - )), - ) - } else { - this.child( - h_flex().gap_1().child( - Button::new( - SharedString::from(format!("start_acp_thread-{name}")), - "Start New Thread", - ) - .label_size(LabelSize::Small) - .icon(IconName::Thread) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .on_click(move |_, window, cx| { - window.dispatch_action( - NewExternalAgentThread { - agent: Some(agent.clone()), - } - .boxed_clone(), - cx, - ); - }), - ), - ) - } - }) - } } impl Render for AgentConfiguration { @@ -1237,7 +999,6 @@ impl Render for AgentConfiguration { .size_full() .overflow_y_scroll() .child(self.render_general_settings_section(cx)) - .child(self.render_agent_servers_section(cx)) .child(self.render_context_servers_section(window, cx)) .child(self.render_provider_configuration_section(cx)), ) @@ -1356,109 +1117,3 @@ fn show_unable_to_uninstall_extension_with_context_server( workspace.toggle_status_toast(status_toast, cx); } - -async fn open_new_agent_servers_entry_in_settings_editor( - workspace: WeakEntity, - cx: &mut AsyncWindowContext, -) -> Result<()> { - let settings_editor = workspace - .update_in(cx, |_, window, cx| { - create_and_open_local_file(paths::settings_file(), window, cx, || { - settings::initial_user_settings_content().as_ref().into() - }) - })? - .await? - .downcast::() - .unwrap(); - - settings_editor - .downgrade() - .update_in(cx, |item, window, cx| { - let text = item.buffer().read(cx).snapshot(cx).text(); - - let settings = cx.global::(); - - let mut unique_server_name = None; - let edits = settings.edits_for_update::(&text, |file| { - let server_name: Option = (0..u8::MAX) - .map(|i| { - if i == 0 { - "your_agent".into() - } else { - format!("your_agent_{}", i).into() - } - }) - .find(|name| !file.custom.contains_key(name)); - if let Some(server_name) = server_name { - unique_server_name = Some(server_name.clone()); - file.custom.insert( - server_name, - AgentServerSettings { - command: AgentServerCommand { - path: "path_to_executable".into(), - args: vec![], - env: Some(HashMap::default()), - }, - }, - ); - } - }); - - if edits.is_empty() { - return; - } - - let ranges = edits - .iter() - .map(|(range, _)| range.clone()) - .collect::>(); - - item.edit(edits, cx); - if let Some((unique_server_name, buffer)) = - unique_server_name.zip(item.buffer().read(cx).as_singleton()) - { - let snapshot = buffer.read(cx).snapshot(); - if let Some(range) = - find_text_in_buffer(&unique_server_name, ranges[0].start, &snapshot) - { - item.change_selections( - SelectionEffects::scroll(Autoscroll::newest()), - window, - cx, - |selections| { - selections.select_ranges(vec![range]); - }, - ); - } - } - }) -} - -fn find_text_in_buffer( - text: &str, - start: usize, - snapshot: &language::BufferSnapshot, -) -> Option> { - let chars = text.chars().collect::>(); - - let mut offset = start; - let mut char_offset = 0; - for c in snapshot.chars_at(start) { - if char_offset >= chars.len() { - break; - } - offset += 1; - - if c == chars[char_offset] { - char_offset += 1; - } else { - char_offset = 0; - } - } - - if char_offset == chars.len() { - Some(offset.saturating_sub(chars.len())..offset) - } else { - None - } -} diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 1e1ff95178..e07424987c 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1529,7 +1529,6 @@ impl AgentDiff { | AcpThreadEvent::TokenUsageUpdated | AcpThreadEvent::EntriesRemoved(_) | AcpThreadEvent::ToolAuthorizationRequired - | AcpThreadEvent::PromptCapabilitiesUpdated | AcpThreadEvent::Retry(_) => {} } } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index d1cf748733..469898d10f 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -5,16 +5,12 @@ use std::sync::Arc; use std::time::Duration; use acp_thread::AcpThread; -use agent_servers::AgentServerSettings; use agent2::{DbThreadMetadata, HistoryEntry}; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use serde::{Deserialize, Serialize}; -use zed_actions::OpenBrowser; -use zed_actions::agent::ReauthenticateAgent; use crate::acp::{AcpThreadHistory, ThreadHistoryEvent}; use crate::agent_diff::AgentDiffThread; -use crate::ui::AcpOnboardingModal; use crate::{ AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread, @@ -58,7 +54,9 @@ use gpui::{ Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between, }; use language::LanguageRegistry; -use language_model::{ConfigurationError, ConfiguredModel, LanguageModelRegistry}; +use language_model::{ + ConfigurationError, ConfiguredModel, LanguageModelProviderTosView, LanguageModelRegistry, +}; use project::{DisableAiSettings, Project, ProjectPath, Worktree}; use prompt_store::{PromptBuilder, PromptStore, UserPromptId}; use rules_library::{RulesLibrary, open_rules_library}; @@ -78,10 +76,7 @@ use workspace::{ }; use zed_actions::{ DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize, - agent::{ - OpenAcpOnboardingModal, OpenOnboardingModal, OpenSettings, ResetOnboarding, - ToggleModelSelector, - }, + agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector}, assistant::{OpenRulesLibrary, ToggleFocus}, }; @@ -135,7 +130,7 @@ pub fn init(cx: &mut App) { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); panel.update(cx, |panel, cx| { - panel.external_thread(action.agent.clone(), None, None, window, cx) + panel.external_thread(action.agent, None, None, window, cx) }); } }) @@ -205,9 +200,6 @@ pub fn init(cx: &mut App) { .register_action(|workspace, _: &OpenOnboardingModal, window, cx| { AgentOnboardingModal::toggle(workspace, window, cx) }) - .register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| { - AcpOnboardingModal::toggle(workspace, window, cx) - }) .register_action(|_workspace, _: &ResetOnboarding, window, cx| { window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx); window.refresh(); @@ -249,8 +241,7 @@ enum WhichFontSize { None, } -// TODO unify this with ExternalAgent -#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum AgentType { #[default] Zed, @@ -258,29 +249,23 @@ pub enum AgentType { Gemini, ClaudeCode, NativeAgent, - Custom { - name: SharedString, - settings: AgentServerSettings, - }, } impl AgentType { - fn label(&self) -> SharedString { + fn label(self) -> impl Into { match self { - Self::Zed | Self::TextThread => "Zed Agent".into(), - Self::NativeAgent => "Agent 2".into(), - Self::Gemini => "Gemini CLI".into(), - Self::ClaudeCode => "Claude Code".into(), - Self::Custom { name, .. } => name.into(), + Self::Zed | Self::TextThread => "Zed Agent", + Self::NativeAgent => "Agent 2", + Self::Gemini => "Gemini CLI", + Self::ClaudeCode => "Claude Code", } } - fn icon(&self) -> Option { + fn icon(self) -> Option { match self { Self::Zed | Self::NativeAgent | Self::TextThread => None, Self::Gemini => Some(IconName::AiGemini), Self::ClaudeCode => Some(IconName::AiClaude), - Self::Custom { .. } => Some(IconName::Terminal), } } } @@ -534,7 +519,7 @@ pub struct AgentPanel { impl AgentPanel { fn serialize(&mut self, cx: &mut Context) { let width = self.width; - let selected_agent = self.selected_agent.clone(); + let selected_agent = self.selected_agent; self.pending_serialization = Some(cx.background_spawn(async move { KEY_VALUE_STORE .write_kvp( @@ -598,6 +583,17 @@ impl AgentPanel { None }; + // Wait for the Gemini/Native feature flag to be available. + let client = workspace.read_with(cx, |workspace, _| workspace.client().clone())?; + if !client.status().borrow().is_signed_out() { + cx.update(|_, cx| { + cx.wait_for_flag_or_timeout::( + Duration::from_secs(2), + ) + })? + .await; + } + let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = cx.new(|cx| { Self::new( @@ -613,7 +609,7 @@ impl AgentPanel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width.map(|w| w.round()); if let Some(selected_agent) = serialized_panel.selected_agent { - panel.selected_agent = selected_agent.clone(); + panel.selected_agent = selected_agent; panel.new_agent_thread(selected_agent, window, cx); } cx.notify(); @@ -1023,8 +1019,6 @@ impl AgentPanel { } fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context) { - telemetry::event!("Agent Thread Started", agent = "zed-text"); - let context = self .context_store .update(cx, |context_store, cx| context_store.create(cx)); @@ -1085,17 +1079,14 @@ impl AgentPanel { cx.spawn_in(window, async move |this, cx| { let ext_agent = match agent_choice { Some(agent) => { - cx.background_spawn({ - let agent = agent.clone(); - async move { - if let Some(serialized) = - serde_json::to_string(&LastUsedExternalAgent { agent }).log_err() - { - KEY_VALUE_STORE - .write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized) - .await - .log_err(); - } + cx.background_spawn(async move { + if let Some(serialized) = + serde_json::to_string(&LastUsedExternalAgent { agent }).log_err() + { + KEY_VALUE_STORE + .write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized) + .await + .log_err(); } }) .detach(); @@ -1117,15 +1108,11 @@ impl AgentPanel { } }; - telemetry::event!("Agent Thread Started", agent = ext_agent.name()); - let server = ext_agent.server(fs, history); this.update_in(cx, |this, window, cx| { match ext_agent { - crate::ExternalAgent::Gemini - | crate::ExternalAgent::NativeAgent - | crate::ExternalAgent::Custom { .. } => { + crate::ExternalAgent::Gemini | crate::ExternalAgent::NativeAgent => { if !cx.has_flag::() { return; } @@ -1476,7 +1463,6 @@ impl AgentPanel { tools, self.language_registry.clone(), self.workspace.clone(), - self.project.downgrade(), window, cx, ) @@ -1848,8 +1834,21 @@ impl AgentPanel { menu } + pub fn set_selected_agent( + &mut self, + agent: AgentType, + window: &mut Window, + cx: &mut Context, + ) { + if self.selected_agent != agent { + self.selected_agent = agent; + self.serialize(cx); + } + self.new_agent_thread(agent, window, cx); + } + pub fn selected_agent(&self) -> AgentType { - self.selected_agent.clone() + self.selected_agent } pub fn new_agent_thread( @@ -1858,11 +1857,6 @@ impl AgentPanel { window: &mut Window, cx: &mut Context, ) { - if self.selected_agent != agent { - self.selected_agent = agent.clone(); - self.serialize(cx); - } - match agent { AgentType::Zed => { window.dispatch_action( @@ -1893,13 +1887,6 @@ impl AgentPanel { window, cx, ), - AgentType::Custom { name, settings } => self.external_thread( - Some(crate::ExternalAgent::Custom { name, settings }), - None, - None, - window, - cx, - ), } } @@ -2112,7 +2099,7 @@ impl AgentPanel { .child(title_editor) .into_any_element() } else { - Label::new(thread_view.read(cx).title()) + Label::new(thread_view.read(cx).title(cx)) .color(Color::Muted) .truncate() .into_any_element() @@ -2200,8 +2187,6 @@ impl AgentPanel { "Enable Full Screen" }; - let selected_agent = self.selected_agent.clone(); - PopoverMenu::new("agent-options-menu") .trigger_with_tooltip( IconButton::new("agent-options-menu", IconName::Ellipsis) @@ -2281,11 +2266,6 @@ impl AgentPanel { .action("Settings", Box::new(OpenSettings)) .separator() .action(full_screen_label, Box::new(ToggleZoom)); - - if selected_agent == AgentType::Gemini { - menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent)) - } - menu })) } @@ -2320,8 +2300,6 @@ impl AgentPanel { .menu({ let menu = self.assistant_navigation_menu.clone(); move |window, cx| { - telemetry::event!("View Thread History Clicked"); - if let Some(menu) = menu.as_ref() { menu.update(cx, |_, cx| { cx.defer_in(window, |menu, window, cx| { @@ -2500,8 +2478,6 @@ impl AgentPanel { let workspace = self.workspace.clone(); move |window, cx| { - telemetry::event!("New Thread Clicked"); - let active_thread = active_thread.clone(); Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { menu = menu @@ -2543,7 +2519,7 @@ impl AgentPanel { workspace.panel::(cx) { panel.update(cx, |panel, cx| { - panel.new_agent_thread( + panel.set_selected_agent( AgentType::NativeAgent, window, cx, @@ -2569,7 +2545,7 @@ impl AgentPanel { workspace.panel::(cx) { panel.update(cx, |panel, cx| { - panel.new_agent_thread( + panel.set_selected_agent( AgentType::TextThread, window, cx, @@ -2597,7 +2573,7 @@ impl AgentPanel { workspace.panel::(cx) { panel.update(cx, |panel, cx| { - panel.new_agent_thread( + panel.set_selected_agent( AgentType::Gemini, window, cx, @@ -2624,7 +2600,7 @@ impl AgentPanel { workspace.panel::(cx) { panel.update(cx, |panel, cx| { - panel.new_agent_thread( + panel.set_selected_agent( AgentType::ClaudeCode, window, cx, @@ -2636,64 +2612,13 @@ impl AgentPanel { } }), ) - }) - .when(cx.has_flag::(), |mut menu| { - // Add custom agents from settings - let settings = - agent_servers::AllAgentServersSettings::get_global(cx); - for (agent_name, agent_settings) in &settings.custom { - menu = menu.item( - ContextMenuEntry::new(format!("New {} Thread", agent_name)) - .icon(IconName::Terminal) - .icon_color(Color::Muted) - .handler({ - let workspace = workspace.clone(); - let agent_name = agent_name.clone(); - let agent_settings = agent_settings.clone(); - move |window, cx| { - if let Some(workspace) = workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - if let Some(panel) = - workspace.panel::(cx) - { - panel.update(cx, |panel, cx| { - panel.new_agent_thread( - AgentType::Custom { - name: agent_name - .clone(), - settings: - agent_settings - .clone(), - }, - window, - cx, - ); - }); - } - }); - } - } - }), - ); - } - - menu - }) - .when(cx.has_flag::(), |menu| { - menu.separator().link( - "Add Other Agents", - OpenBrowser { - url: zed_urls::external_agents_docs(cx), - } - .boxed_clone(), - ) }); menu })) } }); - let selected_agent_label = self.selected_agent.label(); + let selected_agent_label = self.selected_agent.label().into(); let selected_agent = div() .id("selected_agent_icon") .when_some(self.selected_agent.icon(), |this, icon| { @@ -3278,6 +3203,17 @@ impl AgentPanel { ConfigurationError::ModelNotFound | ConfigurationError::ProviderNotAuthenticated(_) | ConfigurationError::NoProvider => callout.into_any_element(), + ConfigurationError::ProviderPendingTermsAcceptance(provider) => { + Banner::new() + .severity(Severity::Warning) + .child(h_flex().w_full().children( + provider.render_accept_terms( + LanguageModelProviderTosView::ThreadEmptyState, + cx, + ), + )) + .into_any_element() + } } } @@ -3767,11 +3703,6 @@ impl Render for AgentPanel { } })) .on_action(cx.listener(Self::toggle_burn_mode)) - .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| { - if let Some(thread_view) = this.active_thread_view() { - thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx)) - } - })) .child(self.render_toolbar(window, cx)) .children(self.render_onboarding(window, cx)) .map(|parent| match &self.active_view { diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 110c432df3..6084fd6423 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -28,14 +28,13 @@ use std::rc::Rc; use std::sync::Arc; use agent::{Thread, ThreadId}; -use agent_servers::AgentServerSettings; use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection}; use assistant_slash_command::SlashCommandRegistry; use client::Client; use command_palette_hooks::CommandPaletteFilter; use feature_flags::FeatureFlagAppExt as _; use fs::Fs; -use gpui::{Action, App, Entity, SharedString, actions}; +use gpui::{Action, App, Entity, actions}; use language::LanguageRegistry; use language_model::{ ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, @@ -160,43 +159,25 @@ pub struct NewNativeAgentThreadFromSummary { from_session_id: agent_client_protocol::SessionId, } -// TODO unify this with AgentType -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] enum ExternalAgent { #[default] Gemini, ClaudeCode, NativeAgent, - Custom { - name: SharedString, - settings: AgentServerSettings, - }, } impl ExternalAgent { - fn name(&self) -> &'static str { - match self { - Self::NativeAgent => "zed", - Self::Gemini => "gemini-cli", - Self::ClaudeCode => "claude-code", - Self::Custom { .. } => "custom", - } - } - pub fn server( &self, fs: Arc, history: Entity, ) -> Rc { match self { - Self::Gemini => Rc::new(agent_servers::Gemini), - Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode), - Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)), - Self::Custom { name, settings } => Rc::new(agent_servers::CustomAgentServer::new( - name.clone(), - settings, - )), + ExternalAgent::Gemini => Rc::new(agent_servers::Gemini), + ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode), + ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)), } } } diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index 3633e533da..aceca79dbf 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -6,8 +6,7 @@ use feature_flags::ZedProFeatureFlag; use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task}; use language_model::{ - AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId, - LanguageModelRegistry, + ConfiguredModel, LanguageModel, LanguageModelProviderId, LanguageModelRegistry, }; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate}; @@ -77,7 +76,6 @@ pub struct LanguageModelPickerDelegate { all_models: Arc, filtered_entries: Vec, selected_index: usize, - _authenticate_all_providers_task: Task<()>, _subscriptions: Vec, } @@ -98,7 +96,6 @@ impl LanguageModelPickerDelegate { selected_index: Self::get_active_model_index(&entries, get_active_model(cx)), filtered_entries: entries, get_active_model: Arc::new(get_active_model), - _authenticate_all_providers_task: Self::authenticate_all_providers(cx), _subscriptions: vec![cx.subscribe_in( &LanguageModelRegistry::global(cx), window, @@ -142,56 +139,6 @@ impl LanguageModelPickerDelegate { .unwrap_or(0) } - /// Authenticates all providers in the [`LanguageModelRegistry`]. - /// - /// We do this so that we can populate the language selector with all of the - /// models from the configured providers. - fn authenticate_all_providers(cx: &mut App) -> Task<()> { - let authenticate_all_providers = LanguageModelRegistry::global(cx) - .read(cx) - .providers() - .iter() - .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx))) - .collect::>(); - - cx.spawn(async move |_cx| { - for (provider_id, provider_name, authenticate_task) in authenticate_all_providers { - if let Err(err) = authenticate_task.await { - if matches!(err, AuthenticateError::CredentialsNotFound) { - // Since we're authenticating these providers in the - // background for the purposes of populating the - // language selector, we don't care about providers - // where the credentials are not found. - } else { - // Some providers have noisy failure states that we - // don't want to spam the logs with every time the - // language model selector is initialized. - // - // Ideally these should have more clear failure modes - // that we know are safe to ignore here, like what we do - // with `CredentialsNotFound` above. - match provider_id.0.as_ref() { - "lmstudio" | "ollama" => { - // LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated". - // - // These fail noisily, so we don't log them. - } - "copilot_chat" => { - // Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors. - } - _ => { - log::error!( - "Failed to authenticate provider: {}: {err}", - provider_name.0 - ); - } - } - } - } - } - }) - } - pub fn active_model(&self, cx: &App) -> Option { (self.get_active_model)(cx) } diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 45e7529ec2..bed10e90a7 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -378,13 +378,18 @@ impl MessageEditor { } fn send_to_model(&mut self, window: &mut Window, cx: &mut Context) { - let Some(ConfiguredModel { model, .. }) = self + let Some(ConfiguredModel { model, provider }) = self .thread .update(cx, |thread, cx| thread.get_or_init_configured_model(cx)) else { return; }; + if provider.must_accept_terms(cx) { + cx.notify(); + return; + } + let (user_message, user_message_creases) = self.editor.update(cx, |editor, cx| { let creases = extract_message_creases(editor, cx); let text = editor.text(cx); diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index e9e7eba4b6..9fbd90c4a6 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -190,6 +190,7 @@ pub struct TextThreadEditor { invoked_slash_command_creases: HashMap, _subscriptions: Vec, last_error: Option, + show_accept_terms: bool, pub(crate) slash_menu_handle: PopoverMenuHandle>, // dragged_file_worktrees is used to keep references to worktrees that were added @@ -288,6 +289,7 @@ impl TextThreadEditor { invoked_slash_command_creases: HashMap::default(), _subscriptions, last_error: None, + show_accept_terms: false, slash_menu_handle: Default::default(), dragged_file_worktrees: Vec::new(), language_model_selector: cx.new(|cx| { @@ -361,12 +363,24 @@ impl TextThreadEditor { if self.sending_disabled(cx) { return; } - telemetry::event!("Agent Message Sent", agent = "zed-text"); self.send_to_model(window, cx); } fn send_to_model(&mut self, window: &mut Window, cx: &mut Context) { + let provider = LanguageModelRegistry::read_global(cx) + .default_model() + .map(|default| default.provider); + if provider + .as_ref() + .is_some_and(|provider| provider.must_accept_terms(cx)) + { + self.show_accept_terms = true; + cx.notify(); + return; + } + self.last_error = None; + if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) { let new_selection = { let cursor = user_message @@ -1916,6 +1930,7 @@ impl TextThreadEditor { ConfigurationError::NoProvider | ConfigurationError::ModelNotFound | ConfigurationError::ProviderNotAuthenticated(_) => true, + ConfigurationError::ProviderPendingTermsAcceptance(_) => self.show_accept_terms, } } diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index 600698b07e..ada973cddf 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -1,4 +1,3 @@ -mod acp_onboarding_modal; mod agent_notification; mod burn_mode_tooltip; mod context_pill; @@ -7,7 +6,6 @@ mod onboarding_modal; pub mod preview; mod unavailable_editing_tooltip; -pub use acp_onboarding_modal::*; pub use agent_notification::*; pub use burn_mode_tooltip::*; pub use context_pill::*; diff --git a/crates/agent_ui/src/ui/acp_onboarding_modal.rs b/crates/agent_ui/src/ui/acp_onboarding_modal.rs deleted file mode 100644 index 0ed9de7221..0000000000 --- a/crates/agent_ui/src/ui/acp_onboarding_modal.rs +++ /dev/null @@ -1,254 +0,0 @@ -use client::zed_urls; -use gpui::{ - ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render, - linear_color_stop, linear_gradient, -}; -use ui::{TintColor, Vector, VectorName, prelude::*}; -use workspace::{ModalView, Workspace}; - -use crate::agent_panel::{AgentPanel, AgentType}; - -macro_rules! acp_onboarding_event { - ($name:expr) => { - telemetry::event!($name, source = "ACP Onboarding"); - }; - ($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => { - telemetry::event!($name, source = "ACP Onboarding", $($key $(= $value)?),+); - }; -} - -pub struct AcpOnboardingModal { - focus_handle: FocusHandle, - workspace: Entity, -} - -impl AcpOnboardingModal { - pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { - let workspace_entity = cx.entity(); - workspace.toggle_modal(window, cx, |_window, cx| Self { - workspace: workspace_entity, - focus_handle: cx.focus_handle(), - }); - } - - fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { - self.workspace.update(cx, |workspace, cx| { - workspace.focus_panel::(window, cx); - - if let Some(panel) = workspace.panel::(cx) { - panel.update(cx, |panel, cx| { - panel.new_agent_thread(AgentType::Gemini, window, cx); - }); - } - }); - - cx.emit(DismissEvent); - - acp_onboarding_event!("Open Panel Clicked"); - } - - fn view_docs(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context) { - cx.open_url(&zed_urls::external_agents_docs(cx)); - cx.notify(); - - acp_onboarding_event!("Documentation Link Clicked"); - } - - fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { - cx.emit(DismissEvent); - } -} - -impl EventEmitter for AcpOnboardingModal {} - -impl Focusable for AcpOnboardingModal { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl ModalView for AcpOnboardingModal {} - -impl Render for AcpOnboardingModal { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let illustration_element = |label: bool, opacity: f32| { - h_flex() - .px_1() - .py_0p5() - .gap_1() - .rounded_sm() - .bg(cx.theme().colors().element_active.opacity(0.05)) - .border_1() - .border_color(cx.theme().colors().border) - .border_dashed() - .child( - Icon::new(IconName::Stop) - .size(IconSize::Small) - .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))), - ) - .map(|this| { - if label { - this.child( - Label::new("Your Agent Here") - .size(LabelSize::Small) - .color(Color::Muted), - ) - } else { - this.child( - div().w_16().h_1().rounded_full().bg(cx - .theme() - .colors() - .element_active - .opacity(0.6)), - ) - } - }) - .opacity(opacity) - }; - - let illustration = h_flex() - .relative() - .h(rems_from_px(126.)) - .bg(cx.theme().colors().editor_background) - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .justify_center() - .gap_8() - .rounded_t_md() - .overflow_hidden() - .child( - div().absolute().inset_0().w(px(515.)).h(px(126.)).child( - Vector::new(VectorName::AcpGrid, rems_from_px(515.), rems_from_px(126.)) - .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.02))), - ), - ) - .child(div().absolute().inset_0().size_full().bg(linear_gradient( - 0., - linear_color_stop( - cx.theme().colors().elevated_surface_background.opacity(0.1), - 0.9, - ), - linear_color_stop( - cx.theme().colors().elevated_surface_background.opacity(0.), - 0., - ), - ))) - .child( - div() - .absolute() - .inset_0() - .size_full() - .bg(gpui::black().opacity(0.15)), - ) - .child( - h_flex() - .gap_4() - .child( - Vector::new(VectorName::AcpLogo, rems_from_px(106.), rems_from_px(40.)) - .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))), - ) - .child( - Vector::new( - VectorName::AcpLogoSerif, - rems_from_px(111.), - rems_from_px(41.), - ) - .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))), - ), - ) - .child( - v_flex() - .gap_1p5() - .child(illustration_element(false, 0.15)) - .child(illustration_element(true, 0.3)) - .child( - h_flex() - .pl_1() - .pr_2() - .py_0p5() - .gap_1() - .rounded_sm() - .bg(cx.theme().colors().element_active.opacity(0.2)) - .border_1() - .border_color(cx.theme().colors().border) - .child( - Icon::new(IconName::AiGemini) - .size(IconSize::Small) - .color(Color::Muted), - ) - .child(Label::new("New Gemini CLI Thread").size(LabelSize::Small)), - ) - .child(illustration_element(true, 0.3)) - .child(illustration_element(false, 0.15)), - ); - - let heading = v_flex() - .w_full() - .gap_1() - .child( - Label::new("Now Available") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child(Headline::new("Bring Your Own Agent to Zed").size(HeadlineSize::Large)); - - let copy = "Bring the agent of your choice to Zed via our new Agent Client Protocol (ACP), starting with Google's Gemini CLI integration."; - - let open_panel_button = Button::new("open-panel", "Start with Gemini CLI") - .icon_size(IconSize::Indicator) - .style(ButtonStyle::Tinted(TintColor::Accent)) - .full_width() - .on_click(cx.listener(Self::open_panel)); - - let docs_button = Button::new("add-other-agents", "Add Other Agents") - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Indicator) - .icon_color(Color::Muted) - .full_width() - .on_click(cx.listener(Self::view_docs)); - - let close_button = h_flex().absolute().top_2().right_2().child( - IconButton::new("cancel", IconName::Close).on_click(cx.listener( - |_, _: &ClickEvent, _window, cx| { - acp_onboarding_event!("Canceled", trigger = "X click"); - cx.emit(DismissEvent); - }, - )), - ); - - v_flex() - .id("acp-onboarding") - .key_context("AcpOnboardingModal") - .relative() - .w(rems(34.)) - .h_full() - .elevation_3(cx) - .track_focus(&self.focus_handle(cx)) - .overflow_hidden() - .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| { - acp_onboarding_event!("Canceled", trigger = "Action"); - cx.emit(DismissEvent); - })) - .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| { - this.focus_handle.focus(window); - })) - .child(illustration) - .child( - v_flex() - .p_4() - .gap_2() - .child(heading) - .child(Label::new(copy).color(Color::Muted)) - .child( - v_flex() - .w_full() - .mt_2() - .gap_1() - .child(open_panel_button) - .child(docs_button), - ), - ) - .child(close_button) - } -} diff --git a/crates/agent_ui/src/ui/preview/usage_callouts.rs b/crates/agent_ui/src/ui/preview/usage_callouts.rs index d4d037b976..29b12ea627 100644 --- a/crates/agent_ui/src/ui/preview/usage_callouts.rs +++ b/crates/agent_ui/src/ui/preview/usage_callouts.rs @@ -86,18 +86,23 @@ impl RenderOnce for UsageCallout { (IconName::Warning, Severity::Warning) }; - Callout::new() - .icon(icon) - .severity(severity) - .icon(icon) - .title(title) - .description(message) - .actions_slot( - Button::new("upgrade", button_text) - .label_size(LabelSize::Small) - .on_click(move |_, _, cx| { - cx.open_url(&url); - }), + div() + .border_t_1() + .border_color(cx.theme().colors().border) + .child( + Callout::new() + .icon(icon) + .severity(severity) + .icon(icon) + .title(title) + .description(message) + .actions_slot( + Button::new("upgrade", button_text) + .label_size(LabelSize::Small) + .on_click(move |_, _, cx| { + cx.open_url(&url); + }), + ), ) .into_any_element() } diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index 6d8ac64725..717abebfd1 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -19,7 +19,7 @@ use std::sync::Arc; use client::{Client, UserStore, zed_urls}; use gpui::{AnyElement, Entity, IntoElement, ParentElement}; -use ui::{Divider, RegisterComponent, Tooltip, prelude::*}; +use ui::{Divider, RegisterComponent, TintColor, Tooltip, prelude::*}; #[derive(PartialEq)] pub enum SignInStatus { @@ -43,10 +43,12 @@ impl From for SignInStatus { #[derive(RegisterComponent, IntoElement)] pub struct ZedAiOnboarding { pub sign_in_status: SignInStatus, + pub has_accepted_terms_of_service: bool, pub plan: Option, pub account_too_young: bool, pub continue_with_zed_ai: Arc, pub sign_in: Arc, + pub accept_terms_of_service: Arc, pub dismiss_onboarding: Option>, } @@ -62,9 +64,17 @@ impl ZedAiOnboarding { Self { sign_in_status: status.into(), + has_accepted_terms_of_service: store.has_accepted_terms_of_service(), plan: store.plan(), account_too_young: store.account_too_young(), continue_with_zed_ai, + accept_terms_of_service: Arc::new({ + let store = user_store.clone(); + move |_window, cx| { + let task = store.update(cx, |store, cx| store.accept_terms_of_service(cx)); + task.detach_and_log_err(cx); + } + }), sign_in: Arc::new(move |_window, cx| { cx.spawn({ let client = client.clone(); @@ -84,6 +94,42 @@ impl ZedAiOnboarding { self } + fn render_accept_terms_of_service(&self) -> AnyElement { + v_flex() + .gap_1() + .w_full() + .child(Headline::new("Accept Terms of Service")) + .child( + Label::new("We don’t sell your data, track you across the web, or compromise your privacy.") + .color(Color::Muted) + .mb_2(), + ) + .child( + Button::new("terms_of_service", "Review Terms of Service") + .full_width() + .style(ButtonStyle::Outlined) + .icon(IconName::ArrowUpRight) + .icon_color(Color::Muted) + .icon_size(IconSize::Small) + .on_click(move |_, _window, cx| { + telemetry::event!("Review Terms of Service Clicked"); + cx.open_url(&zed_urls::terms_of_service(cx)) + }), + ) + .child( + Button::new("accept_terms", "Accept") + .full_width() + .style(ButtonStyle::Tinted(TintColor::Accent)) + .on_click({ + let callback = self.accept_terms_of_service.clone(); + move |_, window, cx| { + telemetry::event!("Terms of Service Accepted"); + (callback)(window, cx)} + }), + ) + .into_any_element() + } + fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement { let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn); let plan_definitions = PlanDefinitions; @@ -313,10 +359,14 @@ impl ZedAiOnboarding { impl RenderOnce for ZedAiOnboarding { fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement { if matches!(self.sign_in_status, SignInStatus::SignedIn) { - match self.plan { - None | Some(Plan::ZedFree) => self.render_free_plan_state(cx), - Some(Plan::ZedProTrial) => self.render_trial_state(cx), - Some(Plan::ZedPro) => self.render_pro_plan_state(cx), + if self.has_accepted_terms_of_service { + match self.plan { + None | Some(Plan::ZedFree) => self.render_free_plan_state(cx), + Some(Plan::ZedProTrial) => self.render_trial_state(cx), + Some(Plan::ZedPro) => self.render_pro_plan_state(cx), + } + } else { + self.render_accept_terms_of_service() } } else { self.render_sign_in_disclaimer(cx) @@ -340,15 +390,18 @@ impl Component for ZedAiOnboarding { fn preview(_window: &mut Window, _cx: &mut App) -> Option { fn onboarding( sign_in_status: SignInStatus, + has_accepted_terms_of_service: bool, plan: Option, account_too_young: bool, ) -> AnyElement { ZedAiOnboarding { sign_in_status, + has_accepted_terms_of_service, plan, account_too_young, continue_with_zed_ai: Arc::new(|_, _| {}), sign_in: Arc::new(|_, _| {}), + accept_terms_of_service: Arc::new(|_, _| {}), dismiss_onboarding: None, } .into_any_element() @@ -362,23 +415,27 @@ impl Component for ZedAiOnboarding { .children(vec![ single_example( "Not Signed-in", - onboarding(SignInStatus::SignedOut, None, false), + onboarding(SignInStatus::SignedOut, false, None, false), + ), + single_example( + "Not Accepted ToS", + onboarding(SignInStatus::SignedIn, false, None, false), ), single_example( "Young Account", - onboarding(SignInStatus::SignedIn, None, true), + onboarding(SignInStatus::SignedIn, true, None, true), ), single_example( "Free Plan", - onboarding(SignInStatus::SignedIn, Some(Plan::ZedFree), false), + onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedFree), false), ), single_example( "Pro Trial", - onboarding(SignInStatus::SignedIn, Some(Plan::ZedProTrial), false), + onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedProTrial), false), ), single_example( "Pro Plan", - onboarding(SignInStatus::SignedIn, Some(Plan::ZedPro), false), + onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedPro), false), ), ]) .into_any_element(), diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs index 106dcb0aef..e9639ca075 100644 --- a/crates/ai_onboarding/src/ai_upsell_card.rs +++ b/crates/ai_onboarding/src/ai_upsell_card.rs @@ -12,11 +12,11 @@ use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions} #[derive(IntoElement, RegisterComponent)] pub struct AiUpsellCard { - sign_in_status: SignInStatus, - sign_in: Arc, - account_too_young: bool, - user_plan: Option, - tab_index: Option, + pub sign_in_status: SignInStatus, + pub sign_in: Arc, + pub account_too_young: bool, + pub user_plan: Option, + pub tab_index: Option, } impl AiUpsellCard { @@ -43,11 +43,6 @@ impl AiUpsellCard { tab_index: None, } } - - pub fn tab_index(mut self, tab_index: Option) -> Self { - self.tab_index = tab_index; - self - } } impl RenderOnce for AiUpsellCard { diff --git a/crates/assistant_tools/src/fetch_tool.rs b/crates/assistant_tools/src/fetch_tool.rs index cc22c9fc09..79e205f205 100644 --- a/crates/assistant_tools/src/fetch_tool.rs +++ b/crates/assistant_tools/src/fetch_tool.rs @@ -118,7 +118,7 @@ impl Tool for FetchTool { } fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool { - true + false } fn may_perform_edits(&self) -> bool { diff --git a/crates/assistant_tools/src/find_path_tool.rs b/crates/assistant_tools/src/find_path_tool.rs index d1451132ae..ac2c7a32ab 100644 --- a/crates/assistant_tools/src/find_path_tool.rs +++ b/crates/assistant_tools/src/find_path_tool.rs @@ -435,8 +435,8 @@ mod test { assert_eq!( matches, &[ - PathBuf::from(path!("root/apple/banana/carrot")), - PathBuf::from(path!("root/apple/bandana/carbonara")) + PathBuf::from("root/apple/banana/carrot"), + PathBuf::from("root/apple/bandana/carbonara") ] ); @@ -447,8 +447,8 @@ mod test { assert_eq!( matches, &[ - PathBuf::from(path!("root/apple/banana/carrot")), - PathBuf::from(path!("root/apple/bandana/carbonara")) + PathBuf::from("root/apple/banana/carrot"), + PathBuf::from("root/apple/bandana/carbonara") ] ); } diff --git a/crates/assistant_tools/src/read_file_tool.rs b/crates/assistant_tools/src/read_file_tool.rs index a6e984fca6..766ee3b161 100644 --- a/crates/assistant_tools/src/read_file_tool.rs +++ b/crates/assistant_tools/src/read_file_tool.rs @@ -68,7 +68,7 @@ impl Tool for ReadFileTool { } fn icon(&self) -> IconName { - IconName::ToolSearch + IconName::ToolRead } fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index ae7eb52fd3..8826b5f49c 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -19,6 +19,10 @@ gpui.workspace = true settings.workspace = true schemars.workspace = true serde.workspace = true +parking_lot.workspace = true rodio = { workspace = true, features = [ "wav", "playback", "tracing" ] } util.workspace = true workspace-hack.workspace = true + +[target.'cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))'.dependencies] +libwebrtc = { rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d", git = "https://github.com/zed-industries/livekit-rust-sdks" } diff --git a/crates/audio/src/audio.rs b/crates/audio/src/audio.rs index b4f2c24fef..a5a4e721d3 100644 --- a/crates/audio/src/audio.rs +++ b/crates/audio/src/audio.rs @@ -1,12 +1,15 @@ use anyhow::{Context as _, Result, anyhow}; use collections::HashMap; use gpui::{App, BorrowAppContext, Global}; -use rodio::{Decoder, OutputStream, OutputStreamBuilder, Source, source::Buffered}; +use libwebrtc::native::apm; +use parking_lot::Mutex; +use rodio::{Decoder, OutputStream, OutputStreamBuilder, Source, mixer::Mixer, source::Buffered}; use settings::Settings; -use std::io::Cursor; +use std::{io::Cursor, sync::Arc}; use util::ResultExt; mod audio_settings; +mod rodio_ext; pub use audio_settings::AudioSettings; pub fn init(cx: &mut App) { @@ -38,18 +41,46 @@ impl Sound { } } -#[derive(Default)] pub struct Audio { output_handle: Option, + output_mixer: Option, + echo_canceller: Arc>, source_cache: HashMap>>>>, } +impl Default for Audio { + fn default() -> Self { + Self { + output_handle: Default::default(), + output_mixer: Default::default(), + echo_canceller: Arc::new(Mutex::new(apm::AudioProcessingModule::new( + true, false, false, false, + ))), + source_cache: Default::default(), + } + } +} + impl Global for Audio {} impl Audio { fn ensure_output_exists(&mut self) -> Option<&OutputStream> { if self.output_handle.is_none() { self.output_handle = OutputStreamBuilder::open_default_stream().log_err(); + if let Some(output_handle) = self.output_handle { + let config = output_handle.config(); + let (mixer, source) = + rodio::mixer::mixer(config.channel_count(), config.sample_rate()); + self.output_mixer = Some(mixer); + + let echo_canceller = Arc::clone(&self.echo_canceller); + let source = source.inspect_buffered( + |buffer| echo_canceller.lock().process_reverse_stream(&mut buf), + config.sample_rate().get() as i32, + config.channel_count().get().into(), + ); + output_handle.mixer().add(source); + } } self.output_handle.as_ref() @@ -72,6 +103,7 @@ impl Audio { cx.update_default_global(|this: &mut Self, cx| { let source = this.sound_source(sound, cx).log_err()?; let output_handle = this.ensure_output_exists()?; + output_handle.mixer().add(source); Some(()) }); diff --git a/crates/audio/src/rodio_ext.rs b/crates/audio/src/rodio_ext.rs new file mode 100644 index 0000000000..055ff9003a --- /dev/null +++ b/crates/audio/src/rodio_ext.rs @@ -0,0 +1,93 @@ +use rodio::Source; + +pub trait RodioExt: Source + Sized { + fn process_buffer(self, callback: F) -> ProcessBuffer + where + F: FnMut(&mut [rodio::Sample; N]); + fn inspect_buffer(self, callback: F) -> ProcessBuffer + where + F: FnMut(&[rodio::Sample; N]); +} + +impl RodioExt for S { + fn process_buffer(self, callback: F) -> ProcessBuffer + where + F: FnMut(&mut [rodio::Sample; N]), + { + ProcessBuffer { + inner: self, + callback, + buffer: [0.0; N], + next: N, + } + } + fn inspect_buffer(self, callback: F) -> ProcessBuffer + where + F: FnMut(&[rodio::Sample; N]), + { + InspectBuffer { + inner: self, + callback, + buffer: [0.0; N], + next: N, + } + } +} + +pub struct ProcessBuffer +where + S: Source + Sized, + F: FnMut(&mut [rodio::Sample; N]), +{ + inner: S, + callback: F, + buffer: [rodio::Sample; N], + next: usize, +} + +impl Iterator for ProcessBuffer +where + S: Source + Sized, + F: FnMut(&mut [rodio::Sample; N]), +{ + type Item = rodio::Sample; + + fn next(&mut self) -> Option { + self.next += 1; + if self.next < self.buffer.len() { + let sample = self.buffer[self.next]; + return Some(sample); + } + + for sample in &mut self.buffer { + *sample = self.inner.next()? + } + (self.callback)(&mut self.buffer); + + self.next = 0; + Some(self.buffer[0]) + } +} + +impl Source for ProcessBuffer +where + S: Source + Sized, + F: FnMut(&mut [rodio::Sample; N]), +{ + fn current_span_len(&self) -> Option { + // TODO dvdsk this should be a spanless Source + None + } + + fn channels(&self) -> rodio::ChannelCount { + self.inner.channels() + } + + fn sample_rate(&self) -> rodio::SampleRate { + self.inner.sample_rate() + } + + fn total_duration(&self) -> Option { + self.inner.total_duration() + } +} diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 2bbe7dd1b5..f9b8a10610 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -66,8 +66,6 @@ pub static IMPERSONATE_LOGIN: LazyLock> = LazyLock::new(|| { .and_then(|s| if s.is_empty() { None } else { Some(s) }) }); -pub static USE_WEB_LOGIN: LazyLock = LazyLock::new(|| std::env::var("ZED_WEB_LOGIN").is_ok()); - pub static ADMIN_API_TOKEN: LazyLock> = LazyLock::new(|| { std::env::var("ZED_ADMIN_API_TOKEN") .ok() @@ -1394,13 +1392,11 @@ impl Client { if let Some((login, token)) = IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) { - if !*USE_WEB_LOGIN { - eprintln!("authenticate as admin {login}, {token}"); + eprintln!("authenticate as admin {login}, {token}"); - return this - .authenticate_as_admin(http, login.clone(), token.clone()) - .await; - } + return this + .authenticate_as_admin(http, login.clone(), token.clone()) + .await; } // Start an HTTP server to receive the redirect from Zed's sign-in page. diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index d23eb37519..20f99e3944 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,5 +1,5 @@ use super::{Client, Status, TypedEnvelope, proto}; -use anyhow::{Context as _, Result}; +use anyhow::{Context as _, Result, anyhow}; use chrono::{DateTime, Utc}; use cloud_api_client::websocket_protocol::MessageToClient; use cloud_api_client::{GetAuthenticatedUserResponse, PlanInfo}; @@ -46,6 +46,11 @@ impl ProjectId { } } +#[derive( + Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize, +)] +pub struct DevServerProjectId(pub u64); + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ParticipantIndex(pub u32); @@ -111,6 +116,7 @@ pub struct UserStore { edit_prediction_usage: Option, plan_info: Option, current_user: watch::Receiver>>, + accepted_tos_at: Option>, contacts: Vec>, incoming_contact_requests: Vec>, outgoing_contact_requests: Vec>, @@ -188,6 +194,7 @@ impl UserStore { plan_info: None, model_request_usage: None, edit_prediction_usage: None, + accepted_tos_at: None, contacts: Default::default(), incoming_contact_requests: Default::default(), participant_indices: Default::default(), @@ -264,6 +271,7 @@ impl UserStore { Status::SignedOut => { current_user_tx.send(None).await.ok(); this.update(cx, |this, cx| { + this.accepted_tos_at = None; cx.emit(Event::PrivateUserInfoUpdated); cx.notify(); this.clear_contacts() @@ -783,6 +791,19 @@ impl UserStore { .set_authenticated_user_info(Some(response.user.metrics_id.clone()), staff); } + let accepted_tos_at = { + #[cfg(debug_assertions)] + if std::env::var("ZED_IGNORE_ACCEPTED_TOS").is_ok() { + None + } else { + response.user.accepted_tos_at + } + + #[cfg(not(debug_assertions))] + response.user.accepted_tos_at + }; + + self.accepted_tos_at = Some(accepted_tos_at); self.model_request_usage = Some(ModelRequestUsage(RequestUsage { limit: response.plan.usage.model_requests.limit, amount: response.plan.usage.model_requests.used as i32, @@ -825,6 +846,32 @@ impl UserStore { self.current_user.clone() } + pub fn has_accepted_terms_of_service(&self) -> bool { + self.accepted_tos_at + .is_some_and(|accepted_tos_at| accepted_tos_at.is_some()) + } + + pub fn accept_terms_of_service(&self, cx: &Context) -> Task> { + if self.current_user().is_none() { + return Task::ready(Err(anyhow!("no current user"))); + }; + + let client = self.client.clone(); + cx.spawn(async move |this, cx| -> anyhow::Result<()> { + let client = client.upgrade().context("client not found")?; + let response = client + .cloud_client() + .accept_terms_of_service() + .await + .context("error accepting tos")?; + this.update(cx, |this, cx| { + this.accepted_tos_at = Some(response.user.accepted_tos_at); + cx.emit(Event::PrivateUserInfoUpdated); + })?; + Ok(()) + }) + } + fn load_users( &self, request: impl RequestMessage, diff --git a/crates/client/src/zed_urls.rs b/crates/client/src/zed_urls.rs index 7193c09947..9df41906d7 100644 --- a/crates/client/src/zed_urls.rs +++ b/crates/client/src/zed_urls.rs @@ -43,11 +43,3 @@ pub fn ai_privacy_and_security(cx: &App) -> String { server_url = server_url(cx) ) } - -/// Returns the URL to Zed AI's external agents documentation. -pub fn external_agents_docs(cx: &App) -> String { - format!( - "{server_url}/docs/ai/external-agents", - server_url = server_url(cx) - ) -} diff --git a/crates/cloud_api_client/src/cloud_api_client.rs b/crates/cloud_api_client/src/cloud_api_client.rs index 7fd96fcef0..205f3e2432 100644 --- a/crates/cloud_api_client/src/cloud_api_client.rs +++ b/crates/cloud_api_client/src/cloud_api_client.rs @@ -115,6 +115,34 @@ impl CloudApiClient { })) } + pub async fn accept_terms_of_service(&self) -> Result { + let request = self.build_request( + Request::builder().method(Method::POST).uri( + self.http_client + .build_zed_cloud_url("/client/terms_of_service/accept", &[])? + .as_ref(), + ), + AsyncBody::default(), + )?; + + let mut response = self.http_client.send(request).await?; + + if !response.status().is_success() { + let mut body = String::new(); + response.body_mut().read_to_string(&mut body).await?; + + anyhow::bail!( + "Failed to accept terms of service.\nStatus: {:?}\nBody: {body}", + response.status() + ) + } + + let mut body = String::new(); + response.body_mut().read_to_string(&mut body).await?; + + Ok(serde_json::from_str(&body)?) + } + pub async fn create_llm_token( &self, system_id: Option, diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 1e0c915bcb..d9fd8ffeb2 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -970,7 +970,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T // the follow. workspace_b.update_in(cx_b, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_previous_item(&Default::default(), window, cx); + pane.activate_prev_item(true, window, cx); }); }); executor.run_until_parked(); @@ -1073,7 +1073,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T // Client A cycles through some tabs. workspace_a.update_in(cx_a, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_previous_item(&Default::default(), window, cx); + pane.activate_prev_item(true, window, cx); }); }); executor.run_until_parked(); @@ -1117,7 +1117,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T workspace_a.update_in(cx_a, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_previous_item(&Default::default(), window, cx); + pane.activate_prev_item(true, window, cx); }); }); executor.run_until_parked(); @@ -1164,7 +1164,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T workspace_a.update_in(cx_a, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_previous_item(&Default::default(), window, cx); + pane.activate_prev_item(true, window, cx); }); }); executor.run_until_parked(); diff --git a/crates/command_palette/src/persistence.rs b/crates/command_palette/src/persistence.rs index 01cf403083..5be97c36bc 100644 --- a/crates/command_palette/src/persistence.rs +++ b/crates/command_palette/src/persistence.rs @@ -1,10 +1,7 @@ use anyhow::Result; use db::{ - query, - sqlez::{ - bindable::Column, domain::Domain, statement::Statement, - thread_safe_connection::ThreadSafeConnection, - }, + define_connection, query, + sqlez::{bindable::Column, statement::Statement}, sqlez_macros::sql, }; use serde::{Deserialize, Serialize}; @@ -53,11 +50,8 @@ impl Column for SerializedCommandInvocation { } } -pub struct CommandPaletteDB(ThreadSafeConnection); - -impl Domain for CommandPaletteDB { - const NAME: &str = stringify!(CommandPaletteDB); - const MIGRATIONS: &[&str] = &[sql!( +define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()> = + &[sql!( CREATE TABLE IF NOT EXISTS command_invocations( id INTEGER PRIMARY KEY AUTOINCREMENT, command_name TEXT NOT NULL, @@ -65,9 +59,7 @@ impl Domain for CommandPaletteDB { last_invoked INTEGER DEFAULT (unixepoch()) NOT NULL ) STRICT; )]; -} - -db::static_connection!(COMMAND_PALETTE_HISTORY, CommandPaletteDB, []); +); impl CommandPaletteDB { pub async fn write_command_invocation( diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index 0802bd8bb7..8b790cbec8 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -110,14 +110,11 @@ pub async fn open_test_db(db_name: &str) -> ThreadSafeConnection { } /// Implements a basic DB wrapper for a given domain -/// -/// Arguments: -/// - static variable name for connection -/// - type of connection wrapper -/// - dependencies, whose migrations should be run prior to this domain's migrations #[macro_export] -macro_rules! static_connection { - ($id:ident, $t:ident, [ $($d:ty),* ] $(, $global:ident)?) => { +macro_rules! define_connection { + (pub static ref $id:ident: $t:ident<()> = $migrations:expr; $($global:ident)?) => { + pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection); + impl ::std::ops::Deref for $t { type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection; @@ -126,6 +123,16 @@ macro_rules! static_connection { } } + impl $crate::sqlez::domain::Domain for $t { + fn name() -> &'static str { + stringify!($t) + } + + fn migrations() -> &'static [&'static str] { + $migrations + } + } + impl $t { #[cfg(any(test, feature = "test-support"))] pub async fn open_test_db(name: &'static str) -> Self { @@ -135,8 +142,7 @@ macro_rules! static_connection { #[cfg(any(test, feature = "test-support"))] pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { - #[allow(unused_parens)] - $t($crate::smol::block_on($crate::open_test_db::<($($d,)* $t)>(stringify!($id)))) + $t($crate::smol::block_on($crate::open_test_db::<$t>(stringify!($id)))) }); #[cfg(not(any(test, feature = "test-support")))] @@ -147,10 +153,46 @@ macro_rules! static_connection { } else { $crate::RELEASE_CHANNEL.dev_name() }; - #[allow(unused_parens)] - $t($crate::smol::block_on($crate::open_db::<($($d,)* $t)>(db_dir, scope))) + $t($crate::smol::block_on($crate::open_db::<$t>(db_dir, scope))) }); - } + }; + (pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr; $($global:ident)?) => { + pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection); + + impl ::std::ops::Deref for $t { + type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl $crate::sqlez::domain::Domain for $t { + fn name() -> &'static str { + stringify!($t) + } + + fn migrations() -> &'static [&'static str] { + $migrations + } + } + + #[cfg(any(test, feature = "test-support"))] + pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { + $t($crate::smol::block_on($crate::open_test_db::<($($d),+, $t)>(stringify!($id)))) + }); + + #[cfg(not(any(test, feature = "test-support")))] + pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { + let db_dir = $crate::database_dir(); + let scope = if false $(|| stringify!($global) == "global")? { + "global" + } else { + $crate::RELEASE_CHANNEL.dev_name() + }; + $t($crate::smol::block_on($crate::open_db::<($($d),+, $t)>(db_dir, scope))) + }); + }; } pub fn write_and_log(cx: &App, db_write: impl FnOnce() -> F + Send + 'static) @@ -177,12 +219,17 @@ mod tests { enum BadDB {} impl Domain for BadDB { - const NAME: &str = "db_tests"; - const MIGRATIONS: &[&str] = &[ - sql!(CREATE TABLE test(value);), - // failure because test already exists - sql!(CREATE TABLE test(value);), - ]; + fn name() -> &'static str { + "db_tests" + } + + fn migrations() -> &'static [&'static str] { + &[ + sql!(CREATE TABLE test(value);), + // failure because test already exists + sql!(CREATE TABLE test(value);), + ] + } } let tempdir = tempfile::Builder::new() @@ -204,15 +251,25 @@ mod tests { enum CorruptedDB {} impl Domain for CorruptedDB { - const NAME: &str = "db_tests"; - const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)]; + fn name() -> &'static str { + "db_tests" + } + + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test(value);)] + } } enum GoodDB {} impl Domain for GoodDB { - const NAME: &str = "db_tests"; //Notice same name - const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; + fn name() -> &'static str { + "db_tests" //Notice same name + } + + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test2(value);)] //But different migration + } } let tempdir = tempfile::Builder::new() @@ -248,16 +305,25 @@ mod tests { enum CorruptedDB {} impl Domain for CorruptedDB { - const NAME: &str = "db_tests"; + fn name() -> &'static str { + "db_tests" + } - const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)]; + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test(value);)] + } } enum GoodDB {} impl Domain for GoodDB { - const NAME: &str = "db_tests"; //Notice same name - const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; // But different migration + fn name() -> &'static str { + "db_tests" //Notice same name + } + + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test2(value);)] //But different migration + } } let tempdir = tempfile::Builder::new() diff --git a/crates/db/src/kvp.rs b/crates/db/src/kvp.rs index 8ea877b35b..256b789c9b 100644 --- a/crates/db/src/kvp.rs +++ b/crates/db/src/kvp.rs @@ -2,26 +2,16 @@ use gpui::App; use sqlez_macros::sql; use util::ResultExt as _; -use crate::{ - query, - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - write_and_log, -}; +use crate::{define_connection, query, write_and_log}; -pub struct KeyValueStore(crate::sqlez::thread_safe_connection::ThreadSafeConnection); - -impl Domain for KeyValueStore { - const NAME: &str = stringify!(KeyValueStore); - - const MIGRATIONS: &[&str] = &[sql!( +define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> = + &[sql!( CREATE TABLE IF NOT EXISTS kv_store( key TEXT PRIMARY KEY, value TEXT NOT NULL ) STRICT; )]; -} - -crate::static_connection!(KEY_VALUE_STORE, KeyValueStore, []); +); pub trait Dismissable { const KEY: &'static str; @@ -101,19 +91,15 @@ mod tests { } } -pub struct GlobalKeyValueStore(ThreadSafeConnection); - -impl Domain for GlobalKeyValueStore { - const NAME: &str = stringify!(GlobalKeyValueStore); - const MIGRATIONS: &[&str] = &[sql!( +define_connection!(pub static ref GLOBAL_KEY_VALUE_STORE: GlobalKeyValueStore<()> = + &[sql!( CREATE TABLE IF NOT EXISTS kv_store( key TEXT PRIMARY KEY, value TEXT NOT NULL ) STRICT; )]; -} - -crate::static_connection!(GLOBAL_KEY_VALUE_STORE, GlobalKeyValueStore, [], global); + global +); impl GlobalKeyValueStore { query! { diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index fd678078e8..53b5792e10 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -18,6 +18,7 @@ collections.workspace = true component.workspace = true ctor.workspace = true editor.workspace = true +futures.workspace = true gpui.workspace = true indoc.workspace = true language.workspace = true diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 1c27e820a0..2e20118381 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -13,6 +13,7 @@ use editor::{ DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey, display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId}, }; +use futures::future::join_all; use gpui::{ AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable, Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, @@ -23,6 +24,7 @@ use language::{ }; use project::{ DiagnosticSummary, Project, ProjectPath, + lsp_store::rust_analyzer_ext::{cancel_flycheck, run_flycheck}, project_settings::{DiagnosticSeverity, ProjectSettings}, }; use settings::Settings; @@ -77,10 +79,17 @@ pub(crate) struct ProjectDiagnosticsEditor { paths_to_update: BTreeSet, include_warnings: bool, update_excerpts_task: Option>>, + cargo_diagnostics_fetch: CargoDiagnosticsFetchState, diagnostic_summary_update: Task<()>, _subscription: Subscription, } +struct CargoDiagnosticsFetchState { + fetch_task: Option>, + cancel_task: Option>, + diagnostic_sources: Arc>, +} + impl EventEmitter for ProjectDiagnosticsEditor {} const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50); @@ -251,7 +260,11 @@ impl ProjectDiagnosticsEditor { ) }); this.diagnostics.clear(); - this.update_all_excerpts(window, cx); + this.update_all_diagnostics(false, window, cx); + }) + .detach(); + cx.observe_release(&cx.entity(), |editor, _, cx| { + editor.stop_cargo_diagnostics_fetch(cx); }) .detach(); @@ -268,10 +281,15 @@ impl ProjectDiagnosticsEditor { editor, paths_to_update: Default::default(), update_excerpts_task: None, + cargo_diagnostics_fetch: CargoDiagnosticsFetchState { + fetch_task: None, + cancel_task: None, + diagnostic_sources: Arc::new(Vec::new()), + }, diagnostic_summary_update: Task::ready(()), _subscription: project_event_subscription, }; - this.update_all_excerpts(window, cx); + this.update_all_diagnostics(true, window, cx); this } @@ -355,10 +373,20 @@ impl ProjectDiagnosticsEditor { window: &mut Window, cx: &mut Context, ) { - if self.update_excerpts_task.is_some() { + let fetch_cargo_diagnostics = ProjectSettings::get_global(cx) + .diagnostics + .fetch_cargo_diagnostics(); + + if fetch_cargo_diagnostics { + if self.cargo_diagnostics_fetch.fetch_task.is_some() { + self.stop_cargo_diagnostics_fetch(cx); + } else { + self.update_all_diagnostics(false, window, cx); + } + } else if self.update_excerpts_task.is_some() { self.update_excerpts_task = None; } else { - self.update_all_excerpts(window, cx); + self.update_all_diagnostics(false, window, cx); } cx.notify(); } @@ -376,6 +404,73 @@ impl ProjectDiagnosticsEditor { } } + fn update_all_diagnostics( + &mut self, + first_launch: bool, + window: &mut Window, + cx: &mut Context, + ) { + let cargo_diagnostics_sources = self.cargo_diagnostics_sources(cx); + if cargo_diagnostics_sources.is_empty() { + self.update_all_excerpts(window, cx); + } else if first_launch && !self.summary.is_empty() { + self.update_all_excerpts(window, cx); + } else { + self.fetch_cargo_diagnostics(Arc::new(cargo_diagnostics_sources), cx); + } + } + + fn fetch_cargo_diagnostics( + &mut self, + diagnostics_sources: Arc>, + cx: &mut Context, + ) { + let project = self.project.clone(); + self.cargo_diagnostics_fetch.cancel_task = None; + self.cargo_diagnostics_fetch.fetch_task = None; + self.cargo_diagnostics_fetch.diagnostic_sources = diagnostics_sources.clone(); + if self.cargo_diagnostics_fetch.diagnostic_sources.is_empty() { + return; + } + + self.cargo_diagnostics_fetch.fetch_task = Some(cx.spawn(async move |editor, cx| { + let mut fetch_tasks = Vec::new(); + for buffer_path in diagnostics_sources.iter().cloned() { + if cx + .update(|cx| { + fetch_tasks.push(run_flycheck(project.clone(), buffer_path, cx)); + }) + .is_err() + { + break; + } + } + + let _ = join_all(fetch_tasks).await; + editor + .update(cx, |editor, _| { + editor.cargo_diagnostics_fetch.fetch_task = None; + }) + .ok(); + })); + } + + fn stop_cargo_diagnostics_fetch(&mut self, cx: &mut App) { + self.cargo_diagnostics_fetch.fetch_task = None; + let mut cancel_gasks = Vec::new(); + for buffer_path in std::mem::take(&mut self.cargo_diagnostics_fetch.diagnostic_sources) + .iter() + .cloned() + { + cancel_gasks.push(cancel_flycheck(self.project.clone(), buffer_path, cx)); + } + + self.cargo_diagnostics_fetch.cancel_task = Some(cx.background_spawn(async move { + let _ = join_all(cancel_gasks).await; + log::info!("Finished fetching cargo diagnostics"); + })); + } + /// Enqueue an update of all excerpts. Updates all paths that either /// currently have diagnostics or are currently present in this view. fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context) { @@ -600,6 +695,30 @@ impl ProjectDiagnosticsEditor { }) }) } + + pub fn cargo_diagnostics_sources(&self, cx: &App) -> Vec { + let fetch_cargo_diagnostics = ProjectSettings::get_global(cx) + .diagnostics + .fetch_cargo_diagnostics(); + if !fetch_cargo_diagnostics { + return Vec::new(); + } + self.project + .read(cx) + .worktrees(cx) + .filter_map(|worktree| { + let _cargo_toml_entry = worktree.read(cx).entry_for_path("Cargo.toml")?; + let rust_file_entry = worktree.read(cx).entries(false, 0).find(|entry| { + entry + .path + .extension() + .and_then(|extension| extension.to_str()) + == Some("rs") + })?; + self.project.read(cx).path_for_entry(rust_file_entry.id, cx) + }) + .collect() + } } impl Focusable for ProjectDiagnosticsEditor { diff --git a/crates/diagnostics/src/toolbar_controls.rs b/crates/diagnostics/src/toolbar_controls.rs index 404db39164..e77b80115f 100644 --- a/crates/diagnostics/src/toolbar_controls.rs +++ b/crates/diagnostics/src/toolbar_controls.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use crate::{ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh}; use gpui::{Context, Entity, EventEmitter, ParentElement, Render, WeakEntity, Window}; use ui::prelude::*; @@ -13,18 +15,26 @@ impl Render for ToolbarControls { let mut include_warnings = false; let mut has_stale_excerpts = false; let mut is_updating = false; + let cargo_diagnostics_sources = Arc::new(self.diagnostics().map_or(Vec::new(), |editor| { + editor.read(cx).cargo_diagnostics_sources(cx) + })); + let fetch_cargo_diagnostics = !cargo_diagnostics_sources.is_empty(); if let Some(editor) = self.diagnostics() { let diagnostics = editor.read(cx); include_warnings = diagnostics.include_warnings; has_stale_excerpts = !diagnostics.paths_to_update.is_empty(); - is_updating = diagnostics.update_excerpts_task.is_some() - || diagnostics - .project - .read(cx) - .language_servers_running_disk_based_diagnostics(cx) - .next() - .is_some(); + is_updating = if fetch_cargo_diagnostics { + diagnostics.cargo_diagnostics_fetch.fetch_task.is_some() + } else { + diagnostics.update_excerpts_task.is_some() + || diagnostics + .project + .read(cx) + .language_servers_running_disk_based_diagnostics(cx) + .next() + .is_some() + }; } let tooltip = if include_warnings { @@ -54,6 +64,7 @@ impl Render for ToolbarControls { .on_click(cx.listener(move |toolbar_controls, _, _, cx| { if let Some(diagnostics) = toolbar_controls.diagnostics() { diagnostics.update(cx, |diagnostics, cx| { + diagnostics.stop_cargo_diagnostics_fetch(cx); diagnostics.update_excerpts_task = None; cx.notify(); }); @@ -65,7 +76,7 @@ impl Render for ToolbarControls { IconButton::new("refresh-diagnostics", IconName::ArrowCircle) .icon_color(Color::Info) .shape(IconButtonShape::Square) - .disabled(!has_stale_excerpts) + .disabled(!has_stale_excerpts && !fetch_cargo_diagnostics) .tooltip(Tooltip::for_action_title( "Refresh diagnostics", &ToggleDiagnosticsRefresh, @@ -73,8 +84,17 @@ impl Render for ToolbarControls { .on_click(cx.listener({ move |toolbar_controls, _, window, cx| { if let Some(diagnostics) = toolbar_controls.diagnostics() { + let cargo_diagnostics_sources = + Arc::clone(&cargo_diagnostics_sources); diagnostics.update(cx, move |diagnostics, cx| { - diagnostics.update_all_excerpts(window, cx); + if fetch_cargo_diagnostics { + diagnostics.fetch_cargo_diagnostics( + cargo_diagnostics_sources, + cx, + ); + } else { + diagnostics.update_all_excerpts(window, cx); + } }); } } diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index c8c3dc54b7..c900eb692a 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -19,10 +19,6 @@ static KEYMAP_LINUX: LazyLock = LazyLock::new(|| { load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap") }); -static KEYMAP_WINDOWS: LazyLock = LazyLock::new(|| { - load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap") -}); - static ALL_ACTIONS: LazyLock> = LazyLock::new(dump_all_gpui_actions); const FRONT_MATTER_COMMENT: &str = ""; @@ -220,7 +216,6 @@ fn find_binding(os: &str, action: &str) -> Option { let keymap = match os { "macos" => &KEYMAP_MACOS, "linux" | "freebsd" => &KEYMAP_LINUX, - "windows" => &KEYMAP_WINDOWS, _ => unreachable!("Not a valid OS: {}", os), }; diff --git a/crates/edit_prediction/src/edit_prediction.rs b/crates/edit_prediction/src/edit_prediction.rs index 6b695af1ae..964f202934 100644 --- a/crates/edit_prediction/src/edit_prediction.rs +++ b/crates/edit_prediction/src/edit_prediction.rs @@ -89,6 +89,9 @@ pub trait EditPredictionProvider: 'static + Sized { debounce: bool, cx: &mut Context, ); + fn needs_terms_acceptance(&self, _cx: &App) -> bool { + false + } fn cycle( &mut self, buffer: Entity, @@ -121,6 +124,7 @@ pub trait EditPredictionProviderHandle { fn data_collection_state(&self, cx: &App) -> DataCollectionState; fn usage(&self, cx: &App) -> Option; fn toggle_data_collection(&self, cx: &mut App); + fn needs_terms_acceptance(&self, cx: &App) -> bool; fn is_refreshing(&self, cx: &App) -> bool; fn refresh( &self, @@ -192,6 +196,10 @@ where self.read(cx).is_enabled(buffer, cursor_position, cx) } + fn needs_terms_acceptance(&self, cx: &App) -> bool { + self.read(cx).needs_terms_acceptance(cx) + } + fn is_refreshing(&self, cx: &App) -> bool { self.read(cx).is_refreshing() } diff --git a/crates/edit_prediction_button/src/edit_prediction_button.rs b/crates/edit_prediction_button/src/edit_prediction_button.rs index 0e3fe8cb1a..4f69af7ee4 100644 --- a/crates/edit_prediction_button/src/edit_prediction_button.rs +++ b/crates/edit_prediction_button/src/edit_prediction_button.rs @@ -242,9 +242,13 @@ impl Render for EditPredictionButton { IconName::ZedPredictDisabled }; - if zeta::should_show_upsell_modal() { + if zeta::should_show_upsell_modal(&self.user_store, cx) { let tooltip_meta = if self.user_store.read(cx).current_user().is_some() { - "Choose a Plan" + if self.user_store.read(cx).has_accepted_terms_of_service() { + "Choose a Plan" + } else { + "Accept the Terms of Service" + } } else { "Sign In" }; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 80680ae9c0..a59eb930c3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -253,6 +253,7 @@ pub type RenderDiffHunkControlsFn = Arc< enum ReportEditorEvent { Saved { auto_saved: bool }, EditorOpened, + ZetaTosClicked, Closed, } @@ -261,6 +262,7 @@ impl ReportEditorEvent { match self { Self::Saved { .. } => "Editor Saved", Self::EditorOpened => "Editor Opened", + Self::ZetaTosClicked => "Edit Prediction Provider ToS Clicked", Self::Closed => "Editor Closed", } } @@ -2588,7 +2590,7 @@ impl Editor { || binding .keystrokes() .first() - .is_some_and(|keystroke| keystroke.display_modifiers.modified()) + .is_some_and(|keystroke| keystroke.modifiers.modified()) })) } @@ -7686,16 +7688,16 @@ impl Editor { .keystroke() { modifiers_held = modifiers_held - || (&accept_keystroke.display_modifiers == modifiers - && accept_keystroke.display_modifiers.modified()); + || (&accept_keystroke.modifiers == modifiers + && accept_keystroke.modifiers.modified()); }; if let Some(accept_partial_keystroke) = self .accept_edit_prediction_keybind(true, window, cx) .keystroke() { modifiers_held = modifiers_held - || (&accept_partial_keystroke.display_modifiers == modifiers - && accept_partial_keystroke.display_modifiers.modified()); + || (&accept_partial_keystroke.modifiers == modifiers + && accept_partial_keystroke.modifiers.modified()); } if modifiers_held { @@ -9044,7 +9046,7 @@ impl Editor { let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac; - let modifiers_color = if accept_keystroke.display_modifiers == window.modifiers() { + let modifiers_color = if accept_keystroke.modifiers == window.modifiers() { Color::Accent } else { Color::Muted @@ -9056,19 +9058,19 @@ impl Editor { .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) .text_size(TextSize::XSmall.rems(cx)) .child(h_flex().children(ui::render_modifiers( - &accept_keystroke.display_modifiers, + &accept_keystroke.modifiers, PlatformStyle::platform(), Some(modifiers_color), Some(IconSize::XSmall.rems().into()), true, ))) .when(is_platform_style_mac, |parent| { - parent.child(accept_keystroke.display_key.clone()) + parent.child(accept_keystroke.key.clone()) }) .when(!is_platform_style_mac, |parent| { parent.child( Key::new( - util::capitalize(&accept_keystroke.display_key), + util::capitalize(&accept_keystroke.key), Some(Color::Default), ) .size(Some(IconSize::XSmall.rems().into())), @@ -9171,13 +9173,52 @@ impl Editor { max_width: Pixels, cursor_point: Point, style: &EditorStyle, - accept_keystroke: Option<&gpui::KeybindingKeystroke>, + accept_keystroke: Option<&gpui::Keystroke>, _window: &Window, cx: &mut Context, ) -> Option { let provider = self.edit_prediction_provider.as_ref()?; let provider_icon = Self::get_prediction_provider_icon_name(&self.edit_prediction_provider); + if provider.provider.needs_terms_acceptance(cx) { + return Some( + h_flex() + .min_w(min_width) + .flex_1() + .px_2() + .py_1() + .gap_3() + .elevation_2(cx) + .hover(|style| style.bg(cx.theme().colors().element_hover)) + .id("accept-terms") + .cursor_pointer() + .on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default()) + .on_click(cx.listener(|this, _event, window, cx| { + cx.stop_propagation(); + this.report_editor_event(ReportEditorEvent::ZetaTosClicked, None, cx); + window.dispatch_action( + zed_actions::OpenZedPredictOnboarding.boxed_clone(), + cx, + ); + })) + .child( + h_flex() + .flex_1() + .gap_2() + .child(Icon::new(provider_icon)) + .child(Label::new("Accept Terms of Service")) + .child(div().w_full()) + .child( + Icon::new(IconName::ArrowUpRight) + .color(Color::Muted) + .size(IconSize::Small), + ) + .into_any_element(), + ) + .into_any(), + ); + } + let is_refreshing = provider.provider.is_refreshing(cx); fn pending_completion_container(icon: IconName) -> Div { @@ -9249,7 +9290,7 @@ impl Editor { accept_keystroke.as_ref(), |el, accept_keystroke| { el.child(h_flex().children(ui::render_modifiers( - &accept_keystroke.display_modifiers, + &accept_keystroke.modifiers, PlatformStyle::platform(), Some(Color::Default), Some(IconSize::XSmall.rems().into()), @@ -9319,7 +9360,7 @@ impl Editor { .child(completion), ) .when_some(accept_keystroke, |el, accept_keystroke| { - if !accept_keystroke.display_modifiers.modified() { + if !accept_keystroke.modifiers.modified() { return el; } @@ -9338,7 +9379,7 @@ impl Editor { .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) .when(is_platform_style_mac, |parent| parent.gap_1()) .child(h_flex().children(ui::render_modifiers( - &accept_keystroke.display_modifiers, + &accept_keystroke.modifiers, PlatformStyle::platform(), Some(if !has_completion { Color::Muted diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 2cfdb92593..5b854e3a97 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -57,9 +57,7 @@ use util::{ use workspace::{ CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry, OpenOptions, ViewId, - invalid_buffer_view::InvalidBufferView, item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions}, - register_project_item, }; #[gpui::test] @@ -22715,7 +22713,7 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) { .await .unwrap(); pane.update_in(cx, |pane, window, cx| { - pane.navigate_backward(&Default::default(), window, cx); + pane.navigate_backward(window, cx); }); cx.run_until_parked(); pane.update(cx, |pane, cx| { @@ -24302,7 +24300,7 @@ async fn test_document_colors(cx: &mut TestAppContext) { workspace .update(cx, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.navigate_backward(&Default::default(), window, cx); + pane.navigate_backward(window, cx); }) }) .unwrap(); @@ -24350,41 +24348,6 @@ async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) { }); } -#[gpui::test] -async fn test_non_utf_8_opens(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - cx.update(|cx| { - register_project_item::(cx); - }); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/root1", json!({})).await; - fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd]) - .await; - - let project = Project::test(fs, ["/root1".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let worktree_id = project.update(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }); - - let handle = workspace - .update_in(cx, |workspace, window, cx| { - let project_path = (worktree_id, "one.pdf"); - workspace.open_path(project_path, None, true, window, cx) - }) - .await - .unwrap(); - - assert_eq!( - handle.to_any().entity_type(), - TypeId::of::() - ); -} - #[track_caller] fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec { editor diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 91034829f7..797b0d6634 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -43,10 +43,10 @@ use gpui::{ Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, - KeybindingKeystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, - MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, - ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, - Style, Styled, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, + Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent, + MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle, + ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, + TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background, transparent_black, }; @@ -74,7 +74,6 @@ use std::{ fmt::{self, Write}, iter, mem, ops::{Deref, Range}, - path::{self, Path}, rc::Rc, sync::Arc, time::{Duration, Instant}, @@ -90,8 +89,8 @@ use unicode_segmentation::UnicodeSegmentation; use util::post_inc; use util::{RangeExt, ResultExt, debug_panic}; use workspace::{ - CollaboratorId, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, - item::Item, notifications::NotifyTaskExt, + CollaboratorId, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, item::Item, + notifications::NotifyTaskExt, }; /// Determines what kinds of highlights should be applied to a lines background. @@ -3603,187 +3602,171 @@ impl EditorElement { let focus_handle = editor.focus_handle(cx); let colors = cx.theme().colors(); - let header = div() - .p_1() - .w_full() - .h(FILE_HEADER_HEIGHT as f32 * window.line_height()) - .child( - h_flex() - .size_full() - .gap_2() - .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) - .pl_0p5() - .pr_5() - .rounded_sm() - .when(is_sticky, |el| el.shadow_md()) - .border_1() - .map(|div| { - let border_color = if is_selected - && is_folded - && focus_handle.contains_focused(window, cx) - { - colors.border_focused - } else { - colors.border - }; - div.border_color(border_color) - }) - .bg(colors.editor_subheader_background) - .hover(|style| style.bg(colors.element_hover)) - .map(|header| { - let editor = self.editor.clone(); - let buffer_id = for_excerpt.buffer_id; - let toggle_chevron_icon = - FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path); - header.child( - div() - .hover(|style| style.bg(colors.element_selected)) - .rounded_xs() - .child( - ButtonLike::new("toggle-buffer-fold") - .style(ui::ButtonStyle::Transparent) - .height(px(28.).into()) - .width(px(28.)) - .children(toggle_chevron_icon) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::with_meta_in( - "Toggle Excerpt Fold", - Some(&ToggleFold), - "Alt+click to toggle all", - &focus_handle, - window, - cx, - ) - } - }) - .on_click(move |event, window, cx| { - if event.modifiers().alt { - // Alt+click toggles all buffers - editor.update(cx, |editor, cx| { - editor.toggle_fold_all( - &ToggleFoldAll, + let header = + div() + .p_1() + .w_full() + .h(FILE_HEADER_HEIGHT as f32 * window.line_height()) + .child( + h_flex() + .size_full() + .gap_2() + .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) + .pl_0p5() + .pr_5() + .rounded_sm() + .when(is_sticky, |el| el.shadow_md()) + .border_1() + .map(|div| { + let border_color = if is_selected + && is_folded + && focus_handle.contains_focused(window, cx) + { + colors.border_focused + } else { + colors.border + }; + div.border_color(border_color) + }) + .bg(colors.editor_subheader_background) + .hover(|style| style.bg(colors.element_hover)) + .map(|header| { + let editor = self.editor.clone(); + let buffer_id = for_excerpt.buffer_id; + let toggle_chevron_icon = + FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path); + header.child( + div() + .hover(|style| style.bg(colors.element_selected)) + .rounded_xs() + .child( + ButtonLike::new("toggle-buffer-fold") + .style(ui::ButtonStyle::Transparent) + .height(px(28.).into()) + .width(px(28.)) + .children(toggle_chevron_icon) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::with_meta_in( + "Toggle Excerpt Fold", + Some(&ToggleFold), + "Alt+click to toggle all", + &focus_handle, window, cx, - ); - }); - } else { - // Regular click toggles single buffer - if is_folded { + ) + } + }) + .on_click(move |event, window, cx| { + if event.modifiers().alt { + // Alt+click toggles all buffers editor.update(cx, |editor, cx| { - editor.unfold_buffer(buffer_id, cx); + editor.toggle_fold_all( + &ToggleFoldAll, + window, + cx, + ); }); } else { - editor.update(cx, |editor, cx| { - editor.fold_buffer(buffer_id, cx); - }); - } - } - }), - ), - ) - }) - .children( - editor - .addons - .values() - .filter_map(|addon| { - addon.render_buffer_header_controls(for_excerpt, window, cx) - }) - .take(1), - ) - .child( - h_flex() - .size(Pixels(12.0)) - .justify_center() - .children(indicator), - ) - .child( - h_flex() - .cursor_pointer() - .id("path header block") - .size_full() - .justify_between() - .overflow_hidden() - .child( - h_flex() - .gap_2() - .map(|path_header| { - let filename = filename - .map(SharedString::from) - .unwrap_or_else(|| "untitled".into()); - - path_header - .when(ItemSettings::get_global(cx).file_icons, |el| { - let path = path::Path::new(filename.as_str()); - let icon = FileIcons::get_icon(path, cx) - .unwrap_or_default(); - let icon = - Icon::from_path(icon).color(Color::Muted); - el.child(icon) - }) - .child(Label::new(filename).single_line().when_some( - file_status, - |el, status| { - el.color(if status.is_conflicted() { - Color::Conflict - } else if status.is_modified() { - Color::Modified - } else if status.is_deleted() { - Color::Disabled + // Regular click toggles single buffer + if is_folded { + editor.update(cx, |editor, cx| { + editor.unfold_buffer(buffer_id, cx); + }); } else { - Color::Created - }) - .when(status.is_deleted(), |el| { - el.strikethrough() - }) + editor.update(cx, |editor, cx| { + editor.fold_buffer(buffer_id, cx); + }); + } + } + }), + ), + ) + }) + .children( + editor + .addons + .values() + .filter_map(|addon| { + addon.render_buffer_header_controls(for_excerpt, window, cx) + }) + .take(1), + ) + .children(indicator) + .child( + h_flex() + .cursor_pointer() + .id("path header block") + .size_full() + .justify_between() + .overflow_hidden() + .child( + h_flex() + .gap_2() + .child( + Label::new( + filename + .map(SharedString::from) + .unwrap_or_else(|| "untitled".into()), + ) + .single_line() + .when_some(file_status, |el, status| { + el.color(if status.is_conflicted() { + Color::Conflict + } else if status.is_modified() { + Color::Modified + } else if status.is_deleted() { + Color::Disabled + } else { + Color::Created + }) + .when(status.is_deleted(), |el| el.strikethrough()) + }), + ) + .when_some(parent_path, |then, path| { + then.child(div().child(path).text_color( + if file_status.is_some_and(FileStatus::is_deleted) { + colors.text_disabled + } else { + colors.text_muted }, )) - }) - .when_some(parent_path, |then, path| { - then.child(div().child(path).text_color( - if file_status.is_some_and(FileStatus::is_deleted) { - colors.text_disabled - } else { - colors.text_muted - }, - )) - }), - ) - .when( - can_open_excerpts && is_selected && relative_path.is_some(), - |el| { - el.child( - h_flex() - .id("jump-to-file-button") - .gap_2p5() - .child(Label::new("Jump To File")) - .children( - KeyBinding::for_action_in( - &OpenExcerpts, - &focus_handle, - window, - cx, - ) - .map(|binding| binding.into_any_element()), - ), - ) - }, - ) - .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .on_click(window.listener_for(&self.editor, { - move |editor, e: &ClickEvent, window, cx| { - editor.open_excerpts_common( - Some(jump_data.clone()), - e.modifiers().secondary(), - window, - cx, - ); - } - })), - ), - ); + }), + ) + .when( + can_open_excerpts && is_selected && relative_path.is_some(), + |el| { + el.child( + h_flex() + .id("jump-to-file-button") + .gap_2p5() + .child(Label::new("Jump To File")) + .children( + KeyBinding::for_action_in( + &OpenExcerpts, + &focus_handle, + window, + cx, + ) + .map(|binding| binding.into_any_element()), + ), + ) + }, + ) + .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .on_click(window.listener_for(&self.editor, { + move |editor, e: &ClickEvent, window, cx| { + editor.open_excerpts_common( + Some(jump_data.clone()), + e.modifiers().secondary(), + window, + cx, + ); + } + })), + ), + ); let file = for_excerpt.buffer.file().cloned(); let editor = self.editor.clone(); @@ -3799,31 +3782,25 @@ impl EditorElement { && let Some(worktree) = project.read(cx).worktree_for_id(file.worktree_id(cx), cx) { - let worktree = worktree.read(cx); let relative_path = file.path(); - let entry_for_path = worktree.entry_for_path(relative_path); - let abs_path = entry_for_path.map(|e| { - e.canonical_path.as_deref().map_or_else( - || worktree.abs_path().join(relative_path), - Path::to_path_buf, - ) - }); - let has_relative_path = worktree.root_entry().is_some_and(Entry::is_dir); + let entry_for_path = worktree.read(cx).entry_for_path(relative_path); + let abs_path = entry_for_path.and_then(|e| e.canonical_path.as_deref()); + let has_relative_path = + worktree.read(cx).root_entry().is_some_and(Entry::is_dir); - let parent_abs_path = abs_path - .as_ref() - .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf())); + let parent_abs_path = + abs_path.and_then(|abs_path| Some(abs_path.parent()?.to_path_buf())); let relative_path = has_relative_path .then_some(relative_path) .map(ToOwned::to_owned); let visible_in_project_panel = - relative_path.is_some() && worktree.is_visible(); + relative_path.is_some() && worktree.read(cx).is_visible(); let reveal_in_project_panel = entry_for_path .filter(|_| visible_in_project_panel) .map(|entry| entry.id); menu = menu - .when_some(abs_path, |menu, abs_path| { + .when_some(abs_path.map(ToOwned::to_owned), |menu, abs_path| { menu.entry( "Copy Path", Some(Box::new(zed_actions::workspace::CopyPath)), @@ -7150,7 +7127,7 @@ fn header_jump_data( pub struct AcceptEditPredictionBinding(pub(crate) Option); impl AcceptEditPredictionBinding { - pub fn keystroke(&self) -> Option<&KeybindingKeystroke> { + pub fn keystroke(&self) -> Option<&Keystroke> { if let Some(binding) = self.0.as_ref() { match &binding.keystrokes() { [keystroke, ..] => Some(keystroke), diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index b7110190fd..afc5767de0 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -42,7 +42,6 @@ use ui::{IconDecorationKind, prelude::*}; use util::{ResultExt, TryFutureExt, paths::PathExt}; use workspace::{ CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, - invalid_buffer_view::InvalidBufferView, item::{FollowableItem, Item, ItemEvent, ProjectItem, SaveOptions}, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, }; @@ -1402,16 +1401,6 @@ impl ProjectItem for Editor { editor } - - fn for_broken_project_item( - abs_path: &Path, - is_local: bool, - e: &anyhow::Error, - window: &mut Window, - cx: &mut App, - ) -> Option { - Some(InvalidBufferView::new(abs_path, is_local, e, window, cx)) - } } fn clip_ranges<'a>( diff --git a/crates/editor/src/persistence.rs b/crates/editor/src/persistence.rs index ec7c149b4e..88fde53947 100644 --- a/crates/editor/src/persistence.rs +++ b/crates/editor/src/persistence.rs @@ -1,17 +1,13 @@ use anyhow::Result; -use db::{ - query, - sqlez::{ - bindable::{Bind, Column, StaticColumnCount}, - domain::Domain, - statement::Statement, - }, - sqlez_macros::sql, -}; +use db::sqlez::bindable::{Bind, Column, StaticColumnCount}; +use db::sqlez::statement::Statement; use fs::MTime; use itertools::Itertools as _; use std::path::PathBuf; +use db::sqlez_macros::sql; +use db::{define_connection, query}; + use workspace::{ItemId, WorkspaceDb, WorkspaceId}; #[derive(Clone, Debug, PartialEq, Default)] @@ -87,11 +83,7 @@ impl Column for SerializedEditor { } } -pub struct EditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection); - -impl Domain for EditorDb { - const NAME: &str = stringify!(EditorDb); - +define_connection!( // Current schema shape using pseudo-rust syntax: // editors( // item_id: usize, @@ -121,8 +113,7 @@ impl Domain for EditorDb { // start: usize, // end: usize, // ) - - const MIGRATIONS: &[&str] = &[ + pub static ref DB: EditorDb = &[ sql! ( CREATE TABLE editors( item_id INTEGER NOT NULL, @@ -198,9 +189,7 @@ impl Domain for EditorDb { ) STRICT; ), ]; -} - -db::static_connection!(DB, EditorDb, [WorkspaceDb]); +); // https://www.sqlite.org/limits.html // > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER, diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index cf74ee0a9e..e3d83ab160 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -26,17 +26,6 @@ fn is_rust_language(language: &Language) -> bool { } pub fn apply_related_actions(editor: &Entity, window: &mut Window, cx: &mut App) { - if editor.read(cx).project().is_some_and(|project| { - project - .read(cx) - .language_server_statuses(cx) - .any(|(_, status)| status.name == RUST_ANALYZER_NAME) - }) { - register_action(editor, window, cancel_flycheck_action); - register_action(editor, window, run_flycheck_action); - register_action(editor, window, clear_flycheck_action); - } - if editor .read(cx) .buffer() @@ -49,6 +38,9 @@ pub fn apply_related_actions(editor: &Entity, window: &mut Window, cx: & register_action(editor, window, go_to_parent_module); register_action(editor, window, expand_macro_recursively); register_action(editor, window, open_docs); + register_action(editor, window, cancel_flycheck_action); + register_action(editor, window, run_flycheck_action); + register_action(editor, window, clear_flycheck_action); } } @@ -317,7 +309,7 @@ fn cancel_flycheck_action( let Some(project) = &editor.project else { return; }; - let buffer_id = editor + let Some(buffer_id) = editor .selections .disjoint_anchors() .iter() @@ -329,7 +321,10 @@ fn cancel_flycheck_action( .read(cx) .entry_id(cx)?; project.path_for_entry(entry_id, cx) - }); + }) + else { + return; + }; cancel_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); } @@ -342,7 +337,7 @@ fn run_flycheck_action( let Some(project) = &editor.project else { return; }; - let buffer_id = editor + let Some(buffer_id) = editor .selections .disjoint_anchors() .iter() @@ -354,7 +349,10 @@ fn run_flycheck_action( .read(cx) .entry_id(cx)?; project.path_for_entry(entry_id, cx) - }); + }) + else { + return; + }; run_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); } @@ -367,7 +365,7 @@ fn clear_flycheck_action( let Some(project) = &editor.project else { return; }; - let buffer_id = editor + let Some(buffer_id) = editor .selections .disjoint_anchors() .iter() @@ -379,6 +377,9 @@ fn clear_flycheck_action( .read(cx) .entry_id(cx)?; project.path_for_entry(entry_id, cx) - }); + }) + else { + return; + }; clear_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); } diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index f5f7fc42b3..422979c429 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -98,10 +98,6 @@ impl FeatureFlag for GeminiAndNativeFeatureFlag { // integration too, and we'd like to turn Gemini/Native on in new builds // without enabling Claude Code in old builds. const NAME: &'static str = "gemini-and-native"; - - fn enabled_for_all() -> bool { - true - } } pub struct ClaudeCodeFeatureFlag; @@ -205,7 +201,7 @@ impl FeatureFlagAppExt for App { fn has_flag(&self) -> bool { self.try_global::() .map(|flags| flags.has_flag::()) - .unwrap_or(T::enabled_for_all()) + .unwrap_or(false) } fn is_staff(&self) -> bool { diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 7512152324..8aaaa04729 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1401,16 +1401,13 @@ impl PickerDelegate for FileFinderDelegate { #[cfg(windows)] let raw_query = raw_query.trim().to_owned().replace("/", "\\"); #[cfg(not(windows))] - let raw_query = raw_query.trim(); + let raw_query = raw_query.trim().to_owned(); - let raw_query = raw_query.trim_end_matches(':').to_owned(); - let path = path_position.path.to_str(); - let path_trimmed = path.unwrap_or(&raw_query).trim_end_matches(':'); - let file_query_end = if path_trimmed == raw_query { + let file_query_end = if path_position.path.to_str().unwrap_or(&raw_query) == raw_query { None } else { // Safe to unwrap as we won't get here when the unwrap in if fails - Some(path.unwrap().len()) + Some(path_position.path.to_str().unwrap().len()) }; let query = FileSearchQuery { diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index cd0f203d6a..8203d1b1fd 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -218,7 +218,6 @@ async fn test_matching_paths(cx: &mut TestAppContext) { " ndan ", " band ", "a bandana", - "bandana:", ] { picker .update_in(cx, |picker, window, cx| { @@ -253,53 +252,6 @@ async fn test_matching_paths(cx: &mut TestAppContext) { } } -#[gpui::test] -async fn test_matching_paths_with_colon(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - "a": { - "foo:bar.rs": "", - "foo.rs": "", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - - let (picker, _, cx) = build_find_picker(project, cx); - - // 'foo:' matches both files - cx.simulate_input("foo:"); - picker.update(cx, |picker, _| { - assert_eq!(picker.delegate.matches.len(), 3); - assert_match_at_position(picker, 0, "foo.rs"); - assert_match_at_position(picker, 1, "foo:bar.rs"); - }); - - // 'foo:b' matches one of the files - cx.simulate_input("b"); - picker.update(cx, |picker, _| { - assert_eq!(picker.delegate.matches.len(), 2); - assert_match_at_position(picker, 0, "foo:bar.rs"); - }); - - cx.dispatch_action(editor::actions::Backspace); - - // 'foo:1' matches both files, specifying which row to jump to - cx.simulate_input("1"); - picker.update(cx, |picker, _| { - assert_eq!(picker.delegate.matches.len(), 3); - assert_match_at_position(picker, 0, "foo.rs"); - assert_match_at_position(picker, 1, "foo:bar.rs"); - }); -} - #[gpui::test] async fn test_unicode_paths(cx: &mut TestAppContext) { let app_state = init_test(cx); diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 4ecb4a8829..958a609a09 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -4466,7 +4466,7 @@ fn current_language_model(cx: &Context<'_, GitPanel>) -> Option, pub(crate) keymap: Rc>, pub(crate) keyboard_layout: Box, - pub(crate) keyboard_mapper: Rc, pub(crate) global_action_listeners: FxHashMap>>, pending_effects: VecDeque, @@ -313,7 +312,6 @@ impl App { let text_system = Arc::new(TextSystem::new(platform.text_system())); let entities = EntityMap::new(); let keyboard_layout = platform.keyboard_layout(); - let keyboard_mapper = platform.keyboard_mapper(); let app = Rc::new_cyclic(|this| AppCell { app: RefCell::new(App { @@ -339,7 +337,6 @@ impl App { focus_handles: Arc::new(RwLock::new(SlotMap::with_key())), keymap: Rc::new(RefCell::new(Keymap::default())), keyboard_layout, - keyboard_mapper, global_action_listeners: FxHashMap::default(), pending_effects: VecDeque::new(), pending_notifications: FxHashSet::default(), @@ -379,7 +376,6 @@ impl App { if let Some(app) = app.upgrade() { let cx = &mut app.borrow_mut(); cx.keyboard_layout = cx.platform.keyboard_layout(); - cx.keyboard_mapper = cx.platform.keyboard_mapper(); cx.keyboard_layout_observers .clone() .retain(&(), move |callback| (callback)(cx)); @@ -428,11 +424,6 @@ impl App { self.keyboard_layout.as_ref() } - /// Get the current keyboard mapper. - pub fn keyboard_mapper(&self) -> &Rc { - &self.keyboard_mapper - } - /// Invokes a handler when the current keyboard layout changes pub fn on_keyboard_layout_change(&self, mut callback: F) -> Subscription where diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index b3db09d821..757205fcc3 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -4,7 +4,7 @@ mod context; pub use binding::*; pub use context::*; -use crate::{Action, AsKeystroke, Keystroke, is_no_action}; +use crate::{Action, Keystroke, is_no_action}; use collections::{HashMap, HashSet}; use smallvec::SmallVec; use std::any::TypeId; @@ -141,7 +141,7 @@ impl Keymap { /// only. pub fn bindings_for_input( &self, - input: &[impl AsKeystroke], + input: &[Keystroke], context_stack: &[KeyContext], ) -> (SmallVec<[KeyBinding; 1]>, bool) { let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new(); @@ -192,6 +192,7 @@ impl Keymap { (bindings, !pending.is_empty()) } + /// Check if the given binding is enabled, given a certain key context. /// Returns the deepest depth at which the binding matches, or None if it doesn't match. fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option { @@ -638,7 +639,7 @@ mod tests { fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) { let actual = keymap .bindings_for_action(action) - .map(|binding| binding.keystrokes[0].inner.unparse()) + .map(|binding| binding.keystrokes[0].unparse()) .collect::>(); assert_eq!(actual, expected, "{:?}", action); } diff --git a/crates/gpui/src/keymap/binding.rs b/crates/gpui/src/keymap/binding.rs index a7cf9d5c54..729498d153 100644 --- a/crates/gpui/src/keymap/binding.rs +++ b/crates/gpui/src/keymap/binding.rs @@ -1,15 +1,14 @@ use std::rc::Rc; -use crate::{ - Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate, - KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString, -}; +use collections::HashMap; + +use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString}; use smallvec::SmallVec; /// A keybinding and its associated metadata, from the keymap. pub struct KeyBinding { pub(crate) action: Box, - pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>, + pub(crate) keystrokes: SmallVec<[Keystroke; 2]>, pub(crate) context_predicate: Option>, pub(crate) meta: Option, /// The json input string used when building the keybinding, if any @@ -33,15 +32,7 @@ impl KeyBinding { pub fn new(keystrokes: &str, action: A, context: Option<&str>) -> Self { let context_predicate = context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into()); - Self::load( - keystrokes, - Box::new(action), - context_predicate, - false, - None, - &DummyKeyboardMapper, - ) - .unwrap() + Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap() } /// Load a keybinding from the given raw data. @@ -49,22 +40,24 @@ impl KeyBinding { keystrokes: &str, action: Box, context_predicate: Option>, - use_key_equivalents: bool, + key_equivalents: Option<&HashMap>, action_input: Option, - keyboard_mapper: &dyn PlatformKeyboardMapper, ) -> std::result::Result { - let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes + let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes .split_whitespace() - .map(|source| { - let keystroke = Keystroke::parse(source)?; - Ok(KeybindingKeystroke::new( - keystroke, - use_key_equivalents, - keyboard_mapper, - )) - }) + .map(Keystroke::parse) .collect::>()?; + if let Some(equivalents) = key_equivalents { + for keystroke in keystrokes.iter_mut() { + if keystroke.key.chars().count() == 1 + && let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap()) + { + keystroke.key = key.to_string(); + } + } + } + Ok(Self { keystrokes, action, @@ -86,13 +79,13 @@ impl KeyBinding { } /// Check if the given keystrokes match this binding. - pub fn match_keystrokes(&self, typed: &[impl AsKeystroke]) -> Option { + pub fn match_keystrokes(&self, typed: &[Keystroke]) -> Option { if self.keystrokes.len() < typed.len() { return None; } for (target, typed) in self.keystrokes.iter().zip(typed.iter()) { - if !typed.as_keystroke().should_match(target) { + if !typed.should_match(target) { return None; } } @@ -101,7 +94,7 @@ impl KeyBinding { } /// Get the keystrokes associated with this binding - pub fn keystrokes(&self) -> &[KeybindingKeystroke] { + pub fn keystrokes(&self) -> &[Keystroke] { self.keystrokes.as_slice() } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index f64710bc56..4d2feeaf1d 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -231,6 +231,7 @@ pub(crate) trait Platform: 'static { fn on_quit(&self, callback: Box); fn on_reopen(&self, callback: Box); + fn on_keyboard_layout_change(&self, callback: Box); fn set_menus(&self, menus: Vec, keymap: &Keymap); fn get_menus(&self) -> Option> { @@ -250,6 +251,7 @@ pub(crate) trait Platform: 'static { fn on_app_menu_action(&self, callback: Box); fn on_will_open_app_menu(&self, callback: Box); fn on_validate_app_menu_command(&self, callback: Box bool>); + fn keyboard_layout(&self) -> Box; fn compositor_name(&self) -> &'static str { "" @@ -270,10 +272,6 @@ pub(crate) trait Platform: 'static { fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task>; fn read_credentials(&self, url: &str) -> Task)>>>; fn delete_credentials(&self, url: &str) -> Task>; - - fn keyboard_layout(&self) -> Box; - fn keyboard_mapper(&self) -> Rc; - fn on_keyboard_layout_change(&self, callback: Box); } /// A handle to a platform's display, e.g. a monitor or laptop screen. diff --git a/crates/gpui/src/platform/keyboard.rs b/crates/gpui/src/platform/keyboard.rs index 10b8620258..e28d781520 100644 --- a/crates/gpui/src/platform/keyboard.rs +++ b/crates/gpui/src/platform/keyboard.rs @@ -1,7 +1,3 @@ -use collections::HashMap; - -use crate::{KeybindingKeystroke, Keystroke}; - /// A trait for platform-specific keyboard layouts pub trait PlatformKeyboardLayout { /// Get the keyboard layout ID, which should be unique to the layout @@ -9,33 +5,3 @@ pub trait PlatformKeyboardLayout { /// Get the keyboard layout display name fn name(&self) -> &str; } - -/// A trait for platform-specific keyboard mappings -pub trait PlatformKeyboardMapper { - /// Map a key equivalent to its platform-specific representation - fn map_key_equivalent( - &self, - keystroke: Keystroke, - use_key_equivalents: bool, - ) -> KeybindingKeystroke; - /// Get the key equivalents for the current keyboard layout, - /// only used on macOS - fn get_key_equivalents(&self) -> Option<&HashMap>; -} - -/// A dummy implementation of the platform keyboard mapper -pub struct DummyKeyboardMapper; - -impl PlatformKeyboardMapper for DummyKeyboardMapper { - fn map_key_equivalent( - &self, - keystroke: Keystroke, - _use_key_equivalents: bool, - ) -> KeybindingKeystroke { - KeybindingKeystroke::from_keystroke(keystroke) - } - - fn get_key_equivalents(&self) -> Option<&HashMap> { - None - } -} diff --git a/crates/gpui/src/platform/keystroke.rs b/crates/gpui/src/platform/keystroke.rs index 6ce17c3a01..24601eefd6 100644 --- a/crates/gpui/src/platform/keystroke.rs +++ b/crates/gpui/src/platform/keystroke.rs @@ -5,14 +5,6 @@ use std::{ fmt::{Display, Write}, }; -use crate::PlatformKeyboardMapper; - -/// This is a helper trait so that we can simplify the implementation of some functions -pub trait AsKeystroke { - /// Returns the GPUI representation of the keystroke. - fn as_keystroke(&self) -> &Keystroke; -} - /// A keystroke and associated metadata generated by the platform #[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)] pub struct Keystroke { @@ -32,17 +24,6 @@ pub struct Keystroke { pub key_char: Option, } -/// Represents a keystroke that can be used in keybindings and displayed to the user. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct KeybindingKeystroke { - /// The GPUI representation of the keystroke. - pub inner: Keystroke, - /// The modifiers to display. - pub display_modifiers: Modifiers, - /// The key to display. - pub display_key: String, -} - /// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use /// markdown to display it. #[derive(Debug)] @@ -77,7 +58,7 @@ impl Keystroke { /// /// This method assumes that `self` was typed and `target' is in the keymap, and checks /// both possibilities for self against the target. - pub fn should_match(&self, target: &KeybindingKeystroke) -> bool { + pub fn should_match(&self, target: &Keystroke) -> bool { #[cfg(not(target_os = "windows"))] if let Some(key_char) = self .key_char @@ -90,7 +71,7 @@ impl Keystroke { ..Default::default() }; - if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers { + if &target.key == key_char && target.modifiers == ime_modifiers { return true; } } @@ -102,12 +83,12 @@ impl Keystroke { .filter(|key_char| key_char != &&self.key) { // On Windows, if key_char is set, then the typed keystroke produced the key_char - if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() { + if &target.key == key_char && target.modifiers == Modifiers::none() { return true; } } - target.inner.modifiers == self.modifiers && target.inner.key == self.key + target.modifiers == self.modifiers && target.key == self.key } /// key syntax is: @@ -219,7 +200,31 @@ impl Keystroke { /// Produces a representation of this key that Parse can understand. pub fn unparse(&self) -> String { - unparse(&self.modifiers, &self.key) + let mut str = String::new(); + if self.modifiers.function { + str.push_str("fn-"); + } + if self.modifiers.control { + str.push_str("ctrl-"); + } + if self.modifiers.alt { + str.push_str("alt-"); + } + if self.modifiers.platform { + #[cfg(target_os = "macos")] + str.push_str("cmd-"); + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + str.push_str("super-"); + + #[cfg(target_os = "windows")] + str.push_str("win-"); + } + if self.modifiers.shift { + str.push_str("shift-"); + } + str.push_str(&self.key); + str } /// Returns true if this keystroke left @@ -261,32 +266,6 @@ impl Keystroke { } } -impl KeybindingKeystroke { - /// Create a new keybinding keystroke from the given keystroke - pub fn new( - inner: Keystroke, - use_key_equivalents: bool, - keyboard_mapper: &dyn PlatformKeyboardMapper, - ) -> Self { - keyboard_mapper.map_key_equivalent(inner, use_key_equivalents) - } - - pub(crate) fn from_keystroke(keystroke: Keystroke) -> Self { - let key = keystroke.key.clone(); - let modifiers = keystroke.modifiers; - KeybindingKeystroke { - inner: keystroke, - display_modifiers: modifiers, - display_key: key, - } - } - - /// Produces a representation of this key that Parse can understand. - pub fn unparse(&self) -> String { - unparse(&self.display_modifiers, &self.display_key) - } -} - fn is_printable_key(key: &str) -> bool { !matches!( key, @@ -343,15 +322,65 @@ fn is_printable_key(key: &str) -> bool { impl std::fmt::Display for Keystroke { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - display_modifiers(&self.modifiers, f)?; - display_key(&self.key, f) - } -} + if self.modifiers.control { + #[cfg(target_os = "macos")] + f.write_char('^')?; -impl std::fmt::Display for KeybindingKeystroke { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - display_modifiers(&self.display_modifiers, f)?; - display_key(&self.display_key, f) + #[cfg(not(target_os = "macos"))] + write!(f, "ctrl-")?; + } + if self.modifiers.alt { + #[cfg(target_os = "macos")] + f.write_char('⌥')?; + + #[cfg(not(target_os = "macos"))] + write!(f, "alt-")?; + } + if self.modifiers.platform { + #[cfg(target_os = "macos")] + f.write_char('⌘')?; + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + f.write_char('❖')?; + + #[cfg(target_os = "windows")] + f.write_char('⊞')?; + } + if self.modifiers.shift { + #[cfg(target_os = "macos")] + f.write_char('⇧')?; + + #[cfg(not(target_os = "macos"))] + write!(f, "shift-")?; + } + let key = match self.key.as_str() { + #[cfg(target_os = "macos")] + "backspace" => '⌫', + #[cfg(target_os = "macos")] + "up" => '↑', + #[cfg(target_os = "macos")] + "down" => '↓', + #[cfg(target_os = "macos")] + "left" => '←', + #[cfg(target_os = "macos")] + "right" => '→', + #[cfg(target_os = "macos")] + "tab" => '⇥', + #[cfg(target_os = "macos")] + "escape" => '⎋', + #[cfg(target_os = "macos")] + "shift" => '⇧', + #[cfg(target_os = "macos")] + "control" => '⌃', + #[cfg(target_os = "macos")] + "alt" => '⌥', + #[cfg(target_os = "macos")] + "platform" => '⌘', + + key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(), + key => return f.write_str(key), + }; + f.write_char(key) } } @@ -571,110 +600,3 @@ pub struct Capslock { #[serde(default)] pub on: bool, } - -impl AsKeystroke for Keystroke { - fn as_keystroke(&self) -> &Keystroke { - self - } -} - -impl AsKeystroke for KeybindingKeystroke { - fn as_keystroke(&self) -> &Keystroke { - &self.inner - } -} - -fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if modifiers.control { - #[cfg(target_os = "macos")] - f.write_char('^')?; - - #[cfg(not(target_os = "macos"))] - write!(f, "ctrl-")?; - } - if modifiers.alt { - #[cfg(target_os = "macos")] - f.write_char('⌥')?; - - #[cfg(not(target_os = "macos"))] - write!(f, "alt-")?; - } - if modifiers.platform { - #[cfg(target_os = "macos")] - f.write_char('⌘')?; - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - f.write_char('❖')?; - - #[cfg(target_os = "windows")] - f.write_char('⊞')?; - } - if modifiers.shift { - #[cfg(target_os = "macos")] - f.write_char('⇧')?; - - #[cfg(not(target_os = "macos"))] - write!(f, "shift-")?; - } - Ok(()) -} - -fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let key = match key { - #[cfg(target_os = "macos")] - "backspace" => '⌫', - #[cfg(target_os = "macos")] - "up" => '↑', - #[cfg(target_os = "macos")] - "down" => '↓', - #[cfg(target_os = "macos")] - "left" => '←', - #[cfg(target_os = "macos")] - "right" => '→', - #[cfg(target_os = "macos")] - "tab" => '⇥', - #[cfg(target_os = "macos")] - "escape" => '⎋', - #[cfg(target_os = "macos")] - "shift" => '⇧', - #[cfg(target_os = "macos")] - "control" => '⌃', - #[cfg(target_os = "macos")] - "alt" => '⌥', - #[cfg(target_os = "macos")] - "platform" => '⌘', - - key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(), - key => return f.write_str(key), - }; - f.write_char(key) -} - -#[inline] -fn unparse(modifiers: &Modifiers, key: &str) -> String { - let mut result = String::new(); - if modifiers.function { - result.push_str("fn-"); - } - if modifiers.control { - result.push_str("ctrl-"); - } - if modifiers.alt { - result.push_str("alt-"); - } - if modifiers.platform { - #[cfg(target_os = "macos")] - result.push_str("cmd-"); - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - result.push_str("super-"); - - #[cfg(target_os = "windows")] - result.push_str("win-"); - } - if modifiers.shift { - result.push_str("shift-"); - } - result.push_str(&key); - result -} diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 8bd89fc399..3fb1ef4572 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -25,8 +25,8 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State}; use crate::{ Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions, - Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, - PlatformTextSystem, PlatformWindow, Point, Result, Task, WindowAppearance, WindowParams, px, + Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, + Point, Result, Task, WindowAppearance, WindowParams, px, }; #[cfg(any(feature = "wayland", feature = "x11"))] @@ -144,10 +144,6 @@ impl Platform for P { self.keyboard_layout() } - fn keyboard_mapper(&self) -> Rc { - Rc::new(crate::DummyKeyboardMapper) - } - fn on_keyboard_layout_change(&self, callback: Box) { self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback)); } diff --git a/crates/gpui/src/platform/mac/keyboard.rs b/crates/gpui/src/platform/mac/keyboard.rs index 1409731246..a9f6af3edb 100644 --- a/crates/gpui/src/platform/mac/keyboard.rs +++ b/crates/gpui/src/platform/mac/keyboard.rs @@ -1,9 +1,8 @@ -use collections::HashMap; use std::ffi::{CStr, c_void}; use objc::{msg_send, runtime::Object, sel, sel_impl}; -use crate::{KeybindingKeystroke, Keystroke, PlatformKeyboardLayout, PlatformKeyboardMapper}; +use crate::PlatformKeyboardLayout; use super::{ TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, kTISPropertyInputSourceID, @@ -15,10 +14,6 @@ pub(crate) struct MacKeyboardLayout { name: String, } -pub(crate) struct MacKeyboardMapper { - key_equivalents: Option>, -} - impl PlatformKeyboardLayout for MacKeyboardLayout { fn id(&self) -> &str { &self.id @@ -29,27 +24,6 @@ impl PlatformKeyboardLayout for MacKeyboardLayout { } } -impl PlatformKeyboardMapper for MacKeyboardMapper { - fn map_key_equivalent( - &self, - mut keystroke: Keystroke, - use_key_equivalents: bool, - ) -> KeybindingKeystroke { - if use_key_equivalents && let Some(key_equivalents) = &self.key_equivalents { - if keystroke.key.chars().count() == 1 - && let Some(key) = key_equivalents.get(&keystroke.key.chars().next().unwrap()) - { - keystroke.key = key.to_string(); - } - } - KeybindingKeystroke::from_keystroke(keystroke) - } - - fn get_key_equivalents(&self) -> Option<&HashMap> { - self.key_equivalents.as_ref() - } -} - impl MacKeyboardLayout { pub(crate) fn new() -> Self { unsafe { @@ -73,1428 +47,3 @@ impl MacKeyboardLayout { } } } - -impl MacKeyboardMapper { - pub(crate) fn new(layout_id: &str) -> Self { - let key_equivalents = get_key_equivalents(layout_id); - - Self { key_equivalents } - } -} - -// On some keyboards (e.g. German QWERTZ) it is not possible to type the full ASCII range -// without using option. This means that some of our built in keyboard shortcuts do not work -// for those users. -// -// The way macOS solves this problem is to move shortcuts around so that they are all reachable, -// even if the mnemonic changes. https://developer.apple.com/documentation/swiftui/keyboardshortcut/localization-swift.struct -// -// For example, cmd-> is the "switch window" shortcut because the > key is right above tab. -// To ensure this doesn't cause problems for shortcuts defined for a QWERTY layout, apple moves -// any shortcuts defined as cmd-> to cmd-:. Coincidentally this s also the same keyboard position -// as cmd-> on a QWERTY layout. -// -// Another example is cmd-[ and cmd-], as they cannot be typed without option, those keys are remapped to cmd-ö -// and cmd-ä. These shortcuts are not in the same position as a QWERTY keyboard, because on a QWERTZ keyboard -// the + key is in the way; and shortcuts bound to cmd-+ are still typed as cmd-+ on either keyboard (though the -// specific key moves) -// -// As far as I can tell, there's no way to query the mappings Apple uses except by rendering a menu with every -// possible key combination, and inspecting the UI to see what it rendered. So that's what we did... -// -// These mappings were generated by running https://github.com/ConradIrwin/keyboard-inspector, tidying up the -// output to remove languages with no mappings and other oddities, and converting it to a less verbose representation with: -// jq -s 'map(to_entries | map({key: .key, value: [(.value | to_entries | map(.key) | join("")), (.value | to_entries | map(.value) | join(""))]}) | from_entries) | add' -// From there I used multi-cursor to produce this match statement. -fn get_key_equivalents(layout_id: &str) -> Option> { - let mappings: &[(char, char)] = match layout_id { - "com.apple.keylayout.ABC-AZERTY" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.ABC-QWERTZ" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Albanian" => &[ - ('"', '\''), - (':', 'Ç'), - (';', 'ç'), - ('<', ';'), - ('>', ':'), - ('@', '"'), - ('\'', '@'), - ('\\', 'ë'), - ('`', '<'), - ('|', 'Ë'), - ('~', '>'), - ], - "com.apple.keylayout.Austrian" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Azeri" => &[ - ('"', 'Ə'), - (',', 'ç'), - ('.', 'ş'), - ('/', '.'), - (':', 'I'), - (';', 'ı'), - ('<', 'Ç'), - ('>', 'Ş'), - ('?', ','), - ('W', 'Ü'), - ('[', 'ö'), - ('\'', 'ə'), - (']', 'ğ'), - ('w', 'ü'), - ('{', 'Ö'), - ('|', '/'), - ('}', 'Ğ'), - ], - "com.apple.keylayout.Belgian" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.Brazilian-ABNT2" => &[ - ('"', '`'), - ('/', 'ç'), - ('?', 'Ç'), - ('\'', '´'), - ('\\', '~'), - ('^', '¨'), - ('`', '\''), - ('|', '^'), - ('~', '"'), - ], - "com.apple.keylayout.Brazilian-Pro" => &[('^', 'ˆ'), ('~', '˜')], - "com.apple.keylayout.British" => &[('#', '£')], - "com.apple.keylayout.Canadian-CSA" => &[ - ('"', 'È'), - ('/', 'é'), - ('<', '\''), - ('>', '"'), - ('?', 'É'), - ('[', '^'), - ('\'', 'è'), - ('\\', 'à'), - (']', 'ç'), - ('`', 'ù'), - ('{', '¨'), - ('|', 'À'), - ('}', 'Ç'), - ('~', 'Ù'), - ], - "com.apple.keylayout.Croatian" => &[ - ('"', 'Ć'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Croatian-PC" => &[ - ('"', 'Ć'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Czech" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ě'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ř'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ů'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', ')'), - ('^', '6'), - ('`', '¨'), - ('{', 'Ú'), - ('}', '('), - ('~', '`'), - ], - "com.apple.keylayout.Czech-QWERTY" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ě'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ř'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ů'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', ')'), - ('^', '6'), - ('`', '¨'), - ('{', 'Ú'), - ('}', '('), - ('~', '`'), - ], - "com.apple.keylayout.Danish" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'æ'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ø'), - ('^', '&'), - ('`', '<'), - ('{', 'Æ'), - ('|', '*'), - ('}', 'Ø'), - ('~', '>'), - ], - "com.apple.keylayout.Faroese" => &[ - ('"', 'Ø'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Æ'), - (';', 'æ'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'å'), - ('\'', 'ø'), - ('\\', '\''), - (']', 'ð'), - ('^', '&'), - ('`', '<'), - ('{', 'Å'), - ('|', '*'), - ('}', 'Ð'), - ('~', '>'), - ], - "com.apple.keylayout.Finnish" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.FinnishExtended" => &[ - ('"', 'ˆ'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.FinnishSami-PC" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '@'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.French" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.French-PC" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('-', ')'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '-'), - ('7', 'è'), - ('8', '_'), - ('9', 'ç'), - (':', '§'), - (';', '!'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '*'), - (']', '$'), - ('^', '6'), - ('_', '°'), - ('`', '<'), - ('{', '¨'), - ('|', 'μ'), - ('}', '£'), - ('~', '>'), - ], - "com.apple.keylayout.French-numerical" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.German" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.German-DIN-2137" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Hawaiian" => &[('\'', 'ʻ')], - "com.apple.keylayout.Hungarian" => &[ - ('!', '\''), - ('"', 'Á'), - ('#', '+'), - ('$', '!'), - ('&', '='), - ('(', ')'), - (')', 'Ö'), - ('*', '('), - ('+', 'Ó'), - ('/', 'ü'), - ('0', 'ö'), - (':', 'É'), - (';', 'é'), - ('<', 'Ü'), - ('=', 'ó'), - ('>', ':'), - ('@', '"'), - ('[', 'ő'), - ('\'', 'á'), - ('\\', 'ű'), - (']', 'ú'), - ('^', '/'), - ('`', 'í'), - ('{', 'Ő'), - ('|', 'Ű'), - ('}', 'Ú'), - ('~', 'Í'), - ], - "com.apple.keylayout.Hungarian-QWERTY" => &[ - ('!', '\''), - ('"', 'Á'), - ('#', '+'), - ('$', '!'), - ('&', '='), - ('(', ')'), - (')', 'Ö'), - ('*', '('), - ('+', 'Ó'), - ('/', 'ü'), - ('0', 'ö'), - (':', 'É'), - (';', 'é'), - ('<', 'Ü'), - ('=', 'ó'), - ('>', ':'), - ('@', '"'), - ('[', 'ő'), - ('\'', 'á'), - ('\\', 'ű'), - (']', 'ú'), - ('^', '/'), - ('`', 'í'), - ('{', 'Ő'), - ('|', 'Ű'), - ('}', 'Ú'), - ('~', 'Í'), - ], - "com.apple.keylayout.Icelandic" => &[ - ('"', 'Ö'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'Ð'), - (';', 'ð'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'æ'), - ('\'', 'ö'), - ('\\', 'þ'), - (']', '´'), - ('^', '&'), - ('`', '<'), - ('{', 'Æ'), - ('|', 'Þ'), - ('}', '´'), - ('~', '>'), - ], - "com.apple.keylayout.Irish" => &[('#', '£')], - "com.apple.keylayout.IrishExtended" => &[('#', '£')], - "com.apple.keylayout.Italian" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - (',', ';'), - ('.', ':'), - ('/', ','), - ('0', 'é'), - ('1', '&'), - ('2', '"'), - ('3', '\''), - ('4', '('), - ('5', 'ç'), - ('6', 'è'), - ('7', ')'), - ('8', '£'), - ('9', 'à'), - (':', '!'), - (';', 'ò'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', 'ì'), - ('\'', 'ù'), - ('\\', '§'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '^'), - ('|', '°'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.Italian-Pro" => &[ - ('"', '^'), - ('#', '£'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'é'), - (';', 'è'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ò'), - ('\'', 'ì'), - ('\\', 'ù'), - (']', 'à'), - ('^', '&'), - ('`', '<'), - ('{', 'ç'), - ('|', '§'), - ('}', '°'), - ('~', '>'), - ], - "com.apple.keylayout.LatinAmerican" => &[ - ('"', '¨'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'Ñ'), - (';', 'ñ'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', '{'), - ('\'', '´'), - ('\\', '¿'), - (']', '}'), - ('^', '&'), - ('`', '<'), - ('{', '['), - ('|', '¡'), - ('}', ']'), - ('~', '>'), - ], - "com.apple.keylayout.Lithuanian" => &[ - ('!', 'Ą'), - ('#', 'Ę'), - ('$', 'Ė'), - ('%', 'Į'), - ('&', 'Ų'), - ('*', 'Ū'), - ('+', 'Ž'), - ('1', 'ą'), - ('2', 'č'), - ('3', 'ę'), - ('4', 'ė'), - ('5', 'į'), - ('6', 'š'), - ('7', 'ų'), - ('8', 'ū'), - ('=', 'ž'), - ('@', 'Č'), - ('^', 'Š'), - ], - "com.apple.keylayout.Maltese" => &[ - ('#', '£'), - ('[', 'ġ'), - (']', 'ħ'), - ('`', 'ż'), - ('{', 'Ġ'), - ('}', 'Ħ'), - ('~', 'Ż'), - ], - "com.apple.keylayout.NorthernSami" => &[ - ('"', 'Ŋ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('Q', 'Á'), - ('W', 'Š'), - ('X', 'Č'), - ('[', 'ø'), - ('\'', 'ŋ'), - ('\\', 'đ'), - (']', 'æ'), - ('^', '&'), - ('`', 'ž'), - ('q', 'á'), - ('w', 'š'), - ('x', 'č'), - ('{', 'Ø'), - ('|', 'Đ'), - ('}', 'Æ'), - ('~', 'Ž'), - ], - "com.apple.keylayout.Norwegian" => &[ - ('"', '^'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ø'), - ('\'', '¨'), - ('\\', '@'), - (']', 'æ'), - ('^', '&'), - ('`', '<'), - ('{', 'Ø'), - ('|', '*'), - ('}', 'Æ'), - ('~', '>'), - ], - "com.apple.keylayout.NorwegianExtended" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ø'), - ('\\', '@'), - (']', 'æ'), - ('`', '<'), - ('}', 'Æ'), - ('~', '>'), - ], - "com.apple.keylayout.NorwegianSami-PC" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ø'), - ('\'', '¨'), - ('\\', '@'), - (']', 'æ'), - ('^', '&'), - ('`', '<'), - ('{', 'Ø'), - ('|', '*'), - ('}', 'Æ'), - ('~', '>'), - ], - "com.apple.keylayout.Polish" => &[ - ('!', '§'), - ('"', 'ę'), - ('#', '!'), - ('$', '?'), - ('%', '+'), - ('&', ':'), - ('(', '/'), - (')', '"'), - ('*', '_'), - ('+', ']'), - (',', '.'), - ('.', ','), - ('/', 'ż'), - (':', 'Ł'), - (';', 'ł'), - ('<', 'ś'), - ('=', '['), - ('>', 'ń'), - ('?', 'Ż'), - ('@', '%'), - ('[', 'ó'), - ('\'', 'ą'), - ('\\', ';'), - (']', '('), - ('^', '='), - ('_', 'ć'), - ('`', '<'), - ('{', 'ź'), - ('|', '$'), - ('}', ')'), - ('~', '>'), - ], - "com.apple.keylayout.Portuguese" => &[ - ('"', '`'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'ª'), - (';', 'º'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ç'), - ('\'', '´'), - (']', '~'), - ('^', '&'), - ('`', '<'), - ('{', 'Ç'), - ('}', '^'), - ('~', '>'), - ], - "com.apple.keylayout.Sami-PC" => &[ - ('"', 'Ŋ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('Q', 'Á'), - ('W', 'Š'), - ('X', 'Č'), - ('[', 'ø'), - ('\'', 'ŋ'), - ('\\', 'đ'), - (']', 'æ'), - ('^', '&'), - ('`', 'ž'), - ('q', 'á'), - ('w', 'š'), - ('x', 'č'), - ('{', 'Ø'), - ('|', 'Đ'), - ('}', 'Æ'), - ('~', 'Ž'), - ], - "com.apple.keylayout.Serbian-Latin" => &[ - ('"', 'Ć'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Slovak" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ľ'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ť'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ô'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', 'ä'), - ('^', '6'), - ('`', 'ň'), - ('{', 'Ú'), - ('}', 'Ä'), - ('~', 'Ň'), - ], - "com.apple.keylayout.Slovak-QWERTY" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ľ'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ť'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ô'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', 'ä'), - ('^', '6'), - ('`', 'ň'), - ('{', 'Ú'), - ('}', 'Ä'), - ('~', 'Ň'), - ], - "com.apple.keylayout.Slovenian" => &[ - ('"', 'Ć'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Spanish" => &[ - ('!', '¡'), - ('"', '¨'), - ('.', 'ç'), - ('/', '.'), - (':', 'º'), - (';', '´'), - ('<', '¿'), - ('>', 'Ç'), - ('@', '!'), - ('[', 'ñ'), - ('\'', '`'), - ('\\', '\''), - (']', ';'), - ('^', '/'), - ('`', '<'), - ('{', 'Ñ'), - ('|', '"'), - ('}', ':'), - ('~', '>'), - ], - "com.apple.keylayout.Spanish-ISO" => &[ - ('"', '¨'), - ('#', '·'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('.', 'ç'), - ('/', '.'), - (':', 'º'), - (';', '´'), - ('<', '¿'), - ('>', 'Ç'), - ('@', '"'), - ('[', 'ñ'), - ('\'', '`'), - ('\\', '\''), - (']', ';'), - ('^', '&'), - ('`', '<'), - ('{', 'Ñ'), - ('|', '"'), - ('}', '`'), - ('~', '>'), - ], - "com.apple.keylayout.Swedish" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Swedish-Pro" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.SwedishSami-PC" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '@'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.SwissFrench" => &[ - ('!', '+'), - ('"', '`'), - ('#', '*'), - ('$', 'ç'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('+', '!'), - ('/', '\''), - (':', 'ü'), - (';', 'è'), - ('<', ';'), - ('=', '¨'), - ('>', ':'), - ('@', '"'), - ('[', 'é'), - ('\'', '^'), - ('\\', '$'), - (']', 'à'), - ('^', '&'), - ('`', '<'), - ('{', 'ö'), - ('|', '£'), - ('}', 'ä'), - ('~', '>'), - ], - "com.apple.keylayout.SwissGerman" => &[ - ('!', '+'), - ('"', '`'), - ('#', '*'), - ('$', 'ç'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('+', '!'), - ('/', '\''), - (':', 'è'), - (';', 'ü'), - ('<', ';'), - ('=', '¨'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '^'), - ('\\', '$'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'é'), - ('|', '£'), - ('}', 'à'), - ('~', '>'), - ], - "com.apple.keylayout.Turkish" => &[ - ('"', '-'), - ('#', '"'), - ('$', '\''), - ('%', '('), - ('&', ')'), - ('(', '%'), - (')', ':'), - ('*', '_'), - (',', 'ö'), - ('-', 'ş'), - ('.', 'ç'), - ('/', '.'), - (':', '$'), - ('<', 'Ö'), - ('>', 'Ç'), - ('@', '*'), - ('[', 'ğ'), - ('\'', ','), - ('\\', 'ü'), - (']', 'ı'), - ('^', '/'), - ('_', 'Ş'), - ('`', '<'), - ('{', 'Ğ'), - ('|', 'Ü'), - ('}', 'I'), - ('~', '>'), - ], - "com.apple.keylayout.Turkish-QWERTY-PC" => &[ - ('"', 'I'), - ('#', '^'), - ('$', '+'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('+', ':'), - (',', 'ö'), - ('.', 'ç'), - ('/', '*'), - (':', 'Ş'), - (';', 'ş'), - ('<', 'Ö'), - ('=', '.'), - ('>', 'Ç'), - ('@', '\''), - ('[', 'ğ'), - ('\'', 'ı'), - ('\\', ','), - (']', 'ü'), - ('^', '&'), - ('`', '<'), - ('{', 'Ğ'), - ('|', ';'), - ('}', 'Ü'), - ('~', '>'), - ], - "com.apple.keylayout.Turkish-Standard" => &[ - ('"', 'Ş'), - ('#', '^'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (',', '.'), - ('.', ','), - (':', 'Ç'), - (';', 'ç'), - ('<', ':'), - ('=', '*'), - ('>', ';'), - ('@', '"'), - ('[', 'ğ'), - ('\'', 'ş'), - ('\\', 'ü'), - (']', 'ı'), - ('^', '&'), - ('`', 'ö'), - ('{', 'Ğ'), - ('|', 'Ü'), - ('}', 'I'), - ('~', 'Ö'), - ], - "com.apple.keylayout.Turkmen" => &[ - ('C', 'Ç'), - ('Q', 'Ä'), - ('V', 'Ý'), - ('X', 'Ü'), - ('[', 'ň'), - ('\\', 'ş'), - (']', 'ö'), - ('^', '№'), - ('`', 'ž'), - ('c', 'ç'), - ('q', 'ä'), - ('v', 'ý'), - ('x', 'ü'), - ('{', 'Ň'), - ('|', 'Ş'), - ('}', 'Ö'), - ('~', 'Ž'), - ], - "com.apple.keylayout.USInternational-PC" => &[('^', 'ˆ'), ('~', '˜')], - "com.apple.keylayout.Welsh" => &[('#', '£')], - - _ => return None, - }; - - Some(HashMap::from_iter(mappings.iter().cloned())) -} diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 30453def00..832550dc46 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -1,5 +1,5 @@ use super::{ - BoolExt, MacKeyboardLayout, MacKeyboardMapper, + BoolExt, MacKeyboardLayout, attributed_string::{NSAttributedString, NSMutableAttributedString}, events::key_to_native, renderer, @@ -8,9 +8,8 @@ use crate::{ Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString, CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher, MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform, - PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, - PlatformWindow, Result, SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, - hash, + PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result, + SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, hash, }; use anyhow::{Context as _, anyhow}; use block::ConcreteBlock; @@ -172,7 +171,6 @@ pub(crate) struct MacPlatformState { finish_launching: Option>, dock_menu: Option, menus: Option>, - keyboard_mapper: Rc, } impl Default for MacPlatform { @@ -191,9 +189,6 @@ impl MacPlatform { #[cfg(not(feature = "font-kit"))] let text_system = Arc::new(crate::NoopTextSystem::new()); - let keyboard_layout = MacKeyboardLayout::new(); - let keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id())); - Self(Mutex::new(MacPlatformState { headless, text_system, @@ -214,7 +209,6 @@ impl MacPlatform { dock_menu: None, on_keyboard_layout_change: None, menus: None, - keyboard_mapper, })) } @@ -354,19 +348,19 @@ impl MacPlatform { let mut mask = NSEventModifierFlags::empty(); for (modifier, flag) in &[ ( - keystroke.display_modifiers.platform, + keystroke.modifiers.platform, NSEventModifierFlags::NSCommandKeyMask, ), ( - keystroke.display_modifiers.control, + keystroke.modifiers.control, NSEventModifierFlags::NSControlKeyMask, ), ( - keystroke.display_modifiers.alt, + keystroke.modifiers.alt, NSEventModifierFlags::NSAlternateKeyMask, ), ( - keystroke.display_modifiers.shift, + keystroke.modifiers.shift, NSEventModifierFlags::NSShiftKeyMask, ), ] { @@ -379,7 +373,7 @@ impl MacPlatform { .initWithTitle_action_keyEquivalent_( ns_string(name), selector, - ns_string(key_to_native(&keystroke.display_key).as_ref()), + ns_string(key_to_native(&keystroke.key).as_ref()), ) .autorelease(); if Self::os_version() >= SemanticVersion::new(12, 0, 0) { @@ -888,10 +882,6 @@ impl Platform for MacPlatform { Box::new(MacKeyboardLayout::new()) } - fn keyboard_mapper(&self) -> Rc { - self.0.lock().keyboard_mapper.clone() - } - fn app_path(&self) -> Result { unsafe { let bundle: id = NSBundle::mainBundle(); @@ -1403,8 +1393,6 @@ extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) { extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) { let platform = unsafe { get_mac_platform(this) }; let mut lock = platform.0.lock(); - let keyboard_layout = MacKeyboardLayout::new(); - lock.keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id())); if let Some(mut callback) = lock.on_keyboard_layout_change.take() { drop(lock); callback(); diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index 15b909199f..00afcd81b5 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -1,9 +1,8 @@ use crate::{ AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels, - DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, - PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton, - ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task, - TestDisplay, TestWindow, WindowAppearance, WindowParams, size, + ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout, + PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, + SourceMetadata, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size, }; use anyhow::Result; use collections::VecDeque; @@ -238,10 +237,6 @@ impl Platform for TestPlatform { Box::new(TestKeyboardLayout) } - fn keyboard_mapper(&self) -> Rc { - Rc::new(DummyKeyboardMapper) - } - fn on_keyboard_layout_change(&self, _: Box) {} fn run(&self, _on_finish_launching: Box) { diff --git a/crates/gpui/src/platform/windows/dispatcher.rs b/crates/gpui/src/platform/windows/dispatcher.rs index f554dea128..e5b9c020d5 100644 --- a/crates/gpui/src/platform/windows/dispatcher.rs +++ b/crates/gpui/src/platform/windows/dispatcher.rs @@ -9,8 +9,10 @@ use parking::Parker; use parking_lot::Mutex; use util::ResultExt; use windows::{ + Foundation::TimeSpan, System::Threading::{ - ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemPriority, + ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemOptions, + WorkItemPriority, }, Win32::{ Foundation::{LPARAM, WPARAM}, @@ -54,7 +56,12 @@ impl WindowsDispatcher { Ok(()) }) }; - ThreadPool::RunWithPriorityAsync(&handler, WorkItemPriority::High).log_err(); + ThreadPool::RunWithPriorityAndOptionsAsync( + &handler, + WorkItemPriority::High, + WorkItemOptions::TimeSliced, + ) + .log_err(); } fn dispatch_on_threadpool_after(&self, runnable: Runnable, duration: Duration) { @@ -65,7 +72,12 @@ impl WindowsDispatcher { Ok(()) }) }; - ThreadPoolTimer::CreateTimer(&handler, duration.into()).log_err(); + let delay = TimeSpan { + // A time period expressed in 100-nanosecond units. + // 10,000,000 ticks per second + Duration: (duration.as_nanos() / 100) as i64, + }; + ThreadPoolTimer::CreateTimer(&handler, delay).log_err(); } } diff --git a/crates/gpui/src/platform/windows/keyboard.rs b/crates/gpui/src/platform/windows/keyboard.rs index 0eb97fbb0c..371feb70c2 100644 --- a/crates/gpui/src/platform/windows/keyboard.rs +++ b/crates/gpui/src/platform/windows/keyboard.rs @@ -1,31 +1,22 @@ use anyhow::Result; -use collections::HashMap; use windows::Win32::UI::{ Input::KeyboardAndMouse::{ - GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode, - VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, - VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, - VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT, + GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MapVirtualKeyW, ToUnicode, VIRTUAL_KEY, VK_0, + VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, VK_CONTROL, VK_MENU, + VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_OEM_102, + VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT, }, WindowsAndMessaging::KL_NAMELENGTH, }; use windows_core::HSTRING; -use crate::{ - KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper, -}; +use crate::{Modifiers, PlatformKeyboardLayout}; pub(crate) struct WindowsKeyboardLayout { id: String, name: String, } -pub(crate) struct WindowsKeyboardMapper { - key_to_vkey: HashMap, - vkey_to_key: HashMap, - vkey_to_shifted: HashMap, -} - impl PlatformKeyboardLayout for WindowsKeyboardLayout { fn id(&self) -> &str { &self.id @@ -36,65 +27,6 @@ impl PlatformKeyboardLayout for WindowsKeyboardLayout { } } -impl PlatformKeyboardMapper for WindowsKeyboardMapper { - fn map_key_equivalent( - &self, - mut keystroke: Keystroke, - use_key_equivalents: bool, - ) -> KeybindingKeystroke { - let Some((vkey, shifted_key)) = self.get_vkey_from_key(&keystroke.key, use_key_equivalents) - else { - return KeybindingKeystroke::from_keystroke(keystroke); - }; - if shifted_key && keystroke.modifiers.shift { - log::warn!( - "Keystroke '{}' has both shift and a shifted key, this is likely a bug", - keystroke.key - ); - } - - let shift = shifted_key || keystroke.modifiers.shift; - keystroke.modifiers.shift = false; - - let Some(key) = self.vkey_to_key.get(&vkey).cloned() else { - log::error!( - "Failed to map key equivalent '{:?}' to a valid key", - keystroke - ); - return KeybindingKeystroke::from_keystroke(keystroke); - }; - - keystroke.key = if shift { - let Some(shifted_key) = self.vkey_to_shifted.get(&vkey).cloned() else { - log::error!( - "Failed to map keystroke {:?} with virtual key '{:?}' to a shifted key", - keystroke, - vkey - ); - return KeybindingKeystroke::from_keystroke(keystroke); - }; - shifted_key - } else { - key.clone() - }; - - let modifiers = Modifiers { - shift, - ..keystroke.modifiers - }; - - KeybindingKeystroke { - inner: keystroke, - display_modifiers: modifiers, - display_key: key, - } - } - - fn get_key_equivalents(&self) -> Option<&HashMap> { - None - } -} - impl WindowsKeyboardLayout { pub(crate) fn new() -> Result { let mut buffer = [0u16; KL_NAMELENGTH as usize]; @@ -116,41 +48,6 @@ impl WindowsKeyboardLayout { } } -impl WindowsKeyboardMapper { - pub(crate) fn new() -> Self { - let mut key_to_vkey = HashMap::default(); - let mut vkey_to_key = HashMap::default(); - let mut vkey_to_shifted = HashMap::default(); - for vkey in CANDIDATE_VKEYS { - if let Some(key) = get_key_from_vkey(*vkey) { - key_to_vkey.insert(key.clone(), (vkey.0, false)); - vkey_to_key.insert(vkey.0, key); - } - let scan_code = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_VSC) }; - if scan_code == 0 { - continue; - } - if let Some(shifted_key) = get_shifted_key(*vkey, scan_code) { - key_to_vkey.insert(shifted_key.clone(), (vkey.0, true)); - vkey_to_shifted.insert(vkey.0, shifted_key); - } - } - Self { - key_to_vkey, - vkey_to_key, - vkey_to_shifted, - } - } - - fn get_vkey_from_key(&self, key: &str, use_key_equivalents: bool) -> Option<(u16, bool)> { - if use_key_equivalents { - get_vkey_from_key_with_us_layout(key) - } else { - self.key_to_vkey.get(key).cloned() - } - } -} - pub(crate) fn get_keystroke_key( vkey: VIRTUAL_KEY, scan_code: u32, @@ -243,134 +140,3 @@ pub(crate) fn generate_key_char( _ => None, } } - -fn get_vkey_from_key_with_us_layout(key: &str) -> Option<(u16, bool)> { - match key { - // ` => VK_OEM_3 - "`" => Some((VK_OEM_3.0, false)), - "~" => Some((VK_OEM_3.0, true)), - "1" => Some((VK_1.0, false)), - "!" => Some((VK_1.0, true)), - "2" => Some((VK_2.0, false)), - "@" => Some((VK_2.0, true)), - "3" => Some((VK_3.0, false)), - "#" => Some((VK_3.0, true)), - "4" => Some((VK_4.0, false)), - "$" => Some((VK_4.0, true)), - "5" => Some((VK_5.0, false)), - "%" => Some((VK_5.0, true)), - "6" => Some((VK_6.0, false)), - "^" => Some((VK_6.0, true)), - "7" => Some((VK_7.0, false)), - "&" => Some((VK_7.0, true)), - "8" => Some((VK_8.0, false)), - "*" => Some((VK_8.0, true)), - "9" => Some((VK_9.0, false)), - "(" => Some((VK_9.0, true)), - "0" => Some((VK_0.0, false)), - ")" => Some((VK_0.0, true)), - "-" => Some((VK_OEM_MINUS.0, false)), - "_" => Some((VK_OEM_MINUS.0, true)), - "=" => Some((VK_OEM_PLUS.0, false)), - "+" => Some((VK_OEM_PLUS.0, true)), - "[" => Some((VK_OEM_4.0, false)), - "{" => Some((VK_OEM_4.0, true)), - "]" => Some((VK_OEM_6.0, false)), - "}" => Some((VK_OEM_6.0, true)), - "\\" => Some((VK_OEM_5.0, false)), - "|" => Some((VK_OEM_5.0, true)), - ";" => Some((VK_OEM_1.0, false)), - ":" => Some((VK_OEM_1.0, true)), - "'" => Some((VK_OEM_7.0, false)), - "\"" => Some((VK_OEM_7.0, true)), - "," => Some((VK_OEM_COMMA.0, false)), - "<" => Some((VK_OEM_COMMA.0, true)), - "." => Some((VK_OEM_PERIOD.0, false)), - ">" => Some((VK_OEM_PERIOD.0, true)), - "/" => Some((VK_OEM_2.0, false)), - "?" => Some((VK_OEM_2.0, true)), - _ => None, - } -} - -const CANDIDATE_VKEYS: &[VIRTUAL_KEY] = &[ - VK_OEM_3, - VK_OEM_MINUS, - VK_OEM_PLUS, - VK_OEM_4, - VK_OEM_5, - VK_OEM_6, - VK_OEM_1, - VK_OEM_7, - VK_OEM_COMMA, - VK_OEM_PERIOD, - VK_OEM_2, - VK_OEM_102, - VK_OEM_8, - VK_ABNT_C1, - VK_0, - VK_1, - VK_2, - VK_3, - VK_4, - VK_5, - VK_6, - VK_7, - VK_8, - VK_9, -]; - -#[cfg(test)] -mod tests { - use crate::{Keystroke, Modifiers, PlatformKeyboardMapper, WindowsKeyboardMapper}; - - #[test] - fn test_keyboard_mapper() { - let mapper = WindowsKeyboardMapper::new(); - - // Normal case - let keystroke = Keystroke { - modifiers: Modifiers::control(), - key: "a".to_string(), - key_char: None, - }; - let mapped = mapper.map_key_equivalent(keystroke.clone(), true); - assert_eq!(mapped.inner, keystroke); - assert_eq!(mapped.display_key, "a"); - assert_eq!(mapped.display_modifiers, Modifiers::control()); - - // Shifted case, ctrl-$ - let keystroke = Keystroke { - modifiers: Modifiers::control(), - key: "$".to_string(), - key_char: None, - }; - let mapped = mapper.map_key_equivalent(keystroke.clone(), true); - assert_eq!(mapped.inner, keystroke); - assert_eq!(mapped.display_key, "4"); - assert_eq!(mapped.display_modifiers, Modifiers::control_shift()); - - // Shifted case, but shift is true - let keystroke = Keystroke { - modifiers: Modifiers::control_shift(), - key: "$".to_string(), - key_char: None, - }; - let mapped = mapper.map_key_equivalent(keystroke, true); - assert_eq!(mapped.inner.modifiers, Modifiers::control()); - assert_eq!(mapped.display_key, "4"); - assert_eq!(mapped.display_modifiers, Modifiers::control_shift()); - - // Windows style - let keystroke = Keystroke { - modifiers: Modifiers::control_shift(), - key: "4".to_string(), - key_char: None, - }; - let mapped = mapper.map_key_equivalent(keystroke, true); - assert_eq!(mapped.inner.modifiers, Modifiers::control()); - assert_eq!(mapped.inner.key, "$"); - assert_eq!(mapped.display_key, "4"); - assert_eq!(mapped.display_modifiers, Modifiers::control_shift()); - } -} diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 5ac2be2f23..6202e05fb3 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -351,10 +351,6 @@ impl Platform for WindowsPlatform { ) } - fn keyboard_mapper(&self) -> Rc { - Rc::new(WindowsKeyboardMapper::new()) - } - fn on_keyboard_layout_change(&self, callback: Box) { self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback); } diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index f7363395ae..4fc6039fd7 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -215,7 +215,6 @@ pub enum IconName { Tab, Terminal, TerminalAlt, - TerminalGhost, TextSnippet, TextThread, Thread, diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index 2dca57424b..b96557b391 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -401,19 +401,12 @@ pub fn init(cx: &mut App) { mod persistence { use std::path::PathBuf; - use db::{ - query, - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, - }; + use db::{define_connection, query, sqlez_macros::sql}; use workspace::{ItemId, WorkspaceDb, WorkspaceId}; - pub struct ImageViewerDb(ThreadSafeConnection); - - impl Domain for ImageViewerDb { - const NAME: &str = stringify!(ImageViewerDb); - - const MIGRATIONS: &[&str] = &[sql!( + define_connection! { + pub static ref IMAGE_VIEWER: ImageViewerDb = + &[sql!( CREATE TABLE image_viewers ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -424,11 +417,9 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]; + )]; } - db::static_connection!(IMAGE_VIEWER, ImageViewerDb, [WorkspaceDb]); - impl ImageViewerDb { query! { pub async fn save_image_path( diff --git a/crates/inspector_ui/Cargo.toml b/crates/inspector_ui/Cargo.toml index cefe888974..8e55a8a477 100644 --- a/crates/inspector_ui/Cargo.toml +++ b/crates/inspector_ui/Cargo.toml @@ -24,7 +24,6 @@ serde_json_lenient.workspace = true theme.workspace = true ui.workspace = true util.workspace = true -util_macros.workspace = true workspace-hack.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/inspector_ui/src/div_inspector.rs b/crates/inspector_ui/src/div_inspector.rs index c3d687e57a..0c2b16b9f4 100644 --- a/crates/inspector_ui/src/div_inspector.rs +++ b/crates/inspector_ui/src/div_inspector.rs @@ -25,7 +25,7 @@ use util::split_str_with_ranges; /// Path used for unsaved buffer that contains style json. To support the json language server, this /// matches the name used in the generated schemas. -const ZED_INSPECTOR_STYLE_JSON: &str = util_macros::path!("/zed-inspector-style.json"); +const ZED_INSPECTOR_STYLE_JSON: &str = "/zed-inspector-style.json"; pub(crate) struct DivInspector { state: State, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 4ddc2b3018..b106110c33 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1569,21 +1569,11 @@ impl Buffer { self.send_operation(op, true, cx); } - pub fn buffer_diagnostics( - &self, - for_server: Option, - ) -> Vec<&DiagnosticEntry> { - match for_server { - Some(server_id) => match self.diagnostics.binary_search_by_key(&server_id, |v| v.0) { - Ok(idx) => self.diagnostics[idx].1.iter().collect(), - Err(_) => Vec::new(), - }, - None => self - .diagnostics - .iter() - .flat_map(|(_, diagnostic_set)| diagnostic_set.iter()) - .collect(), - } + pub fn get_diagnostics(&self, server_id: LanguageServerId) -> Option<&DiagnosticSet> { + let Ok(idx) = self.diagnostics.binary_search_by_key(&server_id, |v| v.0) else { + return None; + }; + Some(&self.diagnostics[idx].1) } fn request_autoindent(&mut self, cx: &mut Context) { diff --git a/crates/language_model/src/fake_provider.rs b/crates/language_model/src/fake_provider.rs index b06a475f93..ebfd37d16c 100644 --- a/crates/language_model/src/fake_provider.rs +++ b/crates/language_model/src/fake_provider.rs @@ -4,16 +4,12 @@ use crate::{ LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, }; -use anyhow::anyhow; use futures::{FutureExt, channel::mpsc, future::BoxFuture, stream::BoxStream}; use gpui::{AnyView, App, AsyncApp, Entity, Task, Window}; use http_client::Result; use parking_lot::Mutex; use smol::stream::StreamExt; -use std::sync::{ - Arc, - atomic::{AtomicBool, Ordering::SeqCst}, -}; +use std::sync::Arc; #[derive(Clone)] pub struct FakeLanguageModelProvider { @@ -110,7 +106,6 @@ pub struct FakeLanguageModel { >, )>, >, - forbid_requests: AtomicBool, } impl Default for FakeLanguageModel { @@ -119,20 +114,11 @@ impl Default for FakeLanguageModel { provider_id: LanguageModelProviderId::from("fake".to_string()), provider_name: LanguageModelProviderName::from("Fake".to_string()), current_completion_txs: Mutex::new(Vec::new()), - forbid_requests: AtomicBool::new(false), } } } impl FakeLanguageModel { - pub fn allow_requests(&self) { - self.forbid_requests.store(false, SeqCst); - } - - pub fn forbid_requests(&self) { - self.forbid_requests.store(true, SeqCst); - } - pub fn pending_completions(&self) -> Vec { self.current_completion_txs .lock() @@ -265,18 +251,9 @@ impl LanguageModel for FakeLanguageModel { LanguageModelCompletionError, >, > { - if self.forbid_requests.load(SeqCst) { - async move { - Err(LanguageModelCompletionError::Other(anyhow!( - "requests are forbidden" - ))) - } - .boxed() - } else { - let (tx, rx) = mpsc::unbounded(); - self.current_completion_txs.lock().push((request, tx)); - async move { Ok(rx.boxed()) }.boxed() - } + let (tx, rx) = mpsc::unbounded(); + self.current_completion_txs.lock().push((request, tx)); + async move { Ok(rx.boxed()) }.boxed() } fn as_fake(&self) -> &Self { diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index d5313b6a3a..158bebcbbf 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -14,7 +14,7 @@ use client::Client; use cloud_llm_client::{CompletionMode, CompletionRequestStatus}; use futures::FutureExt; use futures::{StreamExt, future::BoxFuture, stream::BoxStream}; -use gpui::{AnyView, App, AsyncApp, SharedString, Task, Window}; +use gpui::{AnyElement, AnyView, App, AsyncApp, SharedString, Task, Window}; use http_client::{StatusCode, http}; use icons::IconName; use parking_lot::Mutex; @@ -640,14 +640,24 @@ pub trait LanguageModelProvider: 'static { window: &mut Window, cx: &mut App, ) -> AnyView; + fn must_accept_terms(&self, _cx: &App) -> bool { + false + } + fn render_accept_terms( + &self, + _view: LanguageModelProviderTosView, + _cx: &mut App, + ) -> Option { + None + } fn reset_credentials(&self, cx: &mut App) -> Task>; } -#[derive(Default, Clone)] +#[derive(Default, Clone, Copy)] pub enum ConfigurationViewTargetAgent { #[default] ZedAgent, - Other(SharedString), + Other(&'static str), } #[derive(PartialEq, Eq)] diff --git a/crates/language_model/src/registry.rs b/crates/language_model/src/registry.rs index 531c3615dc..bcbb3404a8 100644 --- a/crates/language_model/src/registry.rs +++ b/crates/language_model/src/registry.rs @@ -6,7 +6,6 @@ use collections::BTreeMap; use gpui::{App, Context, Entity, EventEmitter, Global, prelude::*}; use std::{str::FromStr, sync::Arc}; use thiserror::Error; -use util::maybe; pub fn init(cx: &mut App) { let registry = cx.new(|_cx| LanguageModelRegistry::default()); @@ -25,6 +24,9 @@ pub enum ConfigurationError { ModelNotFound, #[error("{} LLM provider is not configured.", .0.name().0)] ProviderNotAuthenticated(Arc), + #[error("Using the {} LLM provider requires accepting the Terms of Service.", + .0.name().0)] + ProviderPendingTermsAcceptance(Arc), } impl std::fmt::Debug for ConfigurationError { @@ -35,6 +37,9 @@ impl std::fmt::Debug for ConfigurationError { Self::ProviderNotAuthenticated(provider) => { write!(f, "ProviderNotAuthenticated({})", provider.id()) } + Self::ProviderPendingTermsAcceptance(provider) => { + write!(f, "ProviderPendingTermsAcceptance({})", provider.id()) + } } } } @@ -42,7 +47,9 @@ impl std::fmt::Debug for ConfigurationError { #[derive(Default)] pub struct LanguageModelRegistry { default_model: Option, - default_fast_model: Option, + /// This model is automatically configured by a user's environment after + /// authenticating all providers. It's only used when default_model is not available. + environment_fallback_model: Option, inline_assistant_model: Option, commit_message_model: Option, thread_summary_model: Option, @@ -98,9 +105,6 @@ impl ConfiguredModel { pub enum Event { DefaultModelChanged, - InlineAssistantModelChanged, - CommitMessageModelChanged, - ThreadSummaryModelChanged, ProviderStateChanged(LanguageModelProviderId), AddedProvider(LanguageModelProviderId), RemovedProvider(LanguageModelProviderId), @@ -194,6 +198,12 @@ impl LanguageModelRegistry { return Some(ConfigurationError::ProviderNotAuthenticated(model.provider)); } + if model.provider.must_accept_terms(cx) { + return Some(ConfigurationError::ProviderPendingTermsAcceptance( + model.provider, + )); + } + None } @@ -226,7 +236,7 @@ impl LanguageModelRegistry { cx: &mut Context, ) { let configured_model = model.and_then(|model| self.select_model(model, cx)); - self.set_inline_assistant_model(configured_model, cx); + self.set_inline_assistant_model(configured_model); } pub fn select_commit_message_model( @@ -235,7 +245,7 @@ impl LanguageModelRegistry { cx: &mut Context, ) { let configured_model = model.and_then(|model| self.select_model(model, cx)); - self.set_commit_message_model(configured_model, cx); + self.set_commit_message_model(configured_model); } pub fn select_thread_summary_model( @@ -244,7 +254,7 @@ impl LanguageModelRegistry { cx: &mut Context, ) { let configured_model = model.and_then(|model| self.select_model(model, cx)); - self.set_thread_summary_model(configured_model, cx); + self.set_thread_summary_model(configured_model); } /// Selects and sets the inline alternatives for language models based on @@ -278,68 +288,60 @@ impl LanguageModelRegistry { } pub fn set_default_model(&mut self, model: Option, cx: &mut Context) { - match (self.default_model.as_ref(), model.as_ref()) { + match (self.default_model(), model.as_ref()) { (Some(old), Some(new)) if old.is_same_as(new) => {} (None, None) => {} _ => cx.emit(Event::DefaultModelChanged), } - self.default_fast_model = maybe!({ - let provider = &model.as_ref()?.provider; - let fast_model = provider.default_fast_model(cx)?; - Some(ConfiguredModel { - provider: provider.clone(), - model: fast_model, - }) - }); self.default_model = model; } - pub fn set_inline_assistant_model( + pub fn set_environment_fallback_model( &mut self, model: Option, cx: &mut Context, ) { - match (self.inline_assistant_model.as_ref(), model.as_ref()) { - (Some(old), Some(new)) if old.is_same_as(new) => {} - (None, None) => {} - _ => cx.emit(Event::InlineAssistantModelChanged), + if self.default_model.is_none() { + match (self.environment_fallback_model.as_ref(), model.as_ref()) { + (Some(old), Some(new)) if old.is_same_as(new) => {} + (None, None) => {} + _ => cx.emit(Event::DefaultModelChanged), + } } + self.environment_fallback_model = model; + } + + pub fn set_inline_assistant_model(&mut self, model: Option) { self.inline_assistant_model = model; } - pub fn set_commit_message_model( - &mut self, - model: Option, - cx: &mut Context, - ) { - match (self.commit_message_model.as_ref(), model.as_ref()) { - (Some(old), Some(new)) if old.is_same_as(new) => {} - (None, None) => {} - _ => cx.emit(Event::CommitMessageModelChanged), - } + pub fn set_commit_message_model(&mut self, model: Option) { self.commit_message_model = model; } - pub fn set_thread_summary_model( - &mut self, - model: Option, - cx: &mut Context, - ) { - match (self.thread_summary_model.as_ref(), model.as_ref()) { - (Some(old), Some(new)) if old.is_same_as(new) => {} - (None, None) => {} - _ => cx.emit(Event::ThreadSummaryModelChanged), - } + pub fn set_thread_summary_model(&mut self, model: Option) { self.thread_summary_model = model; } + #[track_caller] pub fn default_model(&self) -> Option { #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() { return None; } - self.default_model.clone() + self.default_model + .clone() + .or_else(|| self.environment_fallback_model.clone()) + } + + pub fn default_fast_model(&self, cx: &App) -> Option { + let provider = self.default_model()?.provider; + let fast_model = provider.default_fast_model(cx)?; + Some(ConfiguredModel { + provider, + model: fast_model, + }) } pub fn inline_assistant_model(&self) -> Option { @@ -353,7 +355,7 @@ impl LanguageModelRegistry { .or_else(|| self.default_model.clone()) } - pub fn commit_message_model(&self) -> Option { + pub fn commit_message_model(&self, cx: &App) -> Option { #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() { return None; @@ -361,11 +363,11 @@ impl LanguageModelRegistry { self.commit_message_model .clone() - .or_else(|| self.default_fast_model.clone()) + .or_else(|| self.default_fast_model(cx)) .or_else(|| self.default_model.clone()) } - pub fn thread_summary_model(&self) -> Option { + pub fn thread_summary_model(&self, cx: &App) -> Option { #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() { return None; @@ -373,7 +375,7 @@ impl LanguageModelRegistry { self.thread_summary_model .clone() - .or_else(|| self.default_fast_model.clone()) + .or_else(|| self.default_fast_model(cx)) .or_else(|| self.default_model.clone()) } @@ -410,4 +412,34 @@ mod tests { let providers = registry.read(cx).providers(); assert!(providers.is_empty()); } + + #[gpui::test] + async fn test_configure_environment_fallback_model(cx: &mut gpui::TestAppContext) { + let registry = cx.new(|_| LanguageModelRegistry::default()); + + let provider = FakeLanguageModelProvider::default(); + registry.update(cx, |registry, cx| { + registry.register_provider(provider.clone(), cx); + }); + + cx.update(|cx| provider.authenticate(cx)).await.unwrap(); + + registry.update(cx, |registry, cx| { + let provider = registry.provider(&provider.id()).unwrap(); + + registry.set_environment_fallback_model( + Some(ConfiguredModel { + provider: provider.clone(), + model: provider.default_model(cx).unwrap(), + }), + cx, + ); + + let default_model = registry.default_model().unwrap(); + let fallback_model = registry.environment_fallback_model.clone().unwrap(); + + assert_eq!(default_model.model.id(), fallback_model.model.id()); + assert_eq!(default_model.provider.id(), fallback_model.provider.id()); + }); + } } diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml index b5bfb870f6..cd41478668 100644 --- a/crates/language_models/Cargo.toml +++ b/crates/language_models/Cargo.toml @@ -44,6 +44,7 @@ ollama = { workspace = true, features = ["schemars"] } open_ai = { workspace = true, features = ["schemars"] } open_router = { workspace = true, features = ["schemars"] } partial-json-fixer.workspace = true +project.workspace = true release_channel.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs index 738b72b0c9..beed306e74 100644 --- a/crates/language_models/src/language_models.rs +++ b/crates/language_models/src/language_models.rs @@ -3,8 +3,12 @@ use std::sync::Arc; use ::settings::{Settings, SettingsStore}; use client::{Client, UserStore}; use collections::HashSet; -use gpui::{App, Context, Entity}; -use language_model::{LanguageModelProviderId, LanguageModelRegistry}; +use futures::future; +use gpui::{App, AppContext as _, Context, Entity}; +use language_model::{ + AuthenticateError, ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry, +}; +use project::DisableAiSettings; use provider::deepseek::DeepSeekLanguageModelProvider; pub mod provider; @@ -13,7 +17,7 @@ pub mod ui; use crate::provider::anthropic::AnthropicLanguageModelProvider; use crate::provider::bedrock::BedrockLanguageModelProvider; -use crate::provider::cloud::CloudLanguageModelProvider; +use crate::provider::cloud::{self, CloudLanguageModelProvider}; use crate::provider::copilot_chat::CopilotChatLanguageModelProvider; use crate::provider::google::GoogleLanguageModelProvider; use crate::provider::lmstudio::LmStudioLanguageModelProvider; @@ -48,6 +52,13 @@ pub fn init(user_store: Entity, client: Arc, cx: &mut App) { cx, ); }); + + let mut already_authenticated = false; + if !DisableAiSettings::get_global(cx).disable_ai { + authenticate_all_providers(registry.clone(), cx); + already_authenticated = true; + } + cx.observe_global::(move |cx| { let openai_compatible_providers_new = AllLanguageModelSettings::get_global(cx) .openai_compatible @@ -65,6 +76,12 @@ pub fn init(user_store: Entity, client: Arc, cx: &mut App) { ); }); openai_compatible_providers = openai_compatible_providers_new; + already_authenticated = false; + } + + if !DisableAiSettings::get_global(cx).disable_ai && !already_authenticated { + authenticate_all_providers(registry.clone(), cx); + already_authenticated = true; } }) .detach(); @@ -151,3 +168,83 @@ fn register_language_model_providers( registry.register_provider(XAiLanguageModelProvider::new(client.http_client(), cx), cx); registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx); } + +/// Authenticates all providers in the [`LanguageModelRegistry`]. +/// +/// We do this so that we can populate the language selector with all of the +/// models from the configured providers. +/// +/// This function won't do anything if AI is disabled. +fn authenticate_all_providers(registry: Entity, cx: &mut App) { + let providers_to_authenticate = registry + .read(cx) + .providers() + .iter() + .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx))) + .collect::>(); + + let mut tasks = Vec::with_capacity(providers_to_authenticate.len()); + + for (provider_id, provider_name, authenticate_task) in providers_to_authenticate { + tasks.push(cx.background_spawn(async move { + if let Err(err) = authenticate_task.await { + if matches!(err, AuthenticateError::CredentialsNotFound) { + // Since we're authenticating these providers in the + // background for the purposes of populating the + // language selector, we don't care about providers + // where the credentials are not found. + } else { + // Some providers have noisy failure states that we + // don't want to spam the logs with every time the + // language model selector is initialized. + // + // Ideally these should have more clear failure modes + // that we know are safe to ignore here, like what we do + // with `CredentialsNotFound` above. + match provider_id.0.as_ref() { + "lmstudio" | "ollama" => { + // LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated". + // + // These fail noisily, so we don't log them. + } + "copilot_chat" => { + // Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors. + } + _ => { + log::error!( + "Failed to authenticate provider: {}: {err}", + provider_name.0 + ); + } + } + } + } + })); + } + + let all_authenticated_future = future::join_all(tasks); + + cx.spawn(async move |cx| { + all_authenticated_future.await; + + registry + .update(cx, |registry, cx| { + let cloud_provider = registry.provider(&cloud::PROVIDER_ID); + let fallback_model = cloud_provider + .iter() + .chain(registry.providers().iter()) + .find(|provider| provider.is_authenticated(cx)) + .and_then(|provider| { + Some(ConfiguredModel { + provider: provider.clone(), + model: provider + .default_model(cx) + .or_else(|| provider.recommended_models(cx).first().cloned())?, + }) + }); + registry.set_environment_fallback_model(fallback_model, cx); + }) + .ok(); + }) + .detach(); +} diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index c492edeaf5..0d061c0587 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -1041,9 +1041,9 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match &self.target_agent { - ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Anthropic".into(), - ConfigurationViewTargetAgent::Other(agent) => agent.clone(), + .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match self.target_agent { + ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Anthropic", + ConfigurationViewTargetAgent::Other(agent) => agent, }))) .child( List::new() diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index b473d06357..8e4b786935 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -23,9 +23,9 @@ use language_model::{ AuthenticateError, LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, - LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, - LanguageModelToolSchemaFormat, LlmApiToken, ModelRequestLimitReachedError, - PaymentRequiredError, RateLimiter, RefreshLlmTokenListener, + LanguageModelProviderState, LanguageModelProviderTosView, LanguageModelRequest, + LanguageModelToolChoice, LanguageModelToolSchemaFormat, LlmApiToken, + ModelRequestLimitReachedError, PaymentRequiredError, RateLimiter, RefreshLlmTokenListener, }; use release_channel::AppVersion; use schemars::JsonSchema; @@ -44,8 +44,8 @@ use crate::provider::anthropic::{AnthropicEventMapper, count_anthropic_tokens, i use crate::provider::google::{GoogleEventMapper, into_google}; use crate::provider::open_ai::{OpenAiEventMapper, count_open_ai_tokens, into_open_ai}; -const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID; -const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME; +pub const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID; +pub const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME; #[derive(Default, Clone, Debug, PartialEq)] pub struct ZedDotDevSettings { @@ -118,6 +118,7 @@ pub struct State { llm_api_token: LlmApiToken, user_store: Entity, status: client::Status, + accept_terms_of_service_task: Option>>, models: Vec>, default_model: Option>, default_fast_model: Option>, @@ -141,12 +142,13 @@ impl State { llm_api_token: LlmApiToken::default(), user_store, status, + accept_terms_of_service_task: None, models: Vec::new(), default_model: None, default_fast_model: None, recommended_models: Vec::new(), _fetch_models_task: cx.spawn(async move |this, cx| { - maybe!(async move { + maybe!(async { let (client, llm_api_token) = this .read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?; @@ -195,6 +197,24 @@ impl State { state.update(cx, |_, cx| cx.notify()) }) } + + fn has_accepted_terms_of_service(&self, cx: &App) -> bool { + self.user_store.read(cx).has_accepted_terms_of_service() + } + + fn accept_terms_of_service(&mut self, cx: &mut Context) { + let user_store = self.user_store.clone(); + self.accept_terms_of_service_task = Some(cx.spawn(async move |this, cx| { + let _ = user_store + .update(cx, |store, cx| store.accept_terms_of_service(cx))? + .await; + this.update(cx, |this, cx| { + this.accept_terms_of_service_task = None; + cx.notify() + }) + })); + } + fn update_models(&mut self, response: ListModelsResponse, cx: &mut Context) { let mut models = Vec::new(); @@ -364,7 +384,7 @@ impl LanguageModelProvider for CloudLanguageModelProvider { fn is_authenticated(&self, cx: &App) -> bool { let state = self.state.read(cx); - !state.is_signed_out(cx) + !state.is_signed_out(cx) && state.has_accepted_terms_of_service(cx) } fn authenticate(&self, _cx: &mut App) -> Task> { @@ -381,11 +401,112 @@ impl LanguageModelProvider for CloudLanguageModelProvider { .into() } + fn must_accept_terms(&self, cx: &App) -> bool { + !self.state.read(cx).has_accepted_terms_of_service(cx) + } + + fn render_accept_terms( + &self, + view: LanguageModelProviderTosView, + cx: &mut App, + ) -> Option { + let state = self.state.read(cx); + if state.has_accepted_terms_of_service(cx) { + return None; + } + Some( + render_accept_terms(view, state.accept_terms_of_service_task.is_some(), { + let state = self.state.clone(); + move |_window, cx| { + state.update(cx, |state, cx| state.accept_terms_of_service(cx)); + } + }) + .into_any_element(), + ) + } + fn reset_credentials(&self, _cx: &mut App) -> Task> { Task::ready(Ok(())) } } +fn render_accept_terms( + view_kind: LanguageModelProviderTosView, + accept_terms_of_service_in_progress: bool, + accept_terms_callback: impl Fn(&mut Window, &mut App) + 'static, +) -> impl IntoElement { + let thread_fresh_start = matches!(view_kind, LanguageModelProviderTosView::ThreadFreshStart); + let thread_empty_state = matches!(view_kind, LanguageModelProviderTosView::ThreadEmptyState); + + let terms_button = Button::new("terms_of_service", "Terms of Service") + .style(ButtonStyle::Subtle) + .icon(IconName::ArrowUpRight) + .icon_color(Color::Muted) + .icon_size(IconSize::Small) + .when(thread_empty_state, |this| this.label_size(LabelSize::Small)) + .on_click(move |_, _window, cx| cx.open_url("https://zed.dev/terms-of-service")); + + let button_container = h_flex().child( + Button::new("accept_terms", "I accept the Terms of Service") + .when(!thread_empty_state, |this| { + this.full_width() + .style(ButtonStyle::Tinted(TintColor::Accent)) + .icon(IconName::Check) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + }) + .when(thread_empty_state, |this| { + this.style(ButtonStyle::Tinted(TintColor::Warning)) + .label_size(LabelSize::Small) + }) + .disabled(accept_terms_of_service_in_progress) + .on_click(move |_, window, cx| (accept_terms_callback)(window, cx)), + ); + + if thread_empty_state { + h_flex() + .w_full() + .flex_wrap() + .justify_between() + .child( + h_flex() + .child( + Label::new("To start using Zed AI, please read and accept the") + .size(LabelSize::Small), + ) + .child(terms_button), + ) + .child(button_container) + } else { + v_flex() + .w_full() + .gap_2() + .child( + h_flex() + .flex_wrap() + .when(thread_fresh_start, |this| this.justify_center()) + .child(Label::new( + "To start using Zed AI, please read and accept the", + )) + .child(terms_button), + ) + .child({ + match view_kind { + LanguageModelProviderTosView::TextThreadPopup => { + button_container.w_full().justify_end() + } + LanguageModelProviderTosView::Configuration => { + button_container.w_full().justify_start() + } + LanguageModelProviderTosView::ThreadFreshStart => { + button_container.w_full().justify_center() + } + LanguageModelProviderTosView::ThreadEmptyState => div().w_0(), + } + }) + } +} + pub struct CloudLanguageModel { id: LanguageModelId, model: Arc, @@ -986,7 +1107,10 @@ struct ZedAiConfiguration { plan: Option, subscription_period: Option<(DateTime, DateTime)>, eligible_for_trial: bool, + has_accepted_terms_of_service: bool, account_too_young: bool, + accept_terms_of_service_in_progress: bool, + accept_terms_of_service_callback: Arc, sign_in_callback: Arc, } @@ -1052,30 +1176,58 @@ impl RenderOnce for ZedAiConfiguration { ); } - v_flex().gap_2().w_full().map(|this| { - if self.account_too_young { - this.child(young_account_banner).child( - Button::new("upgrade", "Upgrade to Pro") - .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent)) - .full_width() - .on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))), - ) - } else { - this.text_sm() - .child(subscription_text) - .child(manage_subscription_buttons) - } - }) + v_flex() + .gap_2() + .w_full() + .when(!self.has_accepted_terms_of_service, |this| { + this.child(render_accept_terms( + LanguageModelProviderTosView::Configuration, + self.accept_terms_of_service_in_progress, + { + let callback = self.accept_terms_of_service_callback.clone(); + move |window, cx| (callback)(window, cx) + }, + )) + }) + .map(|this| { + if self.has_accepted_terms_of_service && self.account_too_young { + this.child(young_account_banner).child( + Button::new("upgrade", "Upgrade to Pro") + .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent)) + .full_width() + .on_click(|_, _, cx| { + cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)) + }), + ) + } else if self.has_accepted_terms_of_service { + this.text_sm() + .child(subscription_text) + .child(manage_subscription_buttons) + } else { + this + } + }) + .when(self.has_accepted_terms_of_service, |this| this) } } struct ConfigurationView { state: Entity, + accept_terms_of_service_callback: Arc, sign_in_callback: Arc, } impl ConfigurationView { fn new(state: Entity) -> Self { + let accept_terms_of_service_callback = Arc::new({ + let state = state.clone(); + move |_window: &mut Window, cx: &mut App| { + state.update(cx, |state, cx| { + state.accept_terms_of_service(cx); + }); + } + }); + let sign_in_callback = Arc::new({ let state = state.clone(); move |_window: &mut Window, cx: &mut App| { @@ -1087,6 +1239,7 @@ impl ConfigurationView { Self { state, + accept_terms_of_service_callback, sign_in_callback, } } @@ -1102,7 +1255,10 @@ impl Render for ConfigurationView { plan: user_store.plan(), subscription_period: user_store.subscription_period(), eligible_for_trial: user_store.trial_started_at().is_none(), + has_accepted_terms_of_service: state.has_accepted_terms_of_service(cx), account_too_young: user_store.account_too_young(), + accept_terms_of_service_in_progress: state.accept_terms_of_service_task.is_some(), + accept_terms_of_service_callback: self.accept_terms_of_service_callback.clone(), sign_in_callback: self.sign_in_callback.clone(), } } @@ -1127,6 +1283,7 @@ impl Component for ZedAiConfiguration { plan: Option, eligible_for_trial: bool, account_too_young: bool, + has_accepted_terms_of_service: bool, ) -> AnyElement { ZedAiConfiguration { is_connected, @@ -1135,7 +1292,10 @@ impl Component for ZedAiConfiguration { .is_some() .then(|| (Utc::now(), Utc::now() + chrono::Duration::days(7))), eligible_for_trial, + has_accepted_terms_of_service, account_too_young, + accept_terms_of_service_in_progress: false, + accept_terms_of_service_callback: Arc::new(|_, _| {}), sign_in_callback: Arc::new(|_, _| {}), } .into_any_element() @@ -1146,30 +1306,33 @@ impl Component for ZedAiConfiguration { .p_4() .gap_4() .children(vec![ - single_example("Not connected", configuration(false, None, false, false)), + single_example( + "Not connected", + configuration(false, None, false, false, true), + ), single_example( "Accept Terms of Service", - configuration(true, None, true, false), + configuration(true, None, true, false, false), ), single_example( "No Plan - Not eligible for trial", - configuration(true, None, false, false), + configuration(true, None, false, false, true), ), single_example( "No Plan - Eligible for trial", - configuration(true, None, true, false), + configuration(true, None, true, false, true), ), single_example( "Free Plan", - configuration(true, Some(Plan::ZedFree), true, false), + configuration(true, Some(Plan::ZedFree), true, false, true), ), single_example( "Zed Pro Trial Plan", - configuration(true, Some(Plan::ZedProTrial), true, false), + configuration(true, Some(Plan::ZedProTrial), true, false, true), ), single_example( "Zed Pro Plan", - configuration(true, Some(Plan::ZedPro), true, false), + configuration(true, Some(Plan::ZedPro), true, false, true), ), ]) .into_any_element(), diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index f252ab7aa3..566620675e 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -921,9 +921,9 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match &self.target_agent { - ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Google AI".into(), - ConfigurationViewTargetAgent::Other(agent) => agent.clone(), + .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match self.target_agent { + ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Google AI", + ConfigurationViewTargetAgent::Other(agent) => agent, }))) .child( List::new() diff --git a/crates/language_tools/src/key_context_view.rs b/crates/language_tools/src/key_context_view.rs index 4140713544..057259d114 100644 --- a/crates/language_tools/src/key_context_view.rs +++ b/crates/language_tools/src/key_context_view.rs @@ -4,6 +4,7 @@ use gpui::{ }; use itertools::Itertools; use serde_json::json; +use settings::get_key_equivalents; use ui::{Button, ButtonStyle}; use ui::{ ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement, Label, LabelCommon, @@ -168,8 +169,7 @@ impl Item for KeyContextView { impl Render for KeyContextView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { use itertools::Itertools; - - let key_equivalents = cx.keyboard_mapper().get_key_equivalents(); + let key_equivalents = get_key_equivalents(cx.keyboard_layout().id()); v_flex() .id("key-context-view") .overflow_scroll() diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index d5206c1f26..43c0365291 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -1743,5 +1743,6 @@ pub enum Event { } impl EventEmitter for LogStore {} +impl EventEmitter for LspLogView {} impl EventEmitter for LspLogView {} impl EventEmitter for LspLogView {} diff --git a/crates/languages/src/javascript/highlights.scm b/crates/languages/src/javascript/highlights.scm index ebeac7efff..9d5ebbaf71 100644 --- a/crates/languages/src/javascript/highlights.scm +++ b/crates/languages/src/javascript/highlights.scm @@ -231,7 +231,6 @@ "implements" "interface" "keyof" - "module" "namespace" "private" "protected" @@ -251,4 +250,4 @@ (jsx_closing_element ([""]) @punctuation.bracket.jsx) (jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx) (jsx_attribute "=" @punctuation.delimiter.jsx) -(jsx_text) @text.jsx +(jsx_text) @text.jsx \ No newline at end of file diff --git a/crates/languages/src/javascript/injections.scm b/crates/languages/src/javascript/injections.scm index dbec1937b1..7baba5f227 100644 --- a/crates/languages/src/javascript/injections.scm +++ b/crates/languages/src/javascript/injections.scm @@ -11,21 +11,6 @@ (#set! injection.language "css")) ) -(call_expression - function: (member_expression - object: (identifier) @_obj (#eq? @_obj "styled") - property: (property_identifier)) - arguments: (template_string (string_fragment) @injection.content - (#set! injection.language "css")) -) - -(call_expression - function: (call_expression - function: (identifier) @_name (#eq? @_name "styled")) - arguments: (template_string (string_fragment) @injection.content - (#set! injection.language "css")) -) - (call_expression function: (identifier) @_name (#eq? @_name "html") arguments: (template_string) @injection.content diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 3e8dce756b..c6c7357148 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -510,6 +510,20 @@ impl LspAdapter for RustLspAdapter { } } + let cargo_diagnostics_fetched_separately = ProjectSettings::get_global(cx) + .diagnostics + .fetch_cargo_diagnostics(); + if cargo_diagnostics_fetched_separately { + let disable_check_on_save = json!({ + "checkOnSave": false, + }); + if let Some(initialization_options) = &mut original.initialization_options { + merge_json_value_into(disable_check_on_save, initialization_options); + } else { + original.initialization_options = Some(disable_check_on_save); + } + } + Ok(original) } } diff --git a/crates/languages/src/tsx/highlights.scm b/crates/languages/src/tsx/highlights.scm index f7cb987831..5e2fbbf63a 100644 --- a/crates/languages/src/tsx/highlights.scm +++ b/crates/languages/src/tsx/highlights.scm @@ -237,7 +237,6 @@ "implements" "interface" "keyof" - "module" "namespace" "private" "protected" @@ -257,4 +256,4 @@ (jsx_closing_element ([""]) @punctuation.bracket.jsx) (jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx) (jsx_attribute "=" @punctuation.delimiter.jsx) -(jsx_text) @text.jsx +(jsx_text) @text.jsx \ No newline at end of file diff --git a/crates/languages/src/tsx/injections.scm b/crates/languages/src/tsx/injections.scm index 9eec01cc89..48da80995b 100644 --- a/crates/languages/src/tsx/injections.scm +++ b/crates/languages/src/tsx/injections.scm @@ -11,21 +11,6 @@ (#set! injection.language "css")) ) -(call_expression - function: (member_expression - object: (identifier) @_obj (#eq? @_obj "styled") - property: (property_identifier)) - arguments: (template_string (string_fragment) @injection.content - (#set! injection.language "css")) -) - -(call_expression - function: (call_expression - function: (identifier) @_name (#eq? @_name "styled")) - arguments: (template_string (string_fragment) @injection.content - (#set! injection.language "css")) -) - (call_expression function: (identifier) @_name (#eq? @_name "html") arguments: (template_string (string_fragment) @injection.content diff --git a/crates/languages/src/typescript/highlights.scm b/crates/languages/src/typescript/highlights.scm index 84cbbae77d..af37ef6415 100644 --- a/crates/languages/src/typescript/highlights.scm +++ b/crates/languages/src/typescript/highlights.scm @@ -248,7 +248,6 @@ "is" "keyof" "let" - "module" "namespace" "new" "of" @@ -273,4 +272,4 @@ "while" "with" "yield" -] @keyword +] @keyword \ No newline at end of file diff --git a/crates/languages/src/typescript/injections.scm b/crates/languages/src/typescript/injections.scm index 1ca1e9ad59..7affdc5b75 100644 --- a/crates/languages/src/typescript/injections.scm +++ b/crates/languages/src/typescript/injections.scm @@ -15,21 +15,6 @@ (#set! injection.language "css")) ) -(call_expression - function: (member_expression - object: (identifier) @_obj (#eq? @_obj "styled") - property: (property_identifier)) - arguments: (template_string (string_fragment) @injection.content - (#set! injection.language "css")) -) - -(call_expression - function: (call_expression - function: (identifier) @_name (#eq? @_name "styled")) - arguments: (template_string (string_fragment) @injection.content - (#set! injection.language "css")) -) - (call_expression function: (identifier) @_name (#eq? @_name "html") arguments: (template_string) @injection.content diff --git a/crates/livekit_client/Cargo.toml b/crates/livekit_client/Cargo.toml index 3575325ac0..467c2f7437 100644 --- a/crates/livekit_client/Cargo.toml +++ b/crates/livekit_client/Cargo.toml @@ -41,7 +41,8 @@ tokio-tungstenite.workspace = true util.workspace = true workspace-hack.workspace = true -rodio = { workspace = true, features = ["wav_output"] } +rodio = { workspace = true, features = ["wav_output", "recording"] } +dasp_sample = "0.11" [target.'cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))'.dependencies] libwebrtc = { rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d", git = "https://github.com/zed-industries/livekit-rust-sdks" } diff --git a/crates/livekit_client/src/lib.rs b/crates/livekit_client/src/lib.rs index 055aa3704e..71638df2ec 100644 --- a/crates/livekit_client/src/lib.rs +++ b/crates/livekit_client/src/lib.rs @@ -9,19 +9,19 @@ use rodio::DeviceTrait as _; mod record; pub use record::CaptureInput; -#[cfg(not(any( - test, - feature = "test-support", - all(target_os = "windows", target_env = "gnu"), - target_os = "freebsd" -)))] +// #[cfg(not(any( +// test, +// feature = "test-support", +// all(target_os = "windows", target_env = "gnu"), +// target_os = "freebsd" +// )))] mod livekit_client; -#[cfg(not(any( - test, - feature = "test-support", - all(target_os = "windows", target_env = "gnu"), - target_os = "freebsd" -)))] +// #[cfg(not(any( +// test, +// feature = "test-support", +// all(target_os = "windows", target_env = "gnu"), +// target_os = "freebsd" +// )))] pub use livekit_client::*; // If you need proper LSP in livekit_client you've got to comment @@ -29,27 +29,27 @@ pub use livekit_client::*; // - the mods: mock_client & test and their conditional blocks // - the pub use mock_client::* and their conditional blocks -#[cfg(any( - test, - feature = "test-support", - all(target_os = "windows", target_env = "gnu"), - target_os = "freebsd" -))] -mod mock_client; -#[cfg(any( - test, - feature = "test-support", - all(target_os = "windows", target_env = "gnu"), - target_os = "freebsd" -))] -pub mod test; -#[cfg(any( - test, - feature = "test-support", - all(target_os = "windows", target_env = "gnu"), - target_os = "freebsd" -))] -pub use mock_client::*; +// #[cfg(any( +// test, +// feature = "test-support", +// all(target_os = "windows", target_env = "gnu"), +// target_os = "freebsd" +// ))] +// mod mock_client; +// #[cfg(any( +// test, +// feature = "test-support", +// all(target_os = "windows", target_env = "gnu"), +// target_os = "freebsd" +// ))] +// pub mod test; +// #[cfg(any( +// test, +// feature = "test-support", +// all(target_os = "windows", target_env = "gnu"), +// target_os = "freebsd" +// ))] +// pub use mock_client::*; #[derive(Debug, Clone)] pub enum Participant { diff --git a/crates/livekit_client/src/livekit_client.rs b/crates/livekit_client/src/livekit_client.rs index 0751b014f4..c14a29bee3 100644 --- a/crates/livekit_client/src/livekit_client.rs +++ b/crates/livekit_client/src/livekit_client.rs @@ -99,7 +99,7 @@ impl Room { &self, cx: &mut AsyncApp, ) -> Result<(LocalTrackPublication, playback::AudioStream)> { - let (track, stream) = self.playback.capture_local_microphone_track()?; + let (track, stream) = self.playback.capture_local_microphone_track(&cx)?; let publication = self .local_participant() .publish_track( diff --git a/crates/livekit_client/src/livekit_client/playback.rs b/crates/livekit_client/src/livekit_client/playback.rs index d6b64dbaca..8fa0e7929a 100644 --- a/crates/livekit_client/src/livekit_client/playback.rs +++ b/crates/livekit_client/src/livekit_client/playback.rs @@ -1,10 +1,14 @@ use anyhow::{Context as _, Result}; +use audio::AudioSettings; +use cpal::Sample; use cpal::traits::{DeviceTrait, StreamTrait as _}; +use dasp_sample::ToSample; use futures::channel::mpsc::UnboundedSender; use futures::{Stream, StreamExt as _}; use gpui::{ - BackgroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, Task, + AsyncApp, BackgroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, + Task, }; use libwebrtc::native::{apm, audio_mixer, audio_resampler}; use livekit::track; @@ -17,14 +21,19 @@ use livekit::webrtc::{ video_source::{RtcVideoSource, VideoResolution, native::NativeVideoSource}, video_stream::native::NativeVideoStream, }; +use log::info; use parking_lot::Mutex; use rodio::Source; +use rodio::source::{LimitSettings, UniformSourceIterator}; +use settings::Settings; use std::cell::RefCell; +use std::num::NonZero; use std::sync::Weak; use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; +use std::sync::mpsc::{TryRecvError, channel}; use std::time::Duration; use std::{borrow::Cow, collections::VecDeque, sync::Arc, thread}; -use util::{ResultExt as _, maybe}; +use util::{ResultExt as _, debug_panic, maybe}; mod source; @@ -40,8 +49,8 @@ pub(crate) struct AudioStack { // 16kHz, 32kHz and 48kHz. As 48 is the most common "next step up" // for audio output devices like speakers/bluetooth, we just hard-code // this; and downsample when we need to. -const SAMPLE_RATE: u32 = 48000; -const NUM_CHANNELS: u32 = 2; +const SAMPLE_RATE: NonZero = NonZero::new(48000).expect("not zero"); +const NUM_CHANNELS: NonZero = NonZero::new(2).expect("not zero"); pub(crate) fn play_remote_audio_track( track: &livekit::track::RemoteAudioTrack, @@ -50,6 +59,7 @@ pub(crate) fn play_remote_audio_track( let stop_handle = Arc::new(AtomicBool::new(false)); let stop_handle_clone = stop_handle.clone(); let stream = source::LiveKitStream::new(cx.background_executor(), track) + .process_buffer(|| apm) .stoppable() .periodic_access(Duration::from_millis(50), move |s| { if stop_handle.load(Ordering::Relaxed) { @@ -90,8 +100,8 @@ impl AudioStack { let next_ssrc = self.next_ssrc.fetch_add(1, Ordering::Relaxed); let source = AudioMixerSource { ssrc: next_ssrc, - sample_rate: SAMPLE_RATE, - num_channels: NUM_CHANNELS, + sample_rate: SAMPLE_RATE.get(), + num_channels: NUM_CHANNELS.get() as u32, buffer: Arc::default(), }; self.mixer.lock().add_source(source.clone()); @@ -131,7 +141,7 @@ impl AudioStack { let apm = self.apm.clone(); let mixer = self.mixer.clone(); async move { - Self::play_output(apm, mixer, SAMPLE_RATE, NUM_CHANNELS) + Self::play_output(apm, mixer, SAMPLE_RATE.get(), NUM_CHANNELS.get().into()) .await .log_err(); } @@ -142,12 +152,13 @@ impl AudioStack { pub(crate) fn capture_local_microphone_track( &self, + cx: &AsyncApp, ) -> Result<(crate::LocalAudioTrack, AudioStream)> { let source = NativeAudioSource::new( // n.b. this struct's options are always ignored, noise cancellation is provided by apm. AudioSourceOptions::default(), - SAMPLE_RATE, - NUM_CHANNELS, + SAMPLE_RATE.get(), + NUM_CHANNELS.get().into(), 10, ); @@ -166,8 +177,16 @@ impl AudioStack { } } }); + let rodio_pipeline = + AudioSettings::try_read_global(cx, |setting| setting.rodio_audio).unwrap_or(false); let capture_task = self.executor.spawn(async move { - Self::capture_input(apm, frame_tx, SAMPLE_RATE, NUM_CHANNELS).await + if rodio_pipeline { + info!("Using experimental.rodio_audio audio pipeline"); + Self::capture_input_rodio(apm, frame_tx).await + } else { + Self::capture_input(apm, frame_tx, SAMPLE_RATE.get(), NUM_CHANNELS.get().into()) + .await + } }); let on_drop = util::defer(|| { @@ -254,6 +273,86 @@ impl AudioStack { } } + async fn capture_input_rodio( + apm: Arc>, + frame_tx: UnboundedSender>, + ) -> Result<()> { + use crate::livekit_client::playback::source::RodioExt; + const NUM_CHANNELS: usize = 1; + const LIVEKIT_BUFFER_SIZE: usize = + (SAMPLE_RATE.get() as usize / 100) * NUM_CHANNELS as usize; + + let (stream_error_tx, stream_error_rx) = channel(); + + thread::spawn(move || { + let stream = rodio::microphone::MicrophoneBuilder::new() + .default_device()? + .default_config()? + .prefer_sample_rates([ + SAMPLE_RATE, + SAMPLE_RATE.saturating_mul(NonZero::new(2).expect("not zero")), + ]) + .prefer_channel_counts([ + NonZero::new(1).expect("not zero"), + NonZero::new(2).expect("not zero"), + ]) + .prefer_buffer_sizes(512..) + .open_stream()?; + let mut stream = UniformSourceIterator::new( + stream, + NonZero::new(1).expect("1 is not zero"), + SAMPLE_RATE, + ) + .limit(LimitSettings::live_performance()) + .process_buffer::(|buffer| { + let mut int_buffer: [i16; _] = buffer.map(|s| s.to_sample()); + if let Err(e) = apm + .lock() + .process_stream( + &mut int_buffer, + SAMPLE_RATE.get() as i32, + NUM_CHANNELS as i32, + ) + .context("livekit audio processor error") + { + let _ = stream_error_tx.send(e); + } else { + for (sample, processed) in buffer.iter_mut().zip(&int_buffer) { + *sample = (*processed).to_sample_(); + } + } + }) + .automatic_gain_control(1.0, 4.0, 0.0, 5.0); + + loop { + let sampled: Vec<_> = stream + .by_ref() + .take(LIVEKIT_BUFFER_SIZE) + .map(|s| s.to_sample()) + .collect(); + + match stream_error_rx.try_recv() { + Ok(apm_error) => return Err::<(), _>(apm_error), + Err(TryRecvError::Disconnected) => { + debug_panic!("Stream should end on its own without sending an error") + } + Err(TryRecvError::Empty) => (), + } + + frame_tx + .unbounded_send(AudioFrame { + sample_rate: SAMPLE_RATE.get(), + num_channels: NUM_CHANNELS as u32, + samples_per_channel: sampled.len() as u32 / NUM_CHANNELS as u32, + data: Cow::Owned(sampled), + }) + .context("Failed to send audio frame")? + } + }); + + Ok(()) + } + async fn capture_input( apm: Arc>, frame_tx: UnboundedSender>, @@ -346,6 +445,8 @@ impl AudioStack { } } +use crate::livekit_client::playback::source::RodioExt; + use super::LocalVideoTrack; pub enum AudioStream { diff --git a/crates/livekit_client/src/livekit_client/playback/source.rs b/crates/livekit_client/src/livekit_client/playback/source.rs index 021640247d..62d949f95d 100644 --- a/crates/livekit_client/src/livekit_client/playback/source.rs +++ b/crates/livekit_client/src/livekit_client/playback/source.rs @@ -1,3 +1,5 @@ +use std::num::NonZero; + use futures::StreamExt; use libwebrtc::{audio_stream::native::NativeAudioStream, prelude::AudioFrame}; use livekit::track::RemoteAudioTrack; @@ -9,7 +11,11 @@ fn frame_to_samplesbuffer(frame: AudioFrame) -> SamplesBuffer { let samples = frame.data.iter().copied(); let samples = SampleTypeConverter::<_, _>::new(samples); let samples: Vec = samples.collect(); - SamplesBuffer::new(frame.num_channels as u16, frame.sample_rate, samples) + SamplesBuffer::new( + NonZero::new(frame.num_channels as u16).expect("audio frame channels is nonzero"), + NonZero::new(frame.sample_rate).expect("audio frame sample rate is nonzero"), + samples, + ) } pub struct LiveKitStream { @@ -20,8 +26,11 @@ pub struct LiveKitStream { impl LiveKitStream { pub fn new(executor: &gpui::BackgroundExecutor, track: &RemoteAudioTrack) -> Self { - let mut stream = - NativeAudioStream::new(track.rtc_track(), SAMPLE_RATE as i32, NUM_CHANNELS as i32); + let mut stream = NativeAudioStream::new( + track.rtc_track(), + SAMPLE_RATE.get() as i32, + NUM_CHANNELS.get().into(), + ); let (queue_input, queue_output) = rodio::queue::queue(true); // spawn rtc stream let receiver_task = executor.spawn({ diff --git a/crates/livekit_client/src/record.rs b/crates/livekit_client/src/record.rs index 925c0d4c67..ad9d9435d9 100644 --- a/crates/livekit_client/src/record.rs +++ b/crates/livekit_client/src/record.rs @@ -1,5 +1,6 @@ use std::{ env, + num::NonZero, path::{Path, PathBuf}, sync::{Arc, Mutex}, time::Duration, @@ -83,7 +84,11 @@ fn write_out( .expect("Stream has ended, callback cant hold the lock"), ); let samples: Vec = SampleTypeConverter::<_, f32>::new(samples.into_iter()).collect(); - let mut samples = SamplesBuffer::new(config.channels(), config.sample_rate().0, samples); + let mut samples = SamplesBuffer::new( + NonZero::new(config.channels()).expect("config channel is never zero"), + NonZero::new(config.sample_rate().0).expect("config sample_rate is never zero"), + samples, + ); match rodio::output_to_wav(&mut samples, path) { Ok(_) => Ok(()), Err(e) => Err(anyhow::anyhow!("Failed to write wav file: {}", e)), diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 1f607a033a..39a438c512 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -1085,10 +1085,10 @@ impl Element for MarkdownElement { ); el.child( h_flex() - .w_4() + .w_5() .absolute() - .top_1p5() - .right_1p5() + .top_1() + .right_1() .justify_end() .child(codeblock), ) @@ -1115,12 +1115,11 @@ impl Element for MarkdownElement { cx, ); el.child( - h_flex() - .w_4() + div() .absolute() .top_0() .right_0() - .justify_end() + .w_5() .visible_on_hover("code_block") .child(codeblock), ) @@ -1323,7 +1322,7 @@ fn render_copy_code_block_button( .icon_size(IconSize::Small) .style(ButtonStyle::Filled) .shape(ui::IconButtonShape::Square) - .tooltip(Tooltip::text("Copy")) + .tooltip(Tooltip::text("Copy Code")) .on_click({ let markdown = markdown; move |_event, _window, cx| { diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index e27cbf868a..a54d38163d 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -835,7 +835,7 @@ impl MultiBuffer { this.convert_edits_to_buffer_edits(edits, &snapshot, &original_indent_columns); drop(snapshot); - let mut buffer_ids = Vec::with_capacity(buffer_edits.len()); + let mut buffer_ids = Vec::new(); for (buffer_id, mut edits) in buffer_edits { buffer_ids.push(buffer_id); edits.sort_by_key(|edit| edit.range.start); diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index 54c49bc72a..672bcf1cd9 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -283,13 +283,17 @@ pub(crate) fn render_ai_setup_page( v_flex() .mt_2() .gap_6() - .child( - AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx) - .tab_index(Some({ - tab_index += 1; - tab_index - 1 - })), - ) + .child({ + let mut ai_upsell_card = + AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx); + + ai_upsell_card.tab_index = Some({ + tab_index += 1; + tab_index - 1 + }); + + ai_upsell_card + }) .child(render_llm_provider_section( &mut tab_index, workspace, diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 47dfd84894..8fae695854 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -606,7 +606,7 @@ fn render_popular_settings_section( cx: &mut App, ) -> impl IntoElement { const LIGATURE_TOOLTIP: &str = - "Font ligatures combine two characters into one. For example, turning != into ≠."; + "Font ligatures combine two characters into one. For example, turning =/= into ≠."; v_flex() .pt_6() diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 873dd63201..884374a72f 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -850,19 +850,13 @@ impl workspace::SerializableItem for Onboarding { } mod persistence { - use db::{ - query, - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, - }; + use db::{define_connection, query, sqlez_macros::sql}; use workspace::WorkspaceDb; - pub struct OnboardingPagesDb(ThreadSafeConnection); - - impl Domain for OnboardingPagesDb { - const NAME: &str = stringify!(OnboardingPagesDb); - - const MIGRATIONS: &[&str] = &[sql!( + define_connection! { + pub static ref ONBOARDING_PAGES: OnboardingPagesDb = + &[ + sql!( CREATE TABLE onboarding_pages ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -872,11 +866,10 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]; + ), + ]; } - db::static_connection!(ONBOARDING_PAGES, OnboardingPagesDb, [WorkspaceDb]); - impl OnboardingPagesDb { query! { pub async fn save_onboarding_page( diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs index 8ff55d812b..3fe9c32a48 100644 --- a/crates/onboarding/src/welcome.rs +++ b/crates/onboarding/src/welcome.rs @@ -414,19 +414,13 @@ impl workspace::SerializableItem for WelcomePage { } mod persistence { - use db::{ - query, - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, - }; + use db::{define_connection, query, sqlez_macros::sql}; use workspace::WorkspaceDb; - pub struct WelcomePagesDb(ThreadSafeConnection); - - impl Domain for WelcomePagesDb { - const NAME: &str = stringify!(WelcomePagesDb); - - const MIGRATIONS: &[&str] = (&[sql!( + define_connection! { + pub static ref WELCOME_PAGES: WelcomePagesDb = + &[ + sql!( CREATE TABLE welcome_pages ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -436,11 +430,10 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]); + ), + ]; } - db::static_connection!(WELCOME_PAGES, WelcomePagesDb, [WorkspaceDb]); - impl WelcomePagesDb { query! { pub async fn save_welcome_page( diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index 08be82b830..acf6ec434a 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -446,6 +446,7 @@ pub enum ResponseStreamResult { #[derive(Serialize, Deserialize, Debug)] pub struct ResponseStreamEvent { + pub model: String, pub choices: Vec, pub usage: Option, } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index deebaedd74..cc3a0a05bb 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -7588,16 +7588,19 @@ impl LspStore { let snapshot = buffer_handle.read(cx).snapshot(); let buffer = buffer_handle.read(cx); let reused_diagnostics = buffer - .buffer_diagnostics(Some(server_id)) - .iter() - .filter(|v| merge(buffer, &v.diagnostic, cx)) - .map(|v| { - let start = Unclipped(v.range.start.to_point_utf16(&snapshot)); - let end = Unclipped(v.range.end.to_point_utf16(&snapshot)); - DiagnosticEntry { - range: start..end, - diagnostic: v.diagnostic.clone(), - } + .get_diagnostics(server_id) + .into_iter() + .flat_map(|diag| { + diag.iter() + .filter(|v| merge(buffer, &v.diagnostic, cx)) + .map(|v| { + let start = Unclipped(v.range.start.to_point_utf16(&snapshot)); + let end = Unclipped(v.range.end.to_point_utf16(&snapshot)); + DiagnosticEntry { + range: start..end, + diagnostic: v.diagnostic.clone(), + } + }) }) .collect::>(); @@ -9026,22 +9029,13 @@ impl LspStore { lsp_store.update(&mut cx, |lsp_store, cx| { if let Some(server) = lsp_store.language_server_for_id(server_id) { let text_document = if envelope.payload.current_file_only { - let buffer_id = envelope - .payload - .buffer_id - .map(|id| BufferId::new(id)) - .transpose()?; - buffer_id - .and_then(|buffer_id| { - lsp_store - .buffer_store() - .read(cx) - .get(buffer_id) - .and_then(|buffer| { - Some(buffer.read(cx).file()?.as_local()?.abs_path(cx)) - }) - .map(|path| make_text_document_identifier(&path)) - }) + let buffer_id = BufferId::new(envelope.payload.buffer_id)?; + lsp_store + .buffer_store() + .read(cx) + .get(buffer_id) + .and_then(|buffer| Some(buffer.read(cx).file()?.as_local()?.abs_path(cx))) + .map(|path| make_text_document_identifier(&path)) .transpose()? } else { None @@ -11703,11 +11697,12 @@ impl LspStore { // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. } "workspace/symbol" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.workspace_symbol_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.workspace_symbol_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "workspace/fileOperations" => { if let Some(options) = reg.register_options { @@ -11731,11 +11726,12 @@ impl LspStore { } } "textDocument/rangeFormatting" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.document_range_formatting_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.document_range_formatting_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/onTypeFormatting" => { if let Some(options) = reg @@ -11750,32 +11746,36 @@ impl LspStore { } } "textDocument/formatting" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.document_formatting_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.document_formatting_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/rename" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.rename_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.rename_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/inlayHint" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.inlay_hint_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.inlay_hint_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/documentSymbol" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.document_symbol_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.document_symbol_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/codeAction" => { if let Some(options) = reg @@ -11791,11 +11791,12 @@ impl LspStore { } } "textDocument/definition" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.definition_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.definition_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/completion" => { if let Some(caps) = reg @@ -11903,7 +11904,7 @@ impl LspStore { notify_server_capabilities_updated(&server, cx); } } - "textDocument/documentColor" => { + "textDocument/colorProvider" => { if let Some(caps) = reg .register_options .map(serde_json::from_value) @@ -12054,7 +12055,7 @@ impl LspStore { }); notify_server_capabilities_updated(&server, cx); } - "textDocument/documentColor" => { + "textDocument/colorProvider" => { server.update_capabilities(|capabilities| { capabilities.color_provider = None; }); @@ -12174,10 +12175,10 @@ impl LspStore { // https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/client.ts#L2133 fn parse_register_capabilities( reg: lsp::Registration, -) -> Result> { +) -> anyhow::Result>> { Ok(match reg.register_options { - Some(options) => OneOf::Right(serde_json::from_value::(options)?), - None => OneOf::Left(true), + Some(options) => Some(OneOf::Right(serde_json::from_value::(options)?)), + None => Some(OneOf::Left(true)), }) } diff --git a/crates/project/src/lsp_store/rust_analyzer_ext.rs b/crates/project/src/lsp_store/rust_analyzer_ext.rs index 54f63220b1..e5e6338d3c 100644 --- a/crates/project/src/lsp_store/rust_analyzer_ext.rs +++ b/crates/project/src/lsp_store/rust_analyzer_ext.rs @@ -1,8 +1,8 @@ use ::serde::{Deserialize, Serialize}; use anyhow::Context as _; -use gpui::{App, AsyncApp, Entity, Task, WeakEntity}; -use language::{Buffer, ServerHealth}; -use lsp::{LanguageServer, LanguageServerId, LanguageServerName}; +use gpui::{App, Entity, Task, WeakEntity}; +use language::ServerHealth; +use lsp::{LanguageServer, LanguageServerName}; use rpc::proto; use crate::{LspStore, LspStoreEvent, Project, ProjectPath, lsp_store}; @@ -83,32 +83,31 @@ pub fn register_notifications(lsp_store: WeakEntity, language_server: pub fn cancel_flycheck( project: Entity, - buffer_path: Option, + buffer_path: ProjectPath, cx: &mut App, ) -> Task> { let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); let lsp_store = project.read(cx).lsp_store(); - let buffer = buffer_path.map(|buffer_path| { - project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) - }) + let buffer = project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.open_buffer(buffer_path, cx) }) }); cx.spawn(async move |cx| { - let buffer = match buffer { - Some(buffer) => Some(buffer.await?), - None => None, - }; - let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx) + let buffer = buffer.await?; + let Some(rust_analyzer_server) = project.read_with(cx, |project, cx| { + project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) + })? else { return Ok(()); }; + let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?; if let Some((client, project_id)) = upstream_client { let request = proto::LspExtCancelFlycheck { project_id, + buffer_id, language_server_id: rust_analyzer_server.to_proto(), }; client @@ -131,33 +130,28 @@ pub fn cancel_flycheck( pub fn run_flycheck( project: Entity, - buffer_path: Option, + buffer_path: ProjectPath, cx: &mut App, ) -> Task> { let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); let lsp_store = project.read(cx).lsp_store(); - let buffer = buffer_path.map(|buffer_path| { - project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) - }) + let buffer = project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.open_buffer(buffer_path, cx) }) }); cx.spawn(async move |cx| { - let buffer = match buffer { - Some(buffer) => Some(buffer.await?), - None => None, - }; - let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx) + let buffer = buffer.await?; + let Some(rust_analyzer_server) = project.read_with(cx, |project, cx| { + project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) + })? else { return Ok(()); }; + let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?; if let Some((client, project_id)) = upstream_client { - let buffer_id = buffer - .map(|buffer| buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())) - .transpose()?; let request = proto::LspExtRunFlycheck { project_id, buffer_id, @@ -188,32 +182,31 @@ pub fn run_flycheck( pub fn clear_flycheck( project: Entity, - buffer_path: Option, + buffer_path: ProjectPath, cx: &mut App, ) -> Task> { let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); let lsp_store = project.read(cx).lsp_store(); - let buffer = buffer_path.map(|buffer_path| { - project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) - }) + let buffer = project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.open_buffer(buffer_path, cx) }) }); cx.spawn(async move |cx| { - let buffer = match buffer { - Some(buffer) => Some(buffer.await?), - None => None, - }; - let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx) + let buffer = buffer.await?; + let Some(rust_analyzer_server) = project.read_with(cx, |project, cx| { + project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) + })? else { return Ok(()); }; + let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?; if let Some((client, project_id)) = upstream_client { let request = proto::LspExtClearFlycheck { project_id, + buffer_id, language_server_id: rust_analyzer_server.to_proto(), }; client @@ -233,40 +226,3 @@ pub fn clear_flycheck( anyhow::Ok(()) }) } - -fn find_rust_analyzer_server( - project: &Entity, - buffer: Option<&Entity>, - cx: &mut AsyncApp, -) -> Option { - project - .read_with(cx, |project, cx| { - buffer - .and_then(|buffer| { - project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) - }) - // If no rust-analyzer found for the current buffer (e.g. `settings.json`), fall back to the project lookup - // and use project's rust-analyzer if it's the only one. - .or_else(|| { - let rust_analyzer_servers = project - .lsp_store() - .read(cx) - .language_server_statuses - .iter() - .filter_map(|(server_id, server_status)| { - if server_status.name == RUST_ANALYZER_NAME { - Some(*server_id) - } else { - None - } - }) - .collect::>(); - if rust_analyzer_servers.len() == 1 { - rust_analyzer_servers.first().copied() - } else { - None - } - }) - }) - .ok()? -} diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 4447c25129..a6fea4059c 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -181,6 +181,17 @@ pub struct DiagnosticsSettings { /// Settings for showing inline diagnostics. pub inline: InlineDiagnosticsSettings, + + /// Configuration, related to Rust language diagnostics. + pub cargo: Option, +} + +impl DiagnosticsSettings { + pub fn fetch_cargo_diagnostics(&self) -> bool { + self.cargo + .as_ref() + .is_some_and(|cargo_diagnostics| cargo_diagnostics.fetch_cargo_diagnostics) + } } #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] @@ -247,6 +258,7 @@ impl Default for DiagnosticsSettings { include_warnings: true, lsp_pull_diagnostics: LspPullDiagnosticsSettings::default(), inline: InlineDiagnosticsSettings::default(), + cargo: None, } } } @@ -280,6 +292,16 @@ impl Default for GlobalLspSettings { } } +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct CargoDiagnosticsSettings { + /// When enabled, Zed disables rust-analyzer's check on save and starts to query + /// Cargo diagnostics separately. + /// + /// Default: false + #[serde(default)] + pub fetch_cargo_diagnostics: bool, +} + #[derive( Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, JsonSchema, )] diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 5a30a3e9bc..52ec7a9880 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -69,7 +69,6 @@ use workspace::{ notifications::{DetachAndPromptErr, NotifyTaskExt}, }; use worktree::CreatedEntry; -use zed_actions::workspace::OpenWithSystem; const PROJECT_PANEL_KEY: &str = "ProjectPanel"; const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; @@ -256,6 +255,8 @@ actions!( RevealInFileManager, /// Removes the selected folder from the project. RemoveFromProject, + /// Opens the selected file with the system's default application. + OpenWithSystem, /// Cuts the selected file or directory. Cut, /// Pastes the previously cut or copied item. @@ -4089,7 +4090,6 @@ impl ProjectPanel { .when(!is_sticky, |this| { this .when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over)) - .when(settings.drag_and_drop, |this| this .on_drag_move::(cx.listener( move |this, event: &DragMoveEvent, _, cx| { let is_current_target = this.drag_target_entry.as_ref() @@ -4223,7 +4223,7 @@ impl ProjectPanel { } this.drag_onto(selections, entry_id, kind.is_file(), window, cx); }), - )) + ) }) .on_mouse_down( MouseButton::Left, @@ -4434,7 +4434,6 @@ impl ProjectPanel { div() .when(!is_sticky, |div| { div - .when(settings.drag_and_drop, |div| div .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| { this.hover_scroll_task.take(); this.drag_target_entry = None; @@ -4466,7 +4465,7 @@ impl ProjectPanel { } }, - ))) + )) }) .child( Label::new(DELIMITER.clone()) @@ -4486,7 +4485,6 @@ impl ProjectPanel { .when(index != components_len - 1, |div|{ let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned(); div - .when(settings.drag_and_drop, |div| div .on_drag_move(cx.listener( move |this, event: &DragMoveEvent, _, _| { if event.bounds.contains(&event.event.position) { @@ -4524,7 +4522,7 @@ impl ProjectPanel { target.index == index ), |this| { this.bg(item_colors.drag_over) - })) + }) }) }) .on_click(cx.listener(move |this, _, _, cx| { @@ -5032,8 +5030,7 @@ impl ProjectPanel { sticky_parents.reverse(); - let panel_settings = ProjectPanelSettings::get_global(cx); - let git_status_enabled = panel_settings.git_status; + let git_status_enabled = ProjectPanelSettings::get_global(cx).git_status; let root_name = OsStr::new(worktree.root_name()); let git_summaries_by_id = if git_status_enabled { @@ -5117,11 +5114,11 @@ impl Render for ProjectPanel { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let has_worktree = !self.visible_entries.is_empty(); let project = self.project.read(cx); - let panel_settings = ProjectPanelSettings::get_global(cx); - let indent_size = panel_settings.indent_size; - let show_indent_guides = panel_settings.indent_guides.show == ShowIndentGuides::Always; + let indent_size = ProjectPanelSettings::get_global(cx).indent_size; + let show_indent_guides = + ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always; let show_sticky_entries = { - if panel_settings.sticky_scroll { + if ProjectPanelSettings::get_global(cx).sticky_scroll { let is_scrollable = self.scroll_handle.is_scrollable(); let is_scrolled = self.scroll_handle.offset().y < px(0.); is_scrollable && is_scrolled @@ -5209,10 +5206,8 @@ impl Render for ProjectPanel { h_flex() .id("project-panel") .group("project-panel") - .when(panel_settings.drag_and_drop, |this| { - this.on_drag_move(cx.listener(handle_drag_move::)) - .on_drag_move(cx.listener(handle_drag_move::)) - }) + .on_drag_move(cx.listener(handle_drag_move::)) + .on_drag_move(cx.listener(handle_drag_move::)) .size_full() .relative() .on_modifiers_changed(cx.listener( @@ -5550,32 +5545,30 @@ impl Render for ProjectPanel { })), ) .when(is_local, |div| { - div.when(panel_settings.drag_and_drop, |div| { - div.drag_over::(|style, _, _, cx| { - style.bg(cx.theme().colors().drop_target_background) - }) - .on_drop(cx.listener( - move |this, external_paths: &ExternalPaths, window, cx| { - this.drag_target_entry = None; - this.hover_scroll_task.take(); - if let Some(task) = this - .workspace - .update(cx, |workspace, cx| { - workspace.open_workspace_for_paths( - true, - external_paths.paths().to_owned(), - window, - cx, - ) - }) - .log_err() - { - task.detach_and_log_err(cx); - } - cx.stop_propagation(); - }, - )) + div.drag_over::(|style, _, _, cx| { + style.bg(cx.theme().colors().drop_target_background) }) + .on_drop(cx.listener( + move |this, external_paths: &ExternalPaths, window, cx| { + this.drag_target_entry = None; + this.hover_scroll_task.take(); + if let Some(task) = this + .workspace + .update(cx, |workspace, cx| { + workspace.open_workspace_for_paths( + true, + external_paths.paths().to_owned(), + window, + cx, + ) + }) + .log_err() + { + task.detach_and_log_err(cx); + } + cx.stop_propagation(); + }, + )) }) } } diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index fc399d66a7..8a243589ed 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -47,7 +47,6 @@ pub struct ProjectPanelSettings { pub scrollbar: ScrollbarSettings, pub show_diagnostics: ShowDiagnostics, pub hide_root: bool, - pub drag_and_drop: bool, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -161,10 +160,6 @@ pub struct ProjectPanelSettingsContent { /// /// Default: true pub sticky_scroll: Option, - /// Whether to enable drag-and-drop operations in the project panel. - /// - /// Default: true - pub drag_and_drop: Option, } impl Settings for ProjectPanelSettings { diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index 473ef5c38c..ac9c275aa2 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -834,19 +834,21 @@ message LspRunnable { message LspExtCancelFlycheck { uint64 project_id = 1; - uint64 language_server_id = 2; + uint64 buffer_id = 2; + uint64 language_server_id = 3; } message LspExtRunFlycheck { uint64 project_id = 1; - optional uint64 buffer_id = 2; + uint64 buffer_id = 2; uint64 language_server_id = 3; bool current_file_only = 4; } message LspExtClearFlycheck { uint64 project_id = 1; - uint64 language_server_id = 2; + uint64 buffer_id = 2; + uint64 language_server_id = 3; } message LspDiagnosticRelatedInformation { diff --git a/crates/recent_projects/src/disconnected_overlay.rs b/crates/recent_projects/src/disconnected_overlay.rs index 8ffe0ef07c..dd4d788cfd 100644 --- a/crates/recent_projects/src/disconnected_overlay.rs +++ b/crates/recent_projects/src/disconnected_overlay.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, Focusable, Render, WeakEntity}; use project::project_settings::ProjectSettings; use remote::SshConnectionOptions; @@ -101,17 +103,17 @@ impl DisconnectedOverlay { return; }; + let Some(ssh_project) = workspace.read(cx).serialized_ssh_project() else { + return; + }; + let Some(window_handle) = window.window_handle().downcast::() else { return; }; let app_state = workspace.read(cx).app_state().clone(); - let paths = workspace - .read(cx) - .root_paths(cx) - .iter() - .map(|path| path.to_path_buf()) - .collect(); + + let paths = ssh_project.paths.iter().map(PathBuf::from).collect(); cx.spawn_in(window, async move |_, cx| { open_ssh_project( diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index fa57b588cd..2093e96cae 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -19,12 +19,15 @@ use picker::{ pub use remote_servers::RemoteServerProjects; use settings::Settings; pub use ssh_connections::SshSettings; -use std::{path::Path, sync::Arc}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; use ui::{KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*, tooltip_container}; use util::{ResultExt, paths::PathExt}; use workspace::{ - CloseIntent, HistoryManager, ModalView, OpenOptions, PathList, SerializedWorkspaceLocation, - WORKSPACE_DB, Workspace, WorkspaceId, with_active_or_new_workspace, + CloseIntent, HistoryManager, ModalView, OpenOptions, SerializedWorkspaceLocation, WORKSPACE_DB, + Workspace, WorkspaceId, with_active_or_new_workspace, }; use zed_actions::{OpenRecent, OpenRemote}; @@ -151,7 +154,7 @@ impl Render for RecentProjects { pub struct RecentProjectsDelegate { workspace: WeakEntity, - workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>, + workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation)>, selected_match_index: usize, matches: Vec, render_paths: bool, @@ -175,15 +178,12 @@ impl RecentProjectsDelegate { } } - pub fn set_workspaces( - &mut self, - workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>, - ) { + pub fn set_workspaces(&mut self, workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation)>) { self.workspaces = workspaces; self.has_any_non_local_projects = !self .workspaces .iter() - .all(|(_, location, _)| matches!(location, SerializedWorkspaceLocation::Local)); + .all(|(_, location)| matches!(location, SerializedWorkspaceLocation::Local(_, _))); } } impl EventEmitter for RecentProjectsDelegate {} @@ -236,14 +236,15 @@ impl PickerDelegate for RecentProjectsDelegate { .workspaces .iter() .enumerate() - .filter(|(_, (id, _, _))| !self.is_current_workspace(*id, cx)) - .map(|(id, (_, _, paths))| { - let combined_string = paths - .paths() + .filter(|(_, (id, _))| !self.is_current_workspace(*id, cx)) + .map(|(id, (_, location))| { + let combined_string = location + .sorted_paths() .iter() .map(|path| path.compact().to_string_lossy().into_owned()) .collect::>() .join(""); + StringMatchCandidate::new(id, &combined_string) }) .collect::>(); @@ -278,7 +279,7 @@ impl PickerDelegate for RecentProjectsDelegate { .get(self.selected_index()) .zip(self.workspace.upgrade()) { - let (candidate_workspace_id, candidate_workspace_location, candidate_workspace_paths) = + let (candidate_workspace_id, candidate_workspace_location) = &self.workspaces[selected_match.candidate_id]; let replace_current_window = if self.create_new_window { secondary @@ -291,8 +292,8 @@ impl PickerDelegate for RecentProjectsDelegate { Task::ready(Ok(())) } else { match candidate_workspace_location { - SerializedWorkspaceLocation::Local => { - let paths = candidate_workspace_paths.paths().to_vec(); + SerializedWorkspaceLocation::Local(paths, _) => { + let paths = paths.paths().to_vec(); if replace_current_window { cx.spawn_in(window, async move |workspace, cx| { let continue_replacing = workspace @@ -320,7 +321,7 @@ impl PickerDelegate for RecentProjectsDelegate { workspace.open_workspace_for_paths(false, paths, window, cx) } } - SerializedWorkspaceLocation::Ssh(connection) => { + SerializedWorkspaceLocation::Ssh(ssh_project) => { let app_state = workspace.app_state().clone(); let replace_window = if replace_current_window { @@ -336,12 +337,12 @@ impl PickerDelegate for RecentProjectsDelegate { let connection_options = SshSettings::get_global(cx) .connection_options_for( - connection.host.clone(), - connection.port, - connection.user.clone(), + ssh_project.host.clone(), + ssh_project.port, + ssh_project.user.clone(), ); - let paths = candidate_workspace_paths.paths().to_vec(); + let paths = ssh_project.paths.iter().map(PathBuf::from).collect(); cx.spawn_in(window, async move |_, cx| { open_ssh_project( @@ -382,12 +383,12 @@ impl PickerDelegate for RecentProjectsDelegate { ) -> Option { let hit = self.matches.get(ix)?; - let (_, location, paths) = self.workspaces.get(hit.candidate_id)?; + let (_, location) = self.workspaces.get(hit.candidate_id)?; let mut path_start_offset = 0; - let (match_labels, paths): (Vec<_>, Vec<_>) = paths - .paths() + let (match_labels, paths): (Vec<_>, Vec<_>) = location + .sorted_paths() .iter() .map(|p| p.compact()) .map(|path| { @@ -415,9 +416,11 @@ impl PickerDelegate for RecentProjectsDelegate { .gap_3() .when(self.has_any_non_local_projects, |this| { this.child(match location { - SerializedWorkspaceLocation::Local => Icon::new(IconName::Screen) - .color(Color::Muted) - .into_any_element(), + SerializedWorkspaceLocation::Local(_, _) => { + Icon::new(IconName::Screen) + .color(Color::Muted) + .into_any_element() + } SerializedWorkspaceLocation::Ssh(_) => Icon::new(IconName::Server) .color(Color::Muted) .into_any_element(), @@ -565,7 +568,7 @@ impl RecentProjectsDelegate { cx: &mut Context>, ) { if let Some(selected_match) = self.matches.get(ix) { - let (workspace_id, _, _) = self.workspaces[selected_match.candidate_id]; + let (workspace_id, _) = self.workspaces[selected_match.candidate_id]; cx.spawn_in(window, async move |this, cx| { let _ = WORKSPACE_DB.delete_workspace_by_id(workspace_id).await; let workspaces = WORKSPACE_DB @@ -704,8 +707,7 @@ mod tests { }]; delegate.set_workspaces(vec![( WorkspaceId::default(), - SerializedWorkspaceLocation::Local, - PathList::new(&[path!("/test/path")]), + SerializedWorkspaceLocation::from_local_paths(vec![path!("/test/path/")]), )]); }); }) diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 6794018470..c02d0ad7e7 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -52,6 +52,11 @@ use util::{ paths::{PathStyle, RemotePathBuf}, }; +#[derive( + Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize, +)] +pub struct SshProjectId(pub u64); + #[derive(Clone)] pub struct SshSocket { connection_options: SshConnectionOptions, @@ -445,7 +450,7 @@ impl SshSocket { } async fn platform(&self) -> Result { - let uname = self.run_command("sh", &["-lc", "uname -sm"]).await?; + let uname = self.run_command("sh", &["-c", "uname -sm"]).await?; let Some((os, arch)) = uname.split_once(" ") else { anyhow::bail!("unknown uname: {uname:?}") }; @@ -476,7 +481,7 @@ impl SshSocket { } async fn shell(&self) -> String { - match self.run_command("sh", &["-lc", "echo $SHELL"]).await { + match self.run_command("sh", &["-c", "echo $SHELL"]).await { Ok(shell) => shell.trim().to_owned(), Err(e) => { log::error!("Failed to get shell: {e}"); @@ -1533,7 +1538,7 @@ impl RemoteConnection for SshRemoteConnection { let ssh_proxy_process = match self .socket - .ssh_command("sh", &["-lc", &start_proxy_command]) + .ssh_command("sh", &["-c", &start_proxy_command]) // IMPORTANT: we kill this process when we drop the task that uses it. .kill_on_drop(true) .spawn() @@ -1910,7 +1915,7 @@ impl SshRemoteConnection { .run_command( "sh", &[ - "-lc", + "-c", &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), ], ) @@ -1988,7 +1993,7 @@ impl SshRemoteConnection { .run_command( "sh", &[ - "-lc", + "-c", &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), ], ) @@ -2036,7 +2041,7 @@ impl SshRemoteConnection { dst_path = &dst_path.to_string() ) }; - self.socket.run_command("sh", &["-lc", &script]).await?; + self.socket.run_command("sh", &["-c", &script]).await?; Ok(()) } diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index 5dbb9a2771..dcec9f6fe0 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -65,7 +65,6 @@ telemetry_events.workspace = true util.workspace = true watch.workspace = true worktree.workspace = true -thiserror.workspace = true [target.'cfg(not(windows))'.dependencies] crashes.workspace = true diff --git a/crates/remote_server/src/main.rs b/crates/remote_server/src/main.rs index 368c7cb639..03b0c3eda3 100644 --- a/crates/remote_server/src/main.rs +++ b/crates/remote_server/src/main.rs @@ -1,7 +1,6 @@ #![cfg_attr(target_os = "windows", allow(unused, dead_code))] -use clap::Parser; -use remote_server::Commands; +use clap::{Parser, Subcommand}; use std::path::PathBuf; #[derive(Parser)] @@ -22,34 +21,105 @@ struct Cli { printenv: bool, } +#[derive(Subcommand)] +enum Commands { + Run { + #[arg(long)] + log_file: PathBuf, + #[arg(long)] + pid_file: PathBuf, + #[arg(long)] + stdin_socket: PathBuf, + #[arg(long)] + stdout_socket: PathBuf, + #[arg(long)] + stderr_socket: PathBuf, + }, + Proxy { + #[arg(long)] + reconnect: bool, + #[arg(long)] + identifier: String, + }, + Version, +} + #[cfg(windows)] fn main() { unimplemented!() } #[cfg(not(windows))] -fn main() -> anyhow::Result<()> { +fn main() { + use release_channel::{RELEASE_CHANNEL, ReleaseChannel}; + use remote::proxy::ProxyLaunchError; + use remote_server::unix::{execute_proxy, execute_run}; + let cli = Cli::parse(); if let Some(socket_path) = &cli.askpass { askpass::main(socket_path); - return Ok(()); + return; } if let Some(socket) = &cli.crash_handler { crashes::crash_server(socket.as_path()); - return Ok(()); + return; } if cli.printenv { util::shell_env::print_env(); - return Ok(()); + return; } - if let Some(command) = cli.command { - remote_server::run(command) - } else { - eprintln!("usage: remote "); + let result = match cli.command { + Some(Commands::Run { + log_file, + pid_file, + stdin_socket, + stdout_socket, + stderr_socket, + }) => execute_run( + log_file, + pid_file, + stdin_socket, + stdout_socket, + stderr_socket, + ), + Some(Commands::Proxy { + identifier, + reconnect, + }) => match execute_proxy(identifier, reconnect) { + Ok(_) => Ok(()), + Err(err) => { + if let Some(err) = err.downcast_ref::() { + std::process::exit(err.to_exit_code()); + } + Err(err) + } + }, + Some(Commands::Version) => { + let release_channel = *RELEASE_CHANNEL; + match release_channel { + ReleaseChannel::Stable | ReleaseChannel::Preview => { + println!("{}", env!("ZED_PKG_VERSION")) + } + ReleaseChannel::Nightly | ReleaseChannel::Dev => { + println!( + "{}", + option_env!("ZED_COMMIT_SHA").unwrap_or(release_channel.dev_name()) + ) + } + }; + std::process::exit(0); + } + None => { + eprintln!("usage: remote "); + std::process::exit(1); + } + }; + if let Err(error) = result { + log::error!("exiting due to error: {}", error); std::process::exit(1); } } diff --git a/crates/remote_server/src/remote_server.rs b/crates/remote_server/src/remote_server.rs index c14a4828ac..52003969af 100644 --- a/crates/remote_server/src/remote_server.rs +++ b/crates/remote_server/src/remote_server.rs @@ -6,78 +6,4 @@ pub mod unix; #[cfg(test)] mod remote_editing_tests; -use clap::Subcommand; -use std::path::PathBuf; - pub use headless_project::{HeadlessAppState, HeadlessProject}; - -#[derive(Subcommand)] -pub enum Commands { - Run { - #[arg(long)] - log_file: PathBuf, - #[arg(long)] - pid_file: PathBuf, - #[arg(long)] - stdin_socket: PathBuf, - #[arg(long)] - stdout_socket: PathBuf, - #[arg(long)] - stderr_socket: PathBuf, - }, - Proxy { - #[arg(long)] - reconnect: bool, - #[arg(long)] - identifier: String, - }, - Version, -} - -#[cfg(not(windows))] -pub fn run(command: Commands) -> anyhow::Result<()> { - use anyhow::Context; - use release_channel::{RELEASE_CHANNEL, ReleaseChannel}; - use unix::{ExecuteProxyError, execute_proxy, execute_run}; - - match command { - Commands::Run { - log_file, - pid_file, - stdin_socket, - stdout_socket, - stderr_socket, - } => execute_run( - log_file, - pid_file, - stdin_socket, - stdout_socket, - stderr_socket, - ), - Commands::Proxy { - identifier, - reconnect, - } => execute_proxy(identifier, reconnect) - .inspect_err(|err| { - if let ExecuteProxyError::ServerNotRunning(err) = err { - std::process::exit(err.to_exit_code()); - } - }) - .context("running proxy on the remote server"), - Commands::Version => { - let release_channel = *RELEASE_CHANNEL; - match release_channel { - ReleaseChannel::Stable | ReleaseChannel::Preview => { - println!("{}", env!("ZED_PKG_VERSION")) - } - ReleaseChannel::Nightly | ReleaseChannel::Dev => { - println!( - "{}", - option_env!("ZED_COMMIT_SHA").unwrap_or(release_channel.dev_name()) - ) - } - }; - Ok(()) - } - } -} diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index c6d1566d60..b8a7351552 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -36,7 +36,6 @@ use smol::Async; use smol::{net::unix::UnixListener, stream::StreamExt as _}; use std::ffi::OsStr; use std::ops::ControlFlow; -use std::process::ExitStatus; use std::str::FromStr; use std::sync::LazyLock; use std::{env, thread}; @@ -47,7 +46,6 @@ use std::{ sync::Arc, }; use telemetry_events::LocationData; -use thiserror::Error; use util::ResultExt; pub static VERSION: LazyLock<&str> = LazyLock::new(|| match *RELEASE_CHANNEL { @@ -528,23 +526,7 @@ pub fn execute_run( Ok(()) } -#[derive(Debug, Error)] -pub(crate) enum ServerPathError { - #[error("Failed to create server_dir `{path}`")] - CreateServerDir { - #[source] - source: std::io::Error, - path: PathBuf, - }, - #[error("Failed to create logs_dir `{path}`")] - CreateLogsDir { - #[source] - source: std::io::Error, - path: PathBuf, - }, -} - -#[derive(Clone, Debug)] +#[derive(Clone)] struct ServerPaths { log_file: PathBuf, pid_file: PathBuf, @@ -554,19 +536,10 @@ struct ServerPaths { } impl ServerPaths { - fn new(identifier: &str) -> Result { + fn new(identifier: &str) -> Result { let server_dir = paths::remote_server_state_dir().join(identifier); - std::fs::create_dir_all(&server_dir).map_err(|source| { - ServerPathError::CreateServerDir { - source, - path: server_dir.clone(), - } - })?; - let log_dir = logs_dir(); - std::fs::create_dir_all(log_dir).map_err(|source| ServerPathError::CreateLogsDir { - source: source, - path: log_dir.clone(), - })?; + std::fs::create_dir_all(&server_dir)?; + std::fs::create_dir_all(&logs_dir())?; let pid_file = server_dir.join("server.pid"); let stdin_socket = server_dir.join("stdin.sock"); @@ -584,43 +557,7 @@ impl ServerPaths { } } -#[derive(Debug, Error)] -pub(crate) enum ExecuteProxyError { - #[error("Failed to init server paths")] - ServerPath(#[from] ServerPathError), - - #[error(transparent)] - ServerNotRunning(#[from] ProxyLaunchError), - - #[error("Failed to check PidFile '{path}'")] - CheckPidFile { - #[source] - source: CheckPidError, - path: PathBuf, - }, - - #[error("Failed to kill existing server with pid '{pid}'")] - KillRunningServer { - #[source] - source: std::io::Error, - pid: u32, - }, - - #[error("failed to spawn server")] - SpawnServer(#[source] SpawnServerError), - - #[error("stdin_task failed")] - StdinTask(#[source] anyhow::Error), - #[error("stdout_task failed")] - StdoutTask(#[source] anyhow::Error), - #[error("stderr_task failed")] - StderrTask(#[source] anyhow::Error), -} - -pub(crate) fn execute_proxy( - identifier: String, - is_reconnecting: bool, -) -> Result<(), ExecuteProxyError> { +pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> { init_logging_proxy(); let server_paths = ServerPaths::new(&identifier)?; @@ -637,19 +574,12 @@ pub(crate) fn execute_proxy( log::info!("starting proxy process. PID: {}", std::process::id()); - let server_pid = check_pid_file(&server_paths.pid_file).map_err(|source| { - ExecuteProxyError::CheckPidFile { - source, - path: server_paths.pid_file.clone(), - } - })?; + let server_pid = check_pid_file(&server_paths.pid_file)?; let server_running = server_pid.is_some(); if is_reconnecting { if !server_running { log::error!("attempted to reconnect, but no server running"); - return Err(ExecuteProxyError::ServerNotRunning( - ProxyLaunchError::ServerNotRunning, - )); + anyhow::bail!(ProxyLaunchError::ServerNotRunning); } } else { if let Some(pid) = server_pid { @@ -660,7 +590,7 @@ pub(crate) fn execute_proxy( kill_running_server(pid, &server_paths)?; } - spawn_server(&server_paths).map_err(ExecuteProxyError::SpawnServer)?; + spawn_server(&server_paths)?; }; let stdin_task = smol::spawn(async move { @@ -700,9 +630,9 @@ pub(crate) fn execute_proxy( if let Err(forwarding_result) = smol::block_on(async move { futures::select! { - result = stdin_task.fuse() => result.map_err(ExecuteProxyError::StdinTask), - result = stdout_task.fuse() => result.map_err(ExecuteProxyError::StdoutTask), - result = stderr_task.fuse() => result.map_err(ExecuteProxyError::StderrTask), + result = stdin_task.fuse() => result.context("stdin_task failed"), + result = stdout_task.fuse() => result.context("stdout_task failed"), + result = stderr_task.fuse() => result.context("stderr_task failed"), } }) { log::error!( @@ -715,12 +645,12 @@ pub(crate) fn execute_proxy( Ok(()) } -fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<(), ExecuteProxyError> { +fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<()> { log::info!("killing existing server with PID {}", pid); std::process::Command::new("kill") .arg(pid.to_string()) .output() - .map_err(|source| ExecuteProxyError::KillRunningServer { source, pid })?; + .context("failed to kill existing server")?; for file in [ &paths.pid_file, @@ -734,39 +664,18 @@ fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<(), ExecuteProxy Ok(()) } -#[derive(Debug, Error)] -pub(crate) enum SpawnServerError { - #[error("failed to remove stdin socket")] - RemoveStdinSocket(#[source] std::io::Error), - - #[error("failed to remove stdout socket")] - RemoveStdoutSocket(#[source] std::io::Error), - - #[error("failed to remove stderr socket")] - RemoveStderrSocket(#[source] std::io::Error), - - #[error("failed to get current_exe")] - CurrentExe(#[source] std::io::Error), - - #[error("failed to launch server process")] - ProcessStatus(#[source] std::io::Error), - - #[error("failed to launch and detach server process: {status}\n{paths}")] - LaunchStatus { status: ExitStatus, paths: String }, -} - -fn spawn_server(paths: &ServerPaths) -> Result<(), SpawnServerError> { +fn spawn_server(paths: &ServerPaths) -> Result<()> { if paths.stdin_socket.exists() { - std::fs::remove_file(&paths.stdin_socket).map_err(SpawnServerError::RemoveStdinSocket)?; + std::fs::remove_file(&paths.stdin_socket)?; } if paths.stdout_socket.exists() { - std::fs::remove_file(&paths.stdout_socket).map_err(SpawnServerError::RemoveStdoutSocket)?; + std::fs::remove_file(&paths.stdout_socket)?; } if paths.stderr_socket.exists() { - std::fs::remove_file(&paths.stderr_socket).map_err(SpawnServerError::RemoveStderrSocket)?; + std::fs::remove_file(&paths.stderr_socket)?; } - let binary_name = std::env::current_exe().map_err(SpawnServerError::CurrentExe)?; + let binary_name = std::env::current_exe()?; let mut server_process = std::process::Command::new(binary_name); server_process .arg("run") @@ -783,17 +692,11 @@ fn spawn_server(paths: &ServerPaths) -> Result<(), SpawnServerError> { let status = server_process .status() - .map_err(SpawnServerError::ProcessStatus)?; - - if !status.success() { - return Err(SpawnServerError::LaunchStatus { - status, - paths: format!( - "log file: {:?}, pid file: {:?}", - paths.log_file, paths.pid_file, - ), - }); - } + .context("failed to launch server process")?; + anyhow::ensure!( + status.success(), + "failed to launch and detach server process" + ); let mut total_time_waited = std::time::Duration::from_secs(0); let wait_duration = std::time::Duration::from_millis(20); @@ -814,15 +717,7 @@ fn spawn_server(paths: &ServerPaths) -> Result<(), SpawnServerError> { Ok(()) } -#[derive(Debug, Error)] -#[error("Failed to remove PID file for missing process (pid `{pid}`")] -pub(crate) struct CheckPidError { - #[source] - source: std::io::Error, - pid: u32, -} - -fn check_pid_file(path: &Path) -> Result, CheckPidError> { +fn check_pid_file(path: &Path) -> Result> { let Some(pid) = std::fs::read_to_string(&path) .ok() .and_then(|contents| contents.parse::().ok()) @@ -847,7 +742,7 @@ fn check_pid_file(path: &Path) -> Result, CheckPidError> { log::debug!( "Found PID file, but process with that PID does not exist. Removing PID file." ); - std::fs::remove_file(&path).map_err(|source| CheckPidError { source, pid })?; + std::fs::remove_file(&path).context("Failed to remove PID file")?; Ok(None) } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 8ac12588af..c4ba9b5154 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -3905,7 +3905,7 @@ pub mod tests { assert_eq!(workspace.active_pane(), &second_pane); second_pane.update(cx, |this, cx| { assert_eq!(this.active_item_index(), 1); - this.activate_previous_item(&Default::default(), window, cx); + this.activate_prev_item(false, window, cx); assert_eq!(this.active_item_index(), 0); }); workspace.activate_pane_in_direction(workspace::SplitDirection::Left, window, cx); @@ -3940,9 +3940,7 @@ pub mod tests { // Focus the second pane's non-search item window .update(cx, |_workspace, window, cx| { - second_pane.update(cx, |pane, cx| { - pane.activate_next_item(&Default::default(), window, cx) - }); + second_pane.update(cx, |pane, cx| pane.activate_next_item(true, window, cx)); }) .unwrap(); diff --git a/crates/settings/src/key_equivalents.rs b/crates/settings/src/key_equivalents.rs new file mode 100644 index 0000000000..6580137535 --- /dev/null +++ b/crates/settings/src/key_equivalents.rs @@ -0,0 +1,1424 @@ +use collections::HashMap; + +// On some keyboards (e.g. German QWERTZ) it is not possible to type the full ASCII range +// without using option. This means that some of our built in keyboard shortcuts do not work +// for those users. +// +// The way macOS solves this problem is to move shortcuts around so that they are all reachable, +// even if the mnemonic changes. https://developer.apple.com/documentation/swiftui/keyboardshortcut/localization-swift.struct +// +// For example, cmd-> is the "switch window" shortcut because the > key is right above tab. +// To ensure this doesn't cause problems for shortcuts defined for a QWERTY layout, apple moves +// any shortcuts defined as cmd-> to cmd-:. Coincidentally this s also the same keyboard position +// as cmd-> on a QWERTY layout. +// +// Another example is cmd-[ and cmd-], as they cannot be typed without option, those keys are remapped to cmd-ö +// and cmd-ä. These shortcuts are not in the same position as a QWERTY keyboard, because on a QWERTZ keyboard +// the + key is in the way; and shortcuts bound to cmd-+ are still typed as cmd-+ on either keyboard (though the +// specific key moves) +// +// As far as I can tell, there's no way to query the mappings Apple uses except by rendering a menu with every +// possible key combination, and inspecting the UI to see what it rendered. So that's what we did... +// +// These mappings were generated by running https://github.com/ConradIrwin/keyboard-inspector, tidying up the +// output to remove languages with no mappings and other oddities, and converting it to a less verbose representation with: +// jq -s 'map(to_entries | map({key: .key, value: [(.value | to_entries | map(.key) | join("")), (.value | to_entries | map(.value) | join(""))]}) | from_entries) | add' +// From there I used multi-cursor to produce this match statement. +#[cfg(target_os = "macos")] +pub fn get_key_equivalents(layout: &str) -> Option> { + let mappings: &[(char, char)] = match layout { + "com.apple.keylayout.ABC-AZERTY" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.ABC-QWERTZ" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Albanian" => &[ + ('"', '\''), + (':', 'Ç'), + (';', 'ç'), + ('<', ';'), + ('>', ':'), + ('@', '"'), + ('\'', '@'), + ('\\', 'ë'), + ('`', '<'), + ('|', 'Ë'), + ('~', '>'), + ], + "com.apple.keylayout.Austrian" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Azeri" => &[ + ('"', 'Ə'), + (',', 'ç'), + ('.', 'ş'), + ('/', '.'), + (':', 'I'), + (';', 'ı'), + ('<', 'Ç'), + ('>', 'Ş'), + ('?', ','), + ('W', 'Ü'), + ('[', 'ö'), + ('\'', 'ə'), + (']', 'ğ'), + ('w', 'ü'), + ('{', 'Ö'), + ('|', '/'), + ('}', 'Ğ'), + ], + "com.apple.keylayout.Belgian" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.Brazilian-ABNT2" => &[ + ('"', '`'), + ('/', 'ç'), + ('?', 'Ç'), + ('\'', '´'), + ('\\', '~'), + ('^', '¨'), + ('`', '\''), + ('|', '^'), + ('~', '"'), + ], + "com.apple.keylayout.Brazilian-Pro" => &[('^', 'ˆ'), ('~', '˜')], + "com.apple.keylayout.British" => &[('#', '£')], + "com.apple.keylayout.Canadian-CSA" => &[ + ('"', 'È'), + ('/', 'é'), + ('<', '\''), + ('>', '"'), + ('?', 'É'), + ('[', '^'), + ('\'', 'è'), + ('\\', 'à'), + (']', 'ç'), + ('`', 'ù'), + ('{', '¨'), + ('|', 'À'), + ('}', 'Ç'), + ('~', 'Ù'), + ], + "com.apple.keylayout.Croatian" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Croatian-PC" => &[ + ('"', 'Ć'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Czech" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ě'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ř'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ů'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', ')'), + ('^', '6'), + ('`', '¨'), + ('{', 'Ú'), + ('}', '('), + ('~', '`'), + ], + "com.apple.keylayout.Czech-QWERTY" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ě'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ř'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ů'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', ')'), + ('^', '6'), + ('`', '¨'), + ('{', 'Ú'), + ('}', '('), + ('~', '`'), + ], + "com.apple.keylayout.Danish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'æ'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ø'), + ('^', '&'), + ('`', '<'), + ('{', 'Æ'), + ('|', '*'), + ('}', 'Ø'), + ('~', '>'), + ], + "com.apple.keylayout.Faroese" => &[ + ('"', 'Ø'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Æ'), + (';', 'æ'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'å'), + ('\'', 'ø'), + ('\\', '\''), + (']', 'ð'), + ('^', '&'), + ('`', '<'), + ('{', 'Å'), + ('|', '*'), + ('}', 'Ð'), + ('~', '>'), + ], + "com.apple.keylayout.Finnish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.FinnishExtended" => &[ + ('"', 'ˆ'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.FinnishSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '@'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.French" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.French-PC" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('-', ')'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '-'), + ('7', 'è'), + ('8', '_'), + ('9', 'ç'), + (':', '§'), + (';', '!'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '*'), + (']', '$'), + ('^', '6'), + ('_', '°'), + ('`', '<'), + ('{', '¨'), + ('|', 'μ'), + ('}', '£'), + ('~', '>'), + ], + "com.apple.keylayout.French-numerical" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.German" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.German-DIN-2137" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Hawaiian" => &[('\'', 'ʻ')], + "com.apple.keylayout.Hungarian" => &[ + ('!', '\''), + ('"', 'Á'), + ('#', '+'), + ('$', '!'), + ('&', '='), + ('(', ')'), + (')', 'Ö'), + ('*', '('), + ('+', 'Ó'), + ('/', 'ü'), + ('0', 'ö'), + (':', 'É'), + (';', 'é'), + ('<', 'Ü'), + ('=', 'ó'), + ('>', ':'), + ('@', '"'), + ('[', 'ő'), + ('\'', 'á'), + ('\\', 'ű'), + (']', 'ú'), + ('^', '/'), + ('`', 'í'), + ('{', 'Ő'), + ('|', 'Ű'), + ('}', 'Ú'), + ('~', 'Í'), + ], + "com.apple.keylayout.Hungarian-QWERTY" => &[ + ('!', '\''), + ('"', 'Á'), + ('#', '+'), + ('$', '!'), + ('&', '='), + ('(', ')'), + (')', 'Ö'), + ('*', '('), + ('+', 'Ó'), + ('/', 'ü'), + ('0', 'ö'), + (':', 'É'), + (';', 'é'), + ('<', 'Ü'), + ('=', 'ó'), + ('>', ':'), + ('@', '"'), + ('[', 'ő'), + ('\'', 'á'), + ('\\', 'ű'), + (']', 'ú'), + ('^', '/'), + ('`', 'í'), + ('{', 'Ő'), + ('|', 'Ű'), + ('}', 'Ú'), + ('~', 'Í'), + ], + "com.apple.keylayout.Icelandic" => &[ + ('"', 'Ö'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Ð'), + (';', 'ð'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'æ'), + ('\'', 'ö'), + ('\\', 'þ'), + (']', '´'), + ('^', '&'), + ('`', '<'), + ('{', 'Æ'), + ('|', 'Þ'), + ('}', '´'), + ('~', '>'), + ], + "com.apple.keylayout.Irish" => &[('#', '£')], + "com.apple.keylayout.IrishExtended" => &[('#', '£')], + "com.apple.keylayout.Italian" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + (',', ';'), + ('.', ':'), + ('/', ','), + ('0', 'é'), + ('1', '&'), + ('2', '"'), + ('3', '\''), + ('4', '('), + ('5', 'ç'), + ('6', 'è'), + ('7', ')'), + ('8', '£'), + ('9', 'à'), + (':', '!'), + (';', 'ò'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', 'ì'), + ('\'', 'ù'), + ('\\', '§'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '^'), + ('|', '°'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.Italian-Pro" => &[ + ('"', '^'), + ('#', '£'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'é'), + (';', 'è'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ò'), + ('\'', 'ì'), + ('\\', 'ù'), + (']', 'à'), + ('^', '&'), + ('`', '<'), + ('{', 'ç'), + ('|', '§'), + ('}', '°'), + ('~', '>'), + ], + "com.apple.keylayout.LatinAmerican" => &[ + ('"', '¨'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Ñ'), + (';', 'ñ'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', '{'), + ('\'', '´'), + ('\\', '¿'), + (']', '}'), + ('^', '&'), + ('`', '<'), + ('{', '['), + ('|', '¡'), + ('}', ']'), + ('~', '>'), + ], + "com.apple.keylayout.Lithuanian" => &[ + ('!', 'Ą'), + ('#', 'Ę'), + ('$', 'Ė'), + ('%', 'Į'), + ('&', 'Ų'), + ('*', 'Ū'), + ('+', 'Ž'), + ('1', 'ą'), + ('2', 'č'), + ('3', 'ę'), + ('4', 'ė'), + ('5', 'į'), + ('6', 'š'), + ('7', 'ų'), + ('8', 'ū'), + ('=', 'ž'), + ('@', 'Č'), + ('^', 'Š'), + ], + "com.apple.keylayout.Maltese" => &[ + ('#', '£'), + ('[', 'ġ'), + (']', 'ħ'), + ('`', 'ż'), + ('{', 'Ġ'), + ('}', 'Ħ'), + ('~', 'Ż'), + ], + "com.apple.keylayout.NorthernSami" => &[ + ('"', 'Ŋ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('Q', 'Á'), + ('W', 'Š'), + ('X', 'Č'), + ('[', 'ø'), + ('\'', 'ŋ'), + ('\\', 'đ'), + (']', 'æ'), + ('^', '&'), + ('`', 'ž'), + ('q', 'á'), + ('w', 'š'), + ('x', 'č'), + ('{', 'Ø'), + ('|', 'Đ'), + ('}', 'Æ'), + ('~', 'Ž'), + ], + "com.apple.keylayout.Norwegian" => &[ + ('"', '^'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\'', '¨'), + ('\\', '@'), + (']', 'æ'), + ('^', '&'), + ('`', '<'), + ('{', 'Ø'), + ('|', '*'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.NorwegianExtended" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\\', '@'), + (']', 'æ'), + ('`', '<'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.NorwegianSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\'', '¨'), + ('\\', '@'), + (']', 'æ'), + ('^', '&'), + ('`', '<'), + ('{', 'Ø'), + ('|', '*'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.Polish" => &[ + ('!', '§'), + ('"', 'ę'), + ('#', '!'), + ('$', '?'), + ('%', '+'), + ('&', ':'), + ('(', '/'), + (')', '"'), + ('*', '_'), + ('+', ']'), + (',', '.'), + ('.', ','), + ('/', 'ż'), + (':', 'Ł'), + (';', 'ł'), + ('<', 'ś'), + ('=', '['), + ('>', 'ń'), + ('?', 'Ż'), + ('@', '%'), + ('[', 'ó'), + ('\'', 'ą'), + ('\\', ';'), + (']', '('), + ('^', '='), + ('_', 'ć'), + ('`', '<'), + ('{', 'ź'), + ('|', '$'), + ('}', ')'), + ('~', '>'), + ], + "com.apple.keylayout.Portuguese" => &[ + ('"', '`'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'ª'), + (';', 'º'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ç'), + ('\'', '´'), + (']', '~'), + ('^', '&'), + ('`', '<'), + ('{', 'Ç'), + ('}', '^'), + ('~', '>'), + ], + "com.apple.keylayout.Sami-PC" => &[ + ('"', 'Ŋ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('Q', 'Á'), + ('W', 'Š'), + ('X', 'Č'), + ('[', 'ø'), + ('\'', 'ŋ'), + ('\\', 'đ'), + (']', 'æ'), + ('^', '&'), + ('`', 'ž'), + ('q', 'á'), + ('w', 'š'), + ('x', 'č'), + ('{', 'Ø'), + ('|', 'Đ'), + ('}', 'Æ'), + ('~', 'Ž'), + ], + "com.apple.keylayout.Serbian-Latin" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Slovak" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ľ'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ť'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ô'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', 'ä'), + ('^', '6'), + ('`', 'ň'), + ('{', 'Ú'), + ('}', 'Ä'), + ('~', 'Ň'), + ], + "com.apple.keylayout.Slovak-QWERTY" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ľ'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ť'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ô'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', 'ä'), + ('^', '6'), + ('`', 'ň'), + ('{', 'Ú'), + ('}', 'Ä'), + ('~', 'Ň'), + ], + "com.apple.keylayout.Slovenian" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Spanish" => &[ + ('!', '¡'), + ('"', '¨'), + ('.', 'ç'), + ('/', '.'), + (':', 'º'), + (';', '´'), + ('<', '¿'), + ('>', 'Ç'), + ('@', '!'), + ('[', 'ñ'), + ('\'', '`'), + ('\\', '\''), + (']', ';'), + ('^', '/'), + ('`', '<'), + ('{', 'Ñ'), + ('|', '"'), + ('}', ':'), + ('~', '>'), + ], + "com.apple.keylayout.Spanish-ISO" => &[ + ('"', '¨'), + ('#', '·'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('.', 'ç'), + ('/', '.'), + (':', 'º'), + (';', '´'), + ('<', '¿'), + ('>', 'Ç'), + ('@', '"'), + ('[', 'ñ'), + ('\'', '`'), + ('\\', '\''), + (']', ';'), + ('^', '&'), + ('`', '<'), + ('{', 'Ñ'), + ('|', '"'), + ('}', '`'), + ('~', '>'), + ], + "com.apple.keylayout.Swedish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Swedish-Pro" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwedishSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '@'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwissFrench" => &[ + ('!', '+'), + ('"', '`'), + ('#', '*'), + ('$', 'ç'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', '!'), + ('/', '\''), + (':', 'ü'), + (';', 'è'), + ('<', ';'), + ('=', '¨'), + ('>', ':'), + ('@', '"'), + ('[', 'é'), + ('\'', '^'), + ('\\', '$'), + (']', 'à'), + ('^', '&'), + ('`', '<'), + ('{', 'ö'), + ('|', '£'), + ('}', 'ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwissGerman" => &[ + ('!', '+'), + ('"', '`'), + ('#', '*'), + ('$', 'ç'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', '!'), + ('/', '\''), + (':', 'è'), + (';', 'ü'), + ('<', ';'), + ('=', '¨'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '^'), + ('\\', '$'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'é'), + ('|', '£'), + ('}', 'à'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish" => &[ + ('"', '-'), + ('#', '"'), + ('$', '\''), + ('%', '('), + ('&', ')'), + ('(', '%'), + (')', ':'), + ('*', '_'), + (',', 'ö'), + ('-', 'ş'), + ('.', 'ç'), + ('/', '.'), + (':', '$'), + ('<', 'Ö'), + ('>', 'Ç'), + ('@', '*'), + ('[', 'ğ'), + ('\'', ','), + ('\\', 'ü'), + (']', 'ı'), + ('^', '/'), + ('_', 'Ş'), + ('`', '<'), + ('{', 'Ğ'), + ('|', 'Ü'), + ('}', 'I'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish-QWERTY-PC" => &[ + ('"', 'I'), + ('#', '^'), + ('$', '+'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', ':'), + (',', 'ö'), + ('.', 'ç'), + ('/', '*'), + (':', 'Ş'), + (';', 'ş'), + ('<', 'Ö'), + ('=', '.'), + ('>', 'Ç'), + ('@', '\''), + ('[', 'ğ'), + ('\'', 'ı'), + ('\\', ','), + (']', 'ü'), + ('^', '&'), + ('`', '<'), + ('{', 'Ğ'), + ('|', ';'), + ('}', 'Ü'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish-Standard" => &[ + ('"', 'Ş'), + ('#', '^'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (',', '.'), + ('.', ','), + (':', 'Ç'), + (';', 'ç'), + ('<', ':'), + ('=', '*'), + ('>', ';'), + ('@', '"'), + ('[', 'ğ'), + ('\'', 'ş'), + ('\\', 'ü'), + (']', 'ı'), + ('^', '&'), + ('`', 'ö'), + ('{', 'Ğ'), + ('|', 'Ü'), + ('}', 'I'), + ('~', 'Ö'), + ], + "com.apple.keylayout.Turkmen" => &[ + ('C', 'Ç'), + ('Q', 'Ä'), + ('V', 'Ý'), + ('X', 'Ü'), + ('[', 'ň'), + ('\\', 'ş'), + (']', 'ö'), + ('^', '№'), + ('`', 'ž'), + ('c', 'ç'), + ('q', 'ä'), + ('v', 'ý'), + ('x', 'ü'), + ('{', 'Ň'), + ('|', 'Ş'), + ('}', 'Ö'), + ('~', 'Ž'), + ], + "com.apple.keylayout.USInternational-PC" => &[('^', 'ˆ'), ('~', '˜')], + "com.apple.keylayout.Welsh" => &[('#', '£')], + + _ => return None, + }; + + Some(HashMap::from_iter(mappings.iter().cloned())) +} + +#[cfg(not(target_os = "macos"))] +pub fn get_key_equivalents(_layout: &str) -> Option> { + None +} diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 0e8303c4c1..ae3f42853a 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -3,8 +3,7 @@ use collections::{BTreeMap, HashMap, IndexMap}; use fs::Fs; use gpui::{ Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE, - KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, KeybindingKeystroke, Keystroke, - NoAction, SharedString, + KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, Keystroke, NoAction, SharedString, }; use schemars::{JsonSchema, json_schema}; use serde::Deserialize; @@ -212,6 +211,9 @@ impl KeymapFile { } pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult { + let key_equivalents = + crate::key_equivalents::get_key_equivalents(cx.keyboard_layout().id()); + if content.is_empty() { return KeymapFileLoadResult::Success { key_bindings: Vec::new(), @@ -253,6 +255,12 @@ impl KeymapFile { } }; + let key_equivalents = if *use_key_equivalents { + key_equivalents.as_ref() + } else { + None + }; + let mut section_errors = String::new(); if !unrecognized_fields.is_empty() { @@ -270,7 +278,7 @@ impl KeymapFile { keystrokes, action, context_predicate.clone(), - *use_key_equivalents, + key_equivalents, cx, ); match result { @@ -328,7 +336,7 @@ impl KeymapFile { keystrokes: &str, action: &KeymapAction, context: Option>, - use_key_equivalents: bool, + key_equivalents: Option<&HashMap>, cx: &App, ) -> std::result::Result { let (build_result, action_input_string) = match &action.0 { @@ -396,9 +404,8 @@ impl KeymapFile { keystrokes, action, context, - use_key_equivalents, + key_equivalents, action_input_string.map(SharedString::from), - cx.keyboard_mapper().as_ref(), ) { Ok(key_binding) => key_binding, Err(InvalidKeystrokeError { keystroke }) => { @@ -600,7 +607,6 @@ impl KeymapFile { mut operation: KeybindUpdateOperation<'a>, mut keymap_contents: String, tab_size: usize, - keyboard_mapper: &dyn gpui::PlatformKeyboardMapper, ) -> Result { match operation { // if trying to replace a keybinding that is not user-defined, treat it as an add operation @@ -640,7 +646,7 @@ impl KeymapFile { .action_value() .context("Failed to generate target action JSON value")?; let Some((index, keystrokes_str)) = - find_binding(&keymap, &target, &target_action_value, keyboard_mapper) + find_binding(&keymap, &target, &target_action_value) else { anyhow::bail!("Failed to find keybinding to remove"); }; @@ -675,7 +681,7 @@ impl KeymapFile { .context("Failed to generate source action JSON value")?; if let Some((index, keystrokes_str)) = - find_binding(&keymap, &target, &target_action_value, keyboard_mapper) + find_binding(&keymap, &target, &target_action_value) { if target.context == source.context { // if we are only changing the keybinding (common case) @@ -775,7 +781,7 @@ impl KeymapFile { } let use_key_equivalents = from.and_then(|from| { let action_value = from.action_value().context("Failed to serialize action value. `use_key_equivalents` on new keybinding may be incorrect.").log_err()?; - let (index, _) = find_binding(&keymap, &from, &action_value, keyboard_mapper)?; + let (index, _) = find_binding(&keymap, &from, &action_value)?; Some(keymap.0[index].use_key_equivalents) }).unwrap_or(false); if use_key_equivalents { @@ -802,7 +808,6 @@ impl KeymapFile { keymap: &'b KeymapFile, target: &KeybindUpdateTarget<'a>, target_action_value: &Value, - keyboard_mapper: &dyn gpui::PlatformKeyboardMapper, ) -> Option<(usize, &'b str)> { let target_context_parsed = KeyBindingContextPredicate::parse(target.context.unwrap_or("")).ok(); @@ -818,11 +823,8 @@ impl KeymapFile { for (keystrokes_str, action) in bindings { let Ok(keystrokes) = keystrokes_str .split_whitespace() - .map(|source| { - let keystroke = Keystroke::parse(source)?; - Ok(KeybindingKeystroke::new(keystroke, false, keyboard_mapper)) - }) - .collect::, InvalidKeystrokeError>>() + .map(Keystroke::parse) + .collect::, _>>() else { continue; }; @@ -830,7 +832,7 @@ impl KeymapFile { || !keystrokes .iter() .zip(target.keystrokes) - .all(|(a, b)| a.inner.should_match(b)) + .all(|(a, b)| a.should_match(b)) { continue; } @@ -845,7 +847,7 @@ impl KeymapFile { } } -#[derive(Clone, Debug)] +#[derive(Clone)] pub enum KeybindUpdateOperation<'a> { Replace { /// Describes the keybind to create @@ -914,7 +916,7 @@ impl<'a> KeybindUpdateOperation<'a> { #[derive(Debug, Clone)] pub struct KeybindUpdateTarget<'a> { pub context: Option<&'a str>, - pub keystrokes: &'a [KeybindingKeystroke], + pub keystrokes: &'a [Keystroke], pub action_name: &'a str, pub action_arguments: Option<&'a str>, } @@ -939,9 +941,6 @@ impl<'a> KeybindUpdateTarget<'a> { fn keystrokes_unparsed(&self) -> String { let mut keystrokes = String::with_capacity(self.keystrokes.len() * 8); for keystroke in self.keystrokes { - // The reason use `keystroke.unparse()` instead of `keystroke.inner.unparse()` - // here is that, we want the user to use `ctrl-shift-4` instead of `ctrl-$` - // by default on Windows. keystrokes.push_str(&keystroke.unparse()); keystrokes.push(' '); } @@ -960,7 +959,7 @@ impl<'a> KeybindUpdateTarget<'a> { } } -#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug)] +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] pub enum KeybindSource { User, Vim, @@ -1021,7 +1020,7 @@ impl From for KeyBindingMetaIndex { #[cfg(test)] mod tests { - use gpui::{DummyKeyboardMapper, KeybindingKeystroke, Keystroke}; + use gpui::Keystroke; use unindent::Unindent; use crate::{ @@ -1050,27 +1049,16 @@ mod tests { operation: KeybindUpdateOperation, expected: impl ToString, ) { - let result = KeymapFile::update_keybinding( - operation, - input.to_string(), - 4, - &gpui::DummyKeyboardMapper, - ) - .expect("Update succeeded"); + let result = KeymapFile::update_keybinding(operation, input.to_string(), 4) + .expect("Update succeeded"); pretty_assertions::assert_eq!(expected.to_string(), result); } #[track_caller] - fn parse_keystrokes(keystrokes: &str) -> Vec { + fn parse_keystrokes(keystrokes: &str) -> Vec { keystrokes .split(' ') - .map(|s| { - KeybindingKeystroke::new( - Keystroke::parse(s).expect("Keystrokes valid"), - false, - &DummyKeyboardMapper, - ) - }) + .map(|s| Keystroke::parse(s).expect("Keystrokes valid")) .collect() } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 1966755d62..b73ab9ae95 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -1,5 +1,6 @@ mod base_keymap_setting; mod editable_setting_control; +mod key_equivalents; mod keymap_file; mod settings_file; mod settings_json; @@ -13,6 +14,7 @@ use util::asset_str; pub use base_keymap_setting::*; pub use editable_setting_control::*; +pub use key_equivalents::*; pub use keymap_file::{ KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeybindUpdateOperation, KeybindUpdateTarget, KeymapFile, KeymapFileLoadResult, @@ -87,10 +89,7 @@ pub fn default_settings() -> Cow<'static, str> { #[cfg(target_os = "macos")] pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-macos.json"; -#[cfg(target_os = "windows")] -pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-windows.json"; - -#[cfg(not(any(target_os = "macos", target_os = "windows")))] +#[cfg(not(target_os = "macos"))] pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-linux.json"; pub fn default_keymap() -> Cow<'static, str> { diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 76c7166007..9c76725972 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -14,9 +14,9 @@ use gpui::{ Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Global, IsZero, KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or}, - KeyContext, KeybindingKeystroke, Keystroke, MouseButton, PlatformKeyboardMapper, Point, - ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task, - TextStyleRefinement, WeakEntity, actions, anchored, deferred, div, + KeyContext, Keystroke, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Stateful, + StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, actions, anchored, deferred, + div, }; use language::{Language, LanguageConfig, ToOffset as _}; use notifications::status_toast::{StatusToast, ToastIcon}; @@ -174,7 +174,7 @@ impl FilterState { #[derive(Debug, Default, PartialEq, Eq, Clone, Hash)] struct ActionMapping { - keystrokes: Vec, + keystrokes: Vec, context: Option, } @@ -236,7 +236,7 @@ struct ConflictState { } type ConflictKeybindMapping = HashMap< - Vec, + Vec, Vec<( Option, Vec, @@ -414,14 +414,12 @@ impl Focusable for KeymapEditor { } } /// Helper function to check if two keystroke sequences match exactly -fn keystrokes_match_exactly( - keystrokes1: &[KeybindingKeystroke], - keystrokes2: &[KeybindingKeystroke], -) -> bool { +fn keystrokes_match_exactly(keystrokes1: &[Keystroke], keystrokes2: &[Keystroke]) -> bool { keystrokes1.len() == keystrokes2.len() - && keystrokes1.iter().zip(keystrokes2).all(|(k1, k2)| { - k1.inner.key == k2.inner.key && k1.inner.modifiers == k2.inner.modifiers - }) + && keystrokes1 + .iter() + .zip(keystrokes2) + .all(|(k1, k2)| k1.key == k2.key && k1.modifiers == k2.modifiers) } impl KeymapEditor { @@ -511,7 +509,7 @@ impl KeymapEditor { self.filter_editor.read(cx).text(cx) } - fn current_keystroke_query(&self, cx: &App) -> Vec { + fn current_keystroke_query(&self, cx: &App) -> Vec { match self.search_mode { SearchMode::KeyStroke { .. } => self.keystroke_editor.read(cx).keystrokes().to_vec(), SearchMode::Normal => Default::default(), @@ -532,7 +530,7 @@ impl KeymapEditor { let keystroke_query = keystroke_query .into_iter() - .map(|keystroke| keystroke.inner.unparse()) + .map(|keystroke| keystroke.unparse()) .collect::>() .join(" "); @@ -556,7 +554,7 @@ impl KeymapEditor { async fn update_matches( this: WeakEntity, action_query: String, - keystroke_query: Vec, + keystroke_query: Vec, cx: &mut AsyncApp, ) -> anyhow::Result<()> { let action_query = command_palette::normalize_action_query(&action_query); @@ -605,15 +603,13 @@ impl KeymapEditor { { let query = &keystroke_query[query_cursor]; let keystroke = &keystrokes[keystroke_cursor]; - let matches = query - .inner - .modifiers - .is_subset_of(&keystroke.inner.modifiers) - && ((query.inner.key.is_empty() - || query.inner.key == keystroke.inner.key) - && query.inner.key_char.as_ref().is_none_or( - |q_kc| q_kc == &keystroke.inner.key, - )); + let matches = + query.modifiers.is_subset_of(&keystroke.modifiers) + && ((query.key.is_empty() + || query.key == keystroke.key) + && query.key_char.as_ref().is_none_or( + |q_kc| q_kc == &keystroke.key, + )); if matches { found_count += 1; query_cursor += 1; @@ -682,7 +678,7 @@ impl KeymapEditor { .map(KeybindSource::from_meta) .unwrap_or(KeybindSource::Unknown); - let keystroke_text = ui::text_for_keybinding_keystrokes(key_binding.keystrokes(), cx); + let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx); let ui_key_binding = ui::KeyBinding::new_from_gpui(key_binding.clone(), cx) .vim_mode(source == KeybindSource::Vim); @@ -1206,11 +1202,8 @@ impl KeymapEditor { .read(cx) .get_scrollbar_offset(Axis::Vertical), )); - let keyboard_mapper = cx.keyboard_mapper().clone(); - cx.spawn(async move |_, _| { - remove_keybinding(to_remove, &fs, tab_size, keyboard_mapper.as_ref()).await - }) - .detach_and_notify_err(window, cx); + cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await) + .detach_and_notify_err(window, cx); } fn copy_context_to_clipboard( @@ -1429,7 +1422,7 @@ impl ProcessedBinding { .map(|keybind| keybind.get_action_mapping()) } - fn keystrokes(&self) -> Option<&[KeybindingKeystroke]> { + fn keystrokes(&self) -> Option<&[Keystroke]> { self.ui_key_binding() .map(|binding| binding.keystrokes.as_slice()) } @@ -2227,7 +2220,7 @@ impl KeybindingEditorModal { Ok(action_arguments) } - fn validate_keystrokes(&self, cx: &App) -> anyhow::Result> { + fn validate_keystrokes(&self, cx: &App) -> anyhow::Result> { let new_keystrokes = self .keybind_editor .read_with(cx, |editor, _| editor.keystrokes().to_vec()); @@ -2323,7 +2316,6 @@ impl KeybindingEditorModal { }).unwrap_or(Ok(()))?; let create = self.creating; - let keyboard_mapper = cx.keyboard_mapper().clone(); cx.spawn(async move |this, cx| { let action_name = existing_keybind.action().name; @@ -2336,7 +2328,6 @@ impl KeybindingEditorModal { new_action_args.as_deref(), &fs, tab_size, - keyboard_mapper.as_ref(), ) .await { @@ -2454,21 +2445,11 @@ impl KeybindingEditorModal { } } -fn remove_key_char( - KeybindingKeystroke { - inner, - display_modifiers, - display_key, - }: KeybindingKeystroke, -) -> KeybindingKeystroke { - KeybindingKeystroke { - inner: Keystroke { - modifiers: inner.modifiers, - key: inner.key, - key_char: None, - }, - display_modifiers, - display_key, +fn remove_key_char(Keystroke { modifiers, key, .. }: Keystroke) -> Keystroke { + Keystroke { + modifiers, + key, + ..Default::default() } } @@ -3011,7 +2992,6 @@ async fn save_keybinding_update( new_args: Option<&str>, fs: &Arc, tab_size: usize, - keyboard_mapper: &dyn PlatformKeyboardMapper, ) -> anyhow::Result<()> { let keymap_contents = settings::KeymapFile::load_keymap_file(fs) .await @@ -3054,13 +3034,9 @@ async fn save_keybinding_update( let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry(); - let updated_keymap_contents = settings::KeymapFile::update_keybinding( - operation, - keymap_contents, - tab_size, - keyboard_mapper, - ) - .map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?; + let updated_keymap_contents = + settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) + .map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?; fs.write( paths::keymap_file().as_path(), updated_keymap_contents.as_bytes(), @@ -3081,7 +3057,6 @@ async fn remove_keybinding( existing: ProcessedBinding, fs: &Arc, tab_size: usize, - keyboard_mapper: &dyn PlatformKeyboardMapper, ) -> anyhow::Result<()> { let Some(keystrokes) = existing.keystrokes() else { anyhow::bail!("Cannot remove a keybinding that does not exist"); @@ -3105,13 +3080,9 @@ async fn remove_keybinding( }; let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry(); - let updated_keymap_contents = settings::KeymapFile::update_keybinding( - operation, - keymap_contents, - tab_size, - keyboard_mapper, - ) - .context("Failed to update keybinding")?; + let updated_keymap_contents = + settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) + .context("Failed to update keybinding")?; fs.write( paths::keymap_file().as_path(), updated_keymap_contents.as_bytes(), @@ -3377,15 +3348,12 @@ impl SerializableItem for KeymapEditor { } mod persistence { - use db::{query, sqlez::domain::Domain, sqlez_macros::sql}; + use db::{define_connection, query, sqlez_macros::sql}; use workspace::WorkspaceDb; - pub struct KeybindingEditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection); - - impl Domain for KeybindingEditorDb { - const NAME: &str = stringify!(KeybindingEditorDb); - - const MIGRATIONS: &[&str] = &[sql!( + define_connection! { + pub static ref KEYBINDING_EDITORS: KeybindingEditorDb = + &[sql!( CREATE TABLE keybinding_editors ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -3394,11 +3362,9 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]; + )]; } - db::static_connection!(KEYBINDING_EDITORS, KeybindingEditorDb, [WorkspaceDb]); - impl KeybindingEditorDb { query! { pub async fn save_keybinding_editor( diff --git a/crates/settings_ui/src/ui_components/keystroke_input.rs b/crates/settings_ui/src/ui_components/keystroke_input.rs index ca50d5c03d..1b8010853e 100644 --- a/crates/settings_ui/src/ui_components/keystroke_input.rs +++ b/crates/settings_ui/src/ui_components/keystroke_input.rs @@ -1,6 +1,6 @@ use gpui::{ Animation, AnimationExt, Context, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, - KeybindingKeystroke, Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions, + Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions, }; use ui::{ ActiveTheme as _, Color, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize, @@ -42,8 +42,8 @@ impl PartialEq for CloseKeystrokeResult { } pub struct KeystrokeInput { - keystrokes: Vec, - placeholder_keystrokes: Option>, + keystrokes: Vec, + placeholder_keystrokes: Option>, outer_focus_handle: FocusHandle, inner_focus_handle: FocusHandle, intercept_subscription: Option, @@ -70,7 +70,7 @@ impl KeystrokeInput { const KEYSTROKE_COUNT_MAX: usize = 3; pub fn new( - placeholder_keystrokes: Option>, + placeholder_keystrokes: Option>, window: &mut Window, cx: &mut Context, ) -> Self { @@ -97,7 +97,7 @@ impl KeystrokeInput { } } - pub fn set_keystrokes(&mut self, keystrokes: Vec, cx: &mut Context) { + pub fn set_keystrokes(&mut self, keystrokes: Vec, cx: &mut Context) { self.keystrokes = keystrokes; self.keystrokes_changed(cx); } @@ -106,7 +106,7 @@ impl KeystrokeInput { self.search = search; } - pub fn keystrokes(&self) -> &[KeybindingKeystroke] { + pub fn keystrokes(&self) -> &[Keystroke] { if let Some(placeholders) = self.placeholder_keystrokes.as_ref() && self.keystrokes.is_empty() { @@ -116,22 +116,18 @@ impl KeystrokeInput { && self .keystrokes .last() - .is_some_and(|last| last.display_key.is_empty()) + .is_some_and(|last| last.key.is_empty()) { return &self.keystrokes[..self.keystrokes.len() - 1]; } &self.keystrokes } - fn dummy(modifiers: Modifiers) -> KeybindingKeystroke { - KeybindingKeystroke { - inner: Keystroke { - modifiers, - key: "".to_string(), - key_char: None, - }, - display_modifiers: modifiers, - display_key: "".to_string(), + fn dummy(modifiers: Modifiers) -> Keystroke { + Keystroke { + modifiers, + key: "".to_string(), + key_char: None, } } @@ -258,7 +254,7 @@ impl KeystrokeInput { self.keystrokes_changed(cx); if let Some(last) = self.keystrokes.last_mut() - && last.display_key.is_empty() + && last.key.is_empty() && keystrokes_len <= Self::KEYSTROKE_COUNT_MAX { if !self.search && !event.modifiers.modified() { @@ -267,15 +263,13 @@ impl KeystrokeInput { } if self.search { if self.previous_modifiers.modified() { - last.display_modifiers |= event.modifiers; - last.inner.modifiers |= event.modifiers; + last.modifiers |= event.modifiers; } else { self.keystrokes.push(Self::dummy(event.modifiers)); } self.previous_modifiers |= event.modifiers; } else { - last.display_modifiers = event.modifiers; - last.inner.modifiers = event.modifiers; + last.modifiers = event.modifiers; return; } } else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX { @@ -303,17 +297,14 @@ impl KeystrokeInput { return; } - let mut keystroke = - KeybindingKeystroke::new(keystroke.clone(), false, cx.keyboard_mapper().as_ref()); + let mut keystroke = keystroke.clone(); if let Some(last) = self.keystrokes.last() - && last.display_key.is_empty() + && last.key.is_empty() && (!self.search || self.previous_modifiers.modified()) { - let display_key = keystroke.display_key.clone(); - let inner_key = keystroke.inner.key.clone(); + let key = keystroke.key.clone(); keystroke = last.clone(); - keystroke.display_key = display_key; - keystroke.inner.key = inner_key; + keystroke.key = key; self.keystrokes.pop(); } @@ -333,14 +324,11 @@ impl KeystrokeInput { self.keystrokes_changed(cx); if self.search { - self.previous_modifiers = keystroke.display_modifiers; + self.previous_modifiers = keystroke.modifiers; return; } - if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX - && keystroke.display_modifiers.modified() - { - self.keystrokes - .push(Self::dummy(keystroke.display_modifiers)); + if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX && keystroke.modifiers.modified() { + self.keystrokes.push(Self::dummy(keystroke.modifiers)); } } @@ -376,7 +364,7 @@ impl KeystrokeInput { &self.keystrokes }; keystrokes.iter().map(move |keystroke| { - h_flex().children(ui::render_keybinding_keystroke( + h_flex().children(ui::render_keystroke( keystroke, Some(Color::Default), Some(rems(0.875).into()), @@ -821,13 +809,9 @@ mod tests { /// Verifies that the keystrokes match the expected strings #[track_caller] pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self { - let actual: Vec = self.input.read_with(&self.cx, |input, _| { - input - .keystrokes - .iter() - .map(|keystroke| keystroke.inner.clone()) - .collect() - }); + let actual = self + .input + .read_with(&self.cx, |input, _| input.keystrokes.clone()); Self::expect_keystrokes_equal(&actual, expected); self } @@ -955,7 +939,7 @@ mod tests { } struct KeystrokeUpdateTracker { - initial_keystrokes: Vec, + initial_keystrokes: Vec, _subscription: Subscription, input: Entity, received_keystrokes_updated: bool, @@ -999,8 +983,8 @@ mod tests { ); } - fn keystrokes_str(ks: &[KeybindingKeystroke]) -> String { - ks.iter().map(|ks| ks.inner.unparse()).join(" ") + fn keystrokes_str(ks: &[Keystroke]) -> String { + ks.iter().map(|ks| ks.unparse()).join(" ") } } } diff --git a/crates/sqlez/src/domain.rs b/crates/sqlez/src/domain.rs index 5744a67da2..a83f4e18d6 100644 --- a/crates/sqlez/src/domain.rs +++ b/crates/sqlez/src/domain.rs @@ -1,12 +1,8 @@ use crate::connection::Connection; pub trait Domain: 'static { - const NAME: &str; - const MIGRATIONS: &[&str]; - - fn should_allow_migration_change(_index: usize, _old: &str, _new: &str) -> bool { - false - } + fn name() -> &'static str; + fn migrations() -> &'static [&'static str]; } pub trait Migrator: 'static { @@ -21,11 +17,7 @@ impl Migrator for () { impl Migrator for D { fn migrate(connection: &Connection) -> anyhow::Result<()> { - connection.migrate( - Self::NAME, - Self::MIGRATIONS, - Self::should_allow_migration_change, - ) + connection.migrate(Self::name(), Self::migrations()) } } diff --git a/crates/sqlez/src/migrations.rs b/crates/sqlez/src/migrations.rs index 2429ddeb41..7c59ffe658 100644 --- a/crates/sqlez/src/migrations.rs +++ b/crates/sqlez/src/migrations.rs @@ -34,12 +34,7 @@ impl Connection { /// Note: Unlike everything else in SQLez, migrations are run eagerly, without first /// preparing the SQL statements. This makes it possible to do multi-statement schema /// updates in a single string without running into prepare errors. - pub fn migrate( - &self, - domain: &'static str, - migrations: &[&'static str], - mut should_allow_migration_change: impl FnMut(usize, &str, &str) -> bool, - ) -> Result<()> { + pub fn migrate(&self, domain: &'static str, migrations: &[&'static str]) -> Result<()> { self.with_savepoint("migrating", || { // Setup the migrations table unconditionally self.exec(indoc! {" @@ -70,14 +65,9 @@ impl Connection { &sqlformat::QueryParams::None, Default::default(), ); - if completed_migration == migration - || migration.trim().starts_with("-- ALLOW_MIGRATION_CHANGE") - { + if completed_migration == migration { // Migration already run. Continue continue; - } else if should_allow_migration_change(index, &completed_migration, &migration) - { - continue; } else { anyhow::bail!(formatdoc! {" Migration changed for {domain} at step {index} @@ -118,7 +108,6 @@ mod test { a TEXT, b TEXT )"}], - disallow_migration_change, ) .unwrap(); @@ -147,7 +136,6 @@ mod test { d TEXT )"}, ], - disallow_migration_change, ) .unwrap(); @@ -226,11 +214,7 @@ mod test { // Run the migration verifying that the row got dropped connection - .migrate( - "test", - &["DELETE FROM test_table"], - disallow_migration_change, - ) + .migrate("test", &["DELETE FROM test_table"]) .unwrap(); assert_eq!( connection @@ -248,11 +232,7 @@ mod test { // Run the same migration again and verify that the table was left unchanged connection - .migrate( - "test", - &["DELETE FROM test_table"], - disallow_migration_change, - ) + .migrate("test", &["DELETE FROM test_table"]) .unwrap(); assert_eq!( connection @@ -272,28 +252,27 @@ mod test { .migrate( "test migration", &[ - "CREATE TABLE test (col INTEGER)", - "INSERT INTO test (col) VALUES (1)", + indoc! {" + CREATE TABLE test ( + col INTEGER + )"}, + indoc! {" + INSERT INTO test (col) VALUES (1)"}, ], - disallow_migration_change, ) .unwrap(); - let mut migration_changed = false; - // Create another migration with the same domain but different steps let second_migration_result = connection.migrate( "test migration", &[ - "CREATE TABLE test (color INTEGER )", - "INSERT INTO test (color) VALUES (1)", + indoc! {" + CREATE TABLE test ( + color INTEGER + )"}, + indoc! {" + INSERT INTO test (color) VALUES (1)"}, ], - |_, old, new| { - assert_eq!(old, "CREATE TABLE test (col INTEGER)"); - assert_eq!(new, "CREATE TABLE test (color INTEGER)"); - migration_changed = true; - false - }, ); // Verify new migration returns error when run @@ -305,11 +284,7 @@ mod test { let connection = Connection::open_memory(Some("test_create_alter_drop")); connection - .migrate( - "first_migration", - &["CREATE TABLE table1(a TEXT) STRICT;"], - disallow_migration_change, - ) + .migrate("first_migration", &["CREATE TABLE table1(a TEXT) STRICT;"]) .unwrap(); connection @@ -330,7 +305,6 @@ mod test { ALTER TABLE table2 RENAME TO table1; "}], - disallow_migration_change, ) .unwrap(); @@ -338,8 +312,4 @@ mod test { assert_eq!(res, "test text"); } - - fn disallow_migration_change(_: usize, _: &str, _: &str) -> bool { - false - } } diff --git a/crates/sqlez/src/thread_safe_connection.rs b/crates/sqlez/src/thread_safe_connection.rs index 58d3afe78f..afdc96586e 100644 --- a/crates/sqlez/src/thread_safe_connection.rs +++ b/crates/sqlez/src/thread_safe_connection.rs @@ -278,8 +278,12 @@ mod test { enum TestDomain {} impl Domain for TestDomain { - const NAME: &str = "test"; - const MIGRATIONS: &[&str] = &["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"]; + fn name() -> &'static str { + "test" + } + fn migrations() -> &'static [&'static str] { + &["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"] + } } for _ in 0..100 { @@ -308,9 +312,12 @@ mod test { fn wild_zed_lost_failure() { enum TestWorkspace {} impl Domain for TestWorkspace { - const NAME: &str = "workspace"; + fn name() -> &'static str { + "workspace" + } - const MIGRATIONS: &[&str] = &[" + fn migrations() -> &'static [&'static str] { + &[" CREATE TABLE workspaces( workspace_id INTEGER PRIMARY KEY, dock_visible INTEGER, -- Boolean @@ -329,7 +336,8 @@ mod test { ON DELETE CASCADE ON UPDATE CASCADE ) STRICT; - "]; + "] + } } let builder = diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs index bf3ce7b568..7c70bcd5b5 100644 --- a/crates/tab_switcher/src/tab_switcher.rs +++ b/crates/tab_switcher/src/tab_switcher.rs @@ -2,14 +2,12 @@ mod tab_switcher_tests; use collections::HashMap; -use editor::items::{ - entry_diagnostic_aware_icon_decoration_and_color, entry_git_aware_label_color, -}; +use editor::items::entry_git_aware_label_color; use fuzzy::StringMatchCandidate; use gpui::{ Action, AnyElement, App, Context, DismissEvent, Entity, EntityId, EventEmitter, FocusHandle, - Focusable, Modifiers, ModifiersChangedEvent, MouseButton, MouseUpEvent, ParentElement, Point, - Render, Styled, Task, WeakEntity, Window, actions, rems, + Focusable, Modifiers, ModifiersChangedEvent, MouseButton, MouseUpEvent, ParentElement, Render, + Styled, Task, WeakEntity, Window, actions, rems, }; use picker::{Picker, PickerDelegate}; use project::Project; @@ -17,14 +15,11 @@ use schemars::JsonSchema; use serde::Deserialize; use settings::Settings; use std::{cmp::Reverse, sync::Arc}; -use ui::{ - DecoratedIcon, IconDecoration, IconDecorationKind, ListItem, ListItemSpacing, Tooltip, - prelude::*, -}; +use ui::{ListItem, ListItemSpacing, Tooltip, prelude::*}; use util::ResultExt; use workspace::{ ModalView, Pane, SaveIntent, Workspace, - item::{ItemHandle, ItemSettings, ShowDiagnostics, TabContentParams}, + item::{ItemHandle, ItemSettings, TabContentParams}, pane::{Event as PaneEvent, render_item_indicator, tab_details}, }; @@ -238,77 +233,6 @@ pub struct TabSwitcherDelegate { restored_items: bool, } -impl TabMatch { - fn icon( - &self, - project: &Entity, - selected: bool, - window: &Window, - cx: &App, - ) -> Option { - let icon = self.item.tab_icon(window, cx)?; - let item_settings = ItemSettings::get_global(cx); - let show_diagnostics = item_settings.show_diagnostics; - let git_status_color = item_settings - .git_status - .then(|| { - let path = self.item.project_path(cx)?; - let project = project.read(cx); - let entry = project.entry_for_path(&path, cx)?; - let git_status = project - .project_path_git_status(&path, cx) - .map(|status| status.summary()) - .unwrap_or_default(); - Some(entry_git_aware_label_color( - git_status, - entry.is_ignored, - selected, - )) - }) - .flatten(); - let colored_icon = icon.color(git_status_color.unwrap_or_default()); - - let most_sever_diagostic_level = if show_diagnostics == ShowDiagnostics::Off { - None - } else { - let buffer_store = project.read(cx).buffer_store().read(cx); - let buffer = self - .item - .project_path(cx) - .and_then(|path| buffer_store.get_by_path(&path)) - .map(|buffer| buffer.read(cx)); - buffer.and_then(|buffer| { - buffer - .buffer_diagnostics(None) - .iter() - .map(|diagnostic_entry| diagnostic_entry.diagnostic.severity) - .min() - }) - }; - - let decorations = - entry_diagnostic_aware_icon_decoration_and_color(most_sever_diagostic_level) - .filter(|(d, _)| { - *d != IconDecorationKind::Triangle - || show_diagnostics != ShowDiagnostics::Errors - }) - .map(|(icon, color)| { - let knockout_item_color = if selected { - cx.theme().colors().element_selected - } else { - cx.theme().colors().element_background - }; - IconDecoration::new(icon, knockout_item_color, cx) - .color(color.color(cx)) - .position(Point { - x: px(-2.), - y: px(-2.), - }) - }); - Some(DecoratedIcon::new(colored_icon, decorations)) - } -} - impl TabSwitcherDelegate { #[allow(clippy::complexity)] fn new( @@ -650,7 +574,31 @@ impl PickerDelegate for TabSwitcherDelegate { }; let label = tab_match.item.tab_content(params, window, cx); - let icon = tab_match.icon(&self.project, selected, window, cx); + let icon = tab_match.item.tab_icon(window, cx).map(|icon| { + let git_status_color = ItemSettings::get_global(cx) + .git_status + .then(|| { + tab_match + .item + .project_path(cx) + .as_ref() + .and_then(|path| { + let project = self.project.read(cx); + let entry = project.entry_for_path(path, cx)?; + let git_status = project + .project_path_git_status(path, cx) + .map(|status| status.summary()) + .unwrap_or_default(); + Some((entry, git_status)) + }) + .map(|(entry, git_status)| { + entry_git_aware_label_color(git_status, entry.is_ignored, selected) + }) + }) + .flatten(); + + icon.color(git_status_color.unwrap_or_default()) + }); let indicator = render_item_indicator(tab_match.item.boxed_clone(), cx); let indicator_color = if let Some(ref indicator) = indicator { @@ -692,7 +640,7 @@ impl PickerDelegate for TabSwitcherDelegate { .inset(true) .toggle_state(selected) .child(h_flex().w_full().child(label)) - .start_slot::(icon) + .start_slot::(icon) .map(|el| { if self.selected_index == ix { el.end_slot::(close_button) diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index c7ebd314e4..b93b267f58 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -9,11 +9,7 @@ use std::path::{Path, PathBuf}; use ui::{App, Context, Pixels, Window}; use util::ResultExt as _; -use db::{ - query, - sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, -}; +use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql}; use workspace::{ ItemHandle, ItemId, Member, Pane, PaneAxis, PaneGroup, SerializableItem as _, Workspace, WorkspaceDb, WorkspaceId, @@ -379,13 +375,9 @@ impl<'de> Deserialize<'de> for SerializedAxis { } } -pub struct TerminalDb(ThreadSafeConnection); - -impl Domain for TerminalDb { - const NAME: &str = stringify!(TerminalDb); - - const MIGRATIONS: &[&str] = &[ - sql!( +define_connection! { + pub static ref TERMINAL_DB: TerminalDb = + &[sql!( CREATE TABLE terminals ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -422,8 +414,6 @@ impl Domain for TerminalDb { ]; } -db::static_connection!(TERMINAL_DB, TerminalDb, [WorkspaceDb]); - impl TerminalDb { query! { pub async fn update_workspace_id( diff --git a/crates/title_bar/src/onboarding_banner.rs b/crates/title_bar/src/onboarding_banner.rs index 1c28942490..ed43c5277a 100644 --- a/crates/title_bar/src/onboarding_banner.rs +++ b/crates/title_bar/src/onboarding_banner.rs @@ -119,7 +119,7 @@ impl Render for OnboardingBanner { h_flex() .h_full() .gap_1() - .child(Icon::new(self.details.icon_name).size(IconSize::XSmall)) + .child(Icon::new(self.details.icon_name).size(IconSize::Small)) .child( h_flex() .gap_0p5() diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index ad64dac9c6..b84a2800b6 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -275,11 +275,11 @@ impl TitleBar { let banner = cx.new(|cx| { OnboardingBanner::new( - "ACP Onboarding", - IconName::Sparkle, - "Bring Your Own Agent", - Some("Introducing:".into()), - zed_actions::agent::OpenAcpOnboardingModal.boxed_clone(), + "Debugger Onboarding", + IconName::Debug, + "The Debugger", + None, + zed_actions::debugger::OpenOnboardingModal.boxed_clone(), cx, ) }); diff --git a/crates/ui/src/components/callout.rs b/crates/ui/src/components/callout.rs index b1ead18ee7..7ffeda881c 100644 --- a/crates/ui/src/components/callout.rs +++ b/crates/ui/src/components/callout.rs @@ -132,7 +132,6 @@ impl RenderOnce for Callout { h_flex() .min_w_0() - .w_full() .p_2() .gap_2() .items_start() diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 21ab283d88..25575c4f1e 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -561,7 +561,7 @@ impl ContextMenu { action: Some(action.boxed_clone()), handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)), icon: Some(IconName::ArrowUpRight), - icon_size: IconSize::XSmall, + icon_size: IconSize::Small, icon_position: IconPosition::End, icon_color: None, disabled: false, diff --git a/crates/ui/src/components/image.rs b/crates/ui/src/components/image.rs index 6e552ddcee..09c3bbeb94 100644 --- a/crates/ui/src/components/image.rs +++ b/crates/ui/src/components/image.rs @@ -13,9 +13,6 @@ use crate::prelude::*; )] #[strum(serialize_all = "snake_case")] pub enum VectorName { - AcpGrid, - AcpLogo, - AcpLogoSerif, AiGrid, DebuggerGrid, Grid, diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index 81817045dc..1e7bb40c40 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -1,8 +1,8 @@ use crate::PlatformStyle; use crate::{Icon, IconName, IconSize, h_flex, prelude::*}; use gpui::{ - Action, AnyElement, App, FocusHandle, Global, IntoElement, KeybindingKeystroke, Keystroke, - Modifiers, Window, relative, + Action, AnyElement, App, FocusHandle, Global, IntoElement, Keystroke, Modifiers, Window, + relative, }; use itertools::Itertools; @@ -13,7 +13,7 @@ pub struct KeyBinding { /// More than one keystroke produces a chord. /// /// This should always contain at least one keystroke. - pub keystrokes: Vec, + pub keystrokes: Vec, /// The [`PlatformStyle`] to use when displaying this keybinding. platform_style: PlatformStyle, @@ -59,7 +59,7 @@ impl KeyBinding { cx.try_global::().is_some_and(|g| g.0) } - pub fn new(keystrokes: Vec, cx: &App) -> Self { + pub fn new(keystrokes: Vec, cx: &App) -> Self { Self { keystrokes, platform_style: PlatformStyle::platform(), @@ -99,16 +99,16 @@ impl KeyBinding { } fn render_key( - key: &str, + keystroke: &Keystroke, color: Option, platform_style: PlatformStyle, size: impl Into>, ) -> AnyElement { - let key_icon = icon_for_key(key, platform_style); + let key_icon = icon_for_key(keystroke, platform_style); match key_icon { Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(), None => { - let key = util::capitalize(key); + let key = util::capitalize(&keystroke.key); Key::new(&key, color).size(size).into_any_element() } } @@ -124,7 +124,7 @@ impl RenderOnce for KeyBinding { "KEY_BINDING-{}", self.keystrokes .iter() - .map(|k| k.display_key.to_string()) + .map(|k| k.key.to_string()) .collect::>() .join(" ") ) @@ -137,7 +137,7 @@ impl RenderOnce for KeyBinding { .py_0p5() .rounded_xs() .text_color(cx.theme().colors().text_muted) - .children(render_keybinding_keystroke( + .children(render_keystroke( keystroke, color, self.size, @@ -148,8 +148,8 @@ impl RenderOnce for KeyBinding { } } -pub fn render_keybinding_keystroke( - keystroke: &KeybindingKeystroke, +pub fn render_keystroke( + keystroke: &Keystroke, color: Option, size: impl Into>, platform_style: PlatformStyle, @@ -163,39 +163,26 @@ pub fn render_keybinding_keystroke( let size = size.into(); if use_text { - let element = Key::new( - keystroke_text( - &keystroke.display_modifiers, - &keystroke.display_key, - platform_style, - vim_mode, - ), - color, - ) - .size(size) - .into_any_element(); + let element = Key::new(keystroke_text(keystroke, platform_style, vim_mode), color) + .size(size) + .into_any_element(); vec![element] } else { let mut elements = Vec::new(); elements.extend(render_modifiers( - &keystroke.display_modifiers, + &keystroke.modifiers, platform_style, color, size, true, )); - elements.push(render_key( - &keystroke.display_key, - color, - platform_style, - size, - )); + elements.push(render_key(keystroke, color, platform_style, size)); elements } } -fn icon_for_key(key: &str, platform_style: PlatformStyle) -> Option { - match key { +fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option { + match keystroke.key.as_str() { "left" => Some(IconName::ArrowLeft), "right" => Some(IconName::ArrowRight), "up" => Some(IconName::ArrowUp), @@ -392,7 +379,7 @@ impl KeyIcon { /// Returns a textual representation of the key binding for the given [`Action`]. pub fn text_for_action(action: &dyn Action, window: &Window, cx: &App) -> Option { let key_binding = window.highest_precedence_binding_for_action(action)?; - Some(text_for_keybinding_keystrokes(key_binding.keystrokes(), cx)) + Some(text_for_keystrokes(key_binding.keystrokes(), cx)) } pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String { @@ -400,50 +387,22 @@ pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String { let vim_enabled = cx.try_global::().is_some(); keystrokes .iter() - .map(|keystroke| { - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - platform_style, - vim_enabled, - ) - }) + .map(|keystroke| keystroke_text(keystroke, platform_style, vim_enabled)) .join(" ") } -pub fn text_for_keybinding_keystrokes(keystrokes: &[KeybindingKeystroke], cx: &App) -> String { +pub fn text_for_keystroke(keystroke: &Keystroke, cx: &App) -> String { let platform_style = PlatformStyle::platform(); let vim_enabled = cx.try_global::().is_some(); - keystrokes - .iter() - .map(|keystroke| { - keystroke_text( - &keystroke.display_modifiers, - &keystroke.display_key, - platform_style, - vim_enabled, - ) - }) - .join(" ") -} - -pub fn text_for_keystroke(modifiers: &Modifiers, key: &str, cx: &App) -> String { - let platform_style = PlatformStyle::platform(); - let vim_enabled = cx.try_global::().is_some(); - keystroke_text(modifiers, key, platform_style, vim_enabled) + keystroke_text(keystroke, platform_style, vim_enabled) } /// Returns a textual representation of the given [`Keystroke`]. -fn keystroke_text( - modifiers: &Modifiers, - key: &str, - platform_style: PlatformStyle, - vim_mode: bool, -) -> String { +fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode: bool) -> String { let mut text = String::new(); let delimiter = '-'; - if modifiers.function { + if keystroke.modifiers.function { match vim_mode { false => text.push_str("Fn"), true => text.push_str("fn"), @@ -452,7 +411,7 @@ fn keystroke_text( text.push(delimiter); } - if modifiers.control { + if keystroke.modifiers.control { match (platform_style, vim_mode) { (PlatformStyle::Mac, false) => text.push_str("Control"), (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Ctrl"), @@ -462,7 +421,7 @@ fn keystroke_text( text.push(delimiter); } - if modifiers.platform { + if keystroke.modifiers.platform { match (platform_style, vim_mode) { (PlatformStyle::Mac, false) => text.push_str("Command"), (PlatformStyle::Mac, true) => text.push_str("cmd"), @@ -475,7 +434,7 @@ fn keystroke_text( text.push(delimiter); } - if modifiers.alt { + if keystroke.modifiers.alt { match (platform_style, vim_mode) { (PlatformStyle::Mac, false) => text.push_str("Option"), (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Alt"), @@ -485,7 +444,7 @@ fn keystroke_text( text.push(delimiter); } - if modifiers.shift { + if keystroke.modifiers.shift { match (platform_style, vim_mode) { (_, false) => text.push_str("Shift"), (_, true) => text.push_str("shift"), @@ -494,9 +453,9 @@ fn keystroke_text( } if vim_mode { - text.push_str(key) + text.push_str(&keystroke.key) } else { - let key = match key { + let key = match keystroke.key.as_str() { "pageup" => "PageUp", "pagedown" => "PageDown", key => &util::capitalize(key), @@ -603,11 +562,9 @@ mod tests { #[test] fn test_text_for_keystroke() { - let keystroke = Keystroke::parse("cmd-c").unwrap(); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Mac, false ), @@ -615,8 +572,7 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Linux, false ), @@ -624,19 +580,16 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Windows, false ), "Win-C".to_string() ); - let keystroke = Keystroke::parse("ctrl-alt-delete").unwrap(); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("ctrl-alt-delete").unwrap(), PlatformStyle::Mac, false ), @@ -644,8 +597,7 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("ctrl-alt-delete").unwrap(), PlatformStyle::Linux, false ), @@ -653,19 +605,16 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("ctrl-alt-delete").unwrap(), PlatformStyle::Windows, false ), "Ctrl-Alt-Delete".to_string() ); - let keystroke = Keystroke::parse("shift-pageup").unwrap(); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("shift-pageup").unwrap(), PlatformStyle::Mac, false ), @@ -673,8 +622,7 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("shift-pageup").unwrap(), PlatformStyle::Linux, false, ), @@ -682,8 +630,7 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("shift-pageup").unwrap(), PlatformStyle::Windows, false ), diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 726022021d..2bc531268d 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -23,8 +23,6 @@ actions!( HelixInsert, /// Appends at the end of the selection. HelixAppend, - /// Goes to the location of the last modification. - HelixGotoLastModification, ] ); @@ -33,7 +31,6 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, Vim::helix_insert); Vim::action(editor, cx, Vim::helix_append); Vim::action(editor, cx, Vim::helix_yank); - Vim::action(editor, cx, Vim::helix_goto_last_modification); } impl Vim { @@ -433,15 +430,6 @@ impl Vim { }); self.switch_mode(Mode::HelixNormal, true, window, cx); } - - pub fn helix_goto_last_modification( - &mut self, - _: &HelixGotoLastModification, - window: &mut Window, - cx: &mut Context, - ) { - self.jump(".".into(), false, false, window, cx); - } } #[cfg(test)] @@ -453,7 +441,6 @@ mod test { #[gpui::test] async fn test_word_motions(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); // « // ˇ // » @@ -515,7 +502,6 @@ mod test { #[gpui::test] async fn test_delete(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); // test delete a selection cx.set_state( @@ -596,7 +582,6 @@ mod test { #[gpui::test] async fn test_f_and_t(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); cx.set_state( indoc! {" @@ -650,7 +635,6 @@ mod test { #[gpui::test] async fn test_newline_char(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal); @@ -668,7 +652,6 @@ mod test { #[gpui::test] async fn test_insert_selected(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); cx.set_state( indoc! {" «The ˇ»quick brown @@ -691,7 +674,6 @@ mod test { #[gpui::test] async fn test_append(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); // test from the end of the selection cx.set_state( indoc! {" @@ -734,7 +716,6 @@ mod test { #[gpui::test] async fn test_replace(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); // No selection (single character) cx.set_state("ˇaa", Mode::HelixNormal); @@ -782,72 +763,4 @@ mod test { cx.shared_clipboard().assert_eq("worl"); cx.assert_state("hello «worlˇ»d", Mode::HelixNormal); } - #[gpui::test] - async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - // First copy some text to clipboard - cx.set_state("«hello worldˇ»", Mode::HelixNormal); - cx.simulate_keystrokes("y"); - - // Test paste with shift-r on single cursor - cx.set_state("foo ˇbar", Mode::HelixNormal); - cx.simulate_keystrokes("shift-r"); - - cx.assert_state("foo hello worldˇbar", Mode::HelixNormal); - - // Test paste with shift-r on selection - cx.set_state("foo «barˇ» baz", Mode::HelixNormal); - cx.simulate_keystrokes("shift-r"); - - cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal); - } - - #[gpui::test] - async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - // Make a modification at a specific location - cx.set_state("ˇhello", Mode::HelixNormal); - assert_eq!(cx.mode(), Mode::HelixNormal); - cx.simulate_keystrokes("i"); - assert_eq!(cx.mode(), Mode::Insert); - cx.simulate_keystrokes("escape"); - assert_eq!(cx.mode(), Mode::HelixNormal); - } - - #[gpui::test] - async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - // Make a modification at a specific location - cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal); - cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal); - cx.simulate_keystrokes("i"); - cx.simulate_keystrokes("escape"); - cx.simulate_keystrokes("i"); - cx.simulate_keystrokes("m o d i f i e d space"); - cx.simulate_keystrokes("escape"); - - // TODO: this fails, because state is no longer helix - cx.assert_state( - "line one\nline modified ˇtwo\nline three", - Mode::HelixNormal, - ); - - // Move cursor away from the modification - cx.simulate_keystrokes("up"); - - // Use "g ." to go back to last modification - cx.simulate_keystrokes("g ."); - - // Verify we're back at the modification location and still in HelixNormal mode - cx.assert_state( - "line one\nline modifiedˇ two\nline three", - Mode::HelixNormal, - ); - } } diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index dba003ec5f..4fbeec7236 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -203,10 +203,7 @@ impl Vim { // hook into the existing to clear out any vim search state on cmd+f or edit -> find. fn search_deploy(&mut self, _: &buffer_search::Deploy, _: &mut Window, cx: &mut Context) { - // Preserve the current mode when resetting search state - let current_mode = self.mode; self.search = Default::default(); - self.search.prior_mode = current_mode; cx.propagate(); } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index fe4bc7433d..c0176cb12c 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -7,10 +7,8 @@ use crate::{motion::Motion, object::Object}; use anyhow::Result; use collections::HashMap; use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor}; -use db::{ - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, -}; +use db::define_connection; +use db::sqlez_macros::sql; use editor::display_map::{is_invisible, replacement}; use editor::{Anchor, ClipboardSelection, Editor, MultiBuffer, ToPoint as EditorToPoint}; use gpui::{ @@ -1670,12 +1668,8 @@ impl MarksView { } } -pub struct VimDb(ThreadSafeConnection); - -impl Domain for VimDb { - const NAME: &str = stringify!(VimDb); - - const MIGRATIONS: &[&str] = &[ +define_connection! ( + pub static ref DB: VimDb = &[ sql! ( CREATE TABLE vim_marks ( workspace_id INTEGER, @@ -1695,9 +1689,7 @@ impl Domain for VimDb { ON vim_global_marks_paths(workspace_id, mark_name); ), ]; -} - -db::static_connection!(DB, VimDb, [WorkspaceDb]); +); struct SerializedMark { path: Arc, diff --git a/crates/watch/src/watch.rs b/crates/watch/src/watch.rs index 71dab74820..f0ed5b4a18 100644 --- a/crates/watch/src/watch.rs +++ b/crates/watch/src/watch.rs @@ -162,19 +162,6 @@ impl Receiver { pending_waker_id: None, } } - - /// Creates a new [`Receiver`] holding an initial value that will never change. - pub fn constant(value: T) -> Self { - let state = Arc::new(RwLock::new(State { - value, - wakers: BTreeMap::new(), - next_waker_id: WakerId::default(), - version: 0, - closed: false, - })); - - Self { state, version: 0 } - } } impl Receiver { diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 869aa5322e..570657ba8f 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -29,6 +29,7 @@ test-support = [ any_vec.workspace = true anyhow.workspace = true async-recursion.workspace = true +bincode.workspace = true call.workspace = true client.workspace = true clock.workspace = true @@ -79,6 +80,5 @@ project = { workspace = true, features = ["test-support"] } session = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] } http_client = { workspace = true, features = ["test-support"] } -pretty_assertions.workspace = true tempfile.workspace = true zlog.workspace = true diff --git a/crates/workspace/src/history_manager.rs b/crates/workspace/src/history_manager.rs index f68b58ff82..a8387369f4 100644 --- a/crates/workspace/src/history_manager.rs +++ b/crates/workspace/src/history_manager.rs @@ -5,9 +5,7 @@ use smallvec::SmallVec; use ui::App; use util::{ResultExt, paths::PathExt}; -use crate::{ - NewWindow, SerializedWorkspaceLocation, WORKSPACE_DB, WorkspaceId, path_list::PathList, -}; +use crate::{NewWindow, SerializedWorkspaceLocation, WORKSPACE_DB, WorkspaceId}; pub fn init(cx: &mut App) { let manager = cx.new(|_| HistoryManager::new()); @@ -46,13 +44,7 @@ impl HistoryManager { .unwrap_or_default() .into_iter() .rev() - .filter_map(|(id, location, paths)| { - if matches!(location, SerializedWorkspaceLocation::Local) { - Some(HistoryManagerEntry::new(id, &paths)) - } else { - None - } - }) + .map(|(id, location)| HistoryManagerEntry::new(id, &location)) .collect::>(); this.update(cx, |this, cx| { this.history = recent_folders; @@ -126,9 +118,9 @@ impl HistoryManager { } impl HistoryManagerEntry { - pub fn new(id: WorkspaceId, paths: &PathList) -> Self { - let path = paths - .paths() + pub fn new(id: WorkspaceId, location: &SerializedWorkspaceLocation) -> Self { + let path = location + .sorted_paths() .iter() .map(|path| path.compact()) .collect::>(); diff --git a/crates/workspace/src/invalid_buffer_view.rs b/crates/workspace/src/invalid_buffer_view.rs deleted file mode 100644 index b8c0db29d3..0000000000 --- a/crates/workspace/src/invalid_buffer_view.rs +++ /dev/null @@ -1,111 +0,0 @@ -use std::{path::Path, sync::Arc}; - -use gpui::{EventEmitter, FocusHandle, Focusable}; -use ui::{ - App, Button, ButtonCommon, ButtonStyle, Clickable, Context, FluentBuilder, InteractiveElement, - KeyBinding, ParentElement, Render, SharedString, Styled as _, Window, h_flex, v_flex, -}; -use zed_actions::workspace::OpenWithSystem; - -use crate::Item; - -/// A view to display when a certain buffer fails to open. -pub struct InvalidBufferView { - /// Which path was attempted to open. - pub abs_path: Arc, - /// An error message, happened when opening the buffer. - pub error: SharedString, - is_local: bool, - focus_handle: FocusHandle, -} - -impl InvalidBufferView { - pub fn new( - abs_path: &Path, - is_local: bool, - e: &anyhow::Error, - _: &mut Window, - cx: &mut App, - ) -> Self { - Self { - is_local, - abs_path: Arc::from(abs_path), - error: format!("{e}").into(), - focus_handle: cx.focus_handle(), - } - } -} - -impl Item for InvalidBufferView { - type Event = (); - - fn tab_content_text(&self, mut detail: usize, _: &App) -> SharedString { - // Ensure we always render at least the filename. - detail += 1; - - let path = self.abs_path.as_ref(); - - let mut prefix = path; - while detail > 0 { - if let Some(parent) = prefix.parent() { - prefix = parent; - detail -= 1; - } else { - break; - } - } - - let path = if detail > 0 { - path - } else { - path.strip_prefix(prefix).unwrap_or(path) - }; - - SharedString::new(path.to_string_lossy()) - } -} - -impl EventEmitter<()> for InvalidBufferView {} - -impl Focusable for InvalidBufferView { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for InvalidBufferView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl gpui::IntoElement { - let abs_path = self.abs_path.clone(); - v_flex() - .size_full() - .track_focus(&self.focus_handle(cx)) - .flex_none() - .justify_center() - .overflow_hidden() - .key_context("InvalidBuffer") - .child( - h_flex().size_full().justify_center().child( - v_flex() - .justify_center() - .gap_2() - .child(h_flex().justify_center().child("Unsupported file type")) - .when(self.is_local, |contents| { - contents.child( - h_flex().justify_center().child( - Button::new("open-with-system", "Open in Default App") - .on_click(move |_, _, cx| { - cx.open_with_system(&abs_path); - }) - .style(ButtonStyle::Outlined) - .key_binding(KeyBinding::for_action( - &OpenWithSystem, - window, - cx, - )), - ), - ) - }), - ), - ) - } -} diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index db91bd82b9..5a497398f9 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -1,7 +1,6 @@ use crate::{ CollaboratorId, DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory, SerializableItemRegistry, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, - invalid_buffer_view::InvalidBufferView, pane::{self, Pane}, persistence::model::ItemId, searchable::SearchableItemHandle, @@ -23,7 +22,6 @@ use std::{ any::{Any, TypeId}, cell::RefCell, ops::Range, - path::Path, rc::Rc, sync::Arc, time::Duration, @@ -1163,22 +1161,6 @@ pub trait ProjectItem: Item { ) -> Self where Self: Sized; - - /// A fallback handler, which will be called after [`project::ProjectItem::try_open`] fails, - /// with the error from that failure as an argument. - /// Allows to open an item that can gracefully display and handle errors. - fn for_broken_project_item( - _abs_path: &Path, - _is_local: bool, - _e: &anyhow::Error, - _window: &mut Window, - _cx: &mut App, - ) -> Option - where - Self: Sized, - { - None - } } #[derive(Debug)] diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index fe8014d9f7..23c8c0b185 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2,7 +2,6 @@ use crate::{ CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenOptions, OpenTerminal, OpenVisible, SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace, WorkspaceItemBuilder, - invalid_buffer_view::InvalidBufferView, item::{ ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings, ProjectItemKind, SaveOptions, ShowCloseButton, ShowDiagnostics, TabContentParams, @@ -514,7 +513,7 @@ impl Pane { } } - fn alternate_file(&mut self, _: &AlternateFile, window: &mut Window, cx: &mut Context) { + fn alternate_file(&mut self, window: &mut Window, cx: &mut Context) { let (_, alternative) = &self.alternate_file_items; if let Some(alternative) = alternative { let existing = self @@ -788,7 +787,7 @@ impl Pane { !self.nav_history.0.lock().forward_stack.is_empty() } - pub fn navigate_backward(&mut self, _: &GoBack, window: &mut Window, cx: &mut Context) { + pub fn navigate_backward(&mut self, window: &mut Window, cx: &mut Context) { if let Some(workspace) = self.workspace.upgrade() { let pane = cx.entity().downgrade(); window.defer(cx, move |window, cx| { @@ -799,7 +798,7 @@ impl Pane { } } - fn navigate_forward(&mut self, _: &GoForward, window: &mut Window, cx: &mut Context) { + fn navigate_forward(&mut self, window: &mut Window, cx: &mut Context) { if let Some(workspace) = self.workspace.upgrade() { let pane = cx.entity().downgrade(); window.defer(cx, move |window, cx| { @@ -898,43 +897,19 @@ impl Pane { } } } - - let set_up_existing_item = - |index: usize, pane: &mut Self, window: &mut Window, cx: &mut Context| { - // If the item is already open, and the item is a preview item - // and we are not allowing items to open as preview, mark the item as persistent. - if let Some(preview_item_id) = pane.preview_item_id - && let Some(tab) = pane.items.get(index) - && tab.item_id() == preview_item_id - && !allow_preview - { - pane.set_preview_item_id(None, cx); - } - if activate { - pane.activate_item(index, focus_item, focus_item, window, cx); - } - }; - let set_up_new_item = |new_item: Box, - destination_index: Option, - pane: &mut Self, - window: &mut Window, - cx: &mut Context| { - if allow_preview { - pane.set_preview_item_id(Some(new_item.item_id()), cx); - } - pane.add_item_inner( - new_item, - true, - focus_item, - activate, - destination_index, - window, - cx, - ); - }; - if let Some((index, existing_item)) = existing_item { - set_up_existing_item(index, self, window, cx); + // If the item is already open, and the item is a preview item + // and we are not allowing items to open as preview, mark the item as persistent. + if let Some(preview_item_id) = self.preview_item_id + && let Some(tab) = self.items.get(index) + && tab.item_id() == preview_item_id + && !allow_preview + { + self.set_preview_item_id(None, cx); + } + if activate { + self.activate_item(index, focus_item, focus_item, window, cx); + } existing_item } else { // If the item is being opened as preview and we have an existing preview tab, @@ -946,46 +921,21 @@ impl Pane { }; let new_item = build_item(self, window, cx); - // A special case that won't ever get a `project_entry_id` but has to be deduplicated nonetheless. - if let Some(invalid_buffer_view) = new_item.downcast::() { - let mut already_open_view = None; - let mut views_to_close = HashSet::default(); - for existing_error_view in self - .items_of_type::() - .filter(|item| item.read(cx).abs_path == invalid_buffer_view.read(cx).abs_path) - { - if already_open_view.is_none() - && existing_error_view.read(cx).error == invalid_buffer_view.read(cx).error - { - already_open_view = Some(existing_error_view); - } else { - views_to_close.insert(existing_error_view.item_id()); - } - } - let resulting_item = match already_open_view { - Some(already_open_view) => { - if let Some(index) = self.index_for_item_id(already_open_view.item_id()) { - set_up_existing_item(index, self, window, cx); - } - Box::new(already_open_view) as Box<_> - } - None => { - set_up_new_item(new_item.clone(), destination_index, self, window, cx); - new_item - } - }; - - self.close_items(window, cx, SaveIntent::Skip, |existing_item| { - views_to_close.contains(&existing_item) - }) - .detach(); - - resulting_item - } else { - set_up_new_item(new_item.clone(), destination_index, self, window, cx); - new_item + if allow_preview { + self.set_preview_item_id(Some(new_item.item_id()), cx); } + self.add_item_inner( + new_item.clone(), + true, + focus_item, + activate, + destination_index, + window, + cx, + ); + + new_item } } @@ -1283,9 +1233,9 @@ impl Pane { } } - pub fn activate_previous_item( + pub fn activate_prev_item( &mut self, - _: &ActivatePreviousItem, + activate_pane: bool, window: &mut Window, cx: &mut Context, ) { @@ -1295,12 +1245,12 @@ impl Pane { } else if !self.items.is_empty() { index = self.items.len() - 1; } - self.activate_item(index, true, true, window, cx); + self.activate_item(index, activate_pane, activate_pane, window, cx); } pub fn activate_next_item( &mut self, - _: &ActivateNextItem, + activate_pane: bool, window: &mut Window, cx: &mut Context, ) { @@ -1310,15 +1260,10 @@ impl Pane { } else { index = 0; } - self.activate_item(index, true, true, window, cx); + self.activate_item(index, activate_pane, activate_pane, window, cx); } - pub fn swap_item_left( - &mut self, - _: &SwapItemLeft, - window: &mut Window, - cx: &mut Context, - ) { + pub fn swap_item_left(&mut self, window: &mut Window, cx: &mut Context) { let index = self.active_item_index; if index == 0 { return; @@ -1328,14 +1273,9 @@ impl Pane { self.activate_item(index - 1, true, true, window, cx); } - pub fn swap_item_right( - &mut self, - _: &SwapItemRight, - window: &mut Window, - cx: &mut Context, - ) { + pub fn swap_item_right(&mut self, window: &mut Window, cx: &mut Context) { let index = self.active_item_index; - if index + 1 >= self.items.len() { + if index + 1 == self.items.len() { return; } @@ -1343,16 +1283,6 @@ impl Pane { self.activate_item(index + 1, true, true, window, cx); } - pub fn activate_last_item( - &mut self, - _: &ActivateLastItem, - window: &mut Window, - cx: &mut Context, - ) { - let index = self.items.len().saturating_sub(1); - self.activate_item(index, true, true, window, cx); - } - pub fn close_active_item( &mut self, action: &CloseActiveItem, @@ -2901,9 +2831,7 @@ impl Pane { .on_click({ let entity = cx.entity(); move |_, window, cx| { - entity.update(cx, |pane, cx| { - pane.navigate_backward(&Default::default(), window, cx) - }) + entity.update(cx, |pane, cx| pane.navigate_backward(window, cx)) } }) .disabled(!self.can_navigate_backward()) @@ -2918,11 +2846,7 @@ impl Pane { .icon_size(IconSize::Small) .on_click({ let entity = cx.entity(); - move |_, window, cx| { - entity.update(cx, |pane, cx| { - pane.navigate_forward(&Default::default(), window, cx) - }) - } + move |_, window, cx| entity.update(cx, |pane, cx| pane.navigate_forward(window, cx)) }) .disabled(!self.can_navigate_forward()) .tooltip({ @@ -3554,6 +3478,9 @@ impl Render for Pane { .size_full() .flex_none() .overflow_hidden() + .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| { + pane.alternate_file(window, cx); + })) .on_action( cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)), ) @@ -3570,6 +3497,12 @@ impl Render for Pane { .on_action( cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)), ) + .on_action( + cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)), + ) + .on_action( + cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)), + ) .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| { cx.emit(Event::JoinIntoNext); })) @@ -3577,8 +3510,6 @@ impl Render for Pane { cx.emit(Event::JoinAll); })) .on_action(cx.listener(Pane::toggle_zoom)) - .on_action(cx.listener(Self::navigate_backward)) - .on_action(cx.listener(Self::navigate_forward)) .on_action( cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| { pane.activate_item( @@ -3590,14 +3521,33 @@ impl Render for Pane { ); }), ) - .on_action(cx.listener(Self::alternate_file)) - .on_action(cx.listener(Self::activate_last_item)) - .on_action(cx.listener(Self::activate_previous_item)) - .on_action(cx.listener(Self::activate_next_item)) - .on_action(cx.listener(Self::swap_item_left)) - .on_action(cx.listener(Self::swap_item_right)) - .on_action(cx.listener(Self::toggle_pin_tab)) - .on_action(cx.listener(Self::unpin_all_tabs)) + .on_action( + cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| { + pane.activate_item(pane.items.len().saturating_sub(1), true, true, window, cx); + }), + ) + .on_action( + cx.listener(|pane: &mut Pane, _: &ActivatePreviousItem, window, cx| { + pane.activate_prev_item(true, window, cx); + }), + ) + .on_action( + cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| { + pane.activate_next_item(true, window, cx); + }), + ) + .on_action( + cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)), + ) + .on_action( + cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)), + ) + .on_action(cx.listener(|pane, action, window, cx| { + pane.toggle_pin_tab(action, window, cx); + })) + .on_action(cx.listener(|pane, action, window, cx| { + pane.unpin_all_tabs(action, window, cx); + })) .when(PreviewTabsSettings::get_global(cx).enabled, |this| { this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| { if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) { @@ -6452,57 +6402,6 @@ mod tests { .unwrap(); } - #[gpui::test] - async fn test_item_swapping_actions(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - assert_item_labels(&pane, [], cx); - - // Test that these actions do not panic - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_right(&Default::default(), window, cx); - }); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_left(&Default::default(), window, cx); - }); - - add_labeled_item(&pane, "A", false, cx); - add_labeled_item(&pane, "B", false, cx); - add_labeled_item(&pane, "C", false, cx); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_right(&Default::default(), window, cx); - }); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_left(&Default::default(), window, cx); - }); - assert_item_labels(&pane, ["A", "C*", "B"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_left(&Default::default(), window, cx); - }); - assert_item_labels(&pane, ["C*", "A", "B"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_left(&Default::default(), window, cx); - }); - assert_item_labels(&pane, ["C*", "A", "B"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_right(&Default::default(), window, cx); - }); - assert_item_labels(&pane, ["A", "C*", "B"], cx); - } - fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); diff --git a/crates/workspace/src/path_list.rs b/crates/workspace/src/path_list.rs deleted file mode 100644 index cf463e6b22..0000000000 --- a/crates/workspace/src/path_list.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; - -use util::paths::SanitizedPath; - -/// A list of absolute paths, in a specific order. -/// -/// The paths are stored in lexicographic order, so that they can be compared to -/// other path lists without regard to the order of the paths. -#[derive(Default, PartialEq, Eq, Debug, Clone)] -pub struct PathList { - paths: Arc<[PathBuf]>, - order: Arc<[usize]>, -} - -#[derive(Debug)] -pub struct SerializedPathList { - pub paths: String, - pub order: String, -} - -impl PathList { - pub fn new>(paths: &[P]) -> Self { - let mut indexed_paths: Vec<(usize, PathBuf)> = paths - .iter() - .enumerate() - .map(|(ix, path)| (ix, SanitizedPath::from(path).into())) - .collect(); - indexed_paths.sort_by(|(_, a), (_, b)| a.cmp(b)); - let order = indexed_paths.iter().map(|e| e.0).collect::>().into(); - let paths = indexed_paths - .into_iter() - .map(|e| e.1) - .collect::>() - .into(); - Self { order, paths } - } - - pub fn is_empty(&self) -> bool { - self.paths.is_empty() - } - - pub fn paths(&self) -> &[PathBuf] { - self.paths.as_ref() - } - - pub fn order(&self) -> &[usize] { - self.order.as_ref() - } - - pub fn is_lexicographically_ordered(&self) -> bool { - self.order.iter().enumerate().all(|(i, &j)| i == j) - } - - pub fn deserialize(serialized: &SerializedPathList) -> Self { - let mut paths: Vec = if serialized.paths.is_empty() { - Vec::new() - } else { - serialized.paths.split('\n').map(PathBuf::from).collect() - }; - - let mut order: Vec = serialized - .order - .split(',') - .filter_map(|s| s.parse().ok()) - .collect(); - - if !paths.is_sorted() || order.len() != paths.len() { - order = (0..paths.len()).collect(); - paths.sort(); - } - - Self { - paths: paths.into(), - order: order.into(), - } - } - - pub fn serialize(&self) -> SerializedPathList { - use std::fmt::Write as _; - - let mut paths = String::new(); - for path in self.paths.iter() { - if !paths.is_empty() { - paths.push('\n'); - } - paths.push_str(&path.to_string_lossy()); - } - - let mut order = String::new(); - for ix in self.order.iter() { - if !order.is_empty() { - order.push(','); - } - write!(&mut order, "{}", *ix).unwrap(); - } - SerializedPathList { paths, order } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_path_list() { - let list1 = PathList::new(&["a/d", "a/c"]); - let list2 = PathList::new(&["a/c", "a/d"]); - - assert_eq!(list1.paths(), list2.paths()); - assert_ne!(list1, list2); - assert_eq!(list1.order(), &[1, 0]); - assert_eq!(list2.order(), &[0, 1]); - - let list1_deserialized = PathList::deserialize(&list1.serialize()); - assert_eq!(list1_deserialized, list1); - - let list2_deserialized = PathList::deserialize(&list2.serialize()); - assert_eq!(list2_deserialized, list2); - } -} diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index c4ba93bcec..b2d1340a7b 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -9,17 +9,15 @@ use std::{ }; use anyhow::{Context as _, Result, bail}; -use collections::HashMap; -use db::{ - query, - sqlez::{connection::Connection, domain::Domain}, - sqlez_macros::sql, -}; +use client::DevServerProjectId; +use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size}; +use itertools::Itertools; use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint}; use language::{LanguageName, Toolchain}; use project::WorktreeId; +use remote::ssh_session::SshProjectId; use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::{SqlType, Statement}, @@ -30,17 +28,14 @@ use ui::{App, px}; use util::{ResultExt, maybe}; use uuid::Uuid; -use crate::{ - WorkspaceId, - path_list::{PathList, SerializedPathList}, -}; +use crate::WorkspaceId; use model::{ - GroupId, ItemId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, - SerializedSshConnection, SerializedWorkspace, SshConnectionId, + GroupId, ItemId, LocalPaths, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, + SerializedSshProject, SerializedWorkspace, }; -use self::model::{DockStructure, SerializedWorkspaceLocation}; +use self::model::{DockStructure, LocalPathsOrder, SerializedWorkspaceLocation}; #[derive(Copy, Clone, Debug, PartialEq)] pub(crate) struct SerializedAxis(pub(crate) gpui::Axis); @@ -279,189 +274,247 @@ impl sqlez::bindable::Bind for SerializedPixels { } } -pub struct WorkspaceDb(ThreadSafeConnection); +define_connection! { + // Current schema shape using pseudo-rust syntax: + // + // workspaces( + // workspace_id: usize, // Primary key for workspaces + // local_paths: Bincode>, + // local_paths_order: Bincode>, + // dock_visible: bool, // Deprecated + // dock_anchor: DockAnchor, // Deprecated + // dock_pane: Option, // Deprecated + // left_sidebar_open: boolean, + // timestamp: String, // UTC YYYY-MM-DD HH:MM:SS + // window_state: String, // WindowBounds Discriminant + // window_x: Option, // WindowBounds::Fixed RectF x + // window_y: Option, // WindowBounds::Fixed RectF y + // window_width: Option, // WindowBounds::Fixed RectF width + // window_height: Option, // WindowBounds::Fixed RectF height + // display: Option, // Display id + // fullscreen: Option, // Is the window fullscreen? + // centered_layout: Option, // Is the Centered Layout mode activated? + // session_id: Option, // Session id + // window_id: Option, // Window Id + // ) + // + // pane_groups( + // group_id: usize, // Primary key for pane_groups + // workspace_id: usize, // References workspaces table + // parent_group_id: Option, // None indicates that this is the root node + // position: Option, // None indicates that this is the root node + // axis: Option, // 'Vertical', 'Horizontal' + // flexes: Option>, // A JSON array of floats + // ) + // + // panes( + // pane_id: usize, // Primary key for panes + // workspace_id: usize, // References workspaces table + // active: bool, + // ) + // + // center_panes( + // pane_id: usize, // Primary key for center_panes + // parent_group_id: Option, // References pane_groups. If none, this is the root + // position: Option, // None indicates this is the root + // ) + // + // CREATE TABLE items( + // item_id: usize, // This is the item's view id, so this is not unique + // workspace_id: usize, // References workspaces table + // pane_id: usize, // References panes table + // kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global + // position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column + // active: bool, // Indicates if this item is the active one in the pane + // preview: bool // Indicates if this item is a preview item + // ) + // + // CREATE TABLE breakpoints( + // workspace_id: usize Foreign Key, // References workspace table + // path: PathBuf, // The absolute path of the file that this breakpoint belongs to + // breakpoint_location: Vec, // A list of the locations of breakpoints + // kind: int, // The kind of breakpoint (standard, log) + // log_message: String, // log message for log breakpoints, otherwise it's Null + // ) + pub static ref DB: WorkspaceDb<()> = + &[ + sql!( + CREATE TABLE workspaces( + workspace_id INTEGER PRIMARY KEY, + workspace_location BLOB UNIQUE, + dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. + dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. + dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. + left_sidebar_open INTEGER, // Boolean + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY(dock_pane) REFERENCES panes(pane_id) + ) STRICT; -impl Domain for WorkspaceDb { - const NAME: &str = stringify!(WorkspaceDb); + CREATE TABLE pane_groups( + group_id INTEGER PRIMARY KEY, + workspace_id INTEGER NOT NULL, + parent_group_id INTEGER, // NULL indicates that this is a root node + position INTEGER, // NULL indicates that this is a root node + axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal' + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; - const MIGRATIONS: &[&str] = &[ - sql!( - CREATE TABLE workspaces( - workspace_id INTEGER PRIMARY KEY, - workspace_location BLOB UNIQUE, - dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. - dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. - dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. - left_sidebar_open INTEGER, // Boolean - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - FOREIGN KEY(dock_pane) REFERENCES panes(pane_id) - ) STRICT; + CREATE TABLE panes( + pane_id INTEGER PRIMARY KEY, + workspace_id INTEGER NOT NULL, + active INTEGER NOT NULL, // Boolean + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE + ) STRICT; - CREATE TABLE pane_groups( - group_id INTEGER PRIMARY KEY, - workspace_id INTEGER NOT NULL, - parent_group_id INTEGER, // NULL indicates that this is a root node - position INTEGER, // NULL indicates that this is a root node - axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal' - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE, - FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE - ) STRICT; + CREATE TABLE center_panes( + pane_id INTEGER PRIMARY KEY, + parent_group_id INTEGER, // NULL means that this is a root pane + position INTEGER, // NULL means that this is a root pane + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + ON DELETE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; - CREATE TABLE panes( - pane_id INTEGER PRIMARY KEY, - workspace_id INTEGER NOT NULL, - active INTEGER NOT NULL, // Boolean - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE - ) STRICT; - - CREATE TABLE center_panes( - pane_id INTEGER PRIMARY KEY, - parent_group_id INTEGER, // NULL means that this is a root pane - position INTEGER, // NULL means that this is a root pane - FOREIGN KEY(pane_id) REFERENCES panes(pane_id) - ON DELETE CASCADE, - FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE - ) STRICT; - - CREATE TABLE items( - item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique - workspace_id INTEGER NOT NULL, - pane_id INTEGER NOT NULL, - kind TEXT NOT NULL, - position INTEGER NOT NULL, - active INTEGER NOT NULL, - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE, - FOREIGN KEY(pane_id) REFERENCES panes(pane_id) - ON DELETE CASCADE, - PRIMARY KEY(item_id, workspace_id) - ) STRICT; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN window_state TEXT; - ALTER TABLE workspaces ADD COLUMN window_x REAL; - ALTER TABLE workspaces ADD COLUMN window_y REAL; - ALTER TABLE workspaces ADD COLUMN window_width REAL; - ALTER TABLE workspaces ADD COLUMN window_height REAL; - ALTER TABLE workspaces ADD COLUMN display BLOB; - ), - // Drop foreign key constraint from workspaces.dock_pane to panes table. - sql!( - CREATE TABLE workspaces_2( - workspace_id INTEGER PRIMARY KEY, - workspace_location BLOB UNIQUE, - dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. - dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. - dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. - left_sidebar_open INTEGER, // Boolean - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - window_state TEXT, - window_x REAL, - window_y REAL, - window_width REAL, - window_height REAL, - display BLOB - ) STRICT; - INSERT INTO workspaces_2 SELECT * FROM workspaces; - DROP TABLE workspaces; - ALTER TABLE workspaces_2 RENAME TO workspaces; - ), - // Add panels related information - sql!( - ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT; - ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT; - ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT; - ), - // Add panel zoom persistence - sql!( - ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool - ), - // Add pane group flex data - sql!( - ALTER TABLE pane_groups ADD COLUMN flexes TEXT; - ), - // Add fullscreen field to workspace - // Deprecated, `WindowBounds` holds the fullscreen state now. - // Preserving so users can downgrade Zed. - sql!( - ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool - ), - // Add preview field to items - sql!( - ALTER TABLE items ADD COLUMN preview INTEGER; //bool - ), - // Add centered_layout field to workspace - sql!( - ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool - ), - sql!( - CREATE TABLE remote_projects ( - remote_project_id INTEGER NOT NULL UNIQUE, - path TEXT, - dev_server_name TEXT - ); - ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER; - ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths; - ), - sql!( - DROP TABLE remote_projects; - CREATE TABLE dev_server_projects ( - id INTEGER NOT NULL UNIQUE, - path TEXT, - dev_server_name TEXT - ); - ALTER TABLE workspaces DROP COLUMN remote_project_id; - ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL; - ), - sql!( - ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0; - ), - sql!( - CREATE TABLE ssh_projects ( - id INTEGER PRIMARY KEY, - host TEXT NOT NULL, - port INTEGER, - path TEXT NOT NULL, - user TEXT - ); - ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE; - ), - sql!( - ALTER TABLE ssh_projects RENAME COLUMN path TO paths; - ), - sql!( - CREATE TABLE toolchains ( - workspace_id INTEGER, - worktree_id INTEGER, - language_name TEXT NOT NULL, - name TEXT NOT NULL, - path TEXT NOT NULL, - PRIMARY KEY (workspace_id, worktree_id, language_name) - ); - ), - sql!( - ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}"; - ), - sql!( + CREATE TABLE items( + item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique + workspace_id INTEGER NOT NULL, + pane_id INTEGER NOT NULL, + kind TEXT NOT NULL, + position INTEGER NOT NULL, + active INTEGER NOT NULL, + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + ON DELETE CASCADE, + PRIMARY KEY(item_id, workspace_id) + ) STRICT; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN window_state TEXT; + ALTER TABLE workspaces ADD COLUMN window_x REAL; + ALTER TABLE workspaces ADD COLUMN window_y REAL; + ALTER TABLE workspaces ADD COLUMN window_width REAL; + ALTER TABLE workspaces ADD COLUMN window_height REAL; + ALTER TABLE workspaces ADD COLUMN display BLOB; + ), + // Drop foreign key constraint from workspaces.dock_pane to panes table. + sql!( + CREATE TABLE workspaces_2( + workspace_id INTEGER PRIMARY KEY, + workspace_location BLOB UNIQUE, + dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. + dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. + dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. + left_sidebar_open INTEGER, // Boolean + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + window_state TEXT, + window_x REAL, + window_y REAL, + window_width REAL, + window_height REAL, + display BLOB + ) STRICT; + INSERT INTO workspaces_2 SELECT * FROM workspaces; + DROP TABLE workspaces; + ALTER TABLE workspaces_2 RENAME TO workspaces; + ), + // Add panels related information + sql!( + ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT; + ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT; + ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT; + ), + // Add panel zoom persistence + sql!( + ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool + ), + // Add pane group flex data + sql!( + ALTER TABLE pane_groups ADD COLUMN flexes TEXT; + ), + // Add fullscreen field to workspace + // Deprecated, `WindowBounds` holds the fullscreen state now. + // Preserving so users can downgrade Zed. + sql!( + ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool + ), + // Add preview field to items + sql!( + ALTER TABLE items ADD COLUMN preview INTEGER; //bool + ), + // Add centered_layout field to workspace + sql!( + ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool + ), + sql!( + CREATE TABLE remote_projects ( + remote_project_id INTEGER NOT NULL UNIQUE, + path TEXT, + dev_server_name TEXT + ); + ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER; + ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths; + ), + sql!( + DROP TABLE remote_projects; + CREATE TABLE dev_server_projects ( + id INTEGER NOT NULL UNIQUE, + path TEXT, + dev_server_name TEXT + ); + ALTER TABLE workspaces DROP COLUMN remote_project_id; + ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL; + ), + sql!( + ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0; + ), + sql!( + CREATE TABLE ssh_projects ( + id INTEGER PRIMARY KEY, + host TEXT NOT NULL, + port INTEGER, + path TEXT NOT NULL, + user TEXT + ); + ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE; + ), + sql!( + ALTER TABLE ssh_projects RENAME COLUMN path TO paths; + ), + sql!( + CREATE TABLE toolchains ( + workspace_id INTEGER, + worktree_id INTEGER, + language_name TEXT NOT NULL, + name TEXT NOT NULL, + path TEXT NOT NULL, + PRIMARY KEY (workspace_id, worktree_id, language_name) + ); + ), + sql!( + ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}"; + ), + sql!( CREATE TABLE breakpoints ( workspace_id INTEGER NOT NULL, path TEXT NOT NULL, @@ -473,172 +526,39 @@ impl Domain for WorkspaceDb { ON UPDATE CASCADE ); ), - sql!( - ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT; - CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array); - ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT; - ), - sql!( - ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL - ), - sql!( - ALTER TABLE breakpoints DROP COLUMN kind - ), - sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL), - sql!( - ALTER TABLE breakpoints ADD COLUMN condition TEXT; - ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT; - ), - sql!(CREATE TABLE toolchains2 ( - workspace_id INTEGER, - worktree_id INTEGER, - language_name TEXT NOT NULL, - name TEXT NOT NULL, - path TEXT NOT NULL, - raw_json TEXT NOT NULL, - relative_worktree_path TEXT NOT NULL, - PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT; - INSERT INTO toolchains2 - SELECT * FROM toolchains; - DROP TABLE toolchains; - ALTER TABLE toolchains2 RENAME TO toolchains; - ), - sql!( - CREATE TABLE ssh_connections ( - id INTEGER PRIMARY KEY, - host TEXT NOT NULL, - port INTEGER, - user TEXT - ); - - INSERT INTO ssh_connections (host, port, user) - SELECT DISTINCT host, port, user - FROM ssh_projects; - - CREATE TABLE workspaces_2( - workspace_id INTEGER PRIMARY KEY, - paths TEXT, - paths_order TEXT, - ssh_connection_id INTEGER REFERENCES ssh_connections(id), - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - window_state TEXT, - window_x REAL, - window_y REAL, - window_width REAL, - window_height REAL, - display BLOB, - left_dock_visible INTEGER, - left_dock_active_panel TEXT, - right_dock_visible INTEGER, - right_dock_active_panel TEXT, - bottom_dock_visible INTEGER, - bottom_dock_active_panel TEXT, - left_dock_zoom INTEGER, - right_dock_zoom INTEGER, - bottom_dock_zoom INTEGER, - fullscreen INTEGER, - centered_layout INTEGER, - session_id TEXT, - window_id INTEGER - ) STRICT; - - INSERT - INTO workspaces_2 - SELECT - workspaces.workspace_id, - CASE - WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths - ELSE - CASE - WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN - NULL - ELSE - replace(workspaces.local_paths_array, ',', CHAR(10)) - END - END as paths, - - CASE - WHEN ssh_projects.id IS NOT NULL THEN "" - ELSE workspaces.local_paths_order_array - END as paths_order, - - CASE - WHEN ssh_projects.id IS NOT NULL THEN ( - SELECT ssh_connections.id - FROM ssh_connections - WHERE - ssh_connections.host IS ssh_projects.host AND - ssh_connections.port IS ssh_projects.port AND - ssh_connections.user IS ssh_projects.user - ) - ELSE NULL - END as ssh_connection_id, - - workspaces.timestamp, - workspaces.window_state, - workspaces.window_x, - workspaces.window_y, - workspaces.window_width, - workspaces.window_height, - workspaces.display, - workspaces.left_dock_visible, - workspaces.left_dock_active_panel, - workspaces.right_dock_visible, - workspaces.right_dock_active_panel, - workspaces.bottom_dock_visible, - workspaces.bottom_dock_active_panel, - workspaces.left_dock_zoom, - workspaces.right_dock_zoom, - workspaces.bottom_dock_zoom, - workspaces.fullscreen, - workspaces.centered_layout, - workspaces.session_id, - workspaces.window_id - FROM - workspaces LEFT JOIN - ssh_projects ON - workspaces.ssh_project_id = ssh_projects.id; - - DELETE FROM workspaces_2 - WHERE workspace_id NOT IN ( - SELECT MAX(workspace_id) - FROM workspaces_2 - GROUP BY ssh_connection_id, paths - ); - - DROP TABLE ssh_projects; - DROP TABLE workspaces; - ALTER TABLE workspaces_2 RENAME TO workspaces; - - CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths); - ), - // Fix any data from when workspaces.paths were briefly encoded as JSON arrays - sql!( - UPDATE workspaces - SET paths = CASE - WHEN substr(paths, 1, 2) = '[' || '"' AND substr(paths, -2, 2) = '"' || ']' THEN - replace( - substr(paths, 3, length(paths) - 4), - '"' || ',' || '"', - CHAR(10) - ) - ELSE - replace(paths, ',', CHAR(10)) - END - WHERE paths IS NOT NULL - ), + sql!( + ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT; + CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array); + ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT; + ), + sql!( + ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL + ), + sql!( + ALTER TABLE breakpoints DROP COLUMN kind + ), + sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL), + sql!( + ALTER TABLE breakpoints ADD COLUMN condition TEXT; + ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT; + ), + sql!(CREATE TABLE toolchains2 ( + workspace_id INTEGER, + worktree_id INTEGER, + language_name TEXT NOT NULL, + name TEXT NOT NULL, + path TEXT NOT NULL, + raw_json TEXT NOT NULL, + relative_worktree_path TEXT NOT NULL, + PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT; + INSERT INTO toolchains2 + SELECT * FROM toolchains; + DROP TABLE toolchains; + ALTER TABLE toolchains2 RENAME TO toolchains; + ) ]; - - // Allow recovering from bad migration that was initially shipped to nightly - // when introducing the ssh_connections table. - fn should_allow_migration_change(_index: usize, old: &str, new: &str) -> bool { - old.starts_with("CREATE TABLE ssh_connections") - && new.starts_with("CREATE TABLE ssh_connections") - } } -db::static_connection!(DB, WorkspaceDb, []); - impl WorkspaceDb { /// Returns a serialized workspace for the given worktree_roots. If the passed array /// is empty, the most recent workspace is returned instead. If no workspace for the @@ -646,33 +566,17 @@ impl WorkspaceDb { pub(crate) fn workspace_for_roots>( &self, worktree_roots: &[P], - ) -> Option { - self.workspace_for_roots_internal(worktree_roots, None) - } - - pub(crate) fn ssh_workspace_for_roots>( - &self, - worktree_roots: &[P], - ssh_project_id: SshConnectionId, - ) -> Option { - self.workspace_for_roots_internal(worktree_roots, Some(ssh_project_id)) - } - - pub(crate) fn workspace_for_roots_internal>( - &self, - worktree_roots: &[P], - ssh_connection_id: Option, ) -> Option { // paths are sorted before db interactions to ensure that the order of the paths // doesn't affect the workspace selection for existing workspaces - let root_paths = PathList::new(worktree_roots); + let local_paths = LocalPaths::new(worktree_roots); // Note that we re-assign the workspace_id here in case it's empty // and we've grabbed the most recent workspace let ( workspace_id, - paths, - paths_order, + local_paths, + local_paths_order, window_bounds, display, centered_layout, @@ -680,8 +584,8 @@ impl WorkspaceDb { window_id, ): ( WorkspaceId, - String, - String, + Option, + Option, Option, Option, Option, @@ -691,8 +595,8 @@ impl WorkspaceDb { .select_row_bound(sql! { SELECT workspace_id, - paths, - paths_order, + local_paths, + local_paths_order, window_state, window_x, window_y, @@ -711,31 +615,25 @@ impl WorkspaceDb { bottom_dock_zoom, window_id FROM workspaces - WHERE - paths IS ? AND - ssh_connection_id IS ? - LIMIT 1 - }) - .map(|mut prepared_statement| { - (prepared_statement)(( - root_paths.serialize().paths, - ssh_connection_id.map(|id| id.0 as i32), - )) - .unwrap() + WHERE local_paths = ? }) + .and_then(|mut prepared_statement| (prepared_statement)(&local_paths)) .context("No workspaces found") .warn_on_err() .flatten()?; - let paths = PathList::deserialize(&SerializedPathList { - paths, - order: paths_order, - }); + let local_paths = local_paths?; + let location = match local_paths_order { + Some(order) => SerializedWorkspaceLocation::Local(local_paths, order), + None => { + let order = LocalPathsOrder::default_for_paths(&local_paths); + SerializedWorkspaceLocation::Local(local_paths, order) + } + }; Some(SerializedWorkspace { id: workspace_id, - location: SerializedWorkspaceLocation::Local, - paths, + location, center_group: self .get_center_pane_group(workspace_id) .context("Getting center group") @@ -750,6 +648,63 @@ impl WorkspaceDb { }) } + pub(crate) fn workspace_for_ssh_project( + &self, + ssh_project: &SerializedSshProject, + ) -> Option { + let (workspace_id, window_bounds, display, centered_layout, docks, window_id): ( + WorkspaceId, + Option, + Option, + Option, + DockStructure, + Option, + ) = self + .select_row_bound(sql! { + SELECT + workspace_id, + window_state, + window_x, + window_y, + window_width, + window_height, + display, + centered_layout, + left_dock_visible, + left_dock_active_panel, + left_dock_zoom, + right_dock_visible, + right_dock_active_panel, + right_dock_zoom, + bottom_dock_visible, + bottom_dock_active_panel, + bottom_dock_zoom, + window_id + FROM workspaces + WHERE ssh_project_id = ? + }) + .and_then(|mut prepared_statement| (prepared_statement)(ssh_project.id.0)) + .context("No workspaces found") + .warn_on_err() + .flatten()?; + + Some(SerializedWorkspace { + id: workspace_id, + location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()), + center_group: self + .get_center_pane_group(workspace_id) + .context("Getting center group") + .log_err()?, + window_bounds, + centered_layout: centered_layout.unwrap_or(false), + breakpoints: self.breakpoints(workspace_id), + display, + docks, + session_id: None, + window_id, + }) + } + fn breakpoints(&self, workspace_id: WorkspaceId) -> BTreeMap, Vec> { let breakpoints: Result> = self .select_bound(sql! { @@ -799,34 +754,16 @@ impl WorkspaceDb { /// Saves a workspace using the worktree roots. Will garbage collect any workspaces /// that used this workspace previously pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) { - let paths = workspace.paths.serialize(); log::debug!("Saving workspace at location: {:?}", workspace.location); self.write(move |conn| { conn.with_savepoint("update_worktrees", || { - let ssh_connection_id = match &workspace.location { - SerializedWorkspaceLocation::Local => None, - SerializedWorkspaceLocation::Ssh(connection) => { - Some(Self::get_or_create_ssh_connection_query( - conn, - connection.host.clone(), - connection.port, - connection.user.clone(), - )?.0) - } - }; - // Clear out panes and pane_groups conn.exec_bound(sql!( DELETE FROM pane_groups WHERE workspace_id = ?1; DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id) .context("Clearing old panes")?; - conn.exec_bound( - sql!( - DELETE FROM breakpoints WHERE workspace_id = ?1; - DELETE FROM toolchains WHERE workspace_id = ?1; - ) - )?(workspace.id).context("Clearing old breakpoints")?; + conn.exec_bound(sql!(DELETE FROM breakpoints WHERE workspace_id = ?1))?(workspace.id).context("Clearing old breakpoints")?; for (path, breakpoints) in workspace.breakpoints { for bp in breakpoints { @@ -853,73 +790,115 @@ impl WorkspaceDb { } } } + } - conn.exec_bound(sql!( - DELETE - FROM workspaces - WHERE - workspace_id != ?1 AND - paths IS ?2 AND - ssh_connection_id IS ?3 - ))?(( - workspace.id, - paths.paths.clone(), - ssh_connection_id, - )) - .context("clearing out old locations")?; - // Upsert - let query = sql!( - INSERT INTO workspaces( - workspace_id, - paths, - paths_order, - ssh_connection_id, - left_dock_visible, - left_dock_active_panel, - left_dock_zoom, - right_dock_visible, - right_dock_active_panel, - right_dock_zoom, - bottom_dock_visible, - bottom_dock_active_panel, - bottom_dock_zoom, - session_id, - window_id, - timestamp - ) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, CURRENT_TIMESTAMP) - ON CONFLICT DO - UPDATE SET - paths = ?2, - paths_order = ?3, - ssh_connection_id = ?4, - left_dock_visible = ?5, - left_dock_active_panel = ?6, - left_dock_zoom = ?7, - right_dock_visible = ?8, - right_dock_active_panel = ?9, - right_dock_zoom = ?10, - bottom_dock_visible = ?11, - bottom_dock_active_panel = ?12, - bottom_dock_zoom = ?13, - session_id = ?14, - window_id = ?15, - timestamp = CURRENT_TIMESTAMP - ); - let mut prepared_query = conn.exec_bound(query)?; - let args = ( - workspace.id, - paths.paths.clone(), - paths.order.clone(), - ssh_connection_id, - workspace.docks, - workspace.session_id, - workspace.window_id, - ); + match workspace.location { + SerializedWorkspaceLocation::Local(local_paths, local_paths_order) => { + conn.exec_bound(sql!( + DELETE FROM toolchains WHERE workspace_id = ?1; + DELETE FROM workspaces WHERE local_paths = ? AND workspace_id != ? + ))?((&local_paths, workspace.id)) + .context("clearing out old locations")?; - prepared_query(args).context("Updating workspace")?; + // Upsert + let query = sql!( + INSERT INTO workspaces( + workspace_id, + local_paths, + local_paths_order, + left_dock_visible, + left_dock_active_panel, + left_dock_zoom, + right_dock_visible, + right_dock_active_panel, + right_dock_zoom, + bottom_dock_visible, + bottom_dock_active_panel, + bottom_dock_zoom, + session_id, + window_id, + timestamp, + local_paths_array, + local_paths_order_array + ) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, CURRENT_TIMESTAMP, ?15, ?16) + ON CONFLICT DO + UPDATE SET + local_paths = ?2, + local_paths_order = ?3, + left_dock_visible = ?4, + left_dock_active_panel = ?5, + left_dock_zoom = ?6, + right_dock_visible = ?7, + right_dock_active_panel = ?8, + right_dock_zoom = ?9, + bottom_dock_visible = ?10, + bottom_dock_active_panel = ?11, + bottom_dock_zoom = ?12, + session_id = ?13, + window_id = ?14, + timestamp = CURRENT_TIMESTAMP, + local_paths_array = ?15, + local_paths_order_array = ?16 + ); + let mut prepared_query = conn.exec_bound(query)?; + let args = (workspace.id, &local_paths, &local_paths_order, workspace.docks, workspace.session_id, workspace.window_id, local_paths.paths().iter().map(|path| path.to_string_lossy().to_string()).join(","), local_paths_order.order().iter().map(|order| order.to_string()).join(",")); + + prepared_query(args).context("Updating workspace")?; + } + SerializedWorkspaceLocation::Ssh(ssh_project) => { + conn.exec_bound(sql!( + DELETE FROM toolchains WHERE workspace_id = ?1; + DELETE FROM workspaces WHERE ssh_project_id = ? AND workspace_id != ? + ))?((ssh_project.id.0, workspace.id)) + .context("clearing out old locations")?; + + // Upsert + conn.exec_bound(sql!( + INSERT INTO workspaces( + workspace_id, + ssh_project_id, + left_dock_visible, + left_dock_active_panel, + left_dock_zoom, + right_dock_visible, + right_dock_active_panel, + right_dock_zoom, + bottom_dock_visible, + bottom_dock_active_panel, + bottom_dock_zoom, + session_id, + window_id, + timestamp + ) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, CURRENT_TIMESTAMP) + ON CONFLICT DO + UPDATE SET + ssh_project_id = ?2, + left_dock_visible = ?3, + left_dock_active_panel = ?4, + left_dock_zoom = ?5, + right_dock_visible = ?6, + right_dock_active_panel = ?7, + right_dock_zoom = ?8, + bottom_dock_visible = ?9, + bottom_dock_active_panel = ?10, + bottom_dock_zoom = ?11, + session_id = ?12, + window_id = ?13, + timestamp = CURRENT_TIMESTAMP + ))?(( + workspace.id, + ssh_project.id.0, + workspace.docks, + workspace.session_id, + workspace.window_id + )) + .context("Updating workspace")?; + } + } // Save center pane group Self::save_pane_group(conn, workspace.id, &workspace.center_group, None) @@ -932,95 +911,89 @@ impl WorkspaceDb { .await; } - pub(crate) async fn get_or_create_ssh_connection( + pub(crate) async fn get_or_create_ssh_project( &self, host: String, port: Option, + paths: Vec, user: Option, - ) -> Result { - self.write(move |conn| Self::get_or_create_ssh_connection_query(conn, host, port, user)) - .await - } - - fn get_or_create_ssh_connection_query( - this: &Connection, - host: String, - port: Option, - user: Option, - ) -> Result { - if let Some(id) = this.select_row_bound(sql!( - SELECT id FROM ssh_connections WHERE host IS ? AND port IS ? AND user IS ? LIMIT 1 - ))?((host.clone(), port, user.clone()))? + ) -> Result { + let paths = serde_json::to_string(&paths)?; + if let Some(project) = self + .get_ssh_project(host.clone(), port, paths.clone(), user.clone()) + .await? { - Ok(SshConnectionId(id)) + Ok(project) } else { log::debug!("Inserting SSH project at host {host}"); - let id = this.select_row_bound(sql!( - INSERT INTO ssh_connections ( - host, - port, - user - ) VALUES (?1, ?2, ?3) - RETURNING id - ))?((host, port, user))? - .context("failed to insert ssh project")?; - Ok(SshConnectionId(id)) + self.insert_ssh_project(host, port, paths, user) + .await? + .context("failed to insert ssh project") } } + query! { + async fn get_ssh_project(host: String, port: Option, paths: String, user: Option) -> Result> { + SELECT id, host, port, paths, user + FROM ssh_projects + WHERE host IS ? AND port IS ? AND paths IS ? AND user IS ? + LIMIT 1 + } + } + + query! { + async fn insert_ssh_project(host: String, port: Option, paths: String, user: Option) -> Result> { + INSERT INTO ssh_projects( + host, + port, + paths, + user + ) VALUES (?1, ?2, ?3, ?4) + RETURNING id, host, port, paths, user + } + } + + query! { + pub async fn update_ssh_project_paths_query(ssh_project_id: u64, paths: String) -> Result> { + UPDATE ssh_projects + SET paths = ?2 + WHERE id = ?1 + RETURNING id, host, port, paths, user + } + } + + pub(crate) async fn update_ssh_project_paths( + &self, + ssh_project_id: SshProjectId, + new_paths: Vec, + ) -> Result { + let paths = serde_json::to_string(&new_paths)?; + self.update_ssh_project_paths_query(ssh_project_id.0, paths) + .await? + .context("failed to update ssh project paths") + } + query! { pub async fn next_id() -> Result { INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id } } - fn recent_workspaces(&self) -> Result)>> { - Ok(self - .recent_workspaces_query()? - .into_iter() - .map(|(id, paths, order, ssh_connection_id)| { - ( - id, - PathList::deserialize(&SerializedPathList { paths, order }), - ssh_connection_id, - ) - }) - .collect()) - } - query! { - fn recent_workspaces_query() -> Result)>> { - SELECT workspace_id, paths, paths_order, ssh_connection_id + fn recent_workspaces() -> Result)>> { + SELECT workspace_id, local_paths, local_paths_order, ssh_project_id FROM workspaces - WHERE - paths IS NOT NULL OR - ssh_connection_id IS NOT NULL + WHERE local_paths IS NOT NULL + OR ssh_project_id IS NOT NULL ORDER BY timestamp DESC } } - fn session_workspaces( - &self, - session_id: String, - ) -> Result, Option)>> { - Ok(self - .session_workspaces_query(session_id)? - .into_iter() - .map(|(paths, order, window_id, ssh_connection_id)| { - ( - PathList::deserialize(&SerializedPathList { paths, order }), - window_id, - ssh_connection_id.map(SshConnectionId), - ) - }) - .collect()) - } - query! { - fn session_workspaces_query(session_id: String) -> Result, Option)>> { - SELECT paths, paths_order, window_id, ssh_connection_id + fn session_workspaces(session_id: String) -> Result, Option)>> { + SELECT local_paths, local_paths_order, window_id, ssh_project_id FROM workspaces - WHERE session_id = ?1 + WHERE session_id = ?1 AND dev_server_project_id IS NULL ORDER BY timestamp DESC } } @@ -1040,39 +1013,17 @@ impl WorkspaceDb { } } - fn ssh_connections(&self) -> Result> { - Ok(self - .ssh_connections_query()? - .into_iter() - .map(|(id, host, port, user)| { - ( - SshConnectionId(id), - SerializedSshConnection { host, port, user }, - ) - }) - .collect()) - } - query! { - pub fn ssh_connections_query() -> Result, Option)>> { - SELECT id, host, port, user - FROM ssh_connections + fn ssh_projects() -> Result> { + SELECT id, host, port, paths, user + FROM ssh_projects } } - pub(crate) fn ssh_connection(&self, id: SshConnectionId) -> Result { - let row = self.ssh_connection_query(id.0)?; - Ok(SerializedSshConnection { - host: row.0, - port: row.1, - user: row.2, - }) - } - query! { - fn ssh_connection_query(id: u64) -> Result<(String, Option, Option)> { - SELECT host, port, user - FROM ssh_connections + fn ssh_project(id: u64) -> Result { + SELECT id, host, port, paths, user + FROM ssh_projects WHERE id = ? } } @@ -1086,7 +1037,7 @@ impl WorkspaceDb { display, window_state, window_x, window_y, window_width, window_height FROM workspaces - WHERE paths + WHERE local_paths IS NOT NULL ORDER BY timestamp DESC LIMIT 1 @@ -1103,33 +1054,46 @@ impl WorkspaceDb { } } + pub async fn delete_workspace_by_dev_server_project_id( + &self, + id: DevServerProjectId, + ) -> Result<()> { + self.write(move |conn| { + conn.exec_bound(sql!( + DELETE FROM dev_server_projects WHERE id = ? + ))?(id.0)?; + conn.exec_bound(sql!( + DELETE FROM toolchains WHERE workspace_id = ?1; + DELETE FROM workspaces + WHERE dev_server_project_id IS ? + ))?(id.0) + }) + .await + } + // Returns the recent locations which are still valid on disk and deletes ones which no longer // exist. pub async fn recent_workspaces_on_disk( &self, - ) -> Result> { + ) -> Result> { let mut result = Vec::new(); let mut delete_tasks = Vec::new(); - let ssh_connections = self.ssh_connections()?; + let ssh_projects = self.ssh_projects()?; - for (id, paths, ssh_connection_id) in self.recent_workspaces()? { - if let Some(ssh_connection_id) = ssh_connection_id.map(SshConnectionId) { - if let Some(ssh_connection) = ssh_connections.get(&ssh_connection_id) { - result.push(( - id, - SerializedWorkspaceLocation::Ssh(ssh_connection.clone()), - paths, - )); + for (id, location, order, ssh_project_id) in self.recent_workspaces()? { + if let Some(ssh_project_id) = ssh_project_id.map(SshProjectId) { + if let Some(ssh_project) = ssh_projects.iter().find(|rp| rp.id == ssh_project_id) { + result.push((id, SerializedWorkspaceLocation::Ssh(ssh_project.clone()))); } else { delete_tasks.push(self.delete_workspace_by_id(id)); } continue; } - if paths.paths().iter().all(|path| path.exists()) - && paths.paths().iter().any(|path| path.is_dir()) + if location.paths().iter().all(|path| path.exists()) + && location.paths().iter().any(|path| path.is_dir()) { - result.push((id, SerializedWorkspaceLocation::Local, paths)); + result.push((id, SerializedWorkspaceLocation::Local(location, order))); } else { delete_tasks.push(self.delete_workspace_by_id(id)); } @@ -1139,13 +1103,13 @@ impl WorkspaceDb { Ok(result) } - pub async fn last_workspace(&self) -> Result> { + pub async fn last_workspace(&self) -> Result> { Ok(self .recent_workspaces_on_disk() .await? .into_iter() .next() - .map(|(_, location, paths)| (location, paths))) + .map(|(_, location)| location)) } // Returns the locations of the workspaces that were still opened when the last @@ -1156,31 +1120,25 @@ impl WorkspaceDb { &self, last_session_id: &str, last_session_window_stack: Option>, - ) -> Result> { + ) -> Result> { let mut workspaces = Vec::new(); - for (paths, window_id, ssh_connection_id) in + for (location, order, window_id, ssh_project_id) in self.session_workspaces(last_session_id.to_owned())? { - if let Some(ssh_connection_id) = ssh_connection_id { - workspaces.push(( - SerializedWorkspaceLocation::Ssh(self.ssh_connection(ssh_connection_id)?), - paths, - window_id.map(WindowId::from), - )); - } else if paths.paths().iter().all(|path| path.exists()) - && paths.paths().iter().any(|path| path.is_dir()) + if let Some(ssh_project_id) = ssh_project_id { + let location = SerializedWorkspaceLocation::Ssh(self.ssh_project(ssh_project_id)?); + workspaces.push((location, window_id.map(WindowId::from))); + } else if location.paths().iter().all(|path| path.exists()) + && location.paths().iter().any(|path| path.is_dir()) { - workspaces.push(( - SerializedWorkspaceLocation::Local, - paths, - window_id.map(WindowId::from), - )); + let location = SerializedWorkspaceLocation::Local(location, order); + workspaces.push((location, window_id.map(WindowId::from))); } } if let Some(stack) = last_session_window_stack { - workspaces.sort_by_key(|(_, _, window_id)| { + workspaces.sort_by_key(|(_, window_id)| { window_id .and_then(|id| stack.iter().position(|&order_id| order_id == id)) .unwrap_or(usize::MAX) @@ -1189,7 +1147,7 @@ impl WorkspaceDb { Ok(workspaces .into_iter() - .map(|(location, paths, _)| (location, paths)) + .map(|(paths, _)| paths) .collect::>()) } @@ -1541,13 +1499,13 @@ pub fn delete_unloaded_items( #[cfg(test)] mod tests { + use std::thread; + use std::time::Duration; + use super::*; - use crate::persistence::model::{ - SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace, - }; + use crate::persistence::model::SerializedWorkspace; + use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup}; use gpui; - use pretty_assertions::assert_eq; - use std::{thread, time::Duration}; #[gpui::test] async fn test_breakpoints() { @@ -1600,8 +1558,7 @@ mod tests { let workspace = SerializedWorkspace { id, - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1754,8 +1711,7 @@ mod tests { let workspace = SerializedWorkspace { id, - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1801,8 +1757,7 @@ mod tests { let workspace_without_breakpoint = SerializedWorkspace { id, - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1841,7 +1796,6 @@ mod tests { ON DELETE CASCADE ) STRICT; )], - |_, _, _| false, ) .unwrap(); }) @@ -1890,7 +1844,6 @@ mod tests { REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT;)], - |_, _, _| false, ) }) .await @@ -1898,8 +1851,7 @@ mod tests { let mut workspace_1 = SerializedWorkspace { id: WorkspaceId(1), - paths: PathList::new(&["/tmp", "/tmp2"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1912,8 +1864,7 @@ mod tests { let workspace_2 = SerializedWorkspace { id: WorkspaceId(2), - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1942,7 +1893,7 @@ mod tests { }) .await; - workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]); + workspace_1.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp3"]); db.save_workspace(workspace_1.clone()).await; db.save_workspace(workspace_1).await; db.save_workspace(workspace_2).await; @@ -2018,8 +1969,10 @@ mod tests { let workspace = SerializedWorkspace { id: WorkspaceId(5), - paths: PathList::new(&["/tmp", "/tmp2"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::Local( + LocalPaths::new(["/tmp", "/tmp2"]), + LocalPathsOrder::new([1, 0]), + ), center_group, window_bounds: Default::default(), breakpoints: Default::default(), @@ -2051,8 +2004,10 @@ mod tests { let workspace_1 = SerializedWorkspace { id: WorkspaceId(1), - paths: PathList::new(&["/tmp", "/tmp2"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::Local( + LocalPaths::new(["/tmp", "/tmp2"]), + LocalPathsOrder::new([0, 1]), + ), center_group: Default::default(), window_bounds: Default::default(), breakpoints: Default::default(), @@ -2065,8 +2020,7 @@ mod tests { let mut workspace_2 = SerializedWorkspace { id: WorkspaceId(2), - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2095,7 +2049,7 @@ mod tests { assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None); // Test 'mutate' case of updating a pre-existing id - workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]); + workspace_2.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]); db.save_workspace(workspace_2.clone()).await; assert_eq!( @@ -2106,8 +2060,10 @@ mod tests { // Test other mechanism for mutating let mut workspace_3 = SerializedWorkspace { id: WorkspaceId(3), - paths: PathList::new(&["/tmp2", "/tmp"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::Local( + LocalPaths::new(["/tmp", "/tmp2"]), + LocalPathsOrder::new([1, 0]), + ), center_group: Default::default(), window_bounds: Default::default(), breakpoints: Default::default(), @@ -2125,7 +2081,8 @@ mod tests { ); // Make sure that updating paths differently also works - workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]); + workspace_3.location = + SerializedWorkspaceLocation::from_local_paths(["/tmp3", "/tmp4", "/tmp2"]); db.save_workspace(workspace_3.clone()).await; assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None); assert_eq!( @@ -2143,8 +2100,7 @@ mod tests { let workspace_1 = SerializedWorkspace { id: WorkspaceId(1), - paths: PathList::new(&["/tmp1"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp1"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2157,8 +2113,7 @@ mod tests { let workspace_2 = SerializedWorkspace { id: WorkspaceId(2), - paths: PathList::new(&["/tmp2"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp2"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2171,8 +2126,7 @@ mod tests { let workspace_3 = SerializedWorkspace { id: WorkspaceId(3), - paths: PathList::new(&["/tmp3"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp3"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2185,8 +2139,7 @@ mod tests { let workspace_4 = SerializedWorkspace { id: WorkspaceId(4), - paths: PathList::new(&["/tmp4"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp4"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2197,15 +2150,14 @@ mod tests { window_id: None, }; - let connection_id = db - .get_or_create_ssh_connection("my-host".to_string(), Some(1234), None) + let ssh_project = db + .get_or_create_ssh_project("my-host".to_string(), Some(1234), vec![], None) .await .unwrap(); let workspace_5 = SerializedWorkspace { id: WorkspaceId(5), - paths: PathList::default(), - location: SerializedWorkspaceLocation::Ssh(db.ssh_connection(connection_id).unwrap()), + location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2218,8 +2170,10 @@ mod tests { let workspace_6 = SerializedWorkspace { id: WorkspaceId(6), - paths: PathList::new(&["/tmp6a", "/tmp6b", "/tmp6c"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::Local( + LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]), + LocalPathsOrder::new([2, 1, 0]), + ), center_group: Default::default(), window_bounds: Default::default(), breakpoints: Default::default(), @@ -2241,36 +2195,41 @@ mod tests { let locations = db.session_workspaces("session-id-1".to_owned()).unwrap(); assert_eq!(locations.len(), 2); - assert_eq!(locations[0].0, PathList::new(&["/tmp2"])); - assert_eq!(locations[0].1, Some(20)); - assert_eq!(locations[1].0, PathList::new(&["/tmp1"])); - assert_eq!(locations[1].1, Some(10)); + assert_eq!(locations[0].0, LocalPaths::new(["/tmp2"])); + assert_eq!(locations[0].1, LocalPathsOrder::new([0])); + assert_eq!(locations[0].2, Some(20)); + assert_eq!(locations[1].0, LocalPaths::new(["/tmp1"])); + assert_eq!(locations[1].1, LocalPathsOrder::new([0])); + assert_eq!(locations[1].2, Some(10)); let locations = db.session_workspaces("session-id-2".to_owned()).unwrap(); assert_eq!(locations.len(), 2); - assert_eq!(locations[0].0, PathList::default()); - assert_eq!(locations[0].1, Some(50)); - assert_eq!(locations[0].2, Some(connection_id)); - assert_eq!(locations[1].0, PathList::new(&["/tmp3"])); - assert_eq!(locations[1].1, Some(30)); + let empty_paths: Vec<&str> = Vec::new(); + assert_eq!(locations[0].0, LocalPaths::new(empty_paths.iter())); + assert_eq!(locations[0].1, LocalPathsOrder::new([])); + assert_eq!(locations[0].2, Some(50)); + assert_eq!(locations[0].3, Some(ssh_project.id.0)); + assert_eq!(locations[1].0, LocalPaths::new(["/tmp3"])); + assert_eq!(locations[1].1, LocalPathsOrder::new([0])); + assert_eq!(locations[1].2, Some(30)); let locations = db.session_workspaces("session-id-3".to_owned()).unwrap(); assert_eq!(locations.len(), 1); assert_eq!( locations[0].0, - PathList::new(&["/tmp6a", "/tmp6b", "/tmp6c"]), + LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]), ); - assert_eq!(locations[0].1, Some(60)); + assert_eq!(locations[0].1, LocalPathsOrder::new([2, 1, 0])); + assert_eq!(locations[0].2, Some(60)); } fn default_workspace>( - paths: &[P], + workspace_id: &[P], center_group: &SerializedPaneGroup, ) -> SerializedWorkspace { SerializedWorkspace { id: WorkspaceId(4), - paths: PathList::new(paths), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(workspace_id), center_group: center_group.clone(), window_bounds: Default::default(), display: Default::default(), @@ -2293,18 +2252,30 @@ mod tests { WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await; let workspaces = [ - (1, vec![dir1.path()], 9), - (2, vec![dir2.path()], 5), - (3, vec![dir3.path()], 8), - (4, vec![dir4.path()], 2), - (5, vec![dir1.path(), dir2.path(), dir3.path()], 3), - (6, vec![dir4.path(), dir3.path(), dir2.path()], 4), + (1, vec![dir1.path()], vec![0], 9), + (2, vec![dir2.path()], vec![0], 5), + (3, vec![dir3.path()], vec![0], 8), + (4, vec![dir4.path()], vec![0], 2), + ( + 5, + vec![dir1.path(), dir2.path(), dir3.path()], + vec![0, 1, 2], + 3, + ), + ( + 6, + vec![dir2.path(), dir3.path(), dir4.path()], + vec![2, 1, 0], + 4, + ), ] .into_iter() - .map(|(id, paths, window_id)| SerializedWorkspace { + .map(|(id, locations, order, window_id)| SerializedWorkspace { id: WorkspaceId(id), - paths: PathList::new(paths.as_slice()), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::Local( + LocalPaths::new(locations), + LocalPathsOrder::new(order), + ), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2329,37 +2300,39 @@ mod tests { WindowId::from(4), // Bottom ])); - let locations = db + let have = db .last_session_workspace_locations("one-session", stack) .unwrap(); + assert_eq!(have.len(), 6); assert_eq!( - locations, - [ - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir4.path()]) - ), - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir3.path()]) - ), - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir2.path()]) - ), - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir1.path()]) - ), - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir1.path(), dir2.path(), dir3.path()]) - ), - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir4.path(), dir3.path(), dir2.path()]) - ), - ] + have[0], + SerializedWorkspaceLocation::from_local_paths(&[dir4.path()]) + ); + assert_eq!( + have[1], + SerializedWorkspaceLocation::from_local_paths([dir3.path()]) + ); + assert_eq!( + have[2], + SerializedWorkspaceLocation::from_local_paths([dir2.path()]) + ); + assert_eq!( + have[3], + SerializedWorkspaceLocation::from_local_paths([dir1.path()]) + ); + assert_eq!( + have[4], + SerializedWorkspaceLocation::Local( + LocalPaths::new([dir1.path(), dir2.path(), dir3.path()]), + LocalPathsOrder::new([0, 1, 2]), + ), + ); + assert_eq!( + have[5], + SerializedWorkspaceLocation::Local( + LocalPaths::new([dir2.path(), dir3.path(), dir4.path()]), + LocalPathsOrder::new([2, 1, 0]), + ), ); } @@ -2370,7 +2343,7 @@ mod tests { ) .await; - let ssh_connections = [ + let ssh_projects = [ ("host-1", "my-user-1"), ("host-2", "my-user-2"), ("host-3", "my-user-3"), @@ -2378,30 +2351,24 @@ mod tests { ] .into_iter() .map(|(host, user)| async { - db.get_or_create_ssh_connection(host.to_string(), None, Some(user.to_string())) + db.get_or_create_ssh_project(host.to_string(), None, vec![], Some(user.to_string())) .await - .unwrap(); - SerializedSshConnection { - host: host.into(), - port: None, - user: Some(user.into()), - } + .unwrap() }) .collect::>(); - let ssh_connections = futures::future::join_all(ssh_connections).await; + let ssh_projects = futures::future::join_all(ssh_projects).await; let workspaces = [ - (1, ssh_connections[0].clone(), 9), - (2, ssh_connections[1].clone(), 5), - (3, ssh_connections[2].clone(), 8), - (4, ssh_connections[3].clone(), 2), + (1, ssh_projects[0].clone(), 9), + (2, ssh_projects[1].clone(), 5), + (3, ssh_projects[2].clone(), 8), + (4, ssh_projects[3].clone(), 2), ] .into_iter() - .map(|(id, ssh_connection, window_id)| SerializedWorkspace { + .map(|(id, ssh_project, window_id)| SerializedWorkspace { id: WorkspaceId(id), - paths: PathList::default(), - location: SerializedWorkspaceLocation::Ssh(ssh_connection), + location: SerializedWorkspaceLocation::Ssh(ssh_project), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2430,31 +2397,19 @@ mod tests { assert_eq!(have.len(), 4); assert_eq!( have[0], - ( - SerializedWorkspaceLocation::Ssh(ssh_connections[3].clone()), - PathList::default() - ) + SerializedWorkspaceLocation::Ssh(ssh_projects[3].clone()) ); assert_eq!( have[1], - ( - SerializedWorkspaceLocation::Ssh(ssh_connections[2].clone()), - PathList::default() - ) + SerializedWorkspaceLocation::Ssh(ssh_projects[2].clone()) ); assert_eq!( have[2], - ( - SerializedWorkspaceLocation::Ssh(ssh_connections[1].clone()), - PathList::default() - ) + SerializedWorkspaceLocation::Ssh(ssh_projects[1].clone()) ); assert_eq!( have[3], - ( - SerializedWorkspaceLocation::Ssh(ssh_connections[0].clone()), - PathList::default() - ) + SerializedWorkspaceLocation::Ssh(ssh_projects[0].clone()) ); } @@ -2462,110 +2417,116 @@ mod tests { async fn test_get_or_create_ssh_project() { let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await; - let host = "example.com".to_string(); - let port = Some(22_u16); - let user = Some("user".to_string()); + let (host, port, paths, user) = ( + "example.com".to_string(), + Some(22_u16), + vec!["/home/user".to_string(), "/etc/nginx".to_string()], + Some("user".to_string()), + ); - let connection_id = db - .get_or_create_ssh_connection(host.clone(), port, user.clone()) + let project = db + .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone()) .await .unwrap(); + assert_eq!(project.host, host); + assert_eq!(project.paths, paths); + assert_eq!(project.user, user); + // Test that calling the function again with the same parameters returns the same project - let same_connection = db - .get_or_create_ssh_connection(host.clone(), port, user.clone()) + let same_project = db + .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone()) .await .unwrap(); - assert_eq!(connection_id, same_connection); + assert_eq!(project.id, same_project.id); // Test with different parameters - let host2 = "otherexample.com".to_string(); - let port2 = None; - let user2 = Some("otheruser".to_string()); + let (host2, paths2, user2) = ( + "otherexample.com".to_string(), + vec!["/home/otheruser".to_string()], + Some("otheruser".to_string()), + ); - let different_connection = db - .get_or_create_ssh_connection(host2.clone(), port2, user2.clone()) + let different_project = db + .get_or_create_ssh_project(host2.clone(), None, paths2.clone(), user2.clone()) .await .unwrap(); - assert_ne!(connection_id, different_connection); + assert_ne!(project.id, different_project.id); + assert_eq!(different_project.host, host2); + assert_eq!(different_project.paths, paths2); + assert_eq!(different_project.user, user2); } #[gpui::test] async fn test_get_or_create_ssh_project_with_null_user() { let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await; - let (host, port, user) = ("example.com".to_string(), None, None); + let (host, port, paths, user) = ( + "example.com".to_string(), + None, + vec!["/home/user".to_string()], + None, + ); - let connection_id = db - .get_or_create_ssh_connection(host.clone(), port, None) + let project = db + .get_or_create_ssh_project(host.clone(), port, paths.clone(), None) .await .unwrap(); - let same_connection_id = db - .get_or_create_ssh_connection(host.clone(), port, user.clone()) + assert_eq!(project.host, host); + assert_eq!(project.paths, paths); + assert_eq!(project.user, None); + + // Test that calling the function again with the same parameters returns the same project + let same_project = db + .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone()) .await .unwrap(); - assert_eq!(connection_id, same_connection_id); + assert_eq!(project.id, same_project.id); } #[gpui::test] - async fn test_get_ssh_connections() { - let db = WorkspaceDb::open_test_db("test_get_ssh_connections").await; + async fn test_get_ssh_projects() { + let db = WorkspaceDb::open_test_db("test_get_ssh_projects").await; - let connections = [ - ("example.com".to_string(), None, None), + let projects = vec![ + ( + "example.com".to_string(), + None, + vec!["/home/user".to_string()], + None, + ), ( "anotherexample.com".to_string(), Some(123_u16), + vec!["/home/user2".to_string()], Some("user2".to_string()), ), - ("yetanother.com".to_string(), Some(345_u16), None), + ( + "yetanother.com".to_string(), + Some(345_u16), + vec!["/home/user3".to_string(), "/proc/1234/exe".to_string()], + None, + ), ]; - let mut ids = Vec::new(); - for (host, port, user) in connections.iter() { - ids.push( - db.get_or_create_ssh_connection(host.clone(), *port, user.clone()) - .await - .unwrap(), - ); + for (host, port, paths, user) in projects.iter() { + let project = db + .get_or_create_ssh_project(host.clone(), *port, paths.clone(), user.clone()) + .await + .unwrap(); + + assert_eq!(&project.host, host); + assert_eq!(&project.port, port); + assert_eq!(&project.paths, paths); + assert_eq!(&project.user, user); } - let stored_projects = db.ssh_connections().unwrap(); - assert_eq!( - stored_projects, - [ - ( - ids[0], - SerializedSshConnection { - host: "example.com".into(), - port: None, - user: None, - } - ), - ( - ids[1], - SerializedSshConnection { - host: "anotherexample.com".into(), - port: Some(123), - user: Some("user2".into()), - } - ), - ( - ids[2], - SerializedSshConnection { - host: "yetanother.com".into(), - port: Some(345), - user: None, - } - ), - ] - .into_iter() - .collect::>(), - ); + let stored_projects = db.ssh_projects().unwrap(); + assert_eq!(stored_projects.len(), projects.len()); } #[gpui::test] @@ -2698,4 +2659,56 @@ mod tests { assert_eq!(workspace.center_group, new_workspace.center_group); } + + #[gpui::test] + async fn test_update_ssh_project_paths() { + zlog::init_test(); + + let db = WorkspaceDb::open_test_db("test_update_ssh_project_paths").await; + + let (host, port, initial_paths, user) = ( + "example.com".to_string(), + Some(22_u16), + vec!["/home/user".to_string(), "/etc/nginx".to_string()], + Some("user".to_string()), + ); + + let project = db + .get_or_create_ssh_project(host.clone(), port, initial_paths.clone(), user.clone()) + .await + .unwrap(); + + assert_eq!(project.host, host); + assert_eq!(project.paths, initial_paths); + assert_eq!(project.user, user); + + let new_paths = vec![ + "/home/user".to_string(), + "/etc/nginx".to_string(), + "/var/log".to_string(), + "/opt/app".to_string(), + ]; + + let updated_project = db + .update_ssh_project_paths(project.id, new_paths.clone()) + .await + .unwrap(); + + assert_eq!(updated_project.id, project.id); + assert_eq!(updated_project.paths, new_paths); + + let retrieved_project = db + .get_ssh_project( + host.clone(), + port, + serde_json::to_string(&new_paths).unwrap(), + user.clone(), + ) + .await + .unwrap() + .unwrap(); + + assert_eq!(retrieved_project.id, project.id); + assert_eq!(retrieved_project.paths, new_paths); + } } diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 04757d0495..15a54ac62f 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -1,48 +1,256 @@ use super::{SerializedAxis, SerializedWindowBounds}; use crate::{ Member, Pane, PaneAxis, SerializableItemRegistry, Workspace, WorkspaceId, item::ItemHandle, - path_list::PathList, }; -use anyhow::Result; +use anyhow::{Context as _, Result}; use async_recursion::async_recursion; use db::sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; use gpui::{AsyncWindowContext, Entity, WeakEntity}; - +use itertools::Itertools as _; use project::{Project, debugger::breakpoint_store::SourceBreakpoint}; +use remote::ssh_session::SshProjectId; use serde::{Deserialize, Serialize}; use std::{ collections::BTreeMap, path::{Path, PathBuf}, sync::Arc, }; -use util::ResultExt; +use util::{ResultExt, paths::SanitizedPath}; use uuid::Uuid; -#[derive( - Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize, -)] -pub(crate) struct SshConnectionId(pub u64); - #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] -pub struct SerializedSshConnection { +pub struct SerializedSshProject { + pub id: SshProjectId, pub host: String, pub port: Option, + pub paths: Vec, pub user: Option, } +impl SerializedSshProject { + pub fn ssh_urls(&self) -> Vec { + self.paths + .iter() + .map(|path| { + let mut result = String::new(); + if let Some(user) = &self.user { + result.push_str(user); + result.push('@'); + } + result.push_str(&self.host); + if let Some(port) = &self.port { + result.push(':'); + result.push_str(&port.to_string()); + } + result.push_str(path); + PathBuf::from(result) + }) + .collect() + } +} + +impl StaticColumnCount for SerializedSshProject { + fn column_count() -> usize { + 5 + } +} + +impl Bind for &SerializedSshProject { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + let next_index = statement.bind(&self.id.0, start_index)?; + let next_index = statement.bind(&self.host, next_index)?; + let next_index = statement.bind(&self.port, next_index)?; + let raw_paths = serde_json::to_string(&self.paths)?; + let next_index = statement.bind(&raw_paths, next_index)?; + statement.bind(&self.user, next_index) + } +} + +impl Column for SerializedSshProject { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let id = statement.column_int64(start_index)?; + let host = statement.column_text(start_index + 1)?.to_string(); + let (port, _) = Option::::column(statement, start_index + 2)?; + let raw_paths = statement.column_text(start_index + 3)?.to_string(); + let paths: Vec = serde_json::from_str(&raw_paths)?; + + let (user, _) = Option::::column(statement, start_index + 4)?; + + Ok(( + Self { + id: SshProjectId(id as u64), + host, + port, + paths, + user, + }, + start_index + 5, + )) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct LocalPaths(Arc>); + +impl LocalPaths { + pub fn new>(paths: impl IntoIterator) -> Self { + let mut paths: Vec = paths + .into_iter() + .map(|p| SanitizedPath::from(p).into()) + .collect(); + // Ensure all future `zed workspace1 workspace2` and `zed workspace2 workspace1` calls are using the same workspace. + // The actual workspace order is stored in the `LocalPathsOrder` struct. + paths.sort(); + Self(Arc::new(paths)) + } + + pub fn paths(&self) -> &Arc> { + &self.0 + } +} + +impl StaticColumnCount for LocalPaths {} +impl Bind for &LocalPaths { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + statement.bind(&bincode::serialize(&self.0)?, start_index) + } +} + +impl Column for LocalPaths { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let path_blob = statement.column_blob(start_index)?; + let paths: Arc> = if path_blob.is_empty() { + Default::default() + } else { + bincode::deserialize(path_blob).context("Bincode deserialization of paths failed")? + }; + + Ok((Self(paths), start_index + 1)) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct LocalPathsOrder(Vec); + +impl LocalPathsOrder { + pub fn new(order: impl IntoIterator) -> Self { + Self(order.into_iter().collect()) + } + + pub fn order(&self) -> &[usize] { + self.0.as_slice() + } + + pub fn default_for_paths(paths: &LocalPaths) -> Self { + Self::new(0..paths.0.len()) + } +} + +impl StaticColumnCount for LocalPathsOrder {} +impl Bind for &LocalPathsOrder { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + statement.bind(&bincode::serialize(&self.0)?, start_index) + } +} + +impl Column for LocalPathsOrder { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let order_blob = statement.column_blob(start_index)?; + let order = if order_blob.is_empty() { + Vec::new() + } else { + bincode::deserialize(order_blob).context("deserializing workspace root order")? + }; + + Ok((Self(order), start_index + 1)) + } +} + #[derive(Debug, PartialEq, Clone)] pub enum SerializedWorkspaceLocation { - Local, - Ssh(SerializedSshConnection), + Local(LocalPaths, LocalPathsOrder), + Ssh(SerializedSshProject), } impl SerializedWorkspaceLocation { + /// Create a new `SerializedWorkspaceLocation` from a list of local paths. + /// + /// The paths will be sorted and the order will be stored in the `LocalPathsOrder` struct. + /// + /// # Examples + /// + /// ``` + /// use std::path::Path; + /// use zed_workspace::SerializedWorkspaceLocation; + /// + /// let location = SerializedWorkspaceLocation::from_local_paths(vec![ + /// Path::new("path/to/workspace1"), + /// Path::new("path/to/workspace2"), + /// ]); + /// assert_eq!(location, SerializedWorkspaceLocation::Local( + /// LocalPaths::new(vec![ + /// Path::new("path/to/workspace1"), + /// Path::new("path/to/workspace2"), + /// ]), + /// LocalPathsOrder::new(vec![0, 1]), + /// )); + /// ``` + /// + /// ``` + /// use std::path::Path; + /// use zed_workspace::SerializedWorkspaceLocation; + /// + /// let location = SerializedWorkspaceLocation::from_local_paths(vec![ + /// Path::new("path/to/workspace2"), + /// Path::new("path/to/workspace1"), + /// ]); + /// + /// assert_eq!(location, SerializedWorkspaceLocation::Local( + /// LocalPaths::new(vec![ + /// Path::new("path/to/workspace1"), + /// Path::new("path/to/workspace2"), + /// ]), + /// LocalPathsOrder::new(vec![1, 0]), + /// )); + /// ``` + pub fn from_local_paths>(paths: impl IntoIterator) -> Self { + let mut indexed_paths: Vec<_> = paths + .into_iter() + .map(|p| p.as_ref().to_path_buf()) + .enumerate() + .collect(); + + indexed_paths.sort_by(|(_, a), (_, b)| a.cmp(b)); + + let sorted_paths: Vec<_> = indexed_paths.iter().map(|(_, path)| path.clone()).collect(); + let order: Vec<_> = indexed_paths.iter().map(|(index, _)| *index).collect(); + + Self::Local(LocalPaths::new(sorted_paths), LocalPathsOrder::new(order)) + } + /// Get sorted paths pub fn sorted_paths(&self) -> Arc> { - unimplemented!() + match self { + SerializedWorkspaceLocation::Local(paths, order) => { + if order.order().is_empty() { + paths.paths().clone() + } else { + Arc::new( + order + .order() + .iter() + .zip(paths.paths().iter()) + .sorted_by_key(|(i, _)| **i) + .map(|(_, p)| p.clone()) + .collect(), + ) + } + } + SerializedWorkspaceLocation::Ssh(ssh_project) => Arc::new(ssh_project.ssh_urls()), + } } } @@ -50,7 +258,6 @@ impl SerializedWorkspaceLocation { pub(crate) struct SerializedWorkspace { pub(crate) id: WorkspaceId, pub(crate) location: SerializedWorkspaceLocation, - pub(crate) paths: PathList, pub(crate) center_group: SerializedPaneGroup, pub(crate) window_bounds: Option, pub(crate) centered_layout: bool, @@ -374,3 +581,80 @@ impl Column for SerializedItem { )) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_serialize_local_paths() { + let paths = vec!["b", "a", "c"]; + let serialized = SerializedWorkspaceLocation::from_local_paths(paths); + + assert_eq!( + serialized, + SerializedWorkspaceLocation::Local( + LocalPaths::new(vec!["a", "b", "c"]), + LocalPathsOrder::new(vec![1, 0, 2]) + ) + ); + } + + #[test] + fn test_sorted_paths() { + let paths = vec!["b", "a", "c"]; + let serialized = SerializedWorkspaceLocation::from_local_paths(paths); + assert_eq!( + serialized.sorted_paths(), + Arc::new(vec![ + PathBuf::from("b"), + PathBuf::from("a"), + PathBuf::from("c"), + ]) + ); + + let paths = Arc::new(vec![ + PathBuf::from("a"), + PathBuf::from("b"), + PathBuf::from("c"), + ]); + let order = vec![2, 0, 1]; + let serialized = + SerializedWorkspaceLocation::Local(LocalPaths(paths), LocalPathsOrder(order)); + assert_eq!( + serialized.sorted_paths(), + Arc::new(vec![ + PathBuf::from("b"), + PathBuf::from("c"), + PathBuf::from("a"), + ]) + ); + + let paths = Arc::new(vec![ + PathBuf::from("a"), + PathBuf::from("b"), + PathBuf::from("c"), + ]); + let order = vec![]; + let serialized = + SerializedWorkspaceLocation::Local(LocalPaths(paths.clone()), LocalPathsOrder(order)); + assert_eq!(serialized.sorted_paths(), paths); + + let urls = ["/a", "/b", "/c"]; + let serialized = SerializedWorkspaceLocation::Ssh(SerializedSshProject { + id: SshProjectId(0), + host: "host".to_string(), + port: Some(22), + paths: urls.iter().map(|s| s.to_string()).collect(), + user: Some("user".to_string()), + }); + assert_eq!( + serialized.sorted_paths(), + Arc::new( + urls.iter() + .map(|p| PathBuf::from(format!("user@host:22{}", p))) + .collect() + ) + ); + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 044601df97..44aa94fe61 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1,12 +1,10 @@ pub mod dock; pub mod history_manager; -pub mod invalid_buffer_view; pub mod item; mod modal_layer; pub mod notifications; pub mod pane; pub mod pane_group; -mod path_list; mod persistence; pub mod searchable; pub mod shared_screen; @@ -19,7 +17,6 @@ mod workspace_settings; pub use crate::notifications::NotificationFrame; pub use dock::Panel; -pub use path_list::PathList; pub use toast_layer::{ToastAction, ToastLayer, ToastView}; use anyhow::{Context as _, Result, anyhow}; @@ -64,10 +61,13 @@ use notifications::{ }; pub use pane::*; pub use pane_group::*; -use persistence::{DB, SerializedWindowBounds, model::SerializedWorkspace}; +use persistence::{ + DB, SerializedWindowBounds, + model::{SerializedSshProject, SerializedWorkspace}, +}; pub use persistence::{ DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items, - model::{ItemId, SerializedSshConnection, SerializedWorkspaceLocation}, + model::{ItemId, LocalPaths, SerializedWorkspaceLocation}, }; use postage::stream::Stream; use project::{ @@ -612,60 +612,21 @@ impl ProjectItemRegistry { ); self.build_project_item_for_path_fns .push(|project, project_path, window, cx| { - let project_path = project_path.clone(); - let is_file = project - .read(cx) - .entry_for_path(&project_path, cx) - .is_some_and(|entry| entry.is_file()); - let entry_abs_path = project.read(cx).absolute_path(&project_path, cx); - let is_local = project.read(cx).is_local(); let project_item = - ::try_open(project, &project_path, cx)?; + ::try_open(project, project_path, cx)?; let project = project.clone(); Some(window.spawn(cx, async move |cx| { - match project_item.await.with_context(|| { - format!( - "opening project path {:?}", - entry_abs_path.as_deref().unwrap_or(&project_path.path) - ) - }) { - Ok(project_item) => { - let project_item = project_item; - let project_entry_id: Option = - project_item.read_with(cx, project::ProjectItem::entry_id)?; - let build_workspace_item = Box::new( - |pane: &mut Pane, window: &mut Window, cx: &mut Context| { - Box::new(cx.new(|cx| { - T::for_project_item( - project, - Some(pane), - project_item, - window, - cx, - ) - })) as Box - }, - ) as Box<_>; - Ok((project_entry_id, build_workspace_item)) - } - Err(e) => match entry_abs_path.as_deref().filter(|_| is_file) { - Some(abs_path) => match cx.update(|window, cx| { - T::for_broken_project_item(abs_path, is_local, &e, window, cx) - })? { - Some(broken_project_item_view) => { - let build_workspace_item = Box::new( - move |_: &mut Pane, _: &mut Window, cx: &mut Context| { - cx.new(|_| broken_project_item_view).boxed_clone() - }, - ) - as Box<_>; - Ok((None, build_workspace_item)) - } - None => Err(e)?, - }, - None => Err(e)?, + let project_item = project_item.await?; + let project_entry_id: Option = + project_item.read_with(cx, project::ProjectItem::entry_id)?; + let build_workspace_item = Box::new( + |pane: &mut Pane, window: &mut Window, cx: &mut Context| { + Box::new(cx.new(|cx| { + T::for_project_item(project, Some(pane), project_item, window, cx) + })) as Box }, - } + ) as Box<_>; + Ok((project_entry_id, build_workspace_item)) })) }); } @@ -1052,7 +1013,7 @@ pub enum OpenVisible { enum WorkspaceLocation { // Valid local paths or SSH project to serialize - Location(SerializedWorkspaceLocation, PathList), + Location(SerializedWorkspaceLocation), // No valid location found hence clear session id DetachFromSession, // No valid location found to serialize @@ -1136,6 +1097,7 @@ pub struct Workspace { terminal_provider: Option>, debugger_provider: Option>, serializable_items_tx: UnboundedSender>, + serialized_ssh_project: Option, _items_serializer: Task>, session_id: Option, scheduled_tasks: Vec>, @@ -1184,6 +1146,8 @@ impl Workspace { project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => { this.update_window_title(window, cx); + this.update_ssh_paths(cx); + this.serialize_ssh_paths(window, cx); this.serialize_workspace(window, cx); // This event could be triggered by `AddFolderToProject` or `RemoveFromProject`. this.update_history(cx); @@ -1468,7 +1432,7 @@ impl Workspace { serializable_items_tx, _items_serializer, session_id: Some(session_id), - + serialized_ssh_project: None, scheduled_tasks: Vec::new(), } } @@ -1508,9 +1472,20 @@ impl Workspace { let serialized_workspace = persistence::DB.workspace_for_roots(paths_to_open.as_slice()); - if let Some(paths) = serialized_workspace.as_ref().map(|ws| &ws.paths) { - paths_to_open = paths.paths().to_vec(); - if !paths.is_lexicographically_ordered() { + let workspace_location = serialized_workspace + .as_ref() + .map(|ws| &ws.location) + .and_then(|loc| match loc { + SerializedWorkspaceLocation::Local(_, order) => { + Some((loc.sorted_paths(), order.order())) + } + _ => None, + }); + + if let Some((paths, order)) = workspace_location { + paths_to_open = paths.iter().cloned().collect(); + + if order.iter().enumerate().any(|(i, &j)| i != j) { project_handle .update(cx, |project, cx| { project.set_worktrees_reordered(true, cx); @@ -2030,6 +2005,14 @@ impl Workspace { self.debugger_provider.clone() } + pub fn serialized_ssh_project(&self) -> Option { + self.serialized_ssh_project.clone() + } + + pub fn set_serialized_ssh_project(&mut self, serialized_ssh_project: SerializedSshProject) { + self.serialized_ssh_project = Some(serialized_ssh_project); + } + pub fn prompt_for_open_path( &mut self, path_prompt_options: PathPromptOptions, @@ -3396,8 +3379,9 @@ impl Workspace { window: &mut Window, cx: &mut App, ) -> Task, WorkspaceItemBuilder)>> { + let project = self.project().clone(); let registry = cx.default_global::().clone(); - registry.open_path(self.project(), &path, window, cx) + registry.open_path(&project, &path, window, cx) } pub fn find_project_item( @@ -4022,6 +4006,52 @@ impl Workspace { maybe_pane_handle } + pub fn split_pane_with_item( + &mut self, + pane_to_split: WeakEntity, + split_direction: SplitDirection, + from: WeakEntity, + item_id_to_move: EntityId, + window: &mut Window, + cx: &mut Context, + ) { + let Some(pane_to_split) = pane_to_split.upgrade() else { + return; + }; + let Some(from) = from.upgrade() else { + return; + }; + + let new_pane = self.add_pane(window, cx); + move_item(&from, &new_pane, item_id_to_move, 0, true, window, cx); + self.center + .split(&pane_to_split, &new_pane, split_direction) + .unwrap(); + cx.notify(); + } + + pub fn split_pane_with_project_entry( + &mut self, + pane_to_split: WeakEntity, + split_direction: SplitDirection, + project_entry: ProjectEntryId, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + let pane_to_split = pane_to_split.upgrade()?; + let new_pane = self.add_pane(window, cx); + self.center + .split(&pane_to_split, &new_pane, split_direction) + .unwrap(); + + let path = self.project.read(cx).path_for_entry(project_entry, cx)?; + let task = self.open_path(path, Some(new_pane.downgrade()), true, window, cx); + Some(cx.foreground_executor().spawn(async move { + task.await?; + Ok(()) + })) + } + pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context) { let active_item = self.active_pane.read(cx).active_item(); for pane in &self.panes { @@ -5030,12 +5060,59 @@ impl Workspace { self.session_id.clone() } - pub fn root_paths(&self, cx: &App) -> Vec> { + fn local_paths(&self, cx: &App) -> Option>> { let project = self.project().read(cx); - project - .visible_worktrees(cx) - .map(|worktree| worktree.read(cx).abs_path()) - .collect::>() + + if project.is_local() { + Some( + project + .visible_worktrees(cx) + .map(|worktree| worktree.read(cx).abs_path()) + .collect::>(), + ) + } else { + None + } + } + + fn update_ssh_paths(&mut self, cx: &App) { + let project = self.project().read(cx); + if !project.is_local() { + let paths: Vec = project + .visible_worktrees(cx) + .map(|worktree| worktree.read(cx).abs_path().to_string_lossy().to_string()) + .collect(); + if let Some(ssh_project) = &mut self.serialized_ssh_project { + ssh_project.paths = paths; + } + } + } + + fn serialize_ssh_paths(&mut self, window: &mut Window, cx: &mut Context) { + if self._schedule_serialize_ssh_paths.is_none() { + self._schedule_serialize_ssh_paths = + Some(cx.spawn_in(window, async move |this, cx| { + cx.background_executor() + .timer(SERIALIZATION_THROTTLE_TIME) + .await; + this.update_in(cx, |this, window, cx| { + let task = if let Some(ssh_project) = &this.serialized_ssh_project { + let ssh_project_id = ssh_project.id; + let ssh_project_paths = ssh_project.paths.clone(); + window.spawn(cx, async move |_| { + persistence::DB + .update_ssh_project_paths(ssh_project_id, ssh_project_paths) + .await + }) + } else { + Task::ready(Err(anyhow::anyhow!("No SSH project to serialize"))) + }; + task.detach(); + this._schedule_serialize_ssh_paths.take(); + }) + .log_err(); + })); + } } fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context) { @@ -5208,7 +5285,7 @@ impl Workspace { } match self.serialize_workspace_location(cx) { - WorkspaceLocation::Location(location, paths) => { + WorkspaceLocation::Location(location) => { let breakpoints = self.project.update(cx, |project, cx| { project .breakpoint_store() @@ -5222,7 +5299,6 @@ impl Workspace { let serialized_workspace = SerializedWorkspace { id: database_id, location, - paths, center_group, window_bounds, display: Default::default(), @@ -5248,19 +5324,13 @@ impl Workspace { } fn serialize_workspace_location(&self, cx: &App) -> WorkspaceLocation { - let paths = PathList::new(&self.root_paths(cx)); - if let Some(connection) = self.project.read(cx).ssh_connection_options(cx) { - WorkspaceLocation::Location( - SerializedWorkspaceLocation::Ssh(SerializedSshConnection { - host: connection.host, - port: connection.port, - user: connection.username, - }), - paths, - ) - } else if self.project.read(cx).is_local() { - if !paths.is_empty() { - WorkspaceLocation::Location(SerializedWorkspaceLocation::Local, paths) + if let Some(ssh_project) = &self.serialized_ssh_project { + WorkspaceLocation::Location(SerializedWorkspaceLocation::Ssh(ssh_project.clone())) + } else if let Some(local_paths) = self.local_paths(cx) { + if !local_paths.is_empty() { + WorkspaceLocation::Location(SerializedWorkspaceLocation::from_local_paths( + local_paths, + )) } else { WorkspaceLocation::DetachFromSession } @@ -5273,13 +5343,13 @@ impl Workspace { let Some(id) = self.database_id() else { return; }; - if !self.project.read(cx).is_local() { - return; - } + let location = match self.serialize_workspace_location(cx) { + WorkspaceLocation::Location(location) => location, + _ => return, + }; if let Some(manager) = HistoryManager::global(cx) { - let paths = PathList::new(&self.root_paths(cx)); manager.update(cx, |this, cx| { - this.update_history(id, HistoryManagerEntry::new(id, &paths), cx); + this.update_history(id, HistoryManagerEntry::new(id, &location), cx); }); } } @@ -6587,29 +6657,15 @@ impl Render for Workspace { } }) .children(self.zoomed.as_ref().and_then(|view| { - let zoomed_view = view.upgrade()?; - let div = div() + Some(div() .occlude() .absolute() .overflow_hidden() .border_color(colors.border) .bg(colors.background) - .child(zoomed_view) + .child(view.upgrade()?) .inset_0() - .shadow_lg(); - - if !WorkspaceSettings::get_global(cx).zoomed_padding { - return Some(div); - } - - Some(match self.zoomed_position { - Some(DockPosition::Left) => div.right_2().border_r_1(), - Some(DockPosition::Right) => div.left_2().border_l_1(), - Some(DockPosition::Bottom) => div.top_2().border_t_1(), - None => { - div.top_2().bottom_2().left_2().right_2().border_1() - } - }) + .shadow_lg()) })) .children(self.render_notifications(window, cx)), ) @@ -6759,14 +6815,14 @@ impl WorkspaceHandle for Entity { } } -pub async fn last_opened_workspace_location() -> Option<(SerializedWorkspaceLocation, PathList)> { +pub async fn last_opened_workspace_location() -> Option { DB.last_workspace().await.log_err().flatten() } pub fn last_session_workspace_locations( last_session_id: &str, last_session_window_stack: Option>, -) -> Option> { +) -> Option> { DB.last_session_workspace_locations(last_session_id, last_session_window_stack) .log_err() } @@ -7269,7 +7325,7 @@ pub fn open_ssh_project_with_new_connection( cx: &mut App, ) -> Task> { cx.spawn(async move |cx| { - let (workspace_id, serialized_workspace) = + let (serialized_ssh_project, workspace_id, serialized_workspace) = serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?; let session = match cx @@ -7303,6 +7359,7 @@ pub fn open_ssh_project_with_new_connection( open_ssh_project_inner( project, paths, + serialized_ssh_project, workspace_id, serialized_workspace, app_state, @@ -7322,12 +7379,13 @@ pub fn open_ssh_project_with_existing_connection( cx: &mut AsyncApp, ) -> Task> { cx.spawn(async move |cx| { - let (workspace_id, serialized_workspace) = + let (serialized_ssh_project, workspace_id, serialized_workspace) = serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?; open_ssh_project_inner( project, paths, + serialized_ssh_project, workspace_id, serialized_workspace, app_state, @@ -7341,6 +7399,7 @@ pub fn open_ssh_project_with_existing_connection( async fn open_ssh_project_inner( project: Entity, paths: Vec, + serialized_ssh_project: SerializedSshProject, workspace_id: WorkspaceId, serialized_workspace: Option, app_state: Arc, @@ -7393,6 +7452,7 @@ async fn open_ssh_project_inner( let mut workspace = Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx); + workspace.set_serialized_ssh_project(serialized_ssh_project); workspace.update_history(cx); if let Some(ref serialized) = serialized_workspace { @@ -7429,18 +7489,28 @@ fn serialize_ssh_project( connection_options: SshConnectionOptions, paths: Vec, cx: &AsyncApp, -) -> Task)>> { +) -> Task< + Result<( + SerializedSshProject, + WorkspaceId, + Option, + )>, +> { cx.background_spawn(async move { - let ssh_connection_id = persistence::DB - .get_or_create_ssh_connection( + let serialized_ssh_project = persistence::DB + .get_or_create_ssh_project( connection_options.host.clone(), connection_options.port, + paths + .iter() + .map(|path| path.to_string_lossy().to_string()) + .collect::>(), connection_options.username.clone(), ) .await?; let serialized_workspace = - persistence::DB.ssh_workspace_for_roots(&paths, ssh_connection_id); + persistence::DB.workspace_for_ssh_project(&serialized_ssh_project); let workspace_id = if let Some(workspace_id) = serialized_workspace.as_ref().map(|workspace| workspace.id) @@ -7450,7 +7520,7 @@ fn serialize_ssh_project( persistence::DB.next_id().await? }; - Ok((workspace_id, serialized_workspace)) + Ok((serialized_ssh_project, workspace_id, serialized_workspace)) }) } @@ -7997,15 +8067,18 @@ pub fn ssh_workspace_position_from_db( paths_to_open: &[PathBuf], cx: &App, ) -> Task> { - let paths = paths_to_open.to_vec(); + let paths = paths_to_open + .iter() + .map(|path| path.to_string_lossy().to_string()) + .collect::>(); cx.background_spawn(async move { - let ssh_connection_id = persistence::DB - .get_or_create_ssh_connection(host, port, user) + let serialized_ssh_project = persistence::DB + .get_or_create_ssh_project(host, port, paths, user) .await .context("fetching serialized ssh project")?; let serialized_workspace = - persistence::DB.ssh_workspace_for_roots(&paths, ssh_connection_id); + persistence::DB.workspace_for_ssh_project(&serialized_ssh_project); let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() { (Some(WindowBounds::Windowed(bounds)), None) diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 3b6bc1ea97..5635347514 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -29,7 +29,6 @@ pub struct WorkspaceSettings { pub on_last_window_closed: OnLastWindowClosed, pub resize_all_panels_in_dock: Vec, pub close_on_file_delete: bool, - pub zoomed_padding: bool, } #[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema)] @@ -203,12 +202,6 @@ pub struct WorkspaceSettingsContent { /// /// Default: false pub close_on_file_delete: Option, - /// Whether to show padding for zoomed panels. - /// When enabled, zoomed bottom panels will have some top padding, - /// while zoomed left/right panels will have padding to the right/left (respectively). - /// - /// Default: true - pub zoomed_padding: Option, } #[derive(Deserialize)] diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 6f4ead9ebb..c61e23f0a1 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -20,7 +20,6 @@ path = "src/main.rs" [dependencies] activity_indicator.workspace = true -acp_tools.workspace = true agent.workspace = true agent_ui.workspace = true agent_settings.workspace = true diff --git a/crates/zed/resources/info/SupportedPlatforms.plist b/crates/zed/resources/info/SupportedPlatforms.plist deleted file mode 100644 index fd2a4101d8..0000000000 --- a/crates/zed/resources/info/SupportedPlatforms.plist +++ /dev/null @@ -1,4 +0,0 @@ -CFBundleSupportedPlatforms - - MacOSX - diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index e99c8b564b..8beefd5891 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -47,8 +47,8 @@ use theme::{ use util::{ResultExt, TryFutureExt, maybe}; use uuid::Uuid; use workspace::{ - AppState, PathList, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings, - WorkspaceStore, notifications::NotificationId, + AppState, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings, WorkspaceStore, + notifications::NotificationId, }; use zed::{ OpenListener, OpenRequest, RawOpenRequest, app_menus, build_window_options, @@ -566,7 +566,6 @@ pub fn main() { language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx); agent_settings::init(cx); agent_servers::init(cx); - acp_tools::init(cx); web_search::init(cx); web_search_providers::init(app_state.client.clone(), cx); snippet_provider::init(cx); @@ -949,14 +948,15 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp if let Some(locations) = restorable_workspace_locations(cx, &app_state).await { let mut tasks = Vec::new(); - for (location, paths) in locations { + for location in locations { match location { - SerializedWorkspaceLocation::Local => { + SerializedWorkspaceLocation::Local(location, _) => { let app_state = app_state.clone(); + let paths = location.paths().to_vec(); let task = cx.spawn(async move |cx| { let open_task = cx.update(|cx| { workspace::open_paths( - &paths.paths(), + &paths, app_state, workspace::OpenOptions::default(), cx, @@ -978,7 +978,7 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp match connection_options { Ok(connection_options) => recent_projects::open_ssh_project( connection_options, - paths.paths().into_iter().map(PathBuf::from).collect(), + ssh.paths.into_iter().map(PathBuf::from).collect(), app_state, workspace::OpenOptions::default(), cx, @@ -1069,7 +1069,7 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp pub(crate) async fn restorable_workspace_locations( cx: &mut AsyncApp, app_state: &Arc, -) -> Option> { +) -> Option> { let mut restore_behavior = cx .update(|cx| WorkspaceSettings::get(None, cx).restore_on_startup) .ok()?; diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 553444ebdb..638e1dca0e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1308,11 +1308,11 @@ pub fn handle_keymap_file_changes( }) .detach(); - let mut current_layout_id = cx.keyboard_layout().id().to_string(); + let mut current_mapping = settings::get_key_equivalents(cx.keyboard_layout().id()); cx.on_keyboard_layout_change(move |cx| { - let next_layout_id = cx.keyboard_layout().id(); - if next_layout_id != current_layout_id { - current_layout_id = next_layout_id.to_string(); + let next_mapping = settings::get_key_equivalents(cx.keyboard_layout().id()); + if next_mapping != current_mapping { + current_mapping = next_mapping; keyboard_layout_tx.unbounded_send(()).ok(); } }) @@ -4729,7 +4729,7 @@ mod tests { // and key strokes contain the given key bindings .into_iter() - .any(|binding| binding.keystrokes().iter().any(|k| k.display_key == key)), + .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)), "On {} Failed to find {} with key binding {}", line, action.name(), diff --git a/crates/zed/src/zed/component_preview/persistence.rs b/crates/zed/src/zed/component_preview/persistence.rs index c37a4cc389..780f7f7626 100644 --- a/crates/zed/src/zed/component_preview/persistence.rs +++ b/crates/zed/src/zed/component_preview/persistence.rs @@ -1,17 +1,10 @@ use anyhow::Result; -use db::{ - query, - sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, -}; +use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql}; use workspace::{ItemId, WorkspaceDb, WorkspaceId}; -pub struct ComponentPreviewDb(ThreadSafeConnection); - -impl Domain for ComponentPreviewDb { - const NAME: &str = stringify!(ComponentPreviewDb); - - const MIGRATIONS: &[&str] = &[sql!( +define_connection! { + pub static ref COMPONENT_PREVIEW_DB: ComponentPreviewDb = + &[sql!( CREATE TABLE component_previews ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -20,11 +13,9 @@ impl Domain for ComponentPreviewDb { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]; + )]; } -db::static_connection!(COMPONENT_PREVIEW_DB, ComponentPreviewDb, [WorkspaceDb]); - impl ComponentPreviewDb { pub async fn save_active_page( &self, diff --git a/crates/zed/src/zed/edit_prediction_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs index bc2d757fd1..a9abd9bc74 100644 --- a/crates/zed/src/zed/edit_prediction_registry.rs +++ b/crates/zed/src/zed/edit_prediction_registry.rs @@ -75,10 +75,13 @@ pub fn init(client: Arc, user_store: Entity, cx: &mut App) { let new_provider = all_language_settings(None, cx).edit_predictions.provider; if new_provider != provider { + let tos_accepted = user_store.read(cx).has_accepted_terms_of_service(); + telemetry::event!( "Edit Prediction Provider Changed", from = provider, to = new_provider, + zed_ai_tos_accepted = tos_accepted, ); provider = new_provider; @@ -89,6 +92,28 @@ pub fn init(client: Arc, user_store: Entity, cx: &mut App) { user_store.clone(), cx, ); + + if !tos_accepted { + match provider { + EditPredictionProvider::Zed => { + let Some(window) = cx.active_window() else { + return; + }; + + window + .update(cx, |_, window, cx| { + window.dispatch_action( + Box::new(zed_actions::OpenZedPredictOnboarding), + cx, + ); + }) + .ok(); + } + EditPredictionProvider::None + | EditPredictionProvider::Copilot + | EditPredictionProvider::Supermaven => {} + } + } } } }) diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 2194fb7af5..827c7754fa 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -26,7 +26,6 @@ use std::thread; use std::time::Duration; use util::ResultExt; use util::paths::PathWithPosition; -use workspace::PathList; use workspace::item::ItemHandle; use workspace::{AppState, OpenOptions, SerializedWorkspaceLocation, Workspace}; @@ -362,14 +361,12 @@ async fn open_workspaces( if open_new_workspace == Some(true) { Vec::new() } else { - restorable_workspace_locations(cx, &app_state) - .await - .unwrap_or_default() + let locations = restorable_workspace_locations(cx, &app_state).await; + locations.unwrap_or_default() } } else { - vec![( - SerializedWorkspaceLocation::Local, - PathList::new(&paths.into_iter().map(PathBuf::from).collect::>()), + vec![SerializedWorkspaceLocation::from_local_paths( + paths.into_iter().map(PathBuf::from), )] }; @@ -397,9 +394,9 @@ async fn open_workspaces( // If there are paths to open, open a workspace for each grouping of paths let mut errored = false; - for (location, workspace_paths) in grouped_locations { + for location in grouped_locations { match location { - SerializedWorkspaceLocation::Local => { + SerializedWorkspaceLocation::Local(workspace_paths, _) => { let workspace_paths = workspace_paths .paths() .iter() @@ -432,7 +429,7 @@ async fn open_workspaces( cx.spawn(async move |cx| { open_ssh_project( connection_options, - workspace_paths.paths().to_vec(), + ssh.paths.into_iter().map(PathBuf::from).collect(), app_state, OpenOptions::default(), cx, diff --git a/crates/zed/src/zed/quick_action_bar/preview.rs b/crates/zed/src/zed/quick_action_bar/preview.rs index fb5a75f78d..3772104f39 100644 --- a/crates/zed/src/zed/quick_action_bar/preview.rs +++ b/crates/zed/src/zed/quick_action_bar/preview.rs @@ -72,10 +72,7 @@ impl QuickActionBar { Tooltip::with_meta( tooltip_text, Some(open_action_for_tooltip), - format!( - "{} to open in a split", - text_for_keystroke(&alt_click.modifiers, &alt_click.key, cx) - ), + format!("{} to open in a split", text_for_keystroke(&alt_click, cx)), window, cx, ) diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 8f4c42ca49..9455369e9a 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -156,10 +156,7 @@ pub mod workspace { #[action(deprecated_aliases = ["editor::CopyPath", "outline_panel::CopyPath", "project_panel::CopyPath"])] CopyPath, #[action(deprecated_aliases = ["editor::CopyRelativePath", "outline_panel::CopyRelativePath", "project_panel::CopyRelativePath"])] - CopyRelativePath, - /// Opens the selected file with the system's default application. - #[action(deprecated_aliases = ["project_panel::OpenWithSystem"])] - OpenWithSystem, + CopyRelativePath ] ); } @@ -284,17 +281,13 @@ pub mod agent { OpenSettings, /// Opens the agent onboarding modal. OpenOnboardingModal, - /// Opens the ACP onboarding modal. - OpenAcpOnboardingModal, /// Resets the agent onboarding state. ResetOnboarding, /// Starts a chat conversation with the agent. Chat, /// Toggles the language model selector dropdown. #[action(deprecated_aliases = ["assistant::ToggleModelSelector", "assistant2::ToggleModelSelector"])] - ToggleModelSelector, - /// Triggers re-authentication on Gemini - ReauthenticateAgent + ToggleModelSelector ] ); } diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 7b14d12796..916699d29b 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -118,8 +118,12 @@ impl Dismissable for ZedPredictUpsell { } } -pub fn should_show_upsell_modal() -> bool { - !ZedPredictUpsell::dismissed() +pub fn should_show_upsell_modal(user_store: &Entity, cx: &App) -> bool { + if user_store.read(cx).has_accepted_terms_of_service() { + !ZedPredictUpsell::dismissed() + } else { + true + } } #[derive(Clone)] @@ -1543,6 +1547,16 @@ impl edit_prediction::EditPredictionProvider for ZetaEditPredictionProvider { ) -> bool { true } + + fn needs_terms_acceptance(&self, cx: &App) -> bool { + !self + .zeta + .read(cx) + .user_store + .read(cx) + .has_accepted_terms_of_service() + } + fn is_refreshing(&self) -> bool { !self.pending_completions.is_empty() } @@ -1555,6 +1569,10 @@ impl edit_prediction::EditPredictionProvider for ZetaEditPredictionProvider { _debounce: bool, cx: &mut Context, ) { + if self.needs_terms_acceptance(cx) { + return; + } + if self.zeta.read(cx).update_required { return; } diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index a8a4689689..fb139db6e4 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -3243,7 +3243,6 @@ Run the `theme selector: toggle` action in the command palette to see a current "indent_size": 20, "auto_reveal_entries": true, "auto_fold_dirs": true, - "drag_and_drop": true, "scrollbar": { "show": null }, diff --git a/docs/src/diagnostics.md b/docs/src/diagnostics.md index 9603c8197c..a015fbebf8 100644 --- a/docs/src/diagnostics.md +++ b/docs/src/diagnostics.md @@ -51,7 +51,7 @@ To configure, use ```json5 "project_panel": { - "show_diagnostics": "all", + "diagnostics": "all", } ``` diff --git a/docs/src/languages/rust.md b/docs/src/languages/rust.md index 0bfa3ecac7..7695280275 100644 --- a/docs/src/languages/rust.md +++ b/docs/src/languages/rust.md @@ -136,7 +136,22 @@ This is enabled by default and can be configured as ## Manual Cargo Diagnostics fetch By default, rust-analyzer has `checkOnSave: true` enabled, which causes every buffer save to trigger a `cargo check --workspace --all-targets` command. -If disabled with `checkOnSave: false` (see the example of the server configuration json above), it's still possible to fetch the diagnostics manually, with the `editor: run/clear/cancel flycheck` commands in Rust files to refresh cargo diagnostics; the project diagnostics editor will also refresh cargo diagnostics with `editor: run flycheck` command when the setting is enabled. +For lager projects this might introduce excessive wait times, so a more fine-grained triggering could be enabled by altering the + +```json +"diagnostics": { + "cargo": { + // When enabled, Zed disables rust-analyzer's check on save and starts to query + // Cargo diagnostics separately. + "fetch_cargo_diagnostics": false + } +} +``` + +default settings. + +This will stop rust-analyzer from running `cargo check ...` on save, yet still allow to run +`editor: run/clear/cancel flycheck` commands in Rust files to refresh cargo diagnostics; the project diagnostics editor will also refresh cargo diagnostics with `editor: run flycheck` command when the setting is enabled. ## More server configuration diff --git a/docs/src/tasks.md b/docs/src/tasks.md index bff3eac860..9550563432 100644 --- a/docs/src/tasks.md +++ b/docs/src/tasks.md @@ -45,9 +45,9 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to // Whether to show the task line in the output of the spawned task, defaults to `true`. "show_summary": true, // Whether to show the command line in the output of the spawned task, defaults to `true`. - "show_output": true + "show_output": true, // Represents the tags for inline runnable indicators, or spawning multiple tasks at once. - // "tags": [] + "tags": [] } ] ``` diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 4fc5a9ba88..24b2a9d769 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -431,7 +431,6 @@ Project panel can be shown/hidden with {#action project_panel::ToggleFocus} ({#k "auto_reveal_entries": true, // Show file in panel when activating its buffer "auto_fold_dirs": true, // Fold dirs with single subdir "sticky_scroll": true, // Stick parent directories at top of the project panel. - "drag_and_drop": true, // Whether drag and drop is enabled "scrollbar": { // Project panel scrollbar settings "show": null // Show/hide: (auto, system, always, never) }, diff --git a/script/squawk b/script/squawk index 497fcff089..8489206f14 100755 --- a/script/squawk +++ b/script/squawk @@ -15,11 +15,13 @@ SQUAWK_VERSION=0.26.0 SQUAWK_BIN="./target/squawk-$SQUAWK_VERSION" SQUAWK_ARGS="--assume-in-transaction --config script/lib/squawk.toml" -pkgutil --pkg-info com.apple.pkg.RosettaUpdateAuto || /usr/sbin/softwareupdate --install-rosetta --agree-to-license -# When bootstrapping a brand new CI machine, the `target` directory may not exist yet. -mkdir -p "./target" -curl -L -o "$SQUAWK_BIN" "https://github.com/sbdchd/squawk/releases/download/v$SQUAWK_VERSION/squawk-darwin-x86_64" -chmod +x "$SQUAWK_BIN" +if [ ! -f "$SQUAWK_BIN" ]; then + pkgutil --pkg-info com.apple.pkg.RosettaUpdateAuto || /usr/sbin/softwareupdate --install-rosetta --agree-to-license + # When bootstrapping a brand new CI machine, the `target` directory may not exist yet. + mkdir -p "./target" + curl -L -o "$SQUAWK_BIN" "https://github.com/sbdchd/squawk/releases/download/v$SQUAWK_VERSION/squawk-darwin-x86_64" + chmod +x "$SQUAWK_BIN" +fi if [ -n "$SQUAWK_GITHUB_TOKEN" ]; then export SQUAWK_GITHUB_REPO_OWNER=$(echo $GITHUB_REPOSITORY | awk -F/ '{print $1}') diff --git a/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml index 2f9a963abc..d0047962bd 100644 --- a/tooling/workspace-hack/Cargo.toml +++ b/tooling/workspace-hack/Cargo.toml @@ -54,7 +54,6 @@ digest = { version = "0.10", features = ["mac", "oid", "std"] } either = { version = "1", features = ["serde", "use_std"] } euclid = { version = "0.22" } event-listener = { version = "5" } -event-listener-strategy = { version = "0.5" } flate2 = { version = "1", features = ["zlib-rs"] } form_urlencoded = { version = "1" } futures = { version = "0.3", features = ["io-compat"] } @@ -104,11 +103,13 @@ regalloc2 = { version = "0.11", features = ["checker", "enable-serde"] } regex = { version = "1" } regex-automata = { version = "0.4" } regex-syntax = { version = "0.8" } +rodio = { git = "https://github.com/RustAudio/rodio", branch = "microphone", features = ["tracing", "wav_output"] } rust_decimal = { version = "1", default-features = false, features = ["maths", "serde", "std"] } rustc-hash = { version = "1" } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["fs", "net", "std"] } rustls = { version = "0.23", features = ["ring"] } rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] } +schemars = { version = "1", features = ["chrono04", "indexmap2", "semver1"] } sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] } sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] } semver = { version = "1", features = ["serde"] } @@ -183,7 +184,6 @@ digest = { version = "0.10", features = ["mac", "oid", "std"] } either = { version = "1", features = ["serde", "use_std"] } euclid = { version = "0.22" } event-listener = { version = "5" } -event-listener-strategy = { version = "0.5" } flate2 = { version = "1", features = ["zlib-rs"] } form_urlencoded = { version = "1" } futures = { version = "0.3", features = ["io-compat"] } @@ -238,11 +238,13 @@ regalloc2 = { version = "0.11", features = ["checker", "enable-serde"] } regex = { version = "1" } regex-automata = { version = "0.4" } regex-syntax = { version = "0.8" } +rodio = { git = "https://github.com/RustAudio/rodio", branch = "microphone", features = ["tracing", "wav_output"] } rust_decimal = { version = "1", default-features = false, features = ["maths", "serde", "std"] } rustc-hash = { version = "1" } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["fs", "net", "std"] } rustls = { version = "0.23", features = ["ring"] } rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] } +schemars = { version = "1", features = ["chrono04", "indexmap2", "semver1"] } sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] } sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] } semver = { version = "1", features = ["serde"] } @@ -291,6 +293,7 @@ getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-f gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] } hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } +livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } naga = { version = "25", features = ["msl-out", "wgsl-in"] } nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } objc2 = { version = "0.6" } @@ -319,6 +322,7 @@ getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-f gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] } hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } +livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } naga = { version = "25", features = ["msl-out", "wgsl-in"] } nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } objc2 = { version = "0.6" } @@ -348,6 +352,7 @@ getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-f gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] } hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } +livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } naga = { version = "25", features = ["msl-out", "wgsl-in"] } nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } objc2 = { version = "0.6" } @@ -376,6 +381,7 @@ getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-f gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] } hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } +livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } naga = { version = "25", features = ["msl-out", "wgsl-in"] } nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } objc2 = { version = "0.6" } @@ -403,6 +409,7 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } +event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } @@ -413,6 +420,7 @@ inout = { version = "0.1", default-features = false, features = ["block-padding" itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] } linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } +livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } mio = { version = "1", features = ["net", "os-ext"] } naga = { version = "25", features = ["spv-out", "wgsl-in"] } nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] } @@ -443,6 +451,7 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } +event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } @@ -453,6 +462,7 @@ inout = { version = "0.1", default-features = false, features = ["block-padding" itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] } linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } +livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } mio = { version = "1", features = ["net", "os-ext"] } naga = { version = "25", features = ["spv-out", "wgsl-in"] } nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] } @@ -481,6 +491,7 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } +event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } @@ -491,6 +502,7 @@ inout = { version = "0.1", default-features = false, features = ["block-padding" itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] } linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } +livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } mio = { version = "1", features = ["net", "os-ext"] } naga = { version = "25", features = ["spv-out", "wgsl-in"] } nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] } @@ -521,6 +533,7 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } +event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } @@ -531,6 +544,7 @@ inout = { version = "0.1", default-features = false, features = ["block-padding" itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] } linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } +livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } mio = { version = "1", features = ["net", "os-ext"] } naga = { version = "25", features = ["spv-out", "wgsl-in"] } nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] } @@ -560,6 +574,7 @@ getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-f getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] } hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } +livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } ring = { version = "0.17", features = ["std"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event"] } scopeguard = { version = "1" } @@ -571,9 +586,9 @@ tower = { version = "0.5", default-features = false, features = ["timeout", "uti winapi = { version = "0.3", default-features = false, features = ["cfg", "commapi", "consoleapi", "errhandlingapi", "evntrace", "fileapi", "handleapi", "impl-debug", "impl-default", "in6addr", "inaddr", "ioapiset", "knownfolders", "minwinbase", "minwindef", "namedpipeapi", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "synchapi", "sysinfoapi", "timezoneapi", "winbase", "windef", "winerror", "winioctl", "winnt", "winreg", "winsock2", "winuser"] } windows-core = { version = "0.61" } windows-numerics = { version = "0.2" } -windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } -windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Memory", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] } -windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] } +windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } +windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authorization", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming"] } +windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] } [target.x86_64-pc-windows-msvc.build-dependencies] codespan-reporting = { version = "0.12" } @@ -583,6 +598,7 @@ getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-f getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] } hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } +livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] } ring = { version = "0.17", features = ["std"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event"] } @@ -595,9 +611,9 @@ tower = { version = "0.5", default-features = false, features = ["timeout", "uti winapi = { version = "0.3", default-features = false, features = ["cfg", "commapi", "consoleapi", "errhandlingapi", "evntrace", "fileapi", "handleapi", "impl-debug", "impl-default", "in6addr", "inaddr", "ioapiset", "knownfolders", "minwinbase", "minwindef", "namedpipeapi", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "synchapi", "sysinfoapi", "timezoneapi", "winbase", "windef", "winerror", "winioctl", "winnt", "winreg", "winsock2", "winuser"] } windows-core = { version = "0.61" } windows-numerics = { version = "0.2" } -windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } -windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Memory", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] } -windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] } +windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } +windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authorization", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming"] } +windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] } [target.x86_64-unknown-linux-musl.dependencies] aes = { version = "0.8", default-features = false, features = ["zeroize"] } @@ -606,6 +622,7 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } +event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } @@ -616,6 +633,7 @@ inout = { version = "0.1", default-features = false, features = ["block-padding" itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] } linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } +livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } mio = { version = "1", features = ["net", "os-ext"] } naga = { version = "25", features = ["spv-out", "wgsl-in"] } nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] } @@ -646,6 +664,7 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } +event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } @@ -656,6 +675,7 @@ inout = { version = "0.1", default-features = false, features = ["block-padding" itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] } linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } +livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } mio = { version = "1", features = ["net", "os-ext"] } naga = { version = "25", features = ["spv-out", "wgsl-in"] } nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] }