diff --git a/Cargo.lock b/Cargo.lock index e09ab0fa57..32954a86ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -268,12 +268,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "arbitrary" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" - [[package]] name = "arrayref" version = "0.3.7" @@ -582,8 +576,9 @@ dependencies = [ [[package]] name = "async-task" -version = "4.0.3" -source = "git+https://github.com/zed-industries/async-task?rev=341b57d6de98cdfd7b418567b8de2022ca993a6e#341b57d6de98cdfd7b418567b8de2022ca993a6e" +version = "4.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" [[package]] name = "async-tls" @@ -1832,105 +1827,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cranelift-bforest" -version = "0.103.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" -dependencies = [ - "cranelift-entity", -] - -[[package]] -name = "cranelift-codegen" -version = "0.103.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" -dependencies = [ - "bumpalo", - "cranelift-bforest", - "cranelift-codegen-meta", - "cranelift-codegen-shared", - "cranelift-control", - "cranelift-entity", - "cranelift-isle", - "gimli", - "hashbrown 0.14.0", - "log", - "regalloc2", - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cranelift-codegen-meta" -version = "0.103.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" -dependencies = [ - "cranelift-codegen-shared", -] - -[[package]] -name = "cranelift-codegen-shared" -version = "0.103.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" - -[[package]] -name = "cranelift-control" -version = "0.103.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" -dependencies = [ - "arbitrary", -] - -[[package]] -name = "cranelift-entity" -version = "0.103.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" -dependencies = [ - "serde", - "serde_derive", -] - -[[package]] -name = "cranelift-frontend" -version = "0.103.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" -dependencies = [ - "cranelift-codegen", - "log", - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cranelift-isle" -version = "0.103.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" - -[[package]] -name = "cranelift-native" -version = "0.103.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" -dependencies = [ - "cranelift-codegen", - "libc", - "target-lexicon", -] - -[[package]] -name = "cranelift-wasm" -version = "0.103.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" -dependencies = [ - "cranelift-codegen", - "cranelift-entity", - "cranelift-frontend", - "itertools 0.10.5", - "log", - "smallvec", - "wasmparser", - "wasmtime-types", -] - [[package]] name = "crc" version = "3.0.1" @@ -2526,12 +2422,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - [[package]] name = "fallible-streaming-iterator" version = "0.1.9" @@ -2597,7 +2487,6 @@ dependencies = [ "postage", "project", "regex", - "search", "serde", "serde_derive", "settings", @@ -3042,11 +2931,6 @@ name = "gimli" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" -dependencies = [ - "fallible-iterator 0.3.0", - "indexmap 2.0.0", - "stable_deref_trait", -] [[package]] name = "git" @@ -3559,7 +3443,6 @@ checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" dependencies = [ "equivalent", "hashbrown 0.14.0", - "serde", ] [[package]] @@ -3925,12 +3808,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" -[[package]] -name = "leb128" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" - [[package]] name = "libc" version = "0.2.148" @@ -4162,15 +4039,6 @@ dependencies = [ "url", ] -[[package]] -name = "mach" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" -dependencies = [ - "libc", -] - [[package]] name = "mach2" version = "0.4.1" @@ -4249,15 +4117,6 @@ version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" -[[package]] -name = "memfd" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64" -dependencies = [ - "rustix 0.38.21", -] - [[package]] name = "memmap2" version = "0.2.3" @@ -4913,9 +4772,6 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ - "crc32fast", - "hashbrown 0.14.0", - "indexmap 2.0.0", "memchr", ] @@ -5770,15 +5626,6 @@ version = "2.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" -[[package]] -name = "psm" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" -dependencies = [ - "cc", -] - [[package]] name = "ptr_meta" version = "0.1.4" @@ -6052,19 +5899,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "regalloc2" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad156d539c879b7a24a363a2016d77961786e71f48f2e2fc8302a92abd2429a6" -dependencies = [ - "hashbrown 0.13.2", - "log", - "rustc-hash", - "slice-group-by", - "smallvec", -] - [[package]] name = "regex" version = "1.9.5" @@ -6384,7 +6218,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" dependencies = [ "bitflags 2.4.1", - "fallible-iterator 0.2.0", + "fallible-iterator", "fallible-streaming-iterator", "hashlink", "libsqlite3-sys", @@ -7209,12 +7043,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "slice-group-by" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" - [[package]] name = "slotmap" version = "1.0.6" @@ -7327,12 +7155,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be6c3f39c37a4283ee4b43d1311c828f2e1fb0541e76ea0cb1a2abd9ef2f5b3b" -[[package]] -name = "sptr" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" - [[package]] name = "sqlez" version = "0.1.0" @@ -7588,12 +7410,6 @@ dependencies = [ "uuid 1.4.1", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "static_assertions" version = "1.1.0" @@ -7865,12 +7681,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "target-lexicon" -version = "0.12.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" - [[package]] name = "tempdir" version = "0.3.7" @@ -7952,6 +7762,7 @@ dependencies = [ "procinfo", "project", "rand 0.8.5", + "search", "serde", "serde_derive", "settings", @@ -8524,8 +8335,6 @@ source = "git+https://github.com/tree-sitter/tree-sitter?rev=31c40449749c4263a91 dependencies = [ "cc", "regex", - "wasmtime", - "wasmtime-c-api-impl", ] [[package]] @@ -9290,224 +9099,6 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" -[[package]] -name = "wasm-encoder" -version = "0.38.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad2b51884de9c7f4fe2fd1043fccb8dcad4b1e29558146ee57a144d15779f3f" -dependencies = [ - "leb128", -] - -[[package]] -name = "wasmparser" -version = "0.118.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ee9723b928e735d53000dec9eae7b07a60e490c85ab54abb66659fc61bfcd9" -dependencies = [ - "indexmap 2.0.0", - "semver", -] - -[[package]] -name = "wasmtime" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" -dependencies = [ - "anyhow", - "bincode", - "bumpalo", - "cfg-if 1.0.0", - "indexmap 2.0.0", - "libc", - "log", - "object", - "once_cell", - "paste", - "serde", - "serde_derive", - "serde_json", - "target-lexicon", - "wasmparser", - "wasmtime-cranelift", - "wasmtime-environ", - "wasmtime-jit", - "wasmtime-runtime", - "windows-sys 0.48.0", -] - -[[package]] -name = "wasmtime-asm-macros" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" -dependencies = [ - "cfg-if 1.0.0", -] - -[[package]] -name = "wasmtime-c-api-impl" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" -dependencies = [ - "anyhow", - "log", - "once_cell", - "tracing", - "wasmtime", - "wasmtime-c-api-macros", -] - -[[package]] -name = "wasmtime-c-api-macros" -version = "0.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "wasmtime-cranelift" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" -dependencies = [ - "anyhow", - "cfg-if 1.0.0", - "cranelift-codegen", - "cranelift-control", - "cranelift-entity", - "cranelift-frontend", - "cranelift-native", - "cranelift-wasm", - "gimli", - "log", - "object", - "target-lexicon", - "thiserror", - "wasmparser", - "wasmtime-cranelift-shared", - "wasmtime-environ", - "wasmtime-versioned-export-macros", -] - -[[package]] -name = "wasmtime-cranelift-shared" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" -dependencies = [ - "anyhow", - "cranelift-codegen", - "cranelift-control", - "cranelift-native", - "gimli", - "object", - "target-lexicon", - "wasmtime-environ", -] - -[[package]] -name = "wasmtime-environ" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" -dependencies = [ - "anyhow", - "cranelift-entity", - "gimli", - "indexmap 2.0.0", - "log", - "object", - "serde", - "serde_derive", - "target-lexicon", - "thiserror", - "wasmparser", - "wasmtime-types", -] - -[[package]] -name = "wasmtime-jit" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" -dependencies = [ - "anyhow", - "bincode", - "cfg-if 1.0.0", - "gimli", - "log", - "object", - "rustix 0.38.21", - "serde", - "serde_derive", - "target-lexicon", - "wasmtime-environ", - "wasmtime-jit-icache-coherence", - "wasmtime-runtime", - "windows-sys 0.48.0", -] - -[[package]] -name = "wasmtime-jit-icache-coherence" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "wasmtime-runtime" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" -dependencies = [ - "anyhow", - "cc", - "cfg-if 1.0.0", - "indexmap 2.0.0", - "libc", - "log", - "mach", - "memfd", - "memoffset 0.9.0", - "paste", - "psm", - "rustix 0.38.21", - "sptr", - "wasm-encoder", - "wasmtime-asm-macros", - "wasmtime-environ", - "wasmtime-versioned-export-macros", - "wasmtime-wmemcheck", - "windows-sys 0.48.0", -] - -[[package]] -name = "wasmtime-types" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" -dependencies = [ - "cranelift-entity", - "serde", - "serde_derive", - "thiserror", - "wasmparser", -] - -[[package]] -name = "wasmtime-versioned-export-macros" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.37", -] - -[[package]] -name = "wasmtime-wmemcheck" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" - [[package]] name = "web-sys" version = "0.3.64" diff --git a/Cargo.toml b/Cargo.toml index 5449f82b2f..3e3c9e9999 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -127,12 +127,11 @@ thiserror = { version = "1.0.29" } time = { version = "0.3", features = ["serde", "serde-well-known"] } toml = { version = "0.5" } tiktoken-rs = "0.5.7" -tree-sitter = { version = "0.20", features = ["wasm"] } +tree-sitter = { version = "0.20" } unindent = { version = "0.1.7" } pretty_assertions = "1.3.0" git2 = { version = "0.15", default-features = false} uuid = { version = "1.1.2", features = ["v4"] } -wasmtime = "16" tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "7331995b19b8f8aba2d5e26deb51d2195c18bc94" } tree-sitter-c = "0.20.1" @@ -165,8 +164,7 @@ tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev = [patch.crates-io] tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "31c40449749c4263a91a43593831b82229049a4c" } -async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" } -wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", rev = "v16.0.0" } +# wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", rev = "v16.0.0" } # TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457 cocoa = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" } diff --git a/assets/icons/arrow_circle.svg b/assets/icons/arrow_circle.svg new file mode 100644 index 0000000000..750e349e2b --- /dev/null +++ b/assets/icons/arrow_circle.svg @@ -0,0 +1 @@ + diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 5d70000639..3ff0db1a16 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -402,7 +402,7 @@ "cmd-r": "workspace::ToggleRightDock", "cmd-j": "workspace::ToggleBottomDock", "alt-cmd-y": "workspace::CloseAllDocks", - "cmd-shift-f": "workspace::NewSearch", + "cmd-shift-f": "workspace::DeploySearch", "cmd-k cmd-t": "theme_selector::Toggle", "cmd-k cmd-s": "zed::OpenKeymap", "cmd-t": "project_symbols::Toggle", diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index d225463b05..452538b910 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -16,7 +16,7 @@ use ai::{ use ai::prompts::repository_context::PromptCodeSnippet; use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; -use client::{telemetry::AssistantKind, TelemetrySettings}; +use client::telemetry::AssistantKind; use collections::{hash_map, HashMap, HashSet, VecDeque}; use editor::{ display_map::{ @@ -38,7 +38,7 @@ use gpui::{ }; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; use project::Project; -use search::BufferSearchBar; +use search::{buffer_search::DivRegistrar, BufferSearchBar}; use semantic_index::{SemanticIndex, SemanticIndexStatus}; use settings::{Settings, SettingsStore}; use std::{ @@ -1125,8 +1125,6 @@ impl Render for AssistantPanel { .child(Label::new( "Click on the Z button in the status bar to close this panel." )) - .border() - .border_color(gpui::red()) } else { let header = TabBar::new("assistant_header") .start_child( @@ -1158,7 +1156,18 @@ impl Render for AssistantPanel { div() }); + let contents = if self.active_editor().is_some() { + let mut registrar = DivRegistrar::new( + |panel, cx| panel.toolbar.read(cx).item_of_type::(), + cx, + ); + BufferSearchBar::register_inner(&mut registrar); + registrar.into_div() + } else { + div() + }; v_stack() + .key_context("AssistantPanel") .size_full() .on_action(cx.listener(|this, _: &workspace::NewFile, cx| { this.new_conversation(cx); @@ -1177,7 +1186,7 @@ impl Render for AssistantPanel { Some(self.toolbar.clone()) }) .child( - div() + contents .flex_1() .child(if let Some(editor) = self.active_editor() { editor.clone().into_any_element() @@ -1277,8 +1286,8 @@ impl Panel for AssistantPanel { } } - fn icon(&self, _cx: &WindowContext) -> Option { - Some(Icon::Ai) + fn icon(&self, cx: &WindowContext) -> Option { + Some(Icon::Ai).filter(|_| AssistantSettings::get_global(cx).button) } fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { @@ -3529,12 +3538,5 @@ fn report_assistant_event( .default_open_ai_model .clone(); - let telemetry_settings = TelemetrySettings::get_global(cx).clone(); - - telemetry.report_assistant_event( - telemetry_settings, - conversation_id, - assistant_kind, - model.full_name(), - ) + telemetry.report_assistant_event(conversation_id, assistant_kind, model.full_name(), cx) } diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 691b83479f..a2a90d4f2f 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -270,7 +270,7 @@ impl AutoUpdater { ReleaseChannel::Nightly => cx .try_read_global::(|sha, _| release.version != sha.0) .unwrap_or(true), - _ => release.version.parse::()? <= current_version, + _ => release.version.parse::()? > current_version, }; if !should_download { diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index bc8a0809ca..c419043a72 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -5,7 +5,7 @@ pub mod room; use anyhow::{anyhow, Result}; use audio::Audio; use call_settings::CallSettings; -use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE}; +use client::{proto, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE}; use collections::HashSet; use futures::{channel::oneshot, future::Shared, Future, FutureExt}; use gpui::{ @@ -480,9 +480,8 @@ pub fn report_call_event_for_room( cx: &mut AppContext, ) { let telemetry = client.telemetry(); - let telemetry_settings = *TelemetrySettings::get_global(cx); - telemetry.report_call_event(telemetry_settings, operation, Some(room_id), channel_id) + telemetry.report_call_event(operation, Some(room_id), channel_id, cx) } pub fn report_call_event_for_channel( @@ -495,13 +494,11 @@ pub fn report_call_event_for_channel( let telemetry = client.telemetry(); - let telemetry_settings = *TelemetrySettings::get_global(cx); - telemetry.report_call_event( - telemetry_settings, operation, room.map(|r| r.read(cx).id()), Some(channel_id), + cx, ) } diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 789b627fb0..2391c5f3b5 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -177,8 +177,7 @@ impl Telemetry { // TestAppContext ends up calling this function on shutdown and it panics when trying to find the TelemetrySettings #[cfg(not(any(test, feature = "test-support")))] fn shutdown_telemetry(self: &Arc, cx: &mut AppContext) -> impl Future { - let telemetry_settings = TelemetrySettings::get_global(cx).clone(); - self.report_app_event(telemetry_settings, "close", true); + self.report_app_event("close", true, cx); Task::ready(()) } @@ -227,24 +226,11 @@ impl Telemetry { return; }; - let telemetry_settings = if let Ok(telemetry_settings) = - cx.update(|cx| *TelemetrySettings::get_global(cx)) - { - telemetry_settings - } else { - break; - }; - - this.report_memory_event( - telemetry_settings, - process.memory(), - process.virtual_memory(), - ); - this.report_cpu_event( - telemetry_settings, - process.cpu_usage(), - system.cpus().len() as u32, - ); + cx.update(|cx| { + this.report_memory_event(process.memory(), process.virtual_memory(), cx); + this.report_cpu_event(process.cpu_usage(), system.cpus().len() as u32, cx); + }) + .ok(); } }) .detach(); @@ -269,12 +255,12 @@ impl Telemetry { pub fn report_editor_event( self: &Arc, - telemetry_settings: TelemetrySettings, file_extension: Option, vim_mode: bool, operation: &'static str, copilot_enabled: bool, copilot_enabled_for_language: bool, + cx: &AppContext, ) { let event = ClickhouseEvent::Editor { file_extension, @@ -285,15 +271,15 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings, false) + self.report_clickhouse_event(event, false, cx) } pub fn report_copilot_event( self: &Arc, - telemetry_settings: TelemetrySettings, suggestion_id: Option, suggestion_accepted: bool, file_extension: Option, + cx: &AppContext, ) { let event = ClickhouseEvent::Copilot { suggestion_id, @@ -302,15 +288,15 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings, false) + self.report_clickhouse_event(event, false, cx) } pub fn report_assistant_event( self: &Arc, - telemetry_settings: TelemetrySettings, conversation_id: Option, kind: AssistantKind, model: &'static str, + cx: &AppContext, ) { let event = ClickhouseEvent::Assistant { conversation_id, @@ -319,15 +305,15 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings, false) + self.report_clickhouse_event(event, false, cx) } pub fn report_call_event( self: &Arc, - telemetry_settings: TelemetrySettings, operation: &'static str, room_id: Option, channel_id: Option, + cx: &AppContext, ) { let event = ClickhouseEvent::Call { operation, @@ -336,14 +322,14 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings, false) + self.report_clickhouse_event(event, false, cx) } pub fn report_cpu_event( self: &Arc, - telemetry_settings: TelemetrySettings, usage_as_percentage: f32, core_count: u32, + cx: &AppContext, ) { let event = ClickhouseEvent::Cpu { usage_as_percentage, @@ -351,14 +337,14 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings, false) + self.report_clickhouse_event(event, false, cx) } pub fn report_memory_event( self: &Arc, - telemetry_settings: TelemetrySettings, memory_in_bytes: u64, virtual_memory_in_bytes: u64, + cx: &AppContext, ) { let event = ClickhouseEvent::Memory { memory_in_bytes, @@ -366,28 +352,28 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings, false) + self.report_clickhouse_event(event, false, cx) } pub fn report_app_event( self: &Arc, - telemetry_settings: TelemetrySettings, operation: &'static str, immediate_flush: bool, + cx: &AppContext, ) { let event = ClickhouseEvent::App { operation, milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings, immediate_flush) + self.report_clickhouse_event(event, immediate_flush, cx) } pub fn report_setting_event( self: &Arc, - telemetry_settings: TelemetrySettings, setting: &'static str, value: String, + cx: &AppContext, ) { let event = ClickhouseEvent::Setting { setting, @@ -395,7 +381,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings, false) + self.report_clickhouse_event(event, false, cx) } fn milliseconds_since_first_event(&self) -> i64 { @@ -415,10 +401,10 @@ impl Telemetry { fn report_clickhouse_event( self: &Arc, event: ClickhouseEvent, - telemetry_settings: TelemetrySettings, immediate_flush: bool, + cx: &AppContext, ) { - if !telemetry_settings.metrics { + if !TelemetrySettings::get_global(cx).metrics { return; } diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 07a4269567..6f06e9f10f 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -1,1889 +1,1884 @@ -//todo(partially ported) -// use std::{ -// path::Path, -// sync::{ -// atomic::{self, AtomicBool, AtomicUsize}, -// Arc, -// }, -// }; - -// use call::ActiveCall; -// use editor::{ -// test::editor_test_context::{AssertionContextManager, EditorTestContext}, -// Anchor, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, -// ToggleCodeActions, Undo, -// }; -// use gpui::{BackgroundExecutor, TestAppContext, VisualContext, VisualTestContext}; -// use indoc::indoc; -// use language::{ -// language_settings::{AllLanguageSettings, InlayHintSettings}, -// tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, -// }; -// use rpc::RECEIVE_TIMEOUT; -// use serde_json::json; -// use settings::SettingsStore; -// use text::Point; -// use workspace::Workspace; - -// use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; - -// #[gpui::test(iterations = 10)] -// async fn test_host_disconnect( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// cx_c: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(executor).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// let client_c = server.create_client(cx_c, "user_c").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) -// .await; - -// cx_b.update(editor::init); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// serde_json::json!({ -// "a.txt": "a-contents", -// "b.txt": "b-contents", -// }), -// ) -// .await; - -// let active_call_a = cx_a.read(ActiveCall::global); -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - -// let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees().next().unwrap()); -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); - -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// executor.run_until_parked(); - -// assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); - -// let workspace_b = -// cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); -// let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b); - -// let editor_b = workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "b.txt"), None, true, cx) -// }) -// .unwrap() -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// //TODO: focus -// assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx))); -// editor_b.update(cx_b, |editor, cx| editor.insert("X", cx)); -// //todo(is_edited) -// // assert!(workspace_b.is_edited(cx_b)); - -// // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. -// server.forbid_connections(); -// server.disconnect_client(client_a.peer_id().unwrap()); -// executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - -// project_a.read_with(cx_a, |project, _| project.collaborators().is_empty()); - -// project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); - -// project_b.read_with(cx_b, |project, _| project.is_read_only()); - -// assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); - -// // Ensure client B's edited state is reset and that the whole window is blurred. - -// workspace_b.update(cx_b, |_, cx| { -// assert_eq!(cx.focused_view_id(), None); -// }); -// // assert!(!workspace_b.is_edited(cx_b)); - -// // Ensure client B is not prompted to save edits when closing window after disconnecting. -// let can_close = workspace_b -// .update(cx_b, |workspace, cx| workspace.prepare_to_close(true, cx)) -// .await -// .unwrap(); -// assert!(can_close); - -// // Allow client A to reconnect to the server. -// server.allow_connections(); -// executor.advance_clock(RECEIVE_TIMEOUT); - -// // Client B calls client A again after they reconnected. -// let active_call_b = cx_b.read(ActiveCall::global); -// active_call_b -// .update(cx_b, |call, cx| { -// call.invite(client_a.user_id().unwrap(), None, cx) -// }) -// .await -// .unwrap(); -// executor.run_until_parked(); -// active_call_a -// .update(cx_a, |call, cx| call.accept_incoming(cx)) -// .await -// .unwrap(); - -// active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); - -// // Drop client A's connection again. We should still unshare it successfully. -// server.forbid_connections(); -// server.disconnect_client(client_a.peer_id().unwrap()); -// executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - -// project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); -// } - -// #[gpui::test] -// async fn test_newline_above_or_below_does_not_move_guest_cursor( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); - -// client_a -// .fs() -// .insert_tree("/dir", json!({ "a.txt": "Some text\n" })) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); - -// let project_b = client_b.build_remote_project(project_id, cx_b).await; - -// // Open a buffer as client A -// let buffer_a = project_a -// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) -// .await -// .unwrap(); -// let window_a = cx_a.add_empty_window(); -// let editor_a = -// window_a.build_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx)); -// let mut editor_cx_a = EditorTestContext { -// cx: cx_a, -// window: window_a.into(), -// editor: editor_a, -// assertion_cx: AssertionContextManager::new(), -// }; - -// // Open a buffer as client B -// let buffer_b = project_b -// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) -// .await -// .unwrap(); -// let window_b = cx_b.add_empty_window(); -// let editor_b = -// window_b.build_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx)); -// let mut editor_cx_b = EditorTestContext { -// cx: cx_b, -// window: window_b.into(), -// editor: editor_b, -// assertion_cx: AssertionContextManager::new(), -// }; - -// // Test newline above -// editor_cx_a.set_selections_state(indoc! {" -// Some textˇ -// "}); -// editor_cx_b.set_selections_state(indoc! {" -// Some textˇ -// "}); -// editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx)); -// executor.run_until_parked(); -// editor_cx_a.assert_editor_state(indoc! {" -// ˇ -// Some text -// "}); -// editor_cx_b.assert_editor_state(indoc! {" - -// Some textˇ -// "}); - -// // Test newline below -// editor_cx_a.set_selections_state(indoc! {" - -// Some textˇ -// "}); -// editor_cx_b.set_selections_state(indoc! {" - -// Some textˇ -// "}); -// editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx)); -// executor.run_until_parked(); -// editor_cx_a.assert_editor_state(indoc! {" - -// Some text -// ˇ -// "}); -// editor_cx_b.assert_editor_state(indoc! {" - -// Some textˇ - -// "}); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_collaborating_with_completion( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); - -// // Set up a fake language server. -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// completion_provider: Some(lsp::CompletionOptions { -// trigger_characters: Some(vec![".".to_string()]), -// resolve_provider: Some(true), -// ..Default::default() -// }), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// client_a.language_registry().add(Arc::new(language)); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { a }", -// "other.rs": "", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); -// let project_b = client_b.build_remote_project(project_id, cx_b).await; - -// // Open a file in an editor as the guest. -// let buffer_b = project_b -// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); -// let window_b = cx_b.add_empty_window(); -// let editor_b = window_b.build_view(cx_b, |cx| { -// Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx) -// }); - -// let fake_language_server = fake_language_servers.next().await.unwrap(); -// cx_a.foreground().run_until_parked(); - -// buffer_b.read_with(cx_b, |buffer, _| { -// assert!(!buffer.completion_triggers().is_empty()) -// }); - -// // Type a completion trigger character as the guest. -// editor_b.update(cx_b, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input(".", cx); -// cx.focus(&editor_b); -// }); - -// // Receive a completion request as the host's language server. -// // Return some completions from the host's language server. -// cx_a.foreground().start_waiting(); -// fake_language_server -// .handle_request::(|params, _| async move { -// assert_eq!( -// params.text_document_position.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// assert_eq!( -// params.text_document_position.position, -// lsp::Position::new(0, 14), -// ); - -// Ok(Some(lsp::CompletionResponse::Array(vec![ -// lsp::CompletionItem { -// label: "first_method(…)".into(), -// detail: Some("fn(&mut self, B) -> C".into()), -// text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { -// new_text: "first_method($1)".to_string(), -// range: lsp::Range::new( -// lsp::Position::new(0, 14), -// lsp::Position::new(0, 14), -// ), -// })), -// insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), -// ..Default::default() -// }, -// lsp::CompletionItem { -// label: "second_method(…)".into(), -// detail: Some("fn(&mut self, C) -> D".into()), -// text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { -// new_text: "second_method()".to_string(), -// range: lsp::Range::new( -// lsp::Position::new(0, 14), -// lsp::Position::new(0, 14), -// ), -// })), -// insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), -// ..Default::default() -// }, -// ]))) -// }) -// .next() -// .await -// .unwrap(); -// cx_a.foreground().finish_waiting(); - -// // Open the buffer on the host. -// let buffer_a = project_a -// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); -// cx_a.foreground().run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a. }") -// }); - -// // Confirm a completion on the guest. - -// editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible())); -// editor_b.update(cx_b, |editor, cx| { -// editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx); -// assert_eq!(editor.text(cx), "fn main() { a.first_method() }"); -// }); - -// // Return a resolved completion from the host's language server. -// // The resolved completion has an additional text edit. -// fake_language_server.handle_request::( -// |params, _| async move { -// assert_eq!(params.label, "first_method(…)"); -// Ok(lsp::CompletionItem { -// label: "first_method(…)".into(), -// detail: Some("fn(&mut self, B) -> C".into()), -// text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { -// new_text: "first_method($1)".to_string(), -// range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), -// })), -// additional_text_edits: Some(vec![lsp::TextEdit { -// new_text: "use d::SomeTrait;\n".to_string(), -// range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), -// }]), -// insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), -// ..Default::default() -// }) -// }, -// ); - -// // The additional edit is applied. -// cx_a.executor().run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// assert_eq!( -// buffer.text(), -// "use d::SomeTrait;\nfn main() { a.first_method() }" -// ); -// }); - -// buffer_b.read_with(cx_b, |buffer, _| { -// assert_eq!( -// buffer.text(), -// "use d::SomeTrait;\nfn main() { a.first_method() }" -// ); -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_collaborating_with_code_actions( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// // -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); - -// cx_b.update(editor::init); - -// // Set up a fake language server. -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; -// client_a.language_registry().add(Arc::new(language)); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "main.rs": "mod other;\nfn main() { let foo = other::foo(); }", -// "other.rs": "pub fn foo() -> usize { 4 }", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); - -// // Join the project as client B. -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// let window_b = -// cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); -// let workspace_b = window_b.root(cx_b); -// let editor_b = workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// let mut fake_language_server = fake_language_servers.next().await.unwrap(); -// let mut requests = fake_language_server -// .handle_request::(|params, _| async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// assert_eq!(params.range.start, lsp::Position::new(0, 0)); -// assert_eq!(params.range.end, lsp::Position::new(0, 0)); -// Ok(None) -// }); -// executor.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2); -// requests.next().await; - -// // Move cursor to a location that contains code actions. -// editor_b.update(cx_b, |editor, cx| { -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(1, 31)..Point::new(1, 31)]) -// }); -// cx.focus(&editor_b); -// }); - -// let mut requests = fake_language_server -// .handle_request::(|params, _| async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// assert_eq!(params.range.start, lsp::Position::new(1, 31)); -// assert_eq!(params.range.end, lsp::Position::new(1, 31)); - -// Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction( -// lsp::CodeAction { -// title: "Inline into all callers".to_string(), -// edit: Some(lsp::WorkspaceEdit { -// changes: Some( -// [ -// ( -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// vec![lsp::TextEdit::new( -// lsp::Range::new( -// lsp::Position::new(1, 22), -// lsp::Position::new(1, 34), -// ), -// "4".to_string(), -// )], -// ), -// ( -// lsp::Url::from_file_path("/a/other.rs").unwrap(), -// vec![lsp::TextEdit::new( -// lsp::Range::new( -// lsp::Position::new(0, 0), -// lsp::Position::new(0, 27), -// ), -// "".to_string(), -// )], -// ), -// ] -// .into_iter() -// .collect(), -// ), -// ..Default::default() -// }), -// data: Some(json!({ -// "codeActionParams": { -// "range": { -// "start": {"line": 1, "column": 31}, -// "end": {"line": 1, "column": 31}, -// } -// } -// })), -// ..Default::default() -// }, -// )])) -// }); -// executor.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2); -// requests.next().await; - -// // Toggle code actions and wait for them to display. -// editor_b.update(cx_b, |editor, cx| { -// editor.toggle_code_actions( -// &ToggleCodeActions { -// deployed_from_indicator: false, -// }, -// cx, -// ); -// }); -// cx_a.foreground().run_until_parked(); - -// editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible())); - -// fake_language_server.remove_request_handler::(); - -// // Confirming the code action will trigger a resolve request. -// let confirm_action = workspace_b -// .update(cx_b, |workspace, cx| { -// Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx) -// }) -// .unwrap(); -// fake_language_server.handle_request::( -// |_, _| async move { -// Ok(lsp::CodeAction { -// title: "Inline into all callers".to_string(), -// edit: Some(lsp::WorkspaceEdit { -// changes: Some( -// [ -// ( -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// vec![lsp::TextEdit::new( -// lsp::Range::new( -// lsp::Position::new(1, 22), -// lsp::Position::new(1, 34), -// ), -// "4".to_string(), -// )], -// ), -// ( -// lsp::Url::from_file_path("/a/other.rs").unwrap(), -// vec![lsp::TextEdit::new( -// lsp::Range::new( -// lsp::Position::new(0, 0), -// lsp::Position::new(0, 27), -// ), -// "".to_string(), -// )], -// ), -// ] -// .into_iter() -// .collect(), -// ), -// ..Default::default() -// }), -// ..Default::default() -// }) -// }, -// ); - -// // After the action is confirmed, an editor containing both modified files is opened. -// confirm_action.await.unwrap(); - -// let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); -// code_action_editor.update(cx_b, |editor, cx| { -// assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n"); -// editor.undo(&Undo, cx); -// assert_eq!( -// editor.text(cx), -// "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }" -// ); -// editor.redo(&Redo, cx); -// assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n"); -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_collaborating_with_renames( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); - -// cx_b.update(editor::init); - -// // Set up a fake language server. -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions { -// prepare_provider: Some(true), -// work_done_progress_options: Default::default(), -// })), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// client_a.language_registry().add(Arc::new(language)); - -// client_a -// .fs() -// .insert_tree( -// "/dir", -// json!({ -// "one.rs": "const ONE: usize = 1;", -// "two.rs": "const TWO: usize = one::ONE + one::ONE;" -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); -// let project_b = client_b.build_remote_project(project_id, cx_b).await; - -// let window_b = -// cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); -// let workspace_b = window_b.root(cx_b); -// let editor_b = workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "one.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// let fake_language_server = fake_language_servers.next().await.unwrap(); - -// // Move cursor to a location that can be renamed. -// let prepare_rename = editor_b.update(cx_b, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([7..7])); -// editor.rename(&Rename, cx).unwrap() -// }); - -// fake_language_server -// .handle_request::(|params, _| async move { -// assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); -// assert_eq!(params.position, lsp::Position::new(0, 7)); -// Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( -// lsp::Position::new(0, 6), -// lsp::Position::new(0, 9), -// )))) -// }) -// .next() -// .await -// .unwrap(); -// prepare_rename.await.unwrap(); -// editor_b.update(cx_b, |editor, cx| { -// use editor::ToOffset; -// let rename = editor.pending_rename().unwrap(); -// let buffer = editor.buffer().read(cx).snapshot(cx); -// assert_eq!( -// rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer), -// 6..9 -// ); -// rename.editor.update(cx, |rename_editor, cx| { -// rename_editor.buffer().update(cx, |rename_buffer, cx| { -// rename_buffer.edit([(0..3, "THREE")], None, cx); -// }); -// }); -// }); - -// let confirm_rename = workspace_b.update(cx_b, |workspace, cx| { -// Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap() -// }); -// fake_language_server -// .handle_request::(|params, _| async move { -// assert_eq!( -// params.text_document_position.text_document.uri.as_str(), -// "file:///dir/one.rs" -// ); -// assert_eq!( -// params.text_document_position.position, -// lsp::Position::new(0, 6) -// ); -// assert_eq!(params.new_name, "THREE"); -// Ok(Some(lsp::WorkspaceEdit { -// changes: Some( -// [ -// ( -// lsp::Url::from_file_path("/dir/one.rs").unwrap(), -// vec![lsp::TextEdit::new( -// lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), -// "THREE".to_string(), -// )], -// ), -// ( -// lsp::Url::from_file_path("/dir/two.rs").unwrap(), -// vec![ -// lsp::TextEdit::new( -// lsp::Range::new( -// lsp::Position::new(0, 24), -// lsp::Position::new(0, 27), -// ), -// "THREE".to_string(), -// ), -// lsp::TextEdit::new( -// lsp::Range::new( -// lsp::Position::new(0, 35), -// lsp::Position::new(0, 38), -// ), -// "THREE".to_string(), -// ), -// ], -// ), -// ] -// .into_iter() -// .collect(), -// ), -// ..Default::default() -// })) -// }) -// .next() -// .await -// .unwrap(); -// confirm_rename.await.unwrap(); - -// let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); -// rename_editor.update(cx_b, |editor, cx| { -// assert_eq!( -// editor.text(cx), -// "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;" -// ); -// editor.undo(&Undo, cx); -// assert_eq!( -// editor.text(cx), -// "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;" -// ); -// editor.redo(&Redo, cx); -// assert_eq!( -// editor.text(cx), -// "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;" -// ); -// }); - -// // Ensure temporary rename edits cannot be undone/redone. -// editor_b.update(cx_b, |editor, cx| { -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "const ONE: usize = 1;"); -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "const ONE: usize = 1;"); -// editor.redo(&Redo, cx); -// assert_eq!(editor.text(cx), "const THREE: usize = 1;"); -// }) -// } - -// #[gpui::test(iterations = 10)] -// async fn test_language_server_statuses( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); - -// cx_b.update(editor::init); - -// // Set up a fake language server. -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// name: "the-language-server", -// ..Default::default() -// })) -// .await; -// client_a.language_registry().add(Arc::new(language)); - -// client_a -// .fs() -// .insert_tree( -// "/dir", -// json!({ -// "main.rs": "const ONE: usize = 1;", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - -// let _buffer_a = project_a -// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); - -// let fake_language_server = fake_language_servers.next().await.unwrap(); -// fake_language_server.start_progress("the-token").await; -// fake_language_server.notify::(lsp::ProgressParams { -// token: lsp::NumberOrString::String("the-token".to_string()), -// value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report( -// lsp::WorkDoneProgressReport { -// message: Some("the-message".to_string()), -// ..Default::default() -// }, -// )), -// }); -// executor.run_until_parked(); - -// project_a.read_with(cx_a, |project, _| { -// let status = project.language_server_statuses().next().unwrap(); -// assert_eq!(status.name, "the-language-server"); -// assert_eq!(status.pending_work.len(), 1); -// assert_eq!( -// status.pending_work["the-token"].message.as_ref().unwrap(), -// "the-message" -// ); -// }); - -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); -// executor.run_until_parked(); -// let project_b = client_b.build_remote_project(project_id, cx_b).await; - -// project_b.read_with(cx_b, |project, _| { -// let status = project.language_server_statuses().next().unwrap(); -// assert_eq!(status.name, "the-language-server"); -// }); - -// fake_language_server.notify::(lsp::ProgressParams { -// token: lsp::NumberOrString::String("the-token".to_string()), -// value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report( -// lsp::WorkDoneProgressReport { -// message: Some("the-message-2".to_string()), -// ..Default::default() -// }, -// )), -// }); -// executor.run_until_parked(); - -// project_a.read_with(cx_a, |project, _| { -// let status = project.language_server_statuses().next().unwrap(); -// assert_eq!(status.name, "the-language-server"); -// assert_eq!(status.pending_work.len(), 1); -// assert_eq!( -// status.pending_work["the-token"].message.as_ref().unwrap(), -// "the-message-2" -// ); -// }); - -// project_b.read_with(cx_b, |project, _| { -// let status = project.language_server_statuses().next().unwrap(); -// assert_eq!(status.name, "the-language-server"); -// assert_eq!(status.pending_work.len(), 1); -// assert_eq!( -// status.pending_work["the-token"].message.as_ref().unwrap(), -// "the-message-2" -// ); -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_share_project( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// cx_c: &mut TestAppContext, -// ) { -// let window_b = cx_b.add_empty_window(); -// let mut server = TestServer::start(executor).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// let client_c = server.create_client(cx_c, "user_c").await; -// server -// .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); -// let active_call_b = cx_b.read(ActiveCall::global); -// let active_call_c = cx_c.read(ActiveCall::global); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// ".gitignore": "ignored-dir", -// "a.txt": "a-contents", -// "b.txt": "b-contents", -// "ignored-dir": { -// "c.txt": "", -// "d.txt": "", -// } -// }), -// ) -// .await; - -// // Invite client B to collaborate on a project -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// active_call_a -// .update(cx_a, |call, cx| { -// call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx) -// }) -// .await -// .unwrap(); - -// // Join that project as client B - -// let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); -// executor.run_until_parked(); -// let call = incoming_call_b.borrow().clone().unwrap(); -// assert_eq!(call.calling_user.github_login, "user_a"); -// let initial_project = call.initial_project.unwrap(); -// active_call_b -// .update(cx_b, |call, cx| call.accept_incoming(cx)) -// .await -// .unwrap(); -// let client_b_peer_id = client_b.peer_id().unwrap(); -// let project_b = client_b -// .build_remote_project(initial_project.id, cx_b) -// .await; - -// let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id()); - -// executor.run_until_parked(); - -// project_a.read_with(cx_a, |project, _| { -// let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap(); -// assert_eq!(client_b_collaborator.replica_id, replica_id_b); -// }); - -// project_b.read_with(cx_b, |project, cx| { -// let worktree = project.worktrees().next().unwrap().read(cx); -// assert_eq!( -// worktree.paths().map(AsRef::as_ref).collect::>(), -// [ -// Path::new(".gitignore"), -// Path::new("a.txt"), -// Path::new("b.txt"), -// Path::new("ignored-dir"), -// ] -// ); -// }); - -// project_b -// .update(cx_b, |project, cx| { -// let worktree = project.worktrees().next().unwrap(); -// let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap(); -// project.expand_entry(worktree_id, entry.id, cx).unwrap() -// }) -// .await -// .unwrap(); - -// project_b.read_with(cx_b, |project, cx| { -// let worktree = project.worktrees().next().unwrap().read(cx); -// assert_eq!( -// worktree.paths().map(AsRef::as_ref).collect::>(), -// [ -// Path::new(".gitignore"), -// Path::new("a.txt"), -// Path::new("b.txt"), -// Path::new("ignored-dir"), -// Path::new("ignored-dir/c.txt"), -// Path::new("ignored-dir/d.txt"), -// ] -// ); -// }); - -// // Open the same file as client B and client A. -// let buffer_b = project_b -// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx)) -// .await -// .unwrap(); - -// buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents")); - -// project_a.read_with(cx_a, |project, cx| { -// assert!(project.has_open_buffer((worktree_id, "b.txt"), cx)) -// }); -// let buffer_a = project_a -// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx)) -// .await -// .unwrap(); - -// let editor_b = window_b.build_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx)); - -// // Client A sees client B's selection -// executor.run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// buffer -// .snapshot() -// .remote_selections_in_range(Anchor::MIN..Anchor::MAX) -// .count() -// == 1 -// }); - -// // Edit the buffer as client B and see that edit as client A. -// editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx)); -// executor.run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// assert_eq!(buffer.text(), "ok, b-contents") -// }); - -// // Client B can invite client C on a project shared by client A. -// active_call_b -// .update(cx_b, |call, cx| { -// call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx) -// }) -// .await -// .unwrap(); - -// let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming()); -// executor.run_until_parked(); -// let call = incoming_call_c.borrow().clone().unwrap(); -// assert_eq!(call.calling_user.github_login, "user_b"); -// let initial_project = call.initial_project.unwrap(); -// active_call_c -// .update(cx_c, |call, cx| call.accept_incoming(cx)) -// .await -// .unwrap(); -// let _project_c = client_c -// .build_remote_project(initial_project.id, cx_c) -// .await; - -// // Client B closes the editor, and client A sees client B's selections removed. -// cx_b.update(move |_| drop(editor_b)); -// executor.run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// buffer -// .snapshot() -// .remote_selections_in_range(Anchor::MIN..Anchor::MAX) -// .count() -// == 0 -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_on_input_format_from_host_to_guest( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); - -// // Set up a fake language server. -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { -// first_trigger_character: ":".to_string(), -// more_trigger_character: Some(vec![">".to_string()]), -// }), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// client_a.language_registry().add(Arc::new(language)); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { a }", -// "other.rs": "// Test file", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); -// let project_b = client_b.build_remote_project(project_id, cx_b).await; - -// // Open a file in an editor as the host. -// let buffer_a = project_a -// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); -// let window_a = cx_a.add_empty_window(); -// let editor_a = window_a -// .update(cx_a, |_, cx| { -// cx.build_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx)) -// }) -// .unwrap(); - -// let fake_language_server = fake_language_servers.next().await.unwrap(); -// executor.run_until_parked(); - -// // Receive an OnTypeFormatting request as the host's language server. -// // Return some formattings from the host's language server. -// fake_language_server.handle_request::( -// |params, _| async move { -// assert_eq!( -// params.text_document_position.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// assert_eq!( -// params.text_document_position.position, -// lsp::Position::new(0, 14), -// ); - -// Ok(Some(vec![lsp::TextEdit { -// new_text: "~<".to_string(), -// range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), -// }])) -// }, -// ); - -// // Open the buffer on the guest and see that the formattings worked -// let buffer_b = project_b -// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); - -// // Type a on type formatting trigger character as the guest. -// editor_a.update(cx_a, |editor, cx| { -// cx.focus(&editor_a); -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input(">", cx); -// }); - -// executor.run_until_parked(); - -// buffer_b.read_with(cx_b, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a>~< }") -// }); - -// // Undo should remove LSP edits first -// editor_a.update(cx_a, |editor, cx| { -// assert_eq!(editor.text(cx), "fn main() { a>~< }"); -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "fn main() { a> }"); -// }); -// executor.run_until_parked(); - -// buffer_b.read_with(cx_b, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a> }") -// }); - -// editor_a.update(cx_a, |editor, cx| { -// assert_eq!(editor.text(cx), "fn main() { a> }"); -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "fn main() { a }"); -// }); -// executor.run_until_parked(); - -// buffer_b.read_with(cx_b, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a }") -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_on_input_format_from_guest_to_host( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); - -// // Set up a fake language server. -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { -// first_trigger_character: ":".to_string(), -// more_trigger_character: Some(vec![">".to_string()]), -// }), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// client_a.language_registry().add(Arc::new(language)); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { a }", -// "other.rs": "// Test file", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); -// let project_b = client_b.build_remote_project(project_id, cx_b).await; - -// // Open a file in an editor as the guest. -// let buffer_b = project_b -// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); -// let window_b = cx_b.add_empty_window(); -// let editor_b = window_b.build_view(cx_b, |cx| { -// Editor::for_buffer(buffer_b, Some(project_b.clone()), cx) -// }); - -// let fake_language_server = fake_language_servers.next().await.unwrap(); -// executor.run_until_parked(); -// // Type a on type formatting trigger character as the guest. -// editor_b.update(cx_b, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input(":", cx); -// cx.focus(&editor_b); -// }); - -// // Receive an OnTypeFormatting request as the host's language server. -// // Return some formattings from the host's language server. -// cx_a.foreground().start_waiting(); -// fake_language_server -// .handle_request::(|params, _| async move { -// assert_eq!( -// params.text_document_position.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// assert_eq!( -// params.text_document_position.position, -// lsp::Position::new(0, 14), -// ); - -// Ok(Some(vec![lsp::TextEdit { -// new_text: "~:".to_string(), -// range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), -// }])) -// }) -// .next() -// .await -// .unwrap(); -// cx_a.foreground().finish_waiting(); - -// // Open the buffer on the host and see that the formattings worked -// let buffer_a = project_a -// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); -// executor.run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a:~: }") -// }); - -// // Undo should remove LSP edits first -// editor_b.update(cx_b, |editor, cx| { -// assert_eq!(editor.text(cx), "fn main() { a:~: }"); -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "fn main() { a: }"); -// }); -// executor.run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a: }") -// }); - -// editor_b.update(cx_b, |editor, cx| { -// assert_eq!(editor.text(cx), "fn main() { a: }"); -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "fn main() { a }"); -// }); -// executor.run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a }") -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_mutual_editor_inlay_hint_cache_update( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); -// let active_call_b = cx_b.read(ActiveCall::global); - -// cx_a.update(editor::init); -// cx_b.update(editor::init); - -// cx_a.update(|cx| { -// cx.update_global(|store: &mut SettingsStore, cx| { -// store.update_user_settings::(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: false, -// show_other_hints: true, -// }) -// }); -// }); -// }); -// cx_b.update(|cx| { -// cx.update_global(|store: &mut SettingsStore, cx| { -// store.update_user_settings::(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: false, -// show_other_hints: true, -// }) -// }); -// }); -// }); - -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// let language = Arc::new(language); -// client_a.language_registry().add(Arc::clone(&language)); -// client_b.language_registry().add(language); - -// // Client A opens a project. -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", -// "other.rs": "// Test file", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// active_call_a -// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) -// .await -// .unwrap(); -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); - -// // Client B joins the project -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// active_call_b -// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) -// .await -// .unwrap(); - -// let workspace_a = client_a.build_workspace(&project_a, cx_a).root_view(cx_a); -// cx_a.foreground().start_waiting(); - -// // The host opens a rust file. -// let _buffer_a = project_a -// .update(cx_a, |project, cx| { -// project.open_local_buffer("/a/main.rs", cx) -// }) -// .await -// .unwrap(); -// let fake_language_server = fake_language_servers.next().await.unwrap(); -// let editor_a = workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// // Set up the language server to return an additional inlay hint on each request. -// let edits_made = Arc::new(AtomicUsize::new(0)); -// let closure_edits_made = Arc::clone(&edits_made); -// fake_language_server -// .handle_request::(move |params, _| { -// let task_edits_made = Arc::clone(&closure_edits_made); -// async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// let edits_made = task_edits_made.load(atomic::Ordering::Acquire); -// Ok(Some(vec![lsp::InlayHint { -// position: lsp::Position::new(0, edits_made as u32), -// label: lsp::InlayHintLabel::String(edits_made.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }])) -// } -// }) -// .next() -// .await -// .unwrap(); - -// executor.run_until_parked(); - -// let initial_edit = edits_made.load(atomic::Ordering::Acquire); -// editor_a.update(cx_a, |editor, _| { -// assert_eq!( -// vec![initial_edit.to_string()], -// extract_hint_labels(editor), -// "Host should get its first hints when opens an editor" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 1, -// "Host editor update the cache version after every cache/view change", -// ); -// }); -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// let editor_b = workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// executor.run_until_parked(); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec![initial_edit.to_string()], -// extract_hint_labels(editor), -// "Client should get its first hints when opens an editor" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 1, -// "Guest editor update the cache version after every cache/view change" -// ); -// }); - -// let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; -// editor_b.update(cx_b, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone())); -// editor.handle_input(":", cx); -// cx.focus(&editor_b); -// }); - -// executor.run_until_parked(); -// editor_a.update(cx_a, |editor, _| { -// assert_eq!( -// vec![after_client_edit.to_string()], -// extract_hint_labels(editor), -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!(inlay_cache.version(), 2); -// }); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec![after_client_edit.to_string()], -// extract_hint_labels(editor), -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!(inlay_cache.version(), 2); -// }); - -// let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; -// editor_a.update(cx_a, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input("a change to increment both buffers' versions", cx); -// cx.focus(&editor_a); -// }); - -// executor.run_until_parked(); -// editor_a.update(cx_a, |editor, _| { -// assert_eq!( -// vec![after_host_edit.to_string()], -// extract_hint_labels(editor), -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!(inlay_cache.version(), 3); -// }); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec![after_host_edit.to_string()], -// extract_hint_labels(editor), -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!(inlay_cache.version(), 3); -// }); - -// let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; -// fake_language_server -// .request::(()) -// .await -// .expect("inlay refresh request failed"); - -// executor.run_until_parked(); -// editor_a.update(cx_a, |editor, _| { -// assert_eq!( -// vec![after_special_edit_for_refresh.to_string()], -// extract_hint_labels(editor), -// "Host should react to /refresh LSP request" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 4, -// "Host should accepted all edits and bump its cache version every time" -// ); -// }); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec![after_special_edit_for_refresh.to_string()], -// extract_hint_labels(editor), -// "Guest should get a /refresh LSP request propagated by host" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 4, -// "Guest should accepted all edits and bump its cache version every time" -// ); -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_inlay_hint_refresh_is_forwarded( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); -// let active_call_b = cx_b.read(ActiveCall::global); - -// cx_a.update(editor::init); -// cx_b.update(editor::init); - -// cx_a.update(|cx| { -// cx.update_global(|store: &mut SettingsStore, cx| { -// store.update_user_settings::(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: false, -// show_type_hints: false, -// show_parameter_hints: false, -// show_other_hints: false, -// }) -// }); -// }); -// }); -// cx_b.update(|cx| { -// cx.update_global(|store: &mut SettingsStore, cx| { -// store.update_user_settings::(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); -// }); -// }); - -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// let language = Arc::new(language); -// client_a.language_registry().add(Arc::clone(&language)); -// client_b.language_registry().add(language); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", -// "other.rs": "// Test file", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// active_call_a -// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) -// .await -// .unwrap(); -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); - -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// active_call_b -// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) -// .await -// .unwrap(); - -// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// cx_a.foreground().start_waiting(); -// cx_b.foreground().start_waiting(); - -// let editor_a = workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// let editor_b = workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// let other_hints = Arc::new(AtomicBool::new(false)); -// let fake_language_server = fake_language_servers.next().await.unwrap(); -// let closure_other_hints = Arc::clone(&other_hints); -// fake_language_server -// .handle_request::(move |params, _| { -// let task_other_hints = Arc::clone(&closure_other_hints); -// async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// let other_hints = task_other_hints.load(atomic::Ordering::Acquire); -// let character = if other_hints { 0 } else { 2 }; -// let label = if other_hints { -// "other hint" -// } else { -// "initial hint" -// }; -// Ok(Some(vec![lsp::InlayHint { -// position: lsp::Position::new(0, character), -// label: lsp::InlayHintLabel::String(label.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }])) -// } -// }) -// .next() -// .await -// .unwrap(); -// cx_a.foreground().finish_waiting(); -// cx_b.foreground().finish_waiting(); - -// executor.run_until_parked(); -// editor_a.update(cx_a, |editor, _| { -// assert!( -// extract_hint_labels(editor).is_empty(), -// "Host should get no hints due to them turned off" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 0, -// "Turned off hints should not generate version updates" -// ); -// }); - -// executor.run_until_parked(); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec!["initial hint".to_string()], -// extract_hint_labels(editor), -// "Client should get its first hints when opens an editor" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 1, -// "Should update cache verison after first hints" -// ); -// }); - -// other_hints.fetch_or(true, atomic::Ordering::Release); -// fake_language_server -// .request::(()) -// .await -// .expect("inlay refresh request failed"); -// executor.run_until_parked(); -// editor_a.update(cx_a, |editor, _| { -// assert!( -// extract_hint_labels(editor).is_empty(), -// "Host should get nop hints due to them turned off, even after the /refresh" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 0, -// "Turned off hints should not generate version updates, again" -// ); -// }); - -// executor.run_until_parked(); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec!["other hint".to_string()], -// extract_hint_labels(editor), -// "Guest should get a /refresh LSP request propagated by host despite host hints are off" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 2, -// "Guest should accepted all edits and bump its cache version every time" -// ); -// }); -// } - -// fn extract_hint_labels(editor: &Editor) -> Vec { -// let mut labels = Vec::new(); -// for hint in editor.inlay_hint_cache().hints() { -// match hint.label { -// project::InlayHintLabel::String(s) => labels.push(s), -// _ => unreachable!(), -// } -// } -// labels -// } +use std::{ + path::Path, + sync::{ + atomic::{self, AtomicBool, AtomicUsize}, + Arc, + }, +}; + +use call::ActiveCall; +use editor::{ + test::editor_test_context::{AssertionContextManager, EditorTestContext}, + ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, ToggleCodeActions, + Undo, +}; +use futures::StreamExt; +use gpui::{TestAppContext, VisualContext, VisualTestContext}; +use indoc::indoc; +use language::{ + language_settings::{AllLanguageSettings, InlayHintSettings}, + tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, +}; +use rpc::RECEIVE_TIMEOUT; +use serde_json::json; +use settings::SettingsStore; +use text::Point; +use workspace::Workspace; + +use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; + +#[gpui::test(iterations = 10)] +async fn test_host_disconnect( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + let mut server = TestServer::start(cx_a.executor()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + .await; + + cx_b.update(editor::init); + + client_a + .fs() + .insert_tree( + "/a", + serde_json::json!({ + "a.txt": "a-contents", + "b.txt": "b-contents", + }), + ) + .await; + + let active_call_a = cx_a.read(ActiveCall::global); + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + + let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap()); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + let project_b = client_b.build_remote_project(project_id, cx_b).await; + cx_a.background_executor.run_until_parked(); + + assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); + + let workspace_b = + cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); + let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b); + + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "b.txt"), None, true, cx) + }) + .unwrap() + .await + .unwrap() + .downcast::() + .unwrap(); + + //TODO: focus + assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx))); + editor_b.update(cx_b, |editor, cx| editor.insert("X", cx)); + //todo(is_edited) + // assert!(workspace_b.is_edited(cx_b)); + + // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + cx_a.background_executor + .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + project_a.read_with(cx_a, |project, _| project.collaborators().is_empty()); + + project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); + + project_b.read_with(cx_b, |project, _| project.is_read_only()); + + assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); + + // Ensure client B's edited state is reset and that the whole window is blurred. + + workspace_b + .update(cx_b, |_, cx| { + assert_eq!(cx.focused(), None); + }) + .unwrap(); + // assert!(!workspace_b.is_edited(cx_b)); + + // Ensure client B is not prompted to save edits when closing window after disconnecting. + let can_close = workspace_b + .update(cx_b, |workspace, cx| workspace.prepare_to_close(true, cx)) + .unwrap() + .await + .unwrap(); + assert!(can_close); + + // Allow client A to reconnect to the server. + server.allow_connections(); + cx_a.background_executor.advance_clock(RECEIVE_TIMEOUT); + + // Client B calls client A again after they reconnected. + let active_call_b = cx_b.read(ActiveCall::global); + active_call_b + .update(cx_b, |call, cx| { + call.invite(client_a.user_id().unwrap(), None, cx) + }) + .await + .unwrap(); + cx_a.background_executor.run_until_parked(); + active_call_a + .update(cx_a, |call, cx| call.accept_incoming(cx)) + .await + .unwrap(); + + active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + // Drop client A's connection again. We should still unshare it successfully. + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + cx_a.background_executor + .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); +} + +#[gpui::test] +async fn test_newline_above_or_below_does_not_move_guest_cursor( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(cx_a.executor()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let executor = cx_a.executor(); + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + client_a + .fs() + .insert_tree("/dir", json!({ "a.txt": "Some text\n" })) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + let project_b = client_b.build_remote_project(project_id, cx_b).await; + + // Open a buffer as client A + let buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) + .await + .unwrap(); + let window_a = cx_a.add_empty_window(); + let editor_a = + window_a.build_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx)); + + let mut editor_cx_a = EditorTestContext { + cx: VisualTestContext::from_window(window_a, cx_a), + window: window_a.into(), + editor: editor_a, + assertion_cx: AssertionContextManager::new(), + }; + + let window_b = cx_b.add_empty_window(); + let mut cx_b = VisualTestContext::from_window(window_b, cx_b); + + // Open a buffer as client B + let buffer_b = project_b + .update(&mut cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) + .await + .unwrap(); + let editor_b = window_b.build_view(&mut cx_b, |cx| { + Editor::for_buffer(buffer_b, Some(project_b), cx) + }); + let mut editor_cx_b = EditorTestContext { + cx: cx_b, + window: window_b.into(), + editor: editor_b, + assertion_cx: AssertionContextManager::new(), + }; + + // Test newline above + editor_cx_a.set_selections_state(indoc! {" + Some textˇ + "}); + editor_cx_b.set_selections_state(indoc! {" + Some textˇ + "}); + editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx)); + executor.run_until_parked(); + editor_cx_a.assert_editor_state(indoc! {" + ˇ + Some text + "}); + editor_cx_b.assert_editor_state(indoc! {" + + Some textˇ + "}); + + // Test newline below + editor_cx_a.set_selections_state(indoc! {" + + Some textˇ + "}); + editor_cx_b.set_selections_state(indoc! {" + + Some textˇ + "}); + editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx)); + executor.run_until_parked(); + editor_cx_a.assert_editor_state(indoc! {" + + Some text + ˇ + "}); + editor_cx_b.assert_editor_state(indoc! {" + + Some textˇ + + "}); +} + +#[gpui::test(iterations = 10)] +async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + let mut server = TestServer::start(cx_a.executor()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + // Set up a fake language server. + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + resolve_provider: Some(true), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + })) + .await; + client_a.language_registry().add(Arc::new(language)); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a }", + "other.rs": "", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + + // Open a file in an editor as the guest. + let buffer_b = project_b + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + let window_b = cx_b.add_empty_window(); + let editor_b = window_b.build_view(cx_b, |cx| { + Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx) + }); + + let fake_language_server = fake_language_servers.next().await.unwrap(); + cx_a.background_executor.run_until_parked(); + + buffer_b.read_with(cx_b, |buffer, _| { + assert!(!buffer.completion_triggers().is_empty()) + }); + + let mut cx_b = VisualTestContext::from_window(window_b, cx_b); + + // Type a completion trigger character as the guest. + editor_b.update(&mut cx_b, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input(".", cx); + }); + cx_b.focus_view(&editor_b); + + // Receive a completion request as the host's language server. + // Return some completions from the host's language server. + cx_a.executor().start_waiting(); + fake_language_server + .handle_request::(|params, _| async move { + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 14), + ); + + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + label: "first_method(…)".into(), + detail: Some("fn(&mut self, B) -> C".into()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + new_text: "first_method($1)".to_string(), + range: lsp::Range::new( + lsp::Position::new(0, 14), + lsp::Position::new(0, 14), + ), + })), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + ..Default::default() + }, + lsp::CompletionItem { + label: "second_method(…)".into(), + detail: Some("fn(&mut self, C) -> D".into()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + new_text: "second_method()".to_string(), + range: lsp::Range::new( + lsp::Position::new(0, 14), + lsp::Position::new(0, 14), + ), + })), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + ..Default::default() + }, + ]))) + }) + .next() + .await + .unwrap(); + cx_a.executor().finish_waiting(); + + // Open the buffer on the host. + let buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + cx_a.executor().run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a. }") + }); + + // Confirm a completion on the guest. + + editor_b.update(&mut cx_b, |editor, cx| { + assert!(editor.context_menu_visible()); + editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx); + assert_eq!(editor.text(cx), "fn main() { a.first_method() }"); + }); + + // Return a resolved completion from the host's language server. + // The resolved completion has an additional text edit. + fake_language_server.handle_request::( + |params, _| async move { + assert_eq!(params.label, "first_method(…)"); + Ok(lsp::CompletionItem { + label: "first_method(…)".into(), + detail: Some("fn(&mut self, B) -> C".into()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + new_text: "first_method($1)".to_string(), + range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), + })), + additional_text_edits: Some(vec![lsp::TextEdit { + new_text: "use d::SomeTrait;\n".to_string(), + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), + }]), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + ..Default::default() + }) + }, + ); + + // The additional edit is applied. + cx_a.executor().run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!( + buffer.text(), + "use d::SomeTrait;\nfn main() { a.first_method() }" + ); + }); + + buffer_b.read_with(&mut cx_b, |buffer, _| { + assert_eq!( + buffer.text(), + "use d::SomeTrait;\nfn main() { a.first_method() }" + ); + }); +} + +#[gpui::test(iterations = 10)] +async fn test_collaborating_with_code_actions( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(cx_a.executor()).await; + let client_a = server.create_client(cx_a, "user_a").await; + // + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + cx_b.update(editor::init); + + // Set up a fake language server. + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; + client_a.language_registry().add(Arc::new(language)); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "main.rs": "mod other;\nfn main() { let foo = other::foo(); }", + "other.rs": "pub fn foo() -> usize { 4 }", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + // Join the project as client B. + let project_b = client_b.build_remote_project(project_id, cx_b).await; + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let mut requests = fake_language_server + .handle_request::(|params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!(params.range.start, lsp::Position::new(0, 0)); + assert_eq!(params.range.end, lsp::Position::new(0, 0)); + Ok(None) + }); + cx_a.background_executor + .advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2); + requests.next().await; + + // Move cursor to a location that contains code actions. + editor_b.update(cx_b, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(1, 31)..Point::new(1, 31)]) + }); + }); + cx_b.focus_view(&editor_b); + + let mut requests = fake_language_server + .handle_request::(|params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!(params.range.start, lsp::Position::new(1, 31)); + assert_eq!(params.range.end, lsp::Position::new(1, 31)); + + Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction( + lsp::CodeAction { + title: "Inline into all callers".to_string(), + edit: Some(lsp::WorkspaceEdit { + changes: Some( + [ + ( + lsp::Url::from_file_path("/a/main.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(1, 22), + lsp::Position::new(1, 34), + ), + "4".to_string(), + )], + ), + ( + lsp::Url::from_file_path("/a/other.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 27), + ), + "".to_string(), + )], + ), + ] + .into_iter() + .collect(), + ), + ..Default::default() + }), + data: Some(json!({ + "codeActionParams": { + "range": { + "start": {"line": 1, "column": 31}, + "end": {"line": 1, "column": 31}, + } + } + })), + ..Default::default() + }, + )])) + }); + cx_a.background_executor + .advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2); + requests.next().await; + + // Toggle code actions and wait for them to display. + editor_b.update(cx_b, |editor, cx| { + editor.toggle_code_actions( + &ToggleCodeActions { + deployed_from_indicator: false, + }, + cx, + ); + }); + cx_a.background_executor.run_until_parked(); + + editor_b.update(cx_b, |editor, _| assert!(editor.context_menu_visible())); + + fake_language_server.remove_request_handler::(); + + // Confirming the code action will trigger a resolve request. + let confirm_action = editor_b + .update(cx_b, |editor, cx| { + Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, cx) + }) + .unwrap(); + fake_language_server.handle_request::( + |_, _| async move { + Ok(lsp::CodeAction { + title: "Inline into all callers".to_string(), + edit: Some(lsp::WorkspaceEdit { + changes: Some( + [ + ( + lsp::Url::from_file_path("/a/main.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(1, 22), + lsp::Position::new(1, 34), + ), + "4".to_string(), + )], + ), + ( + lsp::Url::from_file_path("/a/other.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 27), + ), + "".to_string(), + )], + ), + ] + .into_iter() + .collect(), + ), + ..Default::default() + }), + ..Default::default() + }) + }, + ); + + // After the action is confirmed, an editor containing both modified files is opened. + confirm_action.await.unwrap(); + + let code_action_editor = workspace_b.update(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + code_action_editor.update(cx_b, |editor, cx| { + assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n"); + editor.undo(&Undo, cx); + assert_eq!( + editor.text(cx), + "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }" + ); + editor.redo(&Redo, cx); + assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n"); + }); +} + +#[gpui::test(iterations = 10)] +async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + let mut server = TestServer::start(cx_a.executor()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + cx_b.update(editor::init); + + // Set up a fake language server. + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions { + prepare_provider: Some(true), + work_done_progress_options: Default::default(), + })), + ..Default::default() + }, + ..Default::default() + })) + .await; + client_a.language_registry().add(Arc::new(language)); + + client_a + .fs() + .insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;" + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "one.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); + + // Move cursor to a location that can be renamed. + let prepare_rename = editor_b.update(cx_b, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([7..7])); + editor.rename(&Rename, cx).unwrap() + }); + + fake_language_server + .handle_request::(|params, _| async move { + assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); + assert_eq!(params.position, lsp::Position::new(0, 7)); + Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( + lsp::Position::new(0, 6), + lsp::Position::new(0, 9), + )))) + }) + .next() + .await + .unwrap(); + prepare_rename.await.unwrap(); + editor_b.update(cx_b, |editor, cx| { + use editor::ToOffset; + let rename = editor.pending_rename().unwrap(); + let buffer = editor.buffer().read(cx).snapshot(cx); + assert_eq!( + rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer), + 6..9 + ); + rename.editor.update(cx, |rename_editor, cx| { + rename_editor.buffer().update(cx, |rename_buffer, cx| { + rename_buffer.edit([(0..3, "THREE")], None, cx); + }); + }); + }); + + let confirm_rename = editor_b.update(cx_b, |editor, cx| { + Editor::confirm_rename(editor, &ConfirmRename, cx).unwrap() + }); + fake_language_server + .handle_request::(|params, _| async move { + assert_eq!( + params.text_document_position.text_document.uri.as_str(), + "file:///dir/one.rs" + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 6) + ); + assert_eq!(params.new_name, "THREE"); + Ok(Some(lsp::WorkspaceEdit { + changes: Some( + [ + ( + lsp::Url::from_file_path("/dir/one.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), + "THREE".to_string(), + )], + ), + ( + lsp::Url::from_file_path("/dir/two.rs").unwrap(), + vec![ + lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 24), + lsp::Position::new(0, 27), + ), + "THREE".to_string(), + ), + lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 35), + lsp::Position::new(0, 38), + ), + "THREE".to_string(), + ), + ], + ), + ] + .into_iter() + .collect(), + ), + ..Default::default() + })) + }) + .next() + .await + .unwrap(); + confirm_rename.await.unwrap(); + + let rename_editor = workspace_b.update(cx_b, |workspace, cx| { + workspace.active_item_as::(cx).unwrap() + }); + + rename_editor.update(cx_b, |editor, cx| { + assert_eq!( + editor.text(cx), + "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;" + ); + editor.undo(&Undo, cx); + assert_eq!( + editor.text(cx), + "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;" + ); + editor.redo(&Redo, cx); + assert_eq!( + editor.text(cx), + "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;" + ); + }); + + // Ensure temporary rename edits cannot be undone/redone. + editor_b.update(cx_b, |editor, cx| { + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "const ONE: usize = 1;"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "const ONE: usize = 1;"); + editor.redo(&Redo, cx); + assert_eq!(editor.text(cx), "const THREE: usize = 1;"); + }) +} + +#[gpui::test(iterations = 10)] +async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + let mut server = TestServer::start(cx_a.executor()).await; + let executor = cx_a.executor(); + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + cx_b.update(editor::init); + + // Set up a fake language server. + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + name: "the-language-server", + ..Default::default() + })) + .await; + client_a.language_registry().add(Arc::new(language)); + + client_a + .fs() + .insert_tree( + "/dir", + json!({ + "main.rs": "const ONE: usize = 1;", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; + + let _buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + + let fake_language_server = fake_language_servers.next().await.unwrap(); + fake_language_server.start_progress("the-token").await; + fake_language_server.notify::(lsp::ProgressParams { + token: lsp::NumberOrString::String("the-token".to_string()), + value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report( + lsp::WorkDoneProgressReport { + message: Some("the-message".to_string()), + ..Default::default() + }, + )), + }); + executor.run_until_parked(); + + project_a.read_with(cx_a, |project, _| { + let status = project.language_server_statuses().next().unwrap(); + assert_eq!(status.name, "the-language-server"); + assert_eq!(status.pending_work.len(), 1); + assert_eq!( + status.pending_work["the-token"].message.as_ref().unwrap(), + "the-message" + ); + }); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + executor.run_until_parked(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + + project_b.read_with(cx_b, |project, _| { + let status = project.language_server_statuses().next().unwrap(); + assert_eq!(status.name, "the-language-server"); + }); + + fake_language_server.notify::(lsp::ProgressParams { + token: lsp::NumberOrString::String("the-token".to_string()), + value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report( + lsp::WorkDoneProgressReport { + message: Some("the-message-2".to_string()), + ..Default::default() + }, + )), + }); + executor.run_until_parked(); + + project_a.read_with(cx_a, |project, _| { + let status = project.language_server_statuses().next().unwrap(); + assert_eq!(status.name, "the-language-server"); + assert_eq!(status.pending_work.len(), 1); + assert_eq!( + status.pending_work["the-token"].message.as_ref().unwrap(), + "the-message-2" + ); + }); + + project_b.read_with(cx_b, |project, _| { + let status = project.language_server_statuses().next().unwrap(); + assert_eq!(status.name, "the-language-server"); + assert_eq!(status.pending_work.len(), 1); + assert_eq!( + status.pending_work["the-token"].message.as_ref().unwrap(), + "the-message-2" + ); + }); +} + +#[gpui::test(iterations = 10)] +async fn test_share_project( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + let executor = cx_a.executor(); + let window_b = cx_b.add_empty_window(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + server + .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + let active_call_c = cx_c.read(ActiveCall::global); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + ".gitignore": "ignored-dir", + "a.txt": "a-contents", + "b.txt": "b-contents", + "ignored-dir": { + "c.txt": "", + "d.txt": "", + } + }), + ) + .await; + + // Invite client B to collaborate on a project + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| { + call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx) + }) + .await + .unwrap(); + + // Join that project as client B + + let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); + executor.run_until_parked(); + let call = incoming_call_b.borrow().clone().unwrap(); + assert_eq!(call.calling_user.github_login, "user_a"); + let initial_project = call.initial_project.unwrap(); + active_call_b + .update(cx_b, |call, cx| call.accept_incoming(cx)) + .await + .unwrap(); + let client_b_peer_id = client_b.peer_id().unwrap(); + let project_b = client_b + .build_remote_project(initial_project.id, cx_b) + .await; + + let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id()); + + executor.run_until_parked(); + + project_a.read_with(cx_a, |project, _| { + let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap(); + assert_eq!(client_b_collaborator.replica_id, replica_id_b); + }); + + project_b.read_with(cx_b, |project, cx| { + let worktree = project.worktrees().next().unwrap().read(cx); + assert_eq!( + worktree.paths().map(AsRef::as_ref).collect::>(), + [ + Path::new(".gitignore"), + Path::new("a.txt"), + Path::new("b.txt"), + Path::new("ignored-dir"), + ] + ); + }); + + project_b + .update(cx_b, |project, cx| { + let worktree = project.worktrees().next().unwrap(); + let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap(); + project.expand_entry(worktree_id, entry.id, cx).unwrap() + }) + .await + .unwrap(); + + project_b.read_with(cx_b, |project, cx| { + let worktree = project.worktrees().next().unwrap().read(cx); + assert_eq!( + worktree.paths().map(AsRef::as_ref).collect::>(), + [ + Path::new(".gitignore"), + Path::new("a.txt"), + Path::new("b.txt"), + Path::new("ignored-dir"), + Path::new("ignored-dir/c.txt"), + Path::new("ignored-dir/d.txt"), + ] + ); + }); + + // Open the same file as client B and client A. + let buffer_b = project_b + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx)) + .await + .unwrap(); + + buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents")); + + project_a.read_with(cx_a, |project, cx| { + assert!(project.has_open_buffer((worktree_id, "b.txt"), cx)) + }); + let buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx)) + .await + .unwrap(); + + let editor_b = window_b.build_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx)); + + // Client A sees client B's selection + executor.run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + buffer + .snapshot() + .remote_selections_in_range(text::Anchor::MIN..text::Anchor::MAX) + .count() + == 1 + }); + + // Edit the buffer as client B and see that edit as client A. + let mut cx_b = VisualTestContext::from_window(window_b, cx_b); + editor_b.update(&mut cx_b, |editor, cx| editor.handle_input("ok, ", cx)); + executor.run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "ok, b-contents") + }); + + // Client B can invite client C on a project shared by client A. + active_call_b + .update(&mut cx_b, |call, cx| { + call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx) + }) + .await + .unwrap(); + + let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming()); + executor.run_until_parked(); + let call = incoming_call_c.borrow().clone().unwrap(); + assert_eq!(call.calling_user.github_login, "user_b"); + let initial_project = call.initial_project.unwrap(); + active_call_c + .update(cx_c, |call, cx| call.accept_incoming(cx)) + .await + .unwrap(); + let _project_c = client_c + .build_remote_project(initial_project.id, cx_c) + .await; + + // Client B closes the editor, and client A sees client B's selections removed. + cx_b.update(move |_| drop(editor_b)); + executor.run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + buffer + .snapshot() + .remote_selections_in_range(text::Anchor::MIN..text::Anchor::MAX) + .count() + == 0 + }); +} + +#[gpui::test(iterations = 10)] +async fn test_on_input_format_from_host_to_guest( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(cx_a.executor()).await; + let executor = cx_a.executor(); + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + // Set up a fake language server. + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { + first_trigger_character: ":".to_string(), + more_trigger_character: Some(vec![">".to_string()]), + }), + ..Default::default() + }, + ..Default::default() + })) + .await; + client_a.language_registry().add(Arc::new(language)); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a }", + "other.rs": "// Test file", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + + // Open a file in an editor as the host. + let buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + let window_a = cx_a.add_empty_window(); + let editor_a = window_a + .update(cx_a, |_, cx| { + cx.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx)) + }) + .unwrap(); + + let fake_language_server = fake_language_servers.next().await.unwrap(); + executor.run_until_parked(); + + // Receive an OnTypeFormatting request as the host's language server. + // Return some formattings from the host's language server. + fake_language_server.handle_request::( + |params, _| async move { + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 14), + ); + + Ok(Some(vec![lsp::TextEdit { + new_text: "~<".to_string(), + range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), + }])) + }, + ); + + // Open the buffer on the guest and see that the formattings worked + let buffer_b = project_b + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + + let mut cx_a = VisualTestContext::from_window(window_a, cx_a); + // Type a on type formatting trigger character as the guest. + cx_a.focus_view(&editor_a); + editor_a.update(&mut cx_a, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input(">", cx); + }); + + executor.run_until_parked(); + + buffer_b.read_with(cx_b, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a>~< }") + }); + + // Undo should remove LSP edits first + editor_a.update(&mut cx_a, |editor, cx| { + assert_eq!(editor.text(cx), "fn main() { a>~< }"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "fn main() { a> }"); + }); + executor.run_until_parked(); + + buffer_b.read_with(cx_b, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a> }") + }); + + editor_a.update(&mut cx_a, |editor, cx| { + assert_eq!(editor.text(cx), "fn main() { a> }"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "fn main() { a }"); + }); + executor.run_until_parked(); + + buffer_b.read_with(cx_b, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a }") + }); +} + +#[gpui::test(iterations = 10)] +async fn test_on_input_format_from_guest_to_host( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(cx_a.executor()).await; + let executor = cx_a.executor(); + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + // Set up a fake language server. + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { + first_trigger_character: ":".to_string(), + more_trigger_character: Some(vec![">".to_string()]), + }), + ..Default::default() + }, + ..Default::default() + })) + .await; + client_a.language_registry().add(Arc::new(language)); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a }", + "other.rs": "// Test file", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + + // Open a file in an editor as the guest. + let buffer_b = project_b + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + let window_b = cx_b.add_empty_window(); + let editor_b = window_b.build_view(cx_b, |cx| { + Editor::for_buffer(buffer_b, Some(project_b.clone()), cx) + }); + + let fake_language_server = fake_language_servers.next().await.unwrap(); + executor.run_until_parked(); + let mut cx_b = VisualTestContext::from_window(window_b, cx_b); + // Type a on type formatting trigger character as the guest. + cx_b.focus_view(&editor_b); + editor_b.update(&mut cx_b, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input(":", cx); + }); + + // Receive an OnTypeFormatting request as the host's language server. + // Return some formattings from the host's language server. + executor.start_waiting(); + fake_language_server + .handle_request::(|params, _| async move { + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 14), + ); + + Ok(Some(vec![lsp::TextEdit { + new_text: "~:".to_string(), + range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), + }])) + }) + .next() + .await + .unwrap(); + executor.finish_waiting(); + + // Open the buffer on the host and see that the formattings worked + let buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + executor.run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a:~: }") + }); + + // Undo should remove LSP edits first + editor_b.update(&mut cx_b, |editor, cx| { + assert_eq!(editor.text(cx), "fn main() { a:~: }"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "fn main() { a: }"); + }); + executor.run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a: }") + }); + + editor_b.update(&mut cx_b, |editor, cx| { + assert_eq!(editor.text(cx), "fn main() { a: }"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "fn main() { a }"); + }); + executor.run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a }") + }); +} + +#[gpui::test(iterations = 10)] +async fn test_mutual_editor_inlay_hint_cache_update( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(cx_a.executor()).await; + let executor = cx_a.executor(); + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + cx_a.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: false, + show_other_hints: true, + }) + }); + }); + }); + cx_b.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: false, + show_other_hints: true, + }) + }); + }); + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let language = Arc::new(language); + client_a.language_registry().add(Arc::clone(&language)); + client_b.language_registry().add(language); + + // Client A opens a project. + client_a + .fs() + .insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", + "other.rs": "// Test file", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + // Client B joins the project + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + executor.start_waiting(); + + // The host opens a rust file. + let _buffer_a = project_a + .update(cx_a, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); + let editor_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Set up the language server to return an additional inlay hint on each request. + let edits_made = Arc::new(AtomicUsize::new(0)); + let closure_edits_made = Arc::clone(&edits_made); + fake_language_server + .handle_request::(move |params, _| { + let task_edits_made = Arc::clone(&closure_edits_made); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + let edits_made = task_edits_made.load(atomic::Ordering::Acquire); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, edits_made as u32), + label: lsp::InlayHintLabel::String(edits_made.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await + .unwrap(); + + executor.run_until_parked(); + + let initial_edit = edits_made.load(atomic::Ordering::Acquire); + editor_a.update(cx_a, |editor, _| { + assert_eq!( + vec![initial_edit.to_string()], + extract_hint_labels(editor), + "Host should get its first hints when opens an editor" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 1, + "Host editor update the cache version after every cache/view change", + ); + }); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + executor.run_until_parked(); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec![initial_edit.to_string()], + extract_hint_labels(editor), + "Client should get its first hints when opens an editor" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 1, + "Guest editor update the cache version after every cache/view change" + ); + }); + + let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; + editor_b.update(cx_b, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone())); + editor.handle_input(":", cx); + }); + cx_b.focus_view(&editor_b); + + executor.run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert_eq!( + vec![after_client_edit.to_string()], + extract_hint_labels(editor), + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.version(), 2); + }); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec![after_client_edit.to_string()], + extract_hint_labels(editor), + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.version(), 2); + }); + + let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; + editor_a.update(cx_a, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input("a change to increment both buffers' versions", cx); + }); + cx_a.focus_view(&editor_a); + + executor.run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert_eq!( + vec![after_host_edit.to_string()], + extract_hint_labels(editor), + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.version(), 3); + }); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec![after_host_edit.to_string()], + extract_hint_labels(editor), + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.version(), 3); + }); + + let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; + fake_language_server + .request::(()) + .await + .expect("inlay refresh request failed"); + + executor.run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert_eq!( + vec![after_special_edit_for_refresh.to_string()], + extract_hint_labels(editor), + "Host should react to /refresh LSP request" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 4, + "Host should accepted all edits and bump its cache version every time" + ); + }); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec![after_special_edit_for_refresh.to_string()], + extract_hint_labels(editor), + "Guest should get a /refresh LSP request propagated by host" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 4, + "Guest should accepted all edits and bump its cache version every time" + ); + }); +} + +#[gpui::test(iterations = 10)] +async fn test_inlay_hint_refresh_is_forwarded( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(cx_a.executor()).await; + let executor = cx_a.executor(); + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + cx_a.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: false, + show_type_hints: false, + show_parameter_hints: false, + show_other_hints: false, + }) + }); + }); + }); + cx_b.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + }); + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let language = Arc::new(language); + client_a.language_registry().add(Arc::clone(&language)); + client_b.language_registry().add(language); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", + "other.rs": "// Test file", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + + cx_a.background_executor.start_waiting(); + + let editor_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + let other_hints = Arc::new(AtomicBool::new(false)); + let fake_language_server = fake_language_servers.next().await.unwrap(); + let closure_other_hints = Arc::clone(&other_hints); + fake_language_server + .handle_request::(move |params, _| { + let task_other_hints = Arc::clone(&closure_other_hints); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + let other_hints = task_other_hints.load(atomic::Ordering::Acquire); + let character = if other_hints { 0 } else { 2 }; + let label = if other_hints { + "other hint" + } else { + "initial hint" + }; + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, character), + label: lsp::InlayHintLabel::String(label.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await + .unwrap(); + executor.finish_waiting(); + + executor.run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert!( + extract_hint_labels(editor).is_empty(), + "Host should get no hints due to them turned off" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 0, + "Turned off hints should not generate version updates" + ); + }); + + executor.run_until_parked(); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec!["initial hint".to_string()], + extract_hint_labels(editor), + "Client should get its first hints when opens an editor" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 1, + "Should update cache verison after first hints" + ); + }); + + other_hints.fetch_or(true, atomic::Ordering::Release); + fake_language_server + .request::(()) + .await + .expect("inlay refresh request failed"); + executor.run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert!( + extract_hint_labels(editor).is_empty(), + "Host should get nop hints due to them turned off, even after the /refresh" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 0, + "Turned off hints should not generate version updates, again" + ); + }); + + executor.run_until_parked(); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec!["other hint".to_string()], + extract_hint_labels(editor), + "Guest should get a /refresh LSP request propagated by host despite host hints are off" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 2, + "Guest should accepted all edits and bump its cache version every time" + ); + }); +} + +fn extract_hint_labels(editor: &Editor) -> Vec { + let mut labels = Vec::new(); + for hint in editor.inlay_hint_cache().hints() { + match hint.label { + project::InlayHintLabel::String(s) => labels.push(s), + _ => unreachable!(), + } + } + labels +} diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 19acb17673..b142fcbe7f 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -607,8 +607,12 @@ impl Panel for ChatPanel { "ChatPanel" } - fn icon(&self, _cx: &WindowContext) -> Option { - Some(ui::Icon::MessageBubbles) + fn icon(&self, cx: &WindowContext) -> Option { + if !is_channels_feature_enabled(cx) { + return None; + } + + Some(ui::Icon::MessageBubbles).filter(|_| ChatPanelSettings::get_global(cx).button) } fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index f8dd4bf0b7..663883f9b4 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -11,15 +11,16 @@ use channel::{Channel, ChannelEvent, ChannelId, ChannelStore}; use client::{Client, Contact, User, UserStore}; use contact_finder::ContactFinder; use db::kvp::KEY_VALUE_STORE; -use editor::Editor; +use editor::{Editor, EditorElement, EditorStyle}; use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, canvas, div, fill, list, overlay, point, prelude::*, px, serde_json, AnyElement, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter, - FocusHandle, FocusableView, InteractiveElement, IntoElement, ListOffset, ListState, Model, - MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce, SharedString, - Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, + FocusHandle, FocusableView, FontStyle, FontWeight, InteractiveElement, IntoElement, ListOffset, + ListState, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, + RenderOnce, SharedString, Styled, Subscription, Task, TextStyle, View, ViewContext, + VisualContext, WeakView, WhiteSpace, }; use menu::{Cancel, Confirm, SelectNext, SelectPrev}; use project::{Fs, Project}; @@ -29,10 +30,9 @@ use settings::{Settings, SettingsStore}; use smallvec::SmallVec; use std::{mem, sync::Arc}; use theme::{ActiveTheme, ThemeSettings}; -use ui::prelude::*; use ui::{ - h_stack, v_stack, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize, - Label, ListHeader, ListItem, Tooltip, + prelude::*, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize, Label, + ListHeader, ListItem, Tooltip, }; use util::{maybe, ResultExt, TryFutureExt}; use workspace::{ @@ -1829,15 +1829,49 @@ impl CollabPanel { .size_full() .child(list(self.list_state.clone()).full()) .child( - v_stack().p_2().child( - v_stack() - .border_primary(cx) - .border_t() - .child(self.filter_editor.clone()), - ), + v_stack() + .child(div().mx_2().border_primary(cx).border_t()) + .child( + v_stack() + .p_2() + .child(self.render_filter_input(&self.filter_editor, cx)), + ), ) } + fn render_filter_input( + &self, + editor: &View, + cx: &mut ViewContext, + ) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + let text_style = TextStyle { + color: if editor.read(cx).read_only(cx) { + cx.theme().colors().text_disabled + } else { + cx.theme().colors().text + }, + font_family: settings.ui_font.family.clone(), + font_features: settings.ui_font.features, + font_size: rems(0.875).into(), + font_weight: FontWeight::NORMAL, + font_style: FontStyle::Normal, + line_height: relative(1.3).into(), + background_color: None, + underline: None, + white_space: WhiteSpace::Normal, + }; + + EditorElement::new( + editor, + EditorStyle { + local_player: cx.theme().players().local(), + text: text_style, + ..Default::default() + }, + ) + } + fn render_header( &self, section: Section, diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 114e89f1c4..4955d01af2 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -164,8 +164,14 @@ impl Render for ChannelModal { .py_1() .rounded_t(px(8.)) .bg(cx.theme().colors().element_background) - .child(IconElement::new(Icon::Hash).size(IconSize::Medium)) - .child(Label::new(channel_name)) + .child( + h_stack() + .w_px() + .flex_1() + .gap_1() + .child(IconElement::new(Icon::Hash).size(IconSize::Medium)) + .child(Label::new(channel_name)), + ) .child( h_stack() .w_full() diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 81a71da27e..1fcce1488b 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -15,7 +15,7 @@ use std::sync::Arc; use theme::{ActiveTheme, PlayerColors}; use ui::{ h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, - IconButton, IconElement, Tooltip, + IconButton, IconElement, TintColor, Tooltip, }; use util::ResultExt; use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu}; @@ -111,6 +111,7 @@ impl Render for CollabTitlebarItem { &room, project_id, ¤t_user, + cx, )) .children( remote_participants.iter().filter_map(|collaborator| { @@ -128,6 +129,7 @@ impl Render for CollabTitlebarItem { &room, project_id, ¤t_user, + cx, )?; Some( @@ -183,6 +185,8 @@ impl Render for CollabTitlebarItem { if is_shared { "Unshare" } else { "Share" }, ) .style(ButtonStyle::Subtle) + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + .selected(is_shared) .label_size(LabelSize::Small) .on_click(cx.listener( move |this, _, cx| { @@ -218,6 +222,7 @@ impl Render for CollabTitlebarItem { .style(ButtonStyle::Subtle) .icon_size(IconSize::Small) .selected(is_muted) + .selected_style(ButtonStyle::Tinted(TintColor::Negative)) .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)), ) }) @@ -231,6 +236,7 @@ impl Render for CollabTitlebarItem { }, ) .style(ButtonStyle::Subtle) + .selected_style(ButtonStyle::Tinted(TintColor::Negative)) .icon_size(IconSize::Small) .selected(is_deafened) .tooltip(move |cx| { @@ -253,6 +259,7 @@ impl Render for CollabTitlebarItem { .style(ButtonStyle::Subtle) .icon_size(IconSize::Small) .selected(is_screen_sharing) + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) .on_click(move |_, cx| { crate::toggle_screen_sharing(&Default::default(), cx) }), @@ -420,6 +427,7 @@ impl CollabTitlebarItem { room: &Room, project_id: Option, current_user: &Arc, + cx: &ViewContext, ) -> Option { if room.role_for_user(user.id) == Some(proto::ChannelRole::Guest) { return None; @@ -432,9 +440,9 @@ impl CollabTitlebarItem { Avatar::new(user.avatar_uri.clone()) .grayscale(!is_present) .border_color(if is_speaking { - gpui::blue() + cx.theme().status().info_border } else if is_muted { - gpui::red() + cx.theme().status().error_border } else { Hsla::default() }), diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index b7a1dbfd3d..bbc2cd4123 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -370,6 +370,7 @@ mod tests { use gpui::TestAppContext; use language::Point; use project::Project; + use settings::KeymapFile; use workspace::{AppState, Workspace}; #[test] @@ -503,7 +504,20 @@ mod tests { workspace::init(app_state.clone(), cx); init(cx); Project::init_settings(cx); - settings::load_default_keymap(cx); + KeymapFile::parse( + r#"[ + { + "bindings": { + "cmd-n": "workspace::NewFile", + "enter": "menu::Confirm", + "cmd-shift-p": "command_palette::Toggle" + } + } + ]"#, + ) + .unwrap() + .add_to_cx(cx) + .unwrap(); app_state }) } diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index d31d6249c6..613fadf7f7 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -646,8 +646,13 @@ impl Item for ProjectDiagnosticsEditor { fn tab_content(&self, _detail: Option, selected: bool, _: &WindowContext) -> AnyElement { if self.summary.error_count == 0 && self.summary.warning_count == 0 { - let label = Label::new("No problems"); - label.into_any_element() + Label::new("No problems") + .color(if selected { + Color::Default + } else { + Color::Muted + }) + .into_any_element() } else { h_stack() .gap_1() @@ -1572,6 +1577,7 @@ mod tests { workspace::init_settings(cx); Project::init_settings(cx); crate::init(cx); + editor::init(cx); }); } diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 9f7be0c04f..da1f77b9af 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -23,11 +23,21 @@ pub struct DiagnosticIndicator { impl Render for DiagnosticIndicator { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) { - (0, 0) => h_stack().child( - IconElement::new(Icon::Check) - .size(IconSize::Small) - .color(Color::Success), - ), + (0, 0) => h_stack().map(|this| { + if !self.in_progress_checks.is_empty() { + this.child( + IconElement::new(Icon::ArrowCircle) + .size(IconSize::Small) + .color(Color::Muted), + ) + } else { + this.child( + IconElement::new(Icon::Check) + .size(IconSize::Small) + .color(Color::Default), + ) + } + }), (0, warning_count) => h_stack() .gap_1() .child( @@ -64,6 +74,7 @@ impl Render for DiagnosticIndicator { Some( Label::new("Checking…") .size(LabelSize::Small) + .color(Color::Muted) .into_any_element(), ) } else if let Some(diagnostic) = &self.current_diagnostic { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e3b0e9874b..455db1d715 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -24,7 +24,7 @@ use ::git::diff::DiffHunk; use aho_corasick::AhoCorasick; use anyhow::{anyhow, Context as _, Result}; use blink_manager::BlinkManager; -use client::{Client, Collaborator, ParticipantIndex, TelemetrySettings}; +use client::{Client, Collaborator, ParticipantIndex}; use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use convert_case::{Case, Casing}; @@ -99,9 +99,9 @@ use sum_tree::TreeMap; use text::{OffsetUtf16, Rope}; use theme::{ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, ThemeColors, ThemeSettings}; use ui::{ - h_stack, ButtonSize, ButtonStyle, Icon, IconButton, ListItem, ListItemSpacing, Popover, Tooltip, + h_stack, prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, ListItem, Popover, + Tooltip, }; -use ui::{prelude::*, IconSize}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{searchable::SearchEvent, ItemNavHistory, Pane, SplitDirection, ViewId, Workspace}; @@ -1259,7 +1259,6 @@ impl CompletionsMenu { div().min_w(px(220.)).max_w(px(540.)).child( ListItem::new(mat.candidate_id) .inset(true) - .spacing(ListItemSpacing::Sparse) .selected(item_ix == selected_item) .on_click(cx.listener(move |editor, _event, cx| { cx.stop_propagation(); @@ -8451,6 +8450,12 @@ impl Editor { }) } + pub fn has_background_highlights(&self) -> bool { + self.background_highlights + .get(&TypeId::of::()) + .map_or(false, |(_, highlights)| !highlights.is_empty()) + } + pub fn background_highlights_in_range( &self, search_range: Range, @@ -8868,14 +8873,8 @@ impl Editor { .map(|a| a.to_string()); let telemetry = project.read(cx).client().telemetry().clone(); - let telemetry_settings = *TelemetrySettings::get_global(cx); - telemetry.report_copilot_event( - telemetry_settings, - suggestion_id, - suggestion_accepted, - file_extension, - ) + telemetry.report_copilot_event(suggestion_id, suggestion_accepted, file_extension, cx) } #[cfg(any(test, feature = "test-support"))] @@ -8913,7 +8912,6 @@ impl Editor { .raw_user_settings() .get("vim_mode") == Some(&serde_json::Value::Bool(true)); - let telemetry_settings = *TelemetrySettings::get_global(cx); let copilot_enabled = all_language_settings(file, cx).copilot_enabled(None, None); let copilot_enabled_for_language = self .buffer @@ -8923,12 +8921,12 @@ impl Editor { let telemetry = project.read(cx).client().telemetry().clone(); telemetry.report_editor_event( - telemetry_settings, file_extension, vim_mode, operation, copilot_enabled, copilot_enabled_for_language, + cx, ) } @@ -9570,7 +9568,7 @@ impl InputHandler for Editor { ) -> Option> { let text_layout_details = self.text_layout_details(cx); let style = &text_layout_details.editor_style; - let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); + let font_id = cx.text_system().resolve_font(&style.text.font()); let font_size = style.text.font_size.to_pixels(cx.rem_size()); let line_height = style.text.line_height_in_pixels(cx.rem_size()); let em_width = cx diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 368fc4b7ca..e96cb5df0e 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -8,6 +8,7 @@ use crate::{ hover_popover::{ self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, }, + items::BufferSearchHighlights, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, show_link_definition, update_go_to_definition_link, update_inlay_link_and_hover_points, GoToDefinitionTrigger, @@ -1229,6 +1230,14 @@ impl EditorElement { return; } + // If a drag took place after we started dragging the scrollbar, + // cancel the scrollbar drag. + if cx.has_active_drag() { + self.editor.update(cx, |editor, cx| { + editor.scroll_manager.set_is_dragging_scrollbar(false, cx); + }); + } + let top = bounds.origin.y; let bottom = bounds.lower_left().y; let right = bounds.lower_right().x; @@ -1275,7 +1284,7 @@ impl EditorElement { let background_ranges = self .editor .read(cx) - .background_highlight_row_ranges::( + .background_highlight_row_ranges::( start_anchor..end_anchor, &layout.position_map.snapshot, 50000, @@ -1766,7 +1775,7 @@ impl EditorElement { let snapshot = editor.snapshot(cx); let style = self.style.clone(); - let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); + let font_id = cx.text_system().resolve_font(&style.text.font()); let font_size = style.text.font_size.to_pixels(cx.rem_size()); let line_height = style.text.line_height_in_pixels(cx.rem_size()); let em_width = cx @@ -1972,7 +1981,7 @@ impl EditorElement { (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs()) || // Selections - (is_singleton && scrollbar_settings.selections && !highlighted_ranges.is_empty()) + (is_singleton && scrollbar_settings.selections && editor.has_background_highlights::()) // Scrollmanager || editor.scroll_manager.scrollbars_visible() } @@ -3779,7 +3788,7 @@ fn compute_auto_height_layout( } let style = editor.style.as_ref().unwrap(); - let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); + let font_id = cx.text_system().resolve_font(&style.text.font()); let font_size = style.text.font_size.to_pixels(cx.rem_size()); let line_height = style.text.line_height_in_pixels(cx.rem_size()); let em_width = cx diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index f358a67253..78f9b15051 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -15,9 +15,11 @@ use language::{ proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt, Point, SelectionGoal, }; +use project::repository::GitFileStatus; use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath}; use rpc::proto::{self, update_view, PeerId}; use settings::Settings; +use workspace::item::ItemSettings; use std::fmt::Write; use std::{ @@ -29,7 +31,7 @@ use std::{ sync::Arc, }; use text::Selection; -use theme::{ActiveTheme, Theme}; +use theme::Theme; use ui::{h_stack, prelude::*, Label}; use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt}; use workspace::{ @@ -580,7 +582,28 @@ impl Item for Editor { } fn tab_content(&self, detail: Option, selected: bool, cx: &WindowContext) -> AnyElement { - let _theme = cx.theme(); + let git_status = if ItemSettings::get_global(cx).git_status { + self.buffer() + .read(cx) + .as_singleton() + .and_then(|buffer| buffer.read(cx).project_path(cx)) + .and_then(|path| self.project.as_ref()?.read(cx).entry_for_path(&path, cx)) + .and_then(|entry| entry.git_status()) + } else { + None + }; + let label_color = match git_status { + Some(GitFileStatus::Added) => Color::Created, + Some(GitFileStatus::Modified) => Color::Modified, + Some(GitFileStatus::Conflict) => Color::Conflict, + None => { + if selected { + Color::Default + } else { + Color::Muted + } + } + }; let description = detail.and_then(|detail| { let path = path_for_buffer(&self.buffer, detail, false, cx)?; @@ -596,11 +619,7 @@ impl Item for Editor { h_stack() .gap_2() - .child(Label::new(self.title(cx).to_string()).color(if selected { - Color::Default - } else { - Color::Muted - })) + .child(Label::new(self.title(cx).to_string()).color(label_color)) .when_some(description, |this, description| { this.child( Label::new(description) diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 42f502daed..04dcf93015 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -930,10 +930,7 @@ mod tests { fn do_work() { «test»(); } "}); - // Deactivating the window dismisses the highlight - cx.update_workspace(|workspace, cx| { - workspace.on_window_activation_changed(cx); - }); + cx.cx.cx.deactivate_window(); cx.assert_editor_text_highlights::(indoc! {" fn test() { do_work(); } fn do_work() { test(); } diff --git a/crates/feedback/Cargo.toml b/crates/feedback/Cargo.toml index c6c8b9f4b2..e14df6a2d4 100644 --- a/crates/feedback/Cargo.toml +++ b/crates/feedback/Cargo.toml @@ -18,7 +18,6 @@ gpui = { path = "../gpui" } language = { path = "../language" } menu = { path = "../menu" } project = { path = "../project" } -search = { path = "../search" } settings = { path = "../settings" } theme = { path = "../theme" } ui = { path = "../ui" } diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 7cf6889054..d654131a56 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -19,7 +19,7 @@ gpui_macros = { path = "../gpui_macros" } util = { path = "../util" } sum_tree = { path = "../sum_tree" } sqlez = { path = "../sqlez" } -async-task = "4.0.3" +async-task = "4.7" backtrace = { version = "0.3", optional = true } ctor.workspace = true linkme = "0.3" diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index f8da622b53..4ad9540043 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -327,6 +327,7 @@ impl AppContext { pub fn refresh(&mut self) { self.pending_effects.push_back(Effect::Refresh); } + pub(crate) fn update(&mut self, update: impl FnOnce(&mut Self) -> R) -> R { self.pending_updates += 1; let result = update(self); @@ -840,10 +841,12 @@ impl AppContext { /// Update the global of the given type with a closure. Unlike `global_mut`, this method provides /// your closure with mutable access to the `AppContext` and the global simultaneously. pub fn update_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R { - let mut global = self.lease_global::(); - let result = f(&mut global, self); - self.end_global_lease(global); - result + self.update(|cx| { + let mut global = cx.lease_global::(); + let result = f(&mut global, cx); + cx.end_global_lease(global); + result + }) } /// Register a callback to be invoked when a global of the given type is updated. @@ -941,6 +944,11 @@ impl AppContext { self.pending_effects.push_back(Effect::Refresh); } + pub fn clear_key_bindings(&mut self) { + self.keymap.lock().clear(); + self.pending_effects.push_back(Effect::Refresh); + } + /// Register a global listener for actions invoked via the keyboard. pub fn on_action(&mut self, listener: impl Fn(&A, &mut Self) + 'static) { self.global_action_listeners diff --git a/crates/gpui/src/app/entity_map.rs b/crates/gpui/src/app/entity_map.rs index f71cfbdebc..97f680560a 100644 --- a/crates/gpui/src/app/entity_map.rs +++ b/crates/gpui/src/app/entity_map.rs @@ -16,6 +16,9 @@ use std::{ thread::panicking, }; +#[cfg(any(test, feature = "test-support"))] +use collections::HashMap; + slotmap::new_key_type! { pub struct EntityId; } impl EntityId { @@ -38,6 +41,8 @@ pub(crate) struct EntityMap { struct EntityRefCounts { counts: SlotMap, dropped_entity_ids: Vec, + #[cfg(any(test, feature = "test-support"))] + leak_detector: LeakDetector, } impl EntityMap { @@ -47,6 +52,11 @@ impl EntityMap { ref_counts: Arc::new(RwLock::new(EntityRefCounts { counts: SlotMap::with_key(), dropped_entity_ids: Vec::new(), + #[cfg(any(test, feature = "test-support"))] + leak_detector: LeakDetector { + next_handle_id: 0, + entity_handles: HashMap::default(), + }, })), } } @@ -156,6 +166,8 @@ pub struct AnyModel { pub(crate) entity_id: EntityId, pub(crate) entity_type: TypeId, entity_map: Weak>, + #[cfg(any(test, feature = "test-support"))] + handle_id: HandleId, } impl AnyModel { @@ -163,7 +175,14 @@ impl AnyModel { Self { entity_id: id, entity_type, - entity_map, + entity_map: entity_map.clone(), + #[cfg(any(test, feature = "test-support"))] + handle_id: entity_map + .upgrade() + .unwrap() + .write() + .leak_detector + .handle_created(id), } } @@ -207,11 +226,20 @@ impl Clone for AnyModel { assert_ne!(prev_count, 0, "Detected over-release of a model."); } - Self { + let this = Self { entity_id: self.entity_id, entity_type: self.entity_type, entity_map: self.entity_map.clone(), - } + #[cfg(any(test, feature = "test-support"))] + handle_id: self + .entity_map + .upgrade() + .unwrap() + .write() + .leak_detector + .handle_created(self.entity_id), + }; + this } } @@ -231,6 +259,14 @@ impl Drop for AnyModel { entity_map.dropped_entity_ids.push(self.entity_id); } } + + #[cfg(any(test, feature = "test-support"))] + if let Some(entity_map) = self.entity_map.upgrade() { + entity_map + .write() + .leak_detector + .handle_dropped(self.entity_id, self.handle_id) + } } } @@ -423,13 +459,43 @@ impl AnyWeakModel { return None; } ref_count.fetch_add(1, SeqCst); + drop(ref_counts); Some(AnyModel { entity_id: self.entity_id, entity_type: self.entity_type, entity_map: self.entity_ref_counts.clone(), + #[cfg(any(test, feature = "test-support"))] + handle_id: self + .entity_ref_counts + .upgrade() + .unwrap() + .write() + .leak_detector + .handle_created(self.entity_id), }) } + + #[cfg(any(test, feature = "test-support"))] + pub fn assert_dropped(&self) { + self.entity_ref_counts + .upgrade() + .unwrap() + .write() + .leak_detector + .assert_dropped(self.entity_id); + + if self + .entity_ref_counts + .upgrade() + .and_then(|ref_counts| Some(ref_counts.read().counts.get(self.entity_id)?.load(SeqCst))) + .is_some() + { + panic!( + "entity was recently dropped but resources are retained until the end of the effect cycle." + ) + } + } } impl From> for AnyWeakModel { @@ -534,6 +600,59 @@ impl PartialEq> for WeakModel { } } +#[cfg(any(test, feature = "test-support"))] +lazy_static::lazy_static! { + static ref LEAK_BACKTRACE: bool = + std::env::var("LEAK_BACKTRACE").map_or(false, |b| !b.is_empty()); +} + +#[cfg(any(test, feature = "test-support"))] +#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)] +pub struct HandleId { + id: u64, // id of the handle itself, not the pointed at object +} + +#[cfg(any(test, feature = "test-support"))] +pub struct LeakDetector { + next_handle_id: u64, + entity_handles: HashMap>>, +} + +#[cfg(any(test, feature = "test-support"))] +impl LeakDetector { + #[track_caller] + pub fn handle_created(&mut self, entity_id: EntityId) -> HandleId { + let id = util::post_inc(&mut self.next_handle_id); + let handle_id = HandleId { id }; + let handles = self.entity_handles.entry(entity_id).or_default(); + handles.insert( + handle_id, + LEAK_BACKTRACE.then(|| backtrace::Backtrace::new_unresolved()), + ); + handle_id + } + + pub fn handle_dropped(&mut self, entity_id: EntityId, handle_id: HandleId) { + let handles = self.entity_handles.entry(entity_id).or_default(); + handles.remove(&handle_id); + } + + pub fn assert_dropped(&mut self, entity_id: EntityId) { + let handles = self.entity_handles.entry(entity_id).or_default(); + if !handles.is_empty() { + for (_, backtrace) in handles { + if let Some(mut backtrace) = backtrace.take() { + backtrace.resolve(); + eprintln!("Leaked handle: {:#?}", backtrace); + } else { + eprintln!("Leaked handle: export LEAK_BACKTRACE to find allocation site"); + } + } + panic!(); + } + } +} + #[cfg(test)] mod test { use crate::EntityMap; diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 44f303ac0b..a5f66be09b 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -1,14 +1,13 @@ use crate::{ div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, - BackgroundExecutor, Bounds, ClipboardItem, Context, Entity, EventEmitter, ForegroundExecutor, - InputEvent, IntoElement, KeyDownEvent, Keystroke, Model, ModelContext, Pixels, Platform, - PlatformWindow, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow, - TestWindowHandlers, TextSystem, View, ViewContext, VisualContext, WindowBounds, WindowContext, - WindowHandle, WindowOptions, + BackgroundExecutor, ClipboardItem, Context, Entity, EventEmitter, ForegroundExecutor, + IntoElement, Keystroke, Model, ModelContext, Pixels, Platform, Render, Result, Size, Task, + TestDispatcher, TestPlatform, TestWindow, TextSystem, View, ViewContext, VisualContext, + WindowContext, WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; -use std::{future::Future, mem, ops::Deref, rc::Rc, sync::Arc, time::Duration}; +use std::{future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration}; #[derive(Clone)] pub struct TestAppContext { @@ -185,42 +184,7 @@ impl TestAppContext { } pub fn simulate_window_resize(&self, window_handle: AnyWindowHandle, size: Size) { - let (mut handlers, scale_factor) = self - .app - .borrow_mut() - .update_window(window_handle, |_, cx| { - let platform_window = cx.window.platform_window.as_test().unwrap(); - let scale_factor = platform_window.scale_factor(); - match &mut platform_window.bounds { - WindowBounds::Fullscreen | WindowBounds::Maximized => { - platform_window.bounds = WindowBounds::Fixed(Bounds { - origin: Point::default(), - size: size.map(|pixels| f64::from(pixels).into()), - }); - } - WindowBounds::Fixed(bounds) => { - bounds.size = size.map(|pixels| f64::from(pixels).into()); - } - } - - ( - mem::take(&mut platform_window.handlers.lock().resize), - scale_factor, - ) - }) - .unwrap(); - - for handler in &mut handlers { - handler(size, scale_factor); - } - - self.app - .borrow_mut() - .update_window(window_handle, |_, cx| { - let platform_window = cx.window.platform_window.as_test().unwrap(); - platform_window.handlers.lock().resize = handlers; - }) - .unwrap(); + self.test_window(window_handle).simulate_resize(size); } pub fn spawn(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task @@ -313,41 +277,22 @@ impl TestAppContext { keystroke: Keystroke, is_held: bool, ) { - let keystroke2 = keystroke.clone(); - let handled = window - .update(self, |_, cx| { - cx.dispatch_event(InputEvent::KeyDown(KeyDownEvent { keystroke, is_held })) - }) - .is_ok_and(|handled| handled); - if handled { - return; - } - - let input_handler = self.update_test_window(window, |window| window.input_handler.clone()); - let Some(input_handler) = input_handler else { - panic!( - "dispatch_keystroke {:?} failed to dispatch action or input", - &keystroke2 - ); - }; - let text = keystroke2.ime_key.unwrap_or(keystroke2.key); - input_handler.lock().replace_text_in_range(None, &text); + self.test_window(window) + .simulate_keystroke(keystroke, is_held) } - pub fn update_test_window( - &mut self, - window: AnyWindowHandle, - f: impl FnOnce(&mut TestWindow) -> R, - ) -> R { - window - .update(self, |_, cx| { - f(cx.window - .platform_window - .as_any_mut() - .downcast_mut::() - .unwrap()) - }) + pub fn test_window(&self, window: AnyWindowHandle) -> TestWindow { + self.app + .borrow_mut() + .windows + .get_mut(window.id) .unwrap() + .as_mut() + .unwrap() + .platform_window + .as_test() + .unwrap() + .clone() } pub fn notifications(&mut self, entity: &impl Entity) -> impl Stream { @@ -563,11 +508,7 @@ impl<'a> VisualTestContext<'a> { } pub fn window_title(&mut self) -> Option { - self.cx - .update_window(self.window, |_, cx| { - cx.window.platform_window.as_test().unwrap().title.clone() - }) - .unwrap() + self.cx.test_window(self.window).0.lock().title.clone() } pub fn simulate_keystrokes(&mut self, keystrokes: &str) { @@ -578,37 +519,11 @@ impl<'a> VisualTestContext<'a> { self.cx.simulate_input(self.window, input) } - pub fn simulate_activation(&mut self) { - self.simulate_window_events(&mut |handlers| { - handlers - .active_status_change - .iter_mut() - .for_each(|f| f(true)); - }) - } - - pub fn simulate_deactivation(&mut self) { - self.simulate_window_events(&mut |handlers| { - handlers - .active_status_change - .iter_mut() - .for_each(|f| f(false)); - }) - } - - fn simulate_window_events(&mut self, f: &mut dyn FnMut(&mut TestWindowHandlers)) { - let handlers = self - .cx - .update_window(self.window, |_, cx| { - cx.window - .platform_window - .as_test() - .unwrap() - .handlers - .clone() - }) - .unwrap(); - f(&mut *handlers.lock()); + pub fn deactivate_window(&mut self) { + if Some(self.window) == self.test_platform.active_window() { + self.test_platform.set_active_window(None) + } + self.background_executor.run_until_parked(); } } diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 364b610eee..2a47a16741 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -1,7 +1,7 @@ use crate::{ - point, px, AnyElement, AvailableSpace, BorrowAppContext, Bounds, DispatchPhase, Element, - IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, - WindowContext, + point, px, AnyElement, AvailableSpace, BorrowAppContext, BorrowWindow, Bounds, ContentMask, + DispatchPhase, Element, IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style, + StyleRefinement, Styled, WindowContext, }; use collections::VecDeque; use refineable::Refineable as _; @@ -317,7 +317,7 @@ impl Element for List { fn paint( &mut self, - bounds: crate::Bounds, + bounds: Bounds, _state: &mut Self::State, cx: &mut crate::WindowContext, ) { @@ -444,13 +444,15 @@ impl Element for List { new_items.append(cursor.suffix(&()), &()); // Paint the visible items - let mut item_origin = bounds.origin; - item_origin.y -= scroll_top.offset_in_item; - for item_element in &mut item_elements { - let item_height = item_element.measure(available_item_space, cx).height; - item_element.draw(item_origin, available_item_space, cx); - item_origin.y += item_height; - } + cx.with_content_mask(Some(ContentMask { bounds }), |cx| { + let mut item_origin = bounds.origin; + item_origin.y -= scroll_top.offset_in_item; + for item_element in &mut item_elements { + let item_height = item_element.measure(available_item_space, cx).height; + item_element.draw(item_origin, available_item_space, cx); + item_origin.y += item_height; + } + }); state.items = new_items; state.last_layout_bounds = Some(bounds); diff --git a/crates/gpui/src/interactive.rs b/crates/gpui/src/interactive.rs index 6fc7cfc8e8..0be917350d 100644 --- a/crates/gpui/src/interactive.rs +++ b/crates/gpui/src/interactive.rs @@ -307,7 +307,10 @@ mod test { div().id("testview").child( div() .key_context("parent") - .on_key_down(cx.listener(|this, _, _| this.saw_key_down = true)) + .on_key_down(cx.listener(|this, _, cx| { + cx.stop_propagation(); + this.saw_key_down = true + })) .on_action( cx.listener(|this: &mut TestView, _: &TestAction, _| { this.saw_action = true @@ -343,6 +346,7 @@ mod test { .update(cx, |test_view, cx| cx.focus(&test_view.focus_handle)) .unwrap(); + cx.dispatch_keystroke(*window, Keystroke::parse("a").unwrap(), false); cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap(), false); window diff --git a/crates/gpui/src/platform/mac/dispatcher.rs b/crates/gpui/src/platform/mac/dispatcher.rs index feb8925426..06bef49b7a 100644 --- a/crates/gpui/src/platform/mac/dispatcher.rs +++ b/crates/gpui/src/platform/mac/dispatcher.rs @@ -11,7 +11,7 @@ use objc::{ }; use parking::{Parker, Unparker}; use parking_lot::Mutex; -use std::{ffi::c_void, sync::Arc, time::Duration}; +use std::{ffi::c_void, ptr::NonNull, sync::Arc, time::Duration}; include!(concat!(env!("OUT_DIR"), "/dispatch_sys.rs")); @@ -47,7 +47,7 @@ impl PlatformDispatcher for MacDispatcher { unsafe { dispatch_async_f( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.try_into().unwrap(), 0), - runnable.into_raw() as *mut c_void, + runnable.into_raw().as_ptr() as *mut c_void, Some(trampoline), ); } @@ -57,7 +57,7 @@ impl PlatformDispatcher for MacDispatcher { unsafe { dispatch_async_f( dispatch_get_main_queue(), - runnable.into_raw() as *mut c_void, + runnable.into_raw().as_ptr() as *mut c_void, Some(trampoline), ); } @@ -71,7 +71,7 @@ impl PlatformDispatcher for MacDispatcher { dispatch_after_f( when, queue, - runnable.into_raw() as *mut c_void, + runnable.into_raw().as_ptr() as *mut c_void, Some(trampoline), ); } @@ -91,6 +91,6 @@ impl PlatformDispatcher for MacDispatcher { } extern "C" fn trampoline(runnable: *mut c_void) { - let task = unsafe { Runnable::from_raw(runnable as *mut ()) }; + let task = unsafe { Runnable::<()>::from_raw(NonNull::new_unchecked(runnable as *mut ())) }; task.run(); } diff --git a/crates/gpui/src/platform/mac/shaders.metal b/crates/gpui/src/platform/mac/shaders.metal index aba01b9d5b..264fa55134 100644 --- a/crates/gpui/src/platform/mac/shaders.metal +++ b/crates/gpui/src/platform/mac/shaders.metal @@ -16,6 +16,7 @@ float gaussian(float x, float sigma); float2 erf(float2 x); float blur_along_x(float x, float y, float sigma, float corner, float2 half_size); +float4 over(float4 below, float4 above); struct QuadVertexOutput { float4 position [[position]]; @@ -108,21 +109,11 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]], color = input.background_color; } else { float inset_distance = distance + border_width; - - // Decrease border's opacity as we move inside the background. - input.border_color.a *= 1. - saturate(0.5 - inset_distance); - - // Alpha-blend the border and the background. - float output_alpha = input.border_color.a + - input.background_color.a * (1. - input.border_color.a); - float3 premultiplied_border_rgb = - input.border_color.rgb * input.border_color.a; - float3 premultiplied_background_rgb = - input.background_color.rgb * input.background_color.a; - float3 premultiplied_output_rgb = - premultiplied_border_rgb + - premultiplied_background_rgb * (1. - input.border_color.a); - color = float4(premultiplied_output_rgb, output_alpha); + // Blend the border on top of the background and then linearly interpolate + // between the two as we slide inside the background. + float4 blended_border = over(input.background_color, input.border_color); + color = mix(blended_border, input.background_color, + saturate(0.5 - inset_distance)); } return color * float4(1., 1., 1., saturate(0.5 - distance)); @@ -653,3 +644,12 @@ float4 distance_from_clip_rect(float2 unit_vertex, Bounds_ScaledPixels bounds, position.y - clip_bounds.origin.y, clip_bounds.origin.y + clip_bounds.size.height - position.y); } + +float4 over(float4 below, float4 above) { + float4 result; + float alpha = above.a + below.a * (1.0 - above.a); + result.rgb = + (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha; + result.a = alpha; + return result; +} diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index 142498ce4b..111fb83921 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -19,7 +19,7 @@ pub struct TestPlatform { background_executor: BackgroundExecutor, foreground_executor: ForegroundExecutor, - pub(crate) active_window: Arc>>, + pub(crate) active_window: RefCell>, active_display: Rc, active_cursor: Mutex, current_clipboard_item: Mutex>, @@ -79,6 +79,28 @@ impl TestPlatform { self.prompts.borrow_mut().multiple_choice.push_back(tx); rx } + + pub(crate) fn set_active_window(&self, window: Option) { + let executor = self.foreground_executor().clone(); + let previous_window = self.active_window.borrow_mut().take(); + *self.active_window.borrow_mut() = window.clone(); + + executor + .spawn(async move { + if let Some(previous_window) = previous_window { + if let Some(window) = window.as_ref() { + if Arc::ptr_eq(&previous_window.0, &window.0) { + return; + } + } + previous_window.simulate_active_status_change(false); + } + if let Some(window) = window { + window.simulate_active_status_change(true); + } + }) + .detach(); + } } // todo!("implement out what our tests needed in GPUI 1") @@ -130,7 +152,10 @@ impl Platform for TestPlatform { } fn active_window(&self) -> Option { - self.active_window.lock().clone() + self.active_window + .borrow() + .as_ref() + .map(|window| window.0.lock().handle) } fn open_window( @@ -139,13 +164,13 @@ impl Platform for TestPlatform { options: WindowOptions, _draw: Box Result>, ) -> Box { - *self.active_window.lock() = Some(handle); - Box::new(TestWindow::new( + let window = TestWindow::new( options, handle, self.weak.clone(), self.active_display.clone(), - )) + ); + Box::new(window) } fn set_display_link_output_callback( diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index dab53e46d9..f089531b0c 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -1,7 +1,7 @@ use crate::{ - px, AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, - PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, Size, TestPlatform, TileId, - WindowAppearance, WindowBounds, WindowOptions, + px, AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, InputEvent, KeyDownEvent, + Keystroke, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, + Size, TestPlatform, TileId, WindowAppearance, WindowBounds, WindowOptions, }; use collections::HashMap; use parking_lot::Mutex; @@ -10,26 +10,25 @@ use std::{ sync::{self, Arc}, }; -#[derive(Default)] -pub(crate) struct TestWindowHandlers { - pub(crate) active_status_change: Vec>, - pub(crate) input: Vec bool>>, - pub(crate) moved: Vec>, - pub(crate) resize: Vec, f32)>>, -} - -pub struct TestWindow { +pub struct TestWindowState { pub(crate) bounds: WindowBounds, pub(crate) handle: AnyWindowHandle, display: Rc, pub(crate) title: Option, pub(crate) edited: bool, - pub(crate) input_handler: Option>>>, - pub(crate) handlers: Arc>, platform: Weak, sprite_atlas: Arc, + + input_callback: Option bool>>, + active_status_change_callback: Option>, + resize_callback: Option, f32)>>, + moved_callback: Option>, + input_handler: Option>, } +#[derive(Clone)] +pub struct TestWindow(pub(crate) Arc>); + impl TestWindow { pub fn new( options: WindowOptions, @@ -37,27 +36,96 @@ impl TestWindow { platform: Weak, display: Rc, ) -> Self { - Self { + Self(Arc::new(Mutex::new(TestWindowState { bounds: options.bounds, display, platform, handle, - input_handler: None, sprite_atlas: Arc::new(TestAtlas::new()), - handlers: Default::default(), title: Default::default(), edited: false, + + input_callback: None, + active_status_change_callback: None, + resize_callback: None, + moved_callback: None, + input_handler: None, + }))) + } + + pub fn simulate_resize(&mut self, size: Size) { + let scale_factor = self.scale_factor(); + let mut lock = self.0.lock(); + let Some(mut callback) = lock.resize_callback.take() else { + return; + }; + match &mut lock.bounds { + WindowBounds::Fullscreen | WindowBounds::Maximized => { + lock.bounds = WindowBounds::Fixed(Bounds { + origin: Point::default(), + size: size.map(|pixels| f64::from(pixels).into()), + }); + } + WindowBounds::Fixed(bounds) => { + bounds.size = size.map(|pixels| f64::from(pixels).into()); + } } + drop(lock); + callback(size, scale_factor); + self.0.lock().resize_callback = Some(callback); + } + + pub(crate) fn simulate_active_status_change(&self, active: bool) { + let mut lock = self.0.lock(); + let Some(mut callback) = lock.active_status_change_callback.take() else { + return; + }; + drop(lock); + callback(active); + self.0.lock().active_status_change_callback = Some(callback); + } + + pub fn simulate_input(&mut self, event: InputEvent) -> bool { + let mut lock = self.0.lock(); + let Some(mut callback) = lock.input_callback.take() else { + return false; + }; + drop(lock); + let result = callback(event); + self.0.lock().input_callback = Some(callback); + result + } + + pub fn simulate_keystroke(&mut self, keystroke: Keystroke, is_held: bool) { + if self.simulate_input(InputEvent::KeyDown(KeyDownEvent { + keystroke: keystroke.clone(), + is_held, + })) { + return; + } + + let mut lock = self.0.lock(); + let Some(mut input_handler) = lock.input_handler.take() else { + panic!( + "simulate_keystroke {:?} input event was not handled and there was no active input", + &keystroke + ); + }; + drop(lock); + let text = keystroke.ime_key.unwrap_or(keystroke.key); + input_handler.replace_text_in_range(None, &text); + + self.0.lock().input_handler = Some(input_handler); } } impl PlatformWindow for TestWindow { fn bounds(&self) -> WindowBounds { - self.bounds + self.0.lock().bounds } fn content_size(&self) -> Size { - let bounds = match self.bounds { + let bounds = match self.bounds() { WindowBounds::Fixed(bounds) => bounds, WindowBounds::Maximized | WindowBounds::Fullscreen => self.display().bounds(), }; @@ -77,7 +145,7 @@ impl PlatformWindow for TestWindow { } fn display(&self) -> std::rc::Rc { - self.display.clone() + self.0.lock().display.clone() } fn mouse_position(&self) -> Point { @@ -93,11 +161,11 @@ impl PlatformWindow for TestWindow { } fn set_input_handler(&mut self, input_handler: Box) { - self.input_handler = Some(Arc::new(Mutex::new(input_handler))); + self.0.lock().input_handler = Some(input_handler); } fn clear_input_handler(&mut self) { - self.input_handler = None; + self.0.lock().input_handler = None; } fn prompt( @@ -106,24 +174,29 @@ impl PlatformWindow for TestWindow { _msg: &str, _answers: &[&str], ) -> futures::channel::oneshot::Receiver { - self.platform.upgrade().expect("platform dropped").prompt() - } - - fn activate(&self) { - *self + self.0 + .lock() .platform .upgrade() .expect("platform dropped") - .active_window - .lock() = Some(self.handle); + .prompt() + } + + fn activate(&self) { + self.0 + .lock() + .platform + .upgrade() + .unwrap() + .set_active_window(Some(self.clone())) } fn set_title(&mut self, title: &str) { - self.title = Some(title.to_owned()); + self.0.lock().title = Some(title.to_owned()); } fn set_edited(&mut self, edited: bool) { - self.edited = edited; + self.0.lock().edited = edited; } fn show_character_palette(&self) { @@ -143,15 +216,15 @@ impl PlatformWindow for TestWindow { } fn on_input(&self, callback: Box bool>) { - self.handlers.lock().input.push(callback) + self.0.lock().input_callback = Some(callback) } fn on_active_status_change(&self, callback: Box) { - self.handlers.lock().active_status_change.push(callback) + self.0.lock().active_status_change_callback = Some(callback) } fn on_resize(&self, callback: Box, f32)>) { - self.handlers.lock().resize.push(callback) + self.0.lock().resize_callback = Some(callback) } fn on_fullscreen(&self, _callback: Box) { @@ -159,7 +232,7 @@ impl PlatformWindow for TestWindow { } fn on_moved(&self, callback: Box) { - self.handlers.lock().moved.push(callback) + self.0.lock().moved_callback = Some(callback) } fn on_should_close(&self, _callback: Box bool>) { @@ -183,7 +256,7 @@ impl PlatformWindow for TestWindow { } fn sprite_atlas(&self) -> sync::Arc { - self.sprite_atlas.clone() + self.0.lock().sprite_atlas.clone() } fn as_test(&mut self) -> Option<&mut TestWindow> { diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index 944a9b78be..3106a5a961 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -15,8 +15,9 @@ use crate::{ use anyhow::anyhow; use collections::FxHashMap; use core::fmt; +use itertools::Itertools; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; -use smallvec::SmallVec; +use smallvec::{smallvec, SmallVec}; use std::{ cmp, fmt::{Debug, Display, Formatter}, @@ -42,6 +43,7 @@ pub struct TextSystem { raster_bounds: RwLock>>, wrapper_pool: Mutex>>, font_runs_pool: Mutex>>, + fallback_font_stack: SmallVec<[Font; 2]>, } impl TextSystem { @@ -54,6 +56,12 @@ impl TextSystem { font_ids_by_font: RwLock::default(), wrapper_pool: Mutex::default(), font_runs_pool: Mutex::default(), + fallback_font_stack: smallvec![ + // TODO: This is currently Zed-specific. + // We should allow GPUI users to provide their own fallback font stack. + font("Zed Mono"), + font("Helvetica") + ], } } @@ -72,6 +80,33 @@ impl TextSystem { } } + /// Resolves the specified font, falling back to the default font stack if + /// the font fails to load. + /// + /// # Panics + /// + /// Panics if the font and none of the fallbacks can be resolved. + pub fn resolve_font(&self, font: &Font) -> FontId { + if let Ok(font_id) = self.font_id(font) { + return font_id; + } + + for fallback in &self.fallback_font_stack { + if let Ok(font_id) = self.font_id(fallback) { + return font_id; + } + } + + panic!( + "failed to resolve font '{}' or any of the fallbacks: {}", + font.family, + self.fallback_font_stack + .iter() + .map(|fallback| &fallback.family) + .join(", ") + ); + } + pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Bounds { self.read_metrics(font_id, |metrics| metrics.bounding_box(font_size)) } @@ -159,7 +194,7 @@ impl TextSystem { ) -> Result> { let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default(); for run in runs.iter() { - let font_id = self.font_id(&run.font)?; + let font_id = self.resolve_font(&run.font); if let Some(last_run) = font_runs.last_mut() { if last_run.font_id == font_id { last_run.len += run.len; @@ -253,7 +288,7 @@ impl TextSystem { last_font = Some(run.font.clone()); font_runs.push(FontRun { len: run_len_within_line, - font_id: self.platform_text_system.font_id(&run.font)?, + font_id: self.resolve_font(&run.font), }); } diff --git a/crates/gpui/src/view.rs b/crates/gpui/src/view.rs index 6b9b452cbb..88e564d27f 100644 --- a/crates/gpui/src/view.rs +++ b/crates/gpui/src/view.rs @@ -143,6 +143,11 @@ impl WeakView { let view = self.upgrade().context("error upgrading view")?; Ok(view.update(cx, f)).flatten() } + + #[cfg(any(test, feature = "test-support"))] + pub fn assert_dropped(&self) { + self.model.assert_dropped() + } } impl Clone for WeakView { diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 7b6541c937..71e6cb9e55 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1583,36 +1583,16 @@ impl<'a> WindowContext<'a> { let mut actions: Vec> = Vec::new(); - // Capture phase let mut context_stack: SmallVec<[KeyContext; 16]> = SmallVec::new(); - self.propagate_event = true; - for node_id in &dispatch_path { let node = self.window.rendered_frame.dispatch_tree.node(*node_id); if let Some(context) = node.context.clone() { context_stack.push(context); } - - for key_listener in node.key_listeners.clone() { - key_listener(event, DispatchPhase::Capture, self); - if !self.propagate_event { - return; - } - } } - // Bubble phase for node_id in dispatch_path.iter().rev() { - // Handle low level key events - let node = self.window.rendered_frame.dispatch_tree.node(*node_id); - for key_listener in node.key_listeners.clone() { - key_listener(event, DispatchPhase::Bubble, self); - if !self.propagate_event { - return; - } - } - // Match keystrokes let node = self.window.rendered_frame.dispatch_tree.node(*node_id); if node.context.is_some() { @@ -1633,6 +1613,7 @@ impl<'a> WindowContext<'a> { self.clear_pending_keystrokes(); } + self.propagate_event = true; for action in actions { self.dispatch_action_on_node(node_id, action.boxed_clone()); if !self.propagate_event { @@ -1640,6 +1621,31 @@ impl<'a> WindowContext<'a> { return; } } + + // Capture phase + for node_id in &dispatch_path { + let node = self.window.rendered_frame.dispatch_tree.node(*node_id); + + for key_listener in node.key_listeners.clone() { + key_listener(event, DispatchPhase::Capture, self); + if !self.propagate_event { + return; + } + } + } + + // Bubble phase + for node_id in dispatch_path.iter().rev() { + // Handle low level key events + let node = self.window.rendered_frame.dispatch_tree.node(*node_id); + for key_listener in node.key_listeners.clone() { + key_listener(event, DispatchPhase::Bubble, self); + if !self.propagate_event { + return; + } + } + } + self.dispatch_keystroke_observers(event, None); } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 2a75050bb0..366d2b0098 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -44,7 +44,7 @@ use std::{ }; use syntax_map::SyntaxSnapshot; use theme::{SyntaxTheme, Theme}; -use tree_sitter::{self, wasmtime, Query, WasmStore}; +use tree_sitter::{self, Query}; use unicase::UniCase; use util::{http::HttpClient, paths::PathExt}; use util::{post_inc, ResultExt, TryFutureExt as _, UnwrapFuture}; @@ -85,14 +85,11 @@ impl LspBinaryStatusSender { thread_local! { static PARSER: RefCell = { - let mut parser = Parser::new(); - parser.set_wasm_store(WasmStore::new(WASM_ENGINE.clone()).unwrap()).unwrap(); - RefCell::new(parser) + RefCell::new(Parser::new()) }; } lazy_static! { - pub static ref WASM_ENGINE: wasmtime::Engine = wasmtime::Engine::default(); pub static ref NEXT_GRAMMAR_ID: AtomicUsize = Default::default(); pub static ref PLAIN_TEXT: Arc = Arc::new(Language::new( LanguageConfig { @@ -116,7 +113,6 @@ pub struct LanguageServerName(pub Arc); pub struct CachedLspAdapter { pub name: LanguageServerName, pub short_name: &'static str, - pub initialization_options: Option, pub disk_based_diagnostic_sources: Vec, pub disk_based_diagnostics_progress_token: Option, pub language_ids: HashMap, @@ -128,7 +124,6 @@ impl CachedLspAdapter { pub async fn new(adapter: Arc) -> Arc { let name = adapter.name().await; let short_name = adapter.short_name(); - let initialization_options = adapter.initialization_options().await; let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources().await; let disk_based_diagnostics_progress_token = adapter.disk_based_diagnostics_progress_token().await; @@ -137,7 +132,6 @@ impl CachedLspAdapter { Arc::new(CachedLspAdapter { name, short_name, - initialization_options, disk_based_diagnostic_sources, disk_based_diagnostics_progress_token, language_ids, @@ -641,8 +635,8 @@ enum AvailableGrammar { get_queries: fn(&str) -> LanguageQueries, }, Wasm { - grammar_name: Arc, - path: Arc, + _grammar_name: Arc, + _path: Arc, }, } @@ -742,7 +736,10 @@ impl LanguageRegistry { state.available_languages.push(AvailableLanguage { id: post_inc(&mut state.next_available_language_id), config, - grammar: AvailableGrammar::Wasm { grammar_name, path }, + grammar: AvailableGrammar::Wasm { + _grammar_name: grammar_name, + _path: path, + }, lsp_adapters: Vec::new(), loaded: false, }); @@ -876,25 +873,8 @@ impl LanguageRegistry { asset_dir, get_queries, } => (grammar, (get_queries)(asset_dir)), - AvailableGrammar::Wasm { grammar_name, path } => { - let mut wasm_path = path.join(grammar_name.as_ref()); - wasm_path.set_extension("wasm"); - let wasm_bytes = std::fs::read(&wasm_path)?; - let grammar = PARSER.with(|parser| { - let mut parser = parser.borrow_mut(); - let mut store = parser.take_wasm_store().unwrap(); - let grammar = - store.load_language(&grammar_name, &wasm_bytes); - parser.set_wasm_store(store).unwrap(); - grammar - })?; - let mut queries = LanguageQueries::default(); - if let Ok(contents) = std::fs::read_to_string( - &path.join("highlights.scm"), - ) { - queries.highlights = Some(contents.into()); - } - (grammar, queries) + AvailableGrammar::Wasm { .. } => { + Err(anyhow!("not supported"))? } }; Language::new(language.config, Some(grammar)) diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index e38de7d373..123149eae2 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -10,7 +10,7 @@ use language::{LanguageServerId, LanguageServerName}; use lsp::IoKind; use project::{search::SearchQuery, Project}; use std::{borrow::Cow, sync::Arc}; -use ui::{h_stack, popover_menu, Button, Checkbox, Clickable, ContextMenu, Label, Selection}; +use ui::{popover_menu, prelude::*, Button, Checkbox, ContextMenu, Label, Selection}; use workspace::{ item::{Item, ItemHandle}, searchable::{SearchEvent, SearchableItem, SearchableItemHandle}, @@ -614,8 +614,14 @@ impl Item for LspLogView { Editor::to_item_events(event, f) } - fn tab_content(&self, _: Option, _: bool, _: &WindowContext<'_>) -> AnyElement { - Label::new("LSP Logs").into_any_element() + fn tab_content(&self, _: Option, selected: bool, _: &WindowContext<'_>) -> AnyElement { + Label::new("LSP Logs") + .color(if selected { + Color::Default + } else { + Color::Muted + }) + .into_any_element() } fn as_searchable(&self, handle: &View) -> Option> { diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index a36264261e..c30564e9bf 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -405,8 +405,14 @@ impl Item for SyntaxTreeView { fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {} - fn tab_content(&self, _: Option, _: bool, _: &WindowContext<'_>) -> AnyElement { - Label::new("Syntax Tree").into_any_element() + fn tab_content(&self, _: Option, selected: bool, _: &WindowContext<'_>) -> AnyElement { + Label::new("Syntax Tree") + .color(if selected { + Color::Default + } else { + Color::Muted + }) + .into_any_element() } fn clone_on_split( diff --git a/crates/notifications/Cargo.toml b/crates/notifications/Cargo.toml index b04f30835c..1425e079d6 100644 --- a/crates/notifications/Cargo.toml +++ b/crates/notifications/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false [lib] -path = "src/notification_store2.rs" +path = "src/notification_store.rs" doctest = false [features] diff --git a/crates/notifications/src/notification_store2.rs b/crates/notifications/src/notification_store.rs similarity index 100% rename from crates/notifications/src/notification_store2.rs rename to crates/notifications/src/notification_store.rs diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ad2e1bb267..fb3eae1945 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2842,15 +2842,6 @@ impl Project { let lsp = project_settings.lsp.get(&adapter.name.0); let override_options = lsp.map(|s| s.initialization_options.clone()).flatten(); - let mut initialization_options = adapter.initialization_options.clone(); - match (&mut initialization_options, override_options) { - (Some(initialization_options), Some(override_options)) => { - merge_json_value_into(override_options, initialization_options); - } - (None, override_options) => initialization_options = override_options, - _ => {} - } - let server_id = pending_server.server_id; let container_dir = pending_server.container_dir.clone(); let state = LanguageServerState::Starting({ @@ -2863,7 +2854,7 @@ impl Project { let result = Self::setup_and_insert_language_server( this.clone(), &worktree_path, - initialization_options, + override_options, pending_server, adapter.clone(), language.clone(), @@ -2984,7 +2975,7 @@ impl Project { async fn setup_and_insert_language_server( this: WeakModel, worktree_path: &Path, - initialization_options: Option, + override_initialization_options: Option, pending_server: PendingLanguageServer, adapter: Arc, language: Arc, @@ -2994,7 +2985,7 @@ impl Project { ) -> Result>> { let language_server = Self::setup_pending_language_server( this.clone(), - initialization_options, + override_initialization_options, pending_server, worktree_path, adapter.clone(), @@ -3024,7 +3015,7 @@ impl Project { async fn setup_pending_language_server( this: WeakModel, - initialization_options: Option, + override_options: Option, pending_server: PendingLanguageServer, worktree_path: &Path, adapter: Arc, @@ -3190,7 +3181,14 @@ impl Project { } }) .detach(); - + let mut initialization_options = adapter.adapter.initialization_options().await; + match (&mut initialization_options, override_options) { + (Some(initialization_options), Some(override_options)) => { + merge_json_value_into(override_options, initialization_options); + } + (None, override_options) => initialization_options = override_options, + _ => {} + } let language_server = language_server.initialize(initialization_options).await?; language_server diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index a685a82b07..a5fb8671f7 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -981,25 +981,16 @@ impl ProjectPanel { } } - fn open_in_terminal(&mut self, _: &OpenInTerminal, _cx: &mut ViewContext) { - todo!() - // if let Some((worktree, entry)) = self.selected_entry(cx) { - // let window = cx.window(); - // let view_id = cx.view_id(); - // let path = worktree.abs_path().join(&entry.path); - - // cx.app_context() - // .spawn(|mut cx| async move { - // window.dispatch_action( - // view_id, - // &workspace::OpenTerminal { - // working_directory: path, - // }, - // &mut cx, - // ); - // }) - // .detach(); - // } + fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext) { + if let Some((worktree, entry)) = self.selected_entry(cx) { + let path = worktree.abs_path().join(&entry.path); + cx.dispatch_action( + workspace::OpenTerminal { + working_directory: path, + } + .boxed_clone(), + ) + } } pub fn new_search_in_directory( @@ -1414,7 +1405,7 @@ impl ProjectPanel { .child(if let Some(icon) = &icon { div().child(IconElement::from_path(icon.to_string()).color(Color::Muted)) } else { - div() + div().size(IconSize::default().rems()).invisible() }) .child( if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) { diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 578a47f95a..3c2760f720 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -395,6 +395,7 @@ mod tests { language::init(cx); Project::init_settings(cx); workspace::init_settings(cx); + editor::init(cx); }); } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 351558b6bb..3f1cbce1bd 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -223,6 +223,7 @@ impl Render for BufferSearchBar { .gap_2() .border_1() .border_color(editor_border) + .min_w(rems(384. / 16.)) .rounded_lg() .child(IconElement::new(Icon::MagnifyingGlass)) .child(self.render_text_input(&self.query_editor, cx)) @@ -422,89 +423,134 @@ impl ToolbarItemView for BufferSearchBar { } } -impl BufferSearchBar { - fn register(workspace: &mut Workspace) { - workspace.register_action(move |workspace, deploy: &Deploy, cx| { - let pane = workspace.active_pane(); +/// Registrar inverts the dependency between search and it's downstream user, allowing said downstream user to register search action without knowing exactly what those actions are. +pub trait SearchActionsRegistrar { + fn register_handler( + &mut self, + callback: fn(&mut BufferSearchBar, &A, &mut ViewContext), + ); +} - pane.update(cx, |this, cx| { - this.toolbar().update(cx, |this, cx| { +type GetSearchBar = + for<'a, 'b> fn(&'a T, &'a mut ViewContext<'b, T>) -> Option>; + +/// Registers search actions on a div that can be taken out. +pub struct DivRegistrar<'a, 'b, T: 'static> { + div: Option
, + cx: &'a mut ViewContext<'b, T>, + search_getter: GetSearchBar, +} + +impl<'a, 'b, T: 'static> DivRegistrar<'a, 'b, T> { + pub fn new(search_getter: GetSearchBar, cx: &'a mut ViewContext<'b, T>) -> Self { + Self { + div: Some(div()), + cx, + search_getter, + } + } + pub fn into_div(self) -> Div { + // This option is always Some; it's an option in the first place because we want to call methods + // on div that require ownership. + self.div.unwrap() + } +} + +impl SearchActionsRegistrar for DivRegistrar<'_, '_, T> { + fn register_handler( + &mut self, + callback: fn(&mut BufferSearchBar, &A, &mut ViewContext), + ) { + let getter = self.search_getter; + self.div = self.div.take().map(|div| { + div.on_action(self.cx.listener(move |this, action, cx| { + (getter)(this, cx) + .clone() + .map(|search_bar| search_bar.update(cx, |this, cx| callback(this, action, cx))); + })) + }); + } +} + +/// Register actions for an active pane. +impl SearchActionsRegistrar for Workspace { + fn register_handler( + &mut self, + callback: fn(&mut BufferSearchBar, &A, &mut ViewContext), + ) { + self.register_action(move |workspace, action: &A, cx| { + let pane = workspace.active_pane(); + pane.update(cx, move |this, cx| { + this.toolbar().update(cx, move |this, cx| { if let Some(search_bar) = this.item_of_type::() { - search_bar.update(cx, |this, cx| { - this.deploy(deploy, cx); - }); - return; + search_bar.update(cx, move |this, cx| callback(this, action, cx)); + cx.notify(); } - let view = cx.new_view(|cx| BufferSearchBar::new(cx)); - this.add_item(view.clone(), cx); - view.update(cx, |this, cx| this.deploy(deploy, cx)); - cx.notify(); }) }); }); - fn register_action( - workspace: &mut Workspace, - update: fn(&mut BufferSearchBar, &A, &mut ViewContext), - ) { - workspace.register_action(move |workspace, action: &A, cx| { - let pane = workspace.active_pane(); - pane.update(cx, move |this, cx| { - this.toolbar().update(cx, move |this, cx| { - if let Some(search_bar) = this.item_of_type::() { - search_bar.update(cx, move |this, cx| update(this, action, cx)); - cx.notify(); - } - }) - }); - }); - } - - register_action(workspace, |this, action: &ToggleCaseSensitive, cx| { + } +} +impl BufferSearchBar { + pub fn register_inner(registrar: &mut impl SearchActionsRegistrar) { + registrar.register_handler(|this, action: &ToggleCaseSensitive, cx| { if this.supported_options().case { this.toggle_case_sensitive(action, cx); } }); - register_action(workspace, |this, action: &ToggleWholeWord, cx| { + + registrar.register_handler(|this, action: &ToggleWholeWord, cx| { if this.supported_options().word { this.toggle_whole_word(action, cx); } }); - register_action(workspace, |this, action: &ToggleReplace, cx| { + + registrar.register_handler(|this, action: &ToggleReplace, cx| { if this.supported_options().replacement { this.toggle_replace(action, cx); } }); - register_action(workspace, |this, _: &ActivateRegexMode, cx| { + + registrar.register_handler(|this, _: &ActivateRegexMode, cx| { if this.supported_options().regex { this.activate_search_mode(SearchMode::Regex, cx); } }); - register_action(workspace, |this, _: &ActivateTextMode, cx| { + + registrar.register_handler(|this, _: &ActivateTextMode, cx| { this.activate_search_mode(SearchMode::Text, cx); }); - register_action(workspace, |this, action: &CycleMode, cx| { + + registrar.register_handler(|this, action: &CycleMode, cx| { if this.supported_options().regex { // If regex is not supported then search has just one mode (text) - in that case there's no point in supporting // cycling. this.cycle_mode(action, cx) } }); - register_action(workspace, |this, action: &SelectNextMatch, cx| { + + registrar.register_handler(|this, action: &SelectNextMatch, cx| { this.select_next_match(action, cx); }); - register_action(workspace, |this, action: &SelectPrevMatch, cx| { + registrar.register_handler(|this, action: &SelectPrevMatch, cx| { this.select_prev_match(action, cx); }); - register_action(workspace, |this, action: &SelectAllMatches, cx| { + registrar.register_handler(|this, action: &SelectAllMatches, cx| { this.select_all_matches(action, cx); }); - register_action(workspace, |this, _: &editor::Cancel, cx| { + registrar.register_handler(|this, _: &editor::Cancel, cx| { if !this.dismissed { this.dismiss(&Dismiss, cx); return; } cx.propagate(); }); + registrar.register_handler(|this, deploy, cx| { + this.deploy(deploy, cx); + }) + } + fn register(workspace: &mut Workspace) { + Self::register_inner(workspace); } pub fn new(cx: &mut ViewContext) -> Self { let query_editor = cx.new_view(|cx| Editor::single_line(cx)); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 94435e8b76..a370cca9f6 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -38,8 +38,8 @@ use std::{ use theme::ThemeSettings; use ui::{ - h_stack, prelude::*, v_stack, Button, Icon, IconButton, IconElement, Label, LabelCommon, - LabelSize, Selectable, Tooltip, + h_stack, prelude::*, v_stack, Icon, IconButton, IconElement, Label, LabelCommon, LabelSize, + Selectable, ToggleButton, Tooltip, }; use util::{paths::PathMatcher, ResultExt as _}; use workspace::{ @@ -61,12 +61,12 @@ struct ActiveSearches(HashMap, WeakView>); struct ActiveSettings(HashMap, ProjectSearchSettings>); pub fn init(cx: &mut AppContext) { - // todo!() po cx.set_global(ActiveSearches::default()); cx.set_global(ActiveSettings::default()); cx.observe_new_views(|workspace: &mut Workspace, _cx| { workspace - .register_action(ProjectSearchView::deploy) + .register_action(ProjectSearchView::new_search) + .register_action(ProjectSearchView::deploy_search) .register_action(ProjectSearchBar::search_in_new); }) .detach(); @@ -288,7 +288,6 @@ impl Render for ProjectSearchView { .size_full() .track_focus(&self.focus_handle) .child(self.results_editor.clone()) - .into_any() } else { let model = self.model.read(cx); let has_no_results = model.no_results.unwrap_or(false); @@ -365,6 +364,7 @@ impl Render for ProjectSearchView { .flex_1() .size_full() .justify_center() + .bg(cx.theme().colors().editor_background) .track_focus(&self.focus_handle) .child( h_stack() @@ -374,7 +374,6 @@ impl Render for ProjectSearchView { .child(v_stack().child(major_text).children(minor_text)) .child(h_stack().flex_1()), ) - .into_any() } } } @@ -943,11 +942,41 @@ impl ProjectSearchView { }); } + // Re-activate the most recently activated search or the most recent if it has been closed. + // If no search exists in the workspace, create a new one. + fn deploy_search( + workspace: &mut Workspace, + _: &workspace::DeploySearch, + cx: &mut ViewContext, + ) { + let active_search = cx + .global::() + .0 + .get(&workspace.project().downgrade()); + let existing = active_search + .and_then(|active_search| { + workspace + .items_of_type::(cx) + .filter(|search| &search.downgrade() == active_search) + .last() + }) + .or_else(|| workspace.item_of_type::(cx)); + Self::existing_or_new_search(workspace, existing, cx) + } + // Add another search tab to the workspace. - fn deploy( + fn new_search( workspace: &mut Workspace, _: &workspace::NewSearch, cx: &mut ViewContext, + ) { + Self::existing_or_new_search(workspace, None, cx) + } + + fn existing_or_new_search( + workspace: &mut Workspace, + existing: Option>, + cx: &mut ViewContext, ) { // Clean up entries for dropped projects cx.update_global(|state: &mut ActiveSearches, _cx| { @@ -964,19 +993,27 @@ impl ProjectSearchView { } }); - let settings = cx - .global::() - .0 - .get(&workspace.project().downgrade()); - - let settings = if let Some(settings) = settings { - Some(settings.clone()) + let search = if let Some(existing) = existing { + workspace.activate_item(&existing, cx); + existing } else { - None - }; + let settings = cx + .global::() + .0 + .get(&workspace.project().downgrade()); - let model = cx.new_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); - let search = cx.new_view(|cx| ProjectSearchView::new(model, cx, settings)); + let settings = if let Some(settings) = settings { + Some(settings.clone()) + } else { + None + }; + + let model = cx.new_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); + let view = cx.new_view(|cx| ProjectSearchView::new(model, cx, settings)); + + workspace.add_item(Box::new(view.clone()), cx); + view + }; workspace.add_item(Box::new(search.clone()), cx); @@ -1641,20 +1678,26 @@ impl Render for ProjectSearchBar { let mode_column = v_stack().items_start().justify_start().child( h_stack() + .gap_2() .child( h_stack() .child( - Button::new("project-search-text-button", "Text") + ToggleButton::new("project-search-text-button", "Text") + .style(ButtonStyle::Filled) + .size(ButtonSize::Large) .selected(search.current_mode == SearchMode::Text) .on_click(cx.listener(|this, _, cx| { this.activate_search_mode(SearchMode::Text, cx) })) .tooltip(|cx| { Tooltip::for_action("Toggle text search", &ActivateTextMode, cx) - }), + }) + .first(), ) .child( - Button::new("project-search-regex-button", "Regex") + ToggleButton::new("project-search-regex-button", "Regex") + .style(ButtonStyle::Filled) + .size(ButtonSize::Large) .selected(search.current_mode == SearchMode::Regex) .on_click(cx.listener(|this, _, cx| { this.activate_search_mode(SearchMode::Regex, cx) @@ -1665,11 +1708,20 @@ impl Render for ProjectSearchBar { &ActivateRegexMode, cx, ) + }) + .map(|this| { + if semantic_is_available { + this.middle() + } else { + this.last() + } }), ) .when(semantic_is_available, |this| { this.child( - Button::new("project-search-semantic-button", "Semantic") + ToggleButton::new("project-search-semantic-button", "Semantic") + .style(ButtonStyle::Filled) + .size(ButtonSize::Large) .selected(search.current_mode == SearchMode::Semantic) .on_click(cx.listener(|this, _, cx| { this.activate_search_mode(SearchMode::Semantic, cx) @@ -1680,7 +1732,8 @@ impl Render for ProjectSearchBar { &ActivateSemanticMode, cx, ) - }), + }) + .last(), ) }), ) @@ -1831,6 +1884,7 @@ impl Render for ProjectSearchBar { .child( h_stack() .justify_between() + .gap_2() .child(query_column) .child(mode_column) .child(replace_column) @@ -2062,7 +2116,7 @@ pub mod tests { } #[gpui::test] - async fn test_project_search_focus(cx: &mut TestAppContext) { + async fn test_deploy_project_search_focus(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); @@ -2103,7 +2157,237 @@ pub mod tests { .update(cx, |toolbar, cx| toolbar.add_item(search_bar, cx)) }); - ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) + ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch, cx) + }) + .unwrap(); + + let Some(search_view) = cx.read(|cx| { + workspace + .read(cx) + .unwrap() + .active_pane() + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + }) else { + panic!("Search view expected to appear after new search event trigger") + }; + + cx.spawn(|mut cx| async move { + window + .update(&mut cx, |_, cx| { + cx.dispatch_action(ToggleFocus.boxed_clone()) + }) + .unwrap(); + }) + .detach(); + cx.background_executor.run_until_parked(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert!( + search_view.query_editor.focus_handle(cx).is_focused(cx), + "Empty search view should be focused after the toggle focus event: no results panel to focus on", + ); + }); + }).unwrap(); + + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + let query_editor = &search_view.query_editor; + assert!( + query_editor.focus_handle(cx).is_focused(cx), + "Search view should be focused after the new search view is activated", + ); + let query_text = query_editor.read(cx).text(cx); + assert!( + query_text.is_empty(), + "New search query should be empty but got '{query_text}'", + ); + let results_text = search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)); + assert!( + results_text.is_empty(), + "Empty search view should have no results but got '{results_text}'" + ); + }); + }) + .unwrap(); + + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + search_view.query_editor.update(cx, |query_editor, cx| { + query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", cx) + }); + search_view.search(cx); + }); + }) + .unwrap(); + cx.background_executor.run_until_parked(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + let results_text = search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)); + assert!( + results_text.is_empty(), + "Search view for mismatching query should have no results but got '{results_text}'" + ); + assert!( + search_view.query_editor.focus_handle(cx).is_focused(cx), + "Search view should be focused after mismatching query had been used in search", + ); + }); + }).unwrap(); + + cx.spawn(|mut cx| async move { + window.update(&mut cx, |_, cx| { + cx.dispatch_action(ToggleFocus.boxed_clone()) + }) + }) + .detach(); + cx.background_executor.run_until_parked(); + window.update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert!( + search_view.query_editor.focus_handle(cx).is_focused(cx), + "Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on", + ); + }); + }).unwrap(); + + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); + search_view.search(cx); + }); + }) + .unwrap(); + cx.background_executor.run_until_parked(); + window.update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)), + "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", + "Search view results should match the query" + ); + assert!( + search_view.results_editor.focus_handle(cx).is_focused(cx), + "Search view with mismatching query should be focused after search results are available", + ); + }); + }).unwrap(); + cx.spawn(|mut cx| async move { + window + .update(&mut cx, |_, cx| { + cx.dispatch_action(ToggleFocus.boxed_clone()) + }) + .unwrap(); + }) + .detach(); + cx.background_executor.run_until_parked(); + window.update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert!( + search_view.results_editor.focus_handle(cx).is_focused(cx), + "Search view with matching query should still have its results editor focused after the toggle focus event", + ); + }); + }).unwrap(); + + workspace + .update(cx, |workspace, cx| { + ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch, cx) + }) + .unwrap(); + window.update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "two", "Query should be updated to first search result after search view 2nd open in a row"); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)), + "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", + "Results should be unchanged after search view 2nd open in a row" + ); + assert!( + search_view.query_editor.focus_handle(cx).is_focused(cx), + "Focus should be moved into query editor again after search view 2nd open in a row" + ); + }); + }).unwrap(); + + cx.spawn(|mut cx| async move { + window + .update(&mut cx, |_, cx| { + cx.dispatch_action(ToggleFocus.boxed_clone()) + }) + .unwrap(); + }) + .detach(); + cx.background_executor.run_until_parked(); + window.update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert!( + search_view.results_editor.focus_handle(cx).is_focused(cx), + "Search view with matching query should switch focus to the results editor after the toggle focus event", + ); + }); + }).unwrap(); + } + + #[gpui::test] + async fn test_new_project_search_focus(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;", + "three.rs": "const THREE: usize = one::ONE + two::TWO;", + "four.rs": "const FOUR: usize = one::ONE + three::THREE;", + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.clone(); + let search_bar = window.build_view(cx, |_| ProjectSearchBar::new()); + + let active_item = cx.read(|cx| { + workspace + .read(cx) + .unwrap() + .active_pane() + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + }); + assert!( + active_item.is_none(), + "Expected no search panel to be active" + ); + + window + .update(cx, move |workspace, cx| { + assert_eq!(workspace.panes().len(), 1); + workspace.panes()[0].update(cx, move |pane, cx| { + pane.toolbar() + .update(cx, |toolbar, cx| toolbar.add_item(search_bar, cx)) + }); + + ProjectSearchView::new_search(workspace, &workspace::NewSearch, cx) }) .unwrap(); @@ -2252,7 +2536,7 @@ pub mod tests { workspace .update(cx, |workspace, cx| { - ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) + ProjectSearchView::new_search(workspace, &workspace::NewSearch, cx) }) .unwrap(); cx.background_executor.run_until_parked(); @@ -2538,7 +2822,7 @@ pub mod tests { .update(cx, |toolbar, cx| toolbar.add_item(search_bar, cx)) }); - ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) + ProjectSearchView::new_search(workspace, &workspace::NewSearch, cx) } }) .unwrap(); diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index 590079c51b..3a43e3f9dd 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -1,4 +1,4 @@ -use crate::{settings_store::SettingsStore, KeymapFile, Settings}; +use crate::{settings_store::SettingsStore, Settings}; use anyhow::Result; use fs::Fs; use futures::{channel::mpsc, StreamExt}; @@ -77,7 +77,6 @@ pub fn handle_settings_file_changes( }); cx.spawn(move |mut cx| async move { while let Some(user_settings_content) = user_settings_file_rx.next().await { - eprintln!("settings file changed"); let result = cx.update_global(|store: &mut SettingsStore, cx| { store .set_user_settings(&user_settings_content, cx) @@ -121,14 +120,3 @@ pub fn update_settings_file( }) .detach_and_log_err(cx); } - -pub fn load_default_keymap(cx: &mut AppContext) { - for path in ["keymaps/default.json", "keymaps/vim.json"] { - KeymapFile::load_asset(path, cx).unwrap(); - } - - // todo!() - // if let Some(asset_path) = settings::get::(cx).asset_path() { - // KeymapFile::load_asset(asset_path, cx).unwrap(); - // } -} diff --git a/crates/terminal/src/mappings/mouse.rs b/crates/terminal/src/mappings/mouse.rs index a32d83d28d..af3e6b640f 100644 --- a/crates/terminal/src/mappings/mouse.rs +++ b/crates/terminal/src/mappings/mouse.rs @@ -158,39 +158,41 @@ pub fn mouse_moved_report(point: AlacPoint, e: &MouseMoveEvent, mode: TermMode) } } -pub fn mouse_side( +pub fn grid_point(pos: Point, cur_size: TerminalSize, display_offset: usize) -> AlacPoint { + grid_point_and_side(pos, cur_size, display_offset).0 +} + +pub fn grid_point_and_side( pos: Point, cur_size: TerminalSize, -) -> alacritty_terminal::index::Direction { - let cell_width = cur_size.cell_width.floor(); - if cell_width == px(0.) { - return Side::Right; - } - - let x = pos.x.floor(); - - let cell_x = cmp::max(px(0.), x - cell_width) % cell_width; - let half_cell_width = (cur_size.cell_width / 2.0).floor(); - let additional_padding = (cur_size.width() - cur_size.cell_width * 2.) % cur_size.cell_width; - let end_of_grid = cur_size.width() - cur_size.cell_width - additional_padding; - - //Width: Pixels or columns? - if cell_x > half_cell_width - // Edge case when mouse leaves the window. - || x >= end_of_grid - { + display_offset: usize, +) -> (AlacPoint, Side) { + let mut col = GridCol((pos.x / cur_size.cell_width) as usize); + let cell_x = cmp::max(px(0.), pos.x) % cur_size.cell_width; + let half_cell_width = cur_size.cell_width / 2.0; + let mut side = if cell_x > half_cell_width { Side::Right } else { Side::Left - } -} + }; -pub fn grid_point(pos: Point, cur_size: TerminalSize, display_offset: usize) -> AlacPoint { - let col = GridCol((pos.x / cur_size.cell_width) as usize); + if col > cur_size.last_column() { + col = cur_size.last_column(); + side = Side::Right; + } let col = min(col, cur_size.last_column()); - let line = (pos.y / cur_size.line_height) as i32; - let line = min(line, cur_size.bottommost_line().0); - AlacPoint::new(GridLine(line - display_offset as i32), col) + let mut line = (pos.y / cur_size.line_height) as i32; + if line > cur_size.bottommost_line() { + line = cur_size.bottommost_line().0 as i32; + side = Side::Right; + } else if line < 0 { + side = Side::Left; + } + + ( + AlacPoint::new(GridLine(line - display_offset as i32), col), + side, + ) } ///Generate the bytes to send to the terminal, from the cell location, a mouse event, and the terminal mode diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index b15bd7c6d6..d43508bdbe 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -28,7 +28,8 @@ use futures::{ }; use mappings::mouse::{ - alt_scroll, grid_point, mouse_button_report, mouse_moved_report, mouse_side, scroll_report, + alt_scroll, grid_point, grid_point_and_side, mouse_button_report, mouse_moved_report, + scroll_report, }; use procinfo::LocalProcessInfo; @@ -61,15 +62,7 @@ use lazy_static::lazy_static; actions!( terminal, - [ - Clear, - Copy, - Paste, - ShowCharacterPalette, - SearchTest, - SendText, - SendKeystroke, - ] + [Clear, Copy, Paste, ShowCharacterPalette, SearchTest,] ); ///Scrolling is unbearably sluggish by default. Alacritty supports a configurable @@ -704,14 +697,12 @@ impl Terminal { } InternalEvent::UpdateSelection(position) => { if let Some(mut selection) = term.selection.take() { - let point = grid_point( + let (point, side) = grid_point_and_side( *position, self.last_content.size, term.grid().display_offset(), ); - let side = mouse_side(*position, self.last_content.size); - selection.update(point, side); term.selection = Some(selection); @@ -1088,12 +1079,11 @@ impl Terminal { let position = e.position - origin; self.last_mouse_position = Some(position); if self.mouse_mode(e.modifiers.shift) { - let point = grid_point( + let (point, side) = grid_point_and_side( position, self.last_content.size, self.last_content.display_offset, ); - let side = mouse_side(position, self.last_content.size); if self.mouse_changed(point, side) { if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) { @@ -1175,15 +1165,12 @@ impl Terminal { } } else if e.button == MouseButton::Left { let position = e.position - origin; - let point = grid_point( + let (point, side) = grid_point_and_side( position, self.last_content.size, self.last_content.display_offset, ); - // Use .opposite so that selection is inclusive of the cell clicked. - let side = mouse_side(position, self.last_content.size); - let selection_type = match e.click_count { 0 => return, //This is a release 1 => Some(SelectionType::Simple), diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index 110199a8d4..d848460183 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -13,7 +13,7 @@ editor = { path = "../editor" } language = { path = "../language" } gpui = { path = "../gpui" } project = { path = "../project" } -# search = { path = "../search" } +search = { path = "../search" } settings = { path = "../settings" } theme = { path = "../theme" } util = { path = "../util" } diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 8be10f9469..ffdca7d813 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -2,10 +2,11 @@ use editor::{Cursor, HighlightedRange, HighlightedRangeLine}; use gpui::{ div, fill, point, px, red, relative, AnyElement, AsyncWindowContext, AvailableSpace, BorrowWindow, Bounds, DispatchPhase, Element, ElementId, ExternalPaths, FocusHandle, Font, - FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveElement, InteractiveElementState, - Interactivity, IntoElement, LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, - Pixels, PlatformInputHandler, Point, ShapedLine, StatefulInteractiveElement, StyleRefinement, - Styled, TextRun, TextStyle, TextSystem, UnderlineStyle, WhiteSpace, WindowContext, + FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveBounds, InteractiveElement, + InteractiveElementState, Interactivity, IntoElement, LayoutId, Model, ModelContext, + ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels, PlatformInputHandler, Point, + ShapedLine, StatefulInteractiveElement, StyleRefinement, Styled, TextRun, TextStyle, + TextSystem, UnderlineStyle, WhiteSpace, WindowContext, }; use itertools::Itertools; use language::CursorShape; @@ -421,7 +422,7 @@ impl TerminalElement { let rem_size = cx.rem_size(); let font_pixels = text_style.font_size.to_pixels(rem_size); let line_height = font_pixels * line_height.to_pixels(rem_size); - let font_id = cx.text_system().font_id(&text_style.font()).unwrap(); + let font_id = cx.text_system().resolve_font(&text_style.font()); // todo!(do we need to keep this unwrap?) let cell_width = text_system @@ -598,33 +599,48 @@ impl TerminalElement { ) { let focus = self.focus.clone(); let terminal = self.terminal.clone(); + let interactive_bounds = InteractiveBounds { + bounds: bounds.intersect(&cx.content_mask().bounds), + stacking_order: cx.stacking_order().clone(), + }; self.interactivity.on_mouse_down(MouseButton::Left, { let terminal = terminal.clone(); let focus = focus.clone(); move |e, cx| { cx.focus(&focus); - //todo!(context menu) - // v.context_menu.update(cx, |menu, _cx| menu.delay_cancel()); terminal.update(cx, |terminal, cx| { terminal.mouse_down(&e, origin); - cx.notify(); }) } }); - self.interactivity.on_mouse_move({ - let terminal = terminal.clone(); - let focus = focus.clone(); - move |e, cx| { - if e.pressed_button.is_some() && focus.is_focused(cx) && !cx.has_active_drag() { + + cx.on_mouse_event({ + let bounds = bounds.clone(); + let focus = self.focus.clone(); + let terminal = self.terminal.clone(); + move |e: &MouseMoveEvent, phase, cx| { + if phase != DispatchPhase::Bubble || !focus.is_focused(cx) { + return; + } + + if e.pressed_button.is_some() && !cx.has_active_drag() { terminal.update(cx, |terminal, cx| { terminal.mouse_drag(e, origin, bounds); cx.notify(); }) } + + if interactive_bounds.visibly_contains(&e.position, cx) { + terminal.update(cx, |terminal, cx| { + terminal.mouse_move(&e, origin); + cx.notify(); + }) + } } }); + self.interactivity.on_mouse_up( MouseButton::Left, TerminalElement::generic_button_handler( @@ -651,19 +667,6 @@ impl TerminalElement { } } }); - - self.interactivity.on_mouse_move({ - let terminal = terminal.clone(); - let focus = focus.clone(); - move |e, cx| { - if focus.is_focused(cx) { - terminal.update(cx, |terminal, cx| { - terminal.mouse_move(&e, origin); - cx.notify(); - }) - } - } - }); self.interactivity.on_scroll_wheel({ let terminal = terminal.clone(); move |e, cx| { diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 9e193e83b7..32bad620ba 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -3,11 +3,12 @@ use std::{path::PathBuf, sync::Arc}; use crate::TerminalView; use db::kvp::KEY_VALUE_STORE; use gpui::{ - actions, div, serde_json, AppContext, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, + actions, serde_json, AppContext, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle, FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use project::Fs; +use search::{buffer_search::DivRegistrar, BufferSearchBar}; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; use terminal::terminal_settings::{TerminalDockPosition, TerminalSettings}; @@ -52,7 +53,7 @@ pub struct TerminalPanel { impl TerminalPanel { fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { - let terminal_panel = cx.view().clone(); + let terminal_panel = cx.view().downgrade(); let pane = cx.new_view(|cx| { let mut pane = Pane::new( workspace.weak_handle(), @@ -76,14 +77,17 @@ impl TerminalPanel { pane.set_can_navigate(false, cx); pane.display_nav_history_buttons(false); pane.set_render_tab_bar_buttons(cx, move |pane, cx| { + let terminal_panel = terminal_panel.clone(); h_stack() .gap_2() .child( IconButton::new("plus", Icon::Plus) .icon_size(IconSize::Small) - .on_click(cx.listener_for(&terminal_panel, |terminal_panel, _, cx| { - terminal_panel.add_terminal(None, cx); - })) + .on_click(move |_, cx| { + terminal_panel + .update(cx, |panel, cx| panel.add_terminal(None, cx)) + .log_err(); + }) .tooltip(|cx| Tooltip::text("New Terminal", cx)), ) .child({ @@ -101,9 +105,9 @@ impl TerminalPanel { }) .into_any_element() }); - // let buffer_search_bar = cx.build_view(search::BufferSearchBar::new); - // pane.toolbar() - // .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx)); + let buffer_search_bar = cx.new_view(search::BufferSearchBar::new); + pane.toolbar() + .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx)); pane }); let subscriptions = vec![ @@ -329,8 +333,20 @@ impl TerminalPanel { impl EventEmitter for TerminalPanel {} impl Render for TerminalPanel { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - div().size_full().child(self.pane.clone()) + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let mut registrar = DivRegistrar::new( + |panel, cx| { + panel + .pane + .read(cx) + .toolbar() + .read(cx) + .item_of_type::() + }, + cx, + ); + BufferSearchBar::register_inner(&mut registrar); + registrar.into_div().size_full().child(self.pane.clone()) } } @@ -410,11 +426,6 @@ impl Panel for TerminalPanel { "TerminalPanel" } - // todo!() - // fn icon_tooltip(&self) -> (String, Option>) { - // ("Terminal Panel".into(), Some(Box::new(ToggleFocus))) - // } - fn icon(&self, _cx: &WindowContext) -> Option { Some(Icon::Terminal) } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index d4dea29b49..d519523974 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -28,7 +28,7 @@ use workspace::{ item::{BreadcrumbText, Item, ItemEvent}, notifications::NotifyResultExt, register_deserializable_item, - searchable::{SearchEvent, SearchOptions, SearchableItem}, + searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}, CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId, }; @@ -57,7 +57,7 @@ pub struct SendText(String); #[derive(Clone, Debug, Default, Deserialize, PartialEq)] pub struct SendKeystroke(String); -impl_actions!(terminal_view, [SendText, SendKeystroke]); +impl_actions!(terminal, [SendText, SendKeystroke]); pub fn init(cx: &mut AppContext) { terminal_panel::init(cx); @@ -714,10 +714,9 @@ impl Item for TerminalView { false } - // todo!(search) - // fn as_searchable(&self, handle: &View) -> Option> { - // Some(Box::new(handle.clone())) - // } + fn as_searchable(&self, handle: &View) -> Option> { + Some(Box::new(handle.clone())) + } fn breadcrumb_location(&self) -> ToolbarItemLocation { ToolbarItemLocation::PrimaryLeft diff --git a/crates/theme/src/themes/andromeda.rs b/crates/theme/src/themes/andromeda.rs index e46e9cf9d0..effdfb85f9 100644 --- a/crates/theme/src/themes/andromeda.rs +++ b/crates/theme/src/themes/andromeda.rs @@ -20,7 +20,7 @@ pub fn andromeda() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x2b2f39ff).into()), - border_variant: Some(rgba(0x2b2f39ff).into()), + border_variant: Some(rgba(0x252931ff).into()), border_focused: Some(rgba(0x183a34ff).into()), border_selected: Some(rgba(0x183a34ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -59,7 +59,7 @@ pub fn andromeda() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0xf7f7f84c).into()), scrollbar_thumb_hover_background: Some(rgba(0x252931ff).into()), scrollbar_thumb_border: Some(rgba(0x252931ff).into()), - scrollbar_track_background: Some(rgba(0x1e2025ff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0x21232aff).into()), editor_foreground: Some(rgba(0xf7f7f8ff).into()), editor_background: Some(rgba(0x1e2025ff).into()), diff --git a/crates/theme/src/themes/atelier.rs b/crates/theme/src/themes/atelier.rs index 75226c669b..2d05a6b65b 100644 --- a/crates/theme/src/themes/atelier.rs +++ b/crates/theme/src/themes/atelier.rs @@ -21,7 +21,7 @@ pub fn atelier() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x969585ff).into()), - border_variant: Some(rgba(0x969585ff).into()), + border_variant: Some(rgba(0xd1d0c6ff).into()), border_focused: Some(rgba(0xbbddc6ff).into()), border_selected: Some(rgba(0xbbddc6ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -60,7 +60,7 @@ pub fn atelier() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0x22221b4c).into()), scrollbar_thumb_hover_background: Some(rgba(0xd1d0c6ff).into()), scrollbar_thumb_border: Some(rgba(0xd1d0c6ff).into()), - scrollbar_track_background: Some(rgba(0xf4f3ecff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0xedece5ff).into()), editor_foreground: Some(rgba(0x302f27ff).into()), editor_background: Some(rgba(0xf4f3ecff).into()), @@ -486,7 +486,7 @@ pub fn atelier() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x665f5cff).into()), - border_variant: Some(rgba(0x665f5cff).into()), + border_variant: Some(rgba(0x3b3431ff).into()), border_focused: Some(rgba(0x192e5bff).into()), border_selected: Some(rgba(0x192e5bff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -525,7 +525,7 @@ pub fn atelier() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0xf1efee4c).into()), scrollbar_thumb_hover_background: Some(rgba(0x3b3431ff).into()), scrollbar_thumb_border: Some(rgba(0x3b3431ff).into()), - scrollbar_track_background: Some(rgba(0x1b1918ff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0x251f1dff).into()), editor_foreground: Some(rgba(0xe6e2e0ff).into()), editor_background: Some(rgba(0x1b1918ff).into()), @@ -951,7 +951,7 @@ pub fn atelier() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x8b968eff).into()), - border_variant: Some(rgba(0x8b968eff).into()), + border_variant: Some(rgba(0xc8d1cbff).into()), border_focused: Some(rgba(0xbed4d6ff).into()), border_selected: Some(rgba(0xbed4d6ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -990,7 +990,7 @@ pub fn atelier() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0x171c194c).into()), scrollbar_thumb_hover_background: Some(rgba(0xc8d1cbff).into()), scrollbar_thumb_border: Some(rgba(0xc8d1cbff).into()), - scrollbar_track_background: Some(rgba(0xecf4eeff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0xe5ede7ff).into()), editor_foreground: Some(rgba(0x232a25ff).into()), editor_background: Some(rgba(0xecf4eeff).into()), @@ -1416,7 +1416,7 @@ pub fn atelier() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x56505eff).into()), - border_variant: Some(rgba(0x56505eff).into()), + border_variant: Some(rgba(0x332f38ff).into()), border_focused: Some(rgba(0x222953ff).into()), border_selected: Some(rgba(0x222953ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -1455,7 +1455,7 @@ pub fn atelier() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0xefecf44c).into()), scrollbar_thumb_hover_background: Some(rgba(0x332f38ff).into()), scrollbar_thumb_border: Some(rgba(0x332f38ff).into()), - scrollbar_track_background: Some(rgba(0x19171cff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0x201e24ff).into()), editor_foreground: Some(rgba(0xe2dfe7ff).into()), editor_background: Some(rgba(0x19171cff).into()), @@ -1881,7 +1881,7 @@ pub fn atelier() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x5d5c4cff).into()), - border_variant: Some(rgba(0x5d5c4cff).into()), + border_variant: Some(rgba(0x3c3b31ff).into()), border_focused: Some(rgba(0x1c3927ff).into()), border_selected: Some(rgba(0x1c3927ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -1920,7 +1920,7 @@ pub fn atelier() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0xf4f3ec4c).into()), scrollbar_thumb_hover_background: Some(rgba(0x3c3b31ff).into()), scrollbar_thumb_border: Some(rgba(0x3c3b31ff).into()), - scrollbar_track_background: Some(rgba(0x22221bff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0x2a2922ff).into()), editor_foreground: Some(rgba(0xe7e6dfff).into()), editor_background: Some(rgba(0x22221bff).into()), @@ -2346,7 +2346,7 @@ pub fn atelier() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x5c6485ff).into()), - border_variant: Some(rgba(0x5c6485ff).into()), + border_variant: Some(rgba(0x363f62ff).into()), border_focused: Some(rgba(0x203348ff).into()), border_selected: Some(rgba(0x203348ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -2385,7 +2385,7 @@ pub fn atelier() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0xf5f7ff4c).into()), scrollbar_thumb_hover_background: Some(rgba(0x363f62ff).into()), scrollbar_thumb_border: Some(rgba(0x363f62ff).into()), - scrollbar_track_background: Some(rgba(0x202746ff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0x252d4fff).into()), editor_foreground: Some(rgba(0xdfe2f1ff).into()), editor_background: Some(rgba(0x202746ff).into()), @@ -2811,7 +2811,7 @@ pub fn atelier() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x9a9fb6ff).into()), - border_variant: Some(rgba(0x9a9fb6ff).into()), + border_variant: Some(rgba(0xccd0e1ff).into()), border_focused: Some(rgba(0xc2d5efff).into()), border_selected: Some(rgba(0xc2d5efff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -2850,7 +2850,7 @@ pub fn atelier() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0x2027464c).into()), scrollbar_thumb_hover_background: Some(rgba(0xccd0e1ff).into()), scrollbar_thumb_border: Some(rgba(0xccd0e1ff).into()), - scrollbar_track_background: Some(rgba(0xf5f7ffff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0xe9ebf7ff).into()), editor_foreground: Some(rgba(0x293256ff).into()), editor_background: Some(rgba(0xf5f7ffff).into()), @@ -3276,7 +3276,7 @@ pub fn atelier() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x6c695cff).into()), - border_variant: Some(rgba(0x6c695cff).into()), + border_variant: Some(rgba(0x3b3933ff).into()), border_focused: Some(rgba(0x263056ff).into()), border_selected: Some(rgba(0x263056ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -3315,7 +3315,7 @@ pub fn atelier() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0xfefbec4c).into()), scrollbar_thumb_hover_background: Some(rgba(0x3b3933ff).into()), scrollbar_thumb_border: Some(rgba(0x3b3933ff).into()), - scrollbar_track_background: Some(rgba(0x20201dff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0x252521ff).into()), editor_foreground: Some(rgba(0xe8e4cfff).into()), editor_background: Some(rgba(0x20201dff).into()), @@ -3741,7 +3741,7 @@ pub fn atelier() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x5c6c5cff).into()), - border_variant: Some(rgba(0x5c6c5cff).into()), + border_variant: Some(rgba(0x333b33ff).into()), border_focused: Some(rgba(0x102668ff).into()), border_selected: Some(rgba(0x102668ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -3780,7 +3780,7 @@ pub fn atelier() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0xf4fbf44c).into()), scrollbar_thumb_hover_background: Some(rgba(0x333b33ff).into()), scrollbar_thumb_border: Some(rgba(0x333b33ff).into()), - scrollbar_track_background: Some(rgba(0x131513ff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0x1d201dff).into()), editor_foreground: Some(rgba(0xcfe8cfff).into()), editor_background: Some(rgba(0x131513ff).into()), @@ -4206,7 +4206,7 @@ pub fn atelier() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x8f8b96ff).into()), - border_variant: Some(rgba(0x8f8b96ff).into()), + border_variant: Some(rgba(0xcbc8d1ff).into()), border_focused: Some(rgba(0xc9c8f3ff).into()), border_selected: Some(rgba(0xc9c8f3ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -4245,7 +4245,7 @@ pub fn atelier() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0x19171c4c).into()), scrollbar_thumb_hover_background: Some(rgba(0xcbc8d1ff).into()), scrollbar_thumb_border: Some(rgba(0xcbc8d1ff).into()), - scrollbar_track_background: Some(rgba(0xefecf4ff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0xe8e5edff).into()), editor_foreground: Some(rgba(0x26232aff).into()), editor_background: Some(rgba(0xefecf4ff).into()), @@ -4671,7 +4671,7 @@ pub fn atelier() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x564e4eff).into()), - border_variant: Some(rgba(0x564e4eff).into()), + border_variant: Some(rgba(0x352f2fff).into()), border_focused: Some(rgba(0x2c2b45ff).into()), border_selected: Some(rgba(0x2c2b45ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -4710,7 +4710,7 @@ pub fn atelier() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0xf4ecec4c).into()), scrollbar_thumb_hover_background: Some(rgba(0x352f2fff).into()), scrollbar_thumb_border: Some(rgba(0x352f2fff).into()), - scrollbar_track_background: Some(rgba(0x1b1818ff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0x231f1fff).into()), editor_foreground: Some(rgba(0xe7dfdfff).into()), editor_background: Some(rgba(0x1b1818ff).into()), @@ -5136,7 +5136,7 @@ pub fn atelier() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x675b67ff).into()), - border_variant: Some(rgba(0x675b67ff).into()), + border_variant: Some(rgba(0x393239ff).into()), border_focused: Some(rgba(0x1a2961ff).into()), border_selected: Some(rgba(0x1a2961ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -5175,7 +5175,7 @@ pub fn atelier() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0xf7f3f74c).into()), scrollbar_thumb_hover_background: Some(rgba(0x393239ff).into()), scrollbar_thumb_border: Some(rgba(0x393239ff).into()), - scrollbar_track_background: Some(rgba(0x1b181bff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0x231e23ff).into()), editor_foreground: Some(rgba(0xd8cad8ff).into()), editor_background: Some(rgba(0x1b181bff).into()), @@ -5601,7 +5601,7 @@ pub fn atelier() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x4f6b78ff).into()), - border_variant: Some(rgba(0x4f6b78ff).into()), + border_variant: Some(rgba(0x2c3b42ff).into()), border_focused: Some(rgba(0x1a2f3cff).into()), border_selected: Some(rgba(0x1a2f3cff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -5640,7 +5640,7 @@ pub fn atelier() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0xebf8ff4c).into()), scrollbar_thumb_hover_background: Some(rgba(0x2c3b42ff).into()), scrollbar_thumb_border: Some(rgba(0x2c3b42ff).into()), - scrollbar_track_background: Some(rgba(0x161b1dff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0x1b2327ff).into()), editor_foreground: Some(rgba(0xc1e4f6ff).into()), editor_background: Some(rgba(0x161b1dff).into()), @@ -6066,7 +6066,7 @@ pub fn atelier() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0xaaa3a1ff).into()), - border_variant: Some(rgba(0xaaa3a1ff).into()), + border_variant: Some(rgba(0xd6d1cfff).into()), border_focused: Some(rgba(0xc6cef7ff).into()), border_selected: Some(rgba(0xc6cef7ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -6105,7 +6105,7 @@ pub fn atelier() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0x1b19184c).into()), scrollbar_thumb_hover_background: Some(rgba(0xd6d1cfff).into()), scrollbar_thumb_border: Some(rgba(0xd6d1cfff).into()), - scrollbar_track_background: Some(rgba(0xf1efeeff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0xebe8e6ff).into()), editor_foreground: Some(rgba(0x2c2421ff).into()), editor_background: Some(rgba(0xf1efeeff).into()), @@ -6531,7 +6531,7 @@ pub fn atelier() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0xa8a48eff).into()), - border_variant: Some(rgba(0xa8a48eff).into()), + border_variant: Some(rgba(0xd7d3beff).into()), border_focused: Some(rgba(0xcdd1f5ff).into()), border_selected: Some(rgba(0xcdd1f5ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -6570,7 +6570,7 @@ pub fn atelier() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0x20201d4c).into()), scrollbar_thumb_hover_background: Some(rgba(0xd7d3beff).into()), scrollbar_thumb_border: Some(rgba(0xd7d3beff).into()), - scrollbar_track_background: Some(rgba(0xfefbecff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0xf2eedcff).into()), editor_foreground: Some(rgba(0x292824ff).into()), editor_background: Some(rgba(0xfefbecff).into()), @@ -6996,7 +6996,7 @@ pub fn atelier() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x8e8989ff).into()), - border_variant: Some(rgba(0x8e8989ff).into()), + border_variant: Some(rgba(0xcfc7c7ff).into()), border_focused: Some(rgba(0xcecaecff).into()), border_selected: Some(rgba(0xcecaecff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -7035,7 +7035,7 @@ pub fn atelier() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0x1b18184c).into()), scrollbar_thumb_hover_background: Some(rgba(0xcfc7c7ff).into()), scrollbar_thumb_border: Some(rgba(0xcfc7c7ff).into()), - scrollbar_track_background: Some(rgba(0xf4ececff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0xede5e5ff).into()), editor_foreground: Some(rgba(0x292424ff).into()), editor_background: Some(rgba(0xf4ececff).into()), @@ -7461,7 +7461,7 @@ pub fn atelier() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x8ea88eff).into()), - border_variant: Some(rgba(0x8ea88eff).into()), + border_variant: Some(rgba(0xbed7beff).into()), border_focused: Some(rgba(0xc9c4fdff).into()), border_selected: Some(rgba(0xc9c4fdff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -7500,7 +7500,7 @@ pub fn atelier() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0x1315134c).into()), scrollbar_thumb_hover_background: Some(rgba(0xbed7beff).into()), scrollbar_thumb_border: Some(rgba(0xbed7beff).into()), - scrollbar_track_background: Some(rgba(0xf4fbf4ff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0xdff0dfff).into()), editor_foreground: Some(rgba(0x242924ff).into()), editor_background: Some(rgba(0xf4fbf4ff).into()), @@ -7926,7 +7926,7 @@ pub fn atelier() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x505e55ff).into()), - border_variant: Some(rgba(0x505e55ff).into()), + border_variant: Some(rgba(0x2f3832ff).into()), border_focused: Some(rgba(0x1f3233ff).into()), border_selected: Some(rgba(0x1f3233ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -7965,7 +7965,7 @@ pub fn atelier() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0xecf4ee4c).into()), scrollbar_thumb_hover_background: Some(rgba(0x2f3832ff).into()), scrollbar_thumb_border: Some(rgba(0x2f3832ff).into()), - scrollbar_track_background: Some(rgba(0x171c19ff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0x1e2420ff).into()), editor_foreground: Some(rgba(0xdfe7e2ff).into()), editor_background: Some(rgba(0x171c19ff).into()), @@ -8391,7 +8391,7 @@ pub fn atelier() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0xad9dadff).into()), - border_variant: Some(rgba(0xad9dadff).into()), + border_variant: Some(rgba(0xcdbecdff).into()), border_focused: Some(rgba(0xcac7faff).into()), border_selected: Some(rgba(0xcac7faff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -8430,7 +8430,7 @@ pub fn atelier() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0x1b181b4c).into()), scrollbar_thumb_hover_background: Some(rgba(0xcdbecdff).into()), scrollbar_thumb_border: Some(rgba(0xcdbecdff).into()), - scrollbar_track_background: Some(rgba(0xf7f3f7ff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0xe5dce5ff).into()), editor_foreground: Some(rgba(0x292329ff).into()), editor_background: Some(rgba(0xf7f3f7ff).into()), @@ -8856,7 +8856,7 @@ pub fn atelier() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x80a4b6ff).into()), - border_variant: Some(rgba(0x80a4b6ff).into()), + border_variant: Some(rgba(0xb0d3e5ff).into()), border_focused: Some(rgba(0xbacfe1ff).into()), border_selected: Some(rgba(0xbacfe1ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -8895,7 +8895,7 @@ pub fn atelier() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0x161b1d4c).into()), scrollbar_thumb_hover_background: Some(rgba(0xb0d3e5ff).into()), scrollbar_thumb_border: Some(rgba(0xb0d3e5ff).into()), - scrollbar_track_background: Some(rgba(0xebf8ffff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0xd3edfaff).into()), editor_foreground: Some(rgba(0x1f292eff).into()), editor_background: Some(rgba(0xebf8ffff).into()), diff --git a/crates/theme/src/themes/ayu.rs b/crates/theme/src/themes/ayu.rs index 9029a01e25..a0402825c1 100644 --- a/crates/theme/src/themes/ayu.rs +++ b/crates/theme/src/themes/ayu.rs @@ -21,7 +21,7 @@ pub fn ayu() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x3f4043ff).into()), - border_variant: Some(rgba(0x3f4043ff).into()), + border_variant: Some(rgba(0x2d2f34ff).into()), border_focused: Some(rgba(0x1b4a6eff).into()), border_selected: Some(rgba(0x1b4a6eff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -60,7 +60,7 @@ pub fn ayu() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0xbfbdb64c).into()), scrollbar_thumb_hover_background: Some(rgba(0x2d2f34ff).into()), scrollbar_thumb_border: Some(rgba(0x2d2f34ff).into()), - scrollbar_track_background: Some(rgba(0x0d1017ff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0x1b1e24ff).into()), editor_foreground: Some(rgba(0xbfbdb6ff).into()), editor_background: Some(rgba(0x0d1017ff).into()), @@ -465,7 +465,7 @@ pub fn ayu() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0xcfd1d2ff).into()), - border_variant: Some(rgba(0xcfd1d2ff).into()), + border_variant: Some(rgba(0xdfe0e1ff).into()), border_focused: Some(rgba(0xc4daf6ff).into()), border_selected: Some(rgba(0xc4daf6ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -504,7 +504,7 @@ pub fn ayu() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0x5c61664c).into()), scrollbar_thumb_hover_background: Some(rgba(0xdfe0e1ff).into()), scrollbar_thumb_border: Some(rgba(0xdfe0e1ff).into()), - scrollbar_track_background: Some(rgba(0xfcfcfcff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0xefeff0ff).into()), editor_foreground: Some(rgba(0x5c6166ff).into()), editor_background: Some(rgba(0xfcfcfcff).into()), @@ -909,7 +909,7 @@ pub fn ayu() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x53565dff).into()), - border_variant: Some(rgba(0x53565dff).into()), + border_variant: Some(rgba(0x43464fff).into()), border_focused: Some(rgba(0x24556fff).into()), border_selected: Some(rgba(0x24556fff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -948,7 +948,7 @@ pub fn ayu() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0xcccac24c).into()), scrollbar_thumb_hover_background: Some(rgba(0x43464fff).into()), scrollbar_thumb_border: Some(rgba(0x43464fff).into()), - scrollbar_track_background: Some(rgba(0x242936ff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0x323641ff).into()), editor_foreground: Some(rgba(0xcccac2ff).into()), editor_background: Some(rgba(0x242936ff).into()), diff --git a/crates/theme/src/themes/gruvbox.rs b/crates/theme/src/themes/gruvbox.rs index 615c29dea8..b6b76bf472 100644 --- a/crates/theme/src/themes/gruvbox.rs +++ b/crates/theme/src/themes/gruvbox.rs @@ -21,7 +21,7 @@ pub fn gruvbox() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0xc9b99aff).into()), - border_variant: Some(rgba(0xc9b99aff).into()), + border_variant: Some(rgba(0xddcca7ff).into()), border_focused: Some(rgba(0xaec6cdff).into()), border_selected: Some(rgba(0xaec6cdff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -60,7 +60,7 @@ pub fn gruvbox() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0x2828284c).into()), scrollbar_thumb_hover_background: Some(rgba(0xddcca7ff).into()), scrollbar_thumb_border: Some(rgba(0xddcca7ff).into()), - scrollbar_track_background: Some(rgba(0xf9f5d7ff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0xefe2bcff).into()), editor_foreground: Some(rgba(0x282828ff).into()), editor_background: Some(rgba(0xf9f5d7ff).into()), @@ -472,7 +472,7 @@ pub fn gruvbox() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x5b534dff).into()), - border_variant: Some(rgba(0x5b534dff).into()), + border_variant: Some(rgba(0x494340ff).into()), border_focused: Some(rgba(0x303a36ff).into()), border_selected: Some(rgba(0x303a36ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -511,7 +511,7 @@ pub fn gruvbox() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0xfbf1c74c).into()), scrollbar_thumb_hover_background: Some(rgba(0x494340ff).into()), scrollbar_thumb_border: Some(rgba(0x494340ff).into()), - scrollbar_track_background: Some(rgba(0x32302fff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0x393634ff).into()), editor_foreground: Some(rgba(0xebdbb2ff).into()), editor_background: Some(rgba(0x32302fff).into()), @@ -923,7 +923,7 @@ pub fn gruvbox() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0xc9b99aff).into()), - border_variant: Some(rgba(0xc9b99aff).into()), + border_variant: Some(rgba(0xddcca7ff).into()), border_focused: Some(rgba(0xaec6cdff).into()), border_selected: Some(rgba(0xaec6cdff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -962,7 +962,7 @@ pub fn gruvbox() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0x2828284c).into()), scrollbar_thumb_hover_background: Some(rgba(0xddcca7ff).into()), scrollbar_thumb_border: Some(rgba(0xddcca7ff).into()), - scrollbar_track_background: Some(rgba(0xfbf1c7ff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0xefe1b8ff).into()), editor_foreground: Some(rgba(0x282828ff).into()), editor_background: Some(rgba(0xfbf1c7ff).into()), @@ -1374,7 +1374,7 @@ pub fn gruvbox() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x5b534dff).into()), - border_variant: Some(rgba(0x5b534dff).into()), + border_variant: Some(rgba(0x494340ff).into()), border_focused: Some(rgba(0x303a36ff).into()), border_selected: Some(rgba(0x303a36ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -1413,7 +1413,7 @@ pub fn gruvbox() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0xfbf1c74c).into()), scrollbar_thumb_hover_background: Some(rgba(0x494340ff).into()), scrollbar_thumb_border: Some(rgba(0x494340ff).into()), - scrollbar_track_background: Some(rgba(0x282828ff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0x373432ff).into()), editor_foreground: Some(rgba(0xebdbb2ff).into()), editor_background: Some(rgba(0x282828ff).into()), @@ -1825,7 +1825,7 @@ pub fn gruvbox() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0xc9b99aff).into()), - border_variant: Some(rgba(0xc9b99aff).into()), + border_variant: Some(rgba(0xddcca7ff).into()), border_focused: Some(rgba(0xaec6cdff).into()), border_selected: Some(rgba(0xaec6cdff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -1864,7 +1864,7 @@ pub fn gruvbox() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0x2828284c).into()), scrollbar_thumb_hover_background: Some(rgba(0xddcca7ff).into()), scrollbar_thumb_border: Some(rgba(0xddcca7ff).into()), - scrollbar_track_background: Some(rgba(0xf2e5bcff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0xeddeb5ff).into()), editor_foreground: Some(rgba(0x282828ff).into()), editor_background: Some(rgba(0xf2e5bcff).into()), @@ -2276,7 +2276,7 @@ pub fn gruvbox() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x5b534dff).into()), - border_variant: Some(rgba(0x5b534dff).into()), + border_variant: Some(rgba(0x494340ff).into()), border_focused: Some(rgba(0x303a36ff).into()), border_selected: Some(rgba(0x303a36ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -2315,7 +2315,7 @@ pub fn gruvbox() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0xfbf1c74c).into()), scrollbar_thumb_hover_background: Some(rgba(0x494340ff).into()), scrollbar_thumb_border: Some(rgba(0x494340ff).into()), - scrollbar_track_background: Some(rgba(0x1d2021ff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0x343130ff).into()), editor_foreground: Some(rgba(0xebdbb2ff).into()), editor_background: Some(rgba(0x1d2021ff).into()), diff --git a/crates/theme/src/themes/one.rs b/crates/theme/src/themes/one.rs index 12fd7e9b66..c941fb294d 100644 --- a/crates/theme/src/themes/one.rs +++ b/crates/theme/src/themes/one.rs @@ -21,7 +21,7 @@ pub fn one() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0xc9c9caff).into()), - border_variant: Some(rgba(0xc9c9caff).into()), + border_variant: Some(rgba(0xdfdfe0ff).into()), border_focused: Some(rgba(0xcbcdf6ff).into()), border_selected: Some(rgba(0xcbcdf6ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -60,7 +60,7 @@ pub fn one() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0x383a414c).into()), scrollbar_thumb_hover_background: Some(rgba(0xdfdfe0ff).into()), scrollbar_thumb_border: Some(rgba(0xdfdfe0ff).into()), - scrollbar_track_background: Some(rgba(0xfafafaff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0xeeeeeeff).into()), editor_foreground: Some(rgba(0x383a41ff).into()), editor_background: Some(rgba(0xfafafaff).into()), @@ -472,7 +472,7 @@ pub fn one() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x464b57ff).into()), - border_variant: Some(rgba(0x464b57ff).into()), + border_variant: Some(rgba(0x363c46ff).into()), border_focused: Some(rgba(0x293c5bff).into()), border_selected: Some(rgba(0x293c5bff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -511,7 +511,7 @@ pub fn one() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0xc8ccd44c).into()), scrollbar_thumb_hover_background: Some(rgba(0x363c46ff).into()), scrollbar_thumb_border: Some(rgba(0x363c46ff).into()), - scrollbar_track_background: Some(rgba(0x282c34ff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0x2e333cff).into()), editor_foreground: Some(rgba(0xacb2beff).into()), editor_background: Some(rgba(0x282c34ff).into()), diff --git a/crates/theme/src/themes/rose_pine.rs b/crates/theme/src/themes/rose_pine.rs index 966d6e5e62..59b8a92437 100644 --- a/crates/theme/src/themes/rose_pine.rs +++ b/crates/theme/src/themes/rose_pine.rs @@ -21,7 +21,7 @@ pub fn rose_pine() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0xdcd6d5ff).into()), - border_variant: Some(rgba(0xdcd6d5ff).into()), + border_variant: Some(rgba(0xe5e0dfff).into()), border_focused: Some(rgba(0xc3d7dbff).into()), border_selected: Some(rgba(0xc3d7dbff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -60,7 +60,7 @@ pub fn rose_pine() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0x5752794c).into()), scrollbar_thumb_hover_background: Some(rgba(0xe5e0dfff).into()), scrollbar_thumb_border: Some(rgba(0xe5e0dfff).into()), - scrollbar_track_background: Some(rgba(0xfaf4edff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0xfdf8f1ff).into()), editor_foreground: Some(rgba(0x575279ff).into()), editor_background: Some(rgba(0xfaf4edff).into()), @@ -479,7 +479,7 @@ pub fn rose_pine() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x504c68ff).into()), - border_variant: Some(rgba(0x504c68ff).into()), + border_variant: Some(rgba(0x322f48ff).into()), border_focused: Some(rgba(0x435255ff).into()), border_selected: Some(rgba(0x435255ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -518,7 +518,7 @@ pub fn rose_pine() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0xe0def44c).into()), scrollbar_thumb_hover_background: Some(rgba(0x322f48ff).into()), scrollbar_thumb_border: Some(rgba(0x322f48ff).into()), - scrollbar_track_background: Some(rgba(0x232136ff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0x27243bff).into()), editor_foreground: Some(rgba(0xe0def4ff).into()), editor_background: Some(rgba(0x232136ff).into()), @@ -937,7 +937,7 @@ pub fn rose_pine() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x423f55ff).into()), - border_variant: Some(rgba(0x423f55ff).into()), + border_variant: Some(rgba(0x232132ff).into()), border_focused: Some(rgba(0x435255ff).into()), border_selected: Some(rgba(0x435255ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -976,7 +976,7 @@ pub fn rose_pine() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0xe0def44c).into()), scrollbar_thumb_hover_background: Some(rgba(0x232132ff).into()), scrollbar_thumb_border: Some(rgba(0x232132ff).into()), - scrollbar_track_background: Some(rgba(0x191724ff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0x1c1a29ff).into()), editor_foreground: Some(rgba(0xe0def4ff).into()), editor_background: Some(rgba(0x191724ff).into()), diff --git a/crates/theme/src/themes/sandcastle.rs b/crates/theme/src/themes/sandcastle.rs index 9ca7a9dff2..bace9e936f 100644 --- a/crates/theme/src/themes/sandcastle.rs +++ b/crates/theme/src/themes/sandcastle.rs @@ -20,7 +20,7 @@ pub fn sandcastle() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x3d4350ff).into()), - border_variant: Some(rgba(0x3d4350ff).into()), + border_variant: Some(rgba(0x313741ff).into()), border_focused: Some(rgba(0x223232ff).into()), border_selected: Some(rgba(0x223232ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -59,7 +59,7 @@ pub fn sandcastle() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0xfdf4c14c).into()), scrollbar_thumb_hover_background: Some(rgba(0x313741ff).into()), scrollbar_thumb_border: Some(rgba(0x313741ff).into()), - scrollbar_track_background: Some(rgba(0x282c34ff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0x2a2f38ff).into()), editor_foreground: Some(rgba(0xfdf4c1ff).into()), editor_background: Some(rgba(0x282c34ff).into()), diff --git a/crates/theme/src/themes/solarized.rs b/crates/theme/src/themes/solarized.rs index 945d99ed5b..c925785b17 100644 --- a/crates/theme/src/themes/solarized.rs +++ b/crates/theme/src/themes/solarized.rs @@ -21,7 +21,7 @@ pub fn solarized() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x9faaa8ff).into()), - border_variant: Some(rgba(0x9faaa8ff).into()), + border_variant: Some(rgba(0xdcdacbff).into()), border_focused: Some(rgba(0xbfd3efff).into()), border_selected: Some(rgba(0xbfd3efff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -60,7 +60,7 @@ pub fn solarized() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0x002b364c).into()), scrollbar_thumb_hover_background: Some(rgba(0xdcdacbff).into()), scrollbar_thumb_border: Some(rgba(0xdcdacbff).into()), - scrollbar_track_background: Some(rgba(0xfdf6e3ff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0xf5eedbff).into()), editor_foreground: Some(rgba(0x002b36ff).into()), editor_background: Some(rgba(0xfdf6e3ff).into()), @@ -465,7 +465,7 @@ pub fn solarized() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x2b4f58ff).into()), - border_variant: Some(rgba(0x2b4f58ff).into()), + border_variant: Some(rgba(0x063541ff).into()), border_focused: Some(rgba(0x1c3249ff).into()), border_selected: Some(rgba(0x1c3249ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -504,7 +504,7 @@ pub fn solarized() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0xfdf6e34c).into()), scrollbar_thumb_hover_background: Some(rgba(0x063541ff).into()), scrollbar_thumb_border: Some(rgba(0x063541ff).into()), - scrollbar_track_background: Some(rgba(0x002b36ff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0x032f3bff).into()), editor_foreground: Some(rgba(0xfdf6e3ff).into()), editor_background: Some(rgba(0x002b36ff).into()), diff --git a/crates/theme/src/themes/summercamp.rs b/crates/theme/src/themes/summercamp.rs index 963608d5db..2ee5f12394 100644 --- a/crates/theme/src/themes/summercamp.rs +++ b/crates/theme/src/themes/summercamp.rs @@ -20,7 +20,7 @@ pub fn summercamp() -> UserThemeFamily { styles: UserThemeStylesRefinement { colors: ThemeColorsRefinement { border: Some(rgba(0x312d21ff).into()), - border_variant: Some(rgba(0x312d21ff).into()), + border_variant: Some(rgba(0x29251bff).into()), border_focused: Some(rgba(0x193761ff).into()), border_selected: Some(rgba(0x193761ff).into()), border_transparent: Some(rgba(0x00000000).into()), @@ -59,7 +59,7 @@ pub fn summercamp() -> UserThemeFamily { scrollbar_thumb_background: Some(rgba(0xf8f5de4c).into()), scrollbar_thumb_hover_background: Some(rgba(0x29251bff).into()), scrollbar_thumb_border: Some(rgba(0x29251bff).into()), - scrollbar_track_background: Some(rgba(0x1c1810ff).into()), + scrollbar_track_background: Some(rgba(0x00000000).into()), scrollbar_track_border: Some(rgba(0x221e15ff).into()), editor_foreground: Some(rgba(0xf8f5deff).into()), editor_background: Some(rgba(0x1c1810ff).into()), diff --git a/crates/theme_importer/src/zed1/converter.rs b/crates/theme_importer/src/zed1/converter.rs index 45563971d1..86c40dfde5 100644 --- a/crates/theme_importer/src/zed1/converter.rs +++ b/crates/theme_importer/src/zed1/converter.rs @@ -187,7 +187,7 @@ impl Zed1ThemeConverter { Ok(ThemeColorsRefinement { border: convert(lowest.base.default.border), - border_variant: convert(lowest.variant.default.border), + border_variant: convert(middle.variant.default.border), border_focused: convert(lowest.accent.hovered.border), border_selected: convert(lowest.accent.default.border), border_transparent: Some(gpui::transparent_black()), @@ -230,7 +230,7 @@ impl Zed1ThemeConverter { .map(|color| color_alpha(color, 0.3)), scrollbar_thumb_hover_background: convert(middle.base.hovered.background), scrollbar_thumb_border: convert(middle.base.default.border), - scrollbar_track_background: convert(highest.base.default.background), + scrollbar_track_background: Some(gpui::transparent_black()), scrollbar_track_border: convert(highest.variant.default.border), editor_foreground: convert(editor.text_color), editor_background: convert(editor.background), diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 44f2069694..cfb98ccd74 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -1,4 +1,4 @@ -use client::{telemetry::Telemetry, TelemetrySettings}; +use client::telemetry::Telemetry; use feature_flags::FeatureFlagAppExt; use fs::Fs; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; @@ -7,7 +7,7 @@ use gpui::{ VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; -use settings::{update_settings_file, Settings, SettingsStore}; +use settings::{update_settings_file, SettingsStore}; use std::sync::Arc; use theme::{Theme, ThemeMeta, ThemeRegistry, ThemeSettings}; use ui::{prelude::*, v_stack, ListItem, ListItemSpacing}; @@ -181,9 +181,8 @@ impl PickerDelegate for ThemeSelectorDelegate { let theme_name = cx.theme().name.clone(); - let telemetry_settings = TelemetrySettings::get_global(cx).clone(); self.telemetry - .report_setting_event(telemetry_settings, "theme", theme_name.to_string()); + .report_setting_event("theme", theme_name.to_string(), cx); update_settings_file::(self.fs.clone(), cx, move |settings| { settings.theme = Some(theme_name.to_string()); diff --git a/crates/ui/docs/todo.md b/crates/ui/docs/todo.md deleted file mode 100644 index e7a053ecf4..0000000000 --- a/crates/ui/docs/todo.md +++ /dev/null @@ -1,25 +0,0 @@ -## Documentation priorities: - -These are the priorities to get documented, in a rough stack rank order: - -- [ ] label -- [ ] button -- [ ] icon_button -- [ ] icon -- [ ] list -- [ ] avatar -- [ ] panel -- [ ] modal -- [ ] palette -- [ ] input -- [ ] facepile -- [ ] player -- [ ] stacks -- [ ] context menu -- [ ] input -- [ ] textarea/multiline input (not built - not an editor) -- [ ] indicator -- [ ] public actor -- [ ] keybinding -- [ ] tab -- [ ] toast diff --git a/crates/ui/src/components/button/button.rs b/crates/ui/src/components/button/button.rs index 958aa66ede..1e60aae03b 100644 --- a/crates/ui/src/components/button/button.rs +++ b/crates/ui/src/components/button/button.rs @@ -92,6 +92,13 @@ impl Selectable for Button { } } +impl SelectableButton for Button { + fn selected_style(mut self, style: ButtonStyle) -> Self { + self.base = self.base.selected_style(style); + self + } +} + impl Disableable for Button { fn disabled(mut self, disabled: bool) -> Self { self.base = self.base.disabled(disabled); diff --git a/crates/ui/src/components/button/button_icon.rs b/crates/ui/src/components/button/button_icon.rs index 29b23747b2..15538bb24d 100644 --- a/crates/ui/src/components/button/button_icon.rs +++ b/crates/ui/src/components/button/button_icon.rs @@ -12,6 +12,7 @@ pub(super) struct ButtonIcon { disabled: bool, selected: bool, selected_icon: Option, + selected_style: Option, } impl ButtonIcon { @@ -23,6 +24,7 @@ impl ButtonIcon { disabled: false, selected: false, selected_icon: None, + selected_style: None, } } @@ -62,6 +64,13 @@ impl Selectable for ButtonIcon { } } +impl SelectableButton for ButtonIcon { + fn selected_style(mut self, style: ButtonStyle) -> Self { + self.selected_style = Some(style); + self + } +} + impl RenderOnce for ButtonIcon { fn render(self, _cx: &mut WindowContext) -> impl IntoElement { let icon = self @@ -71,6 +80,8 @@ impl RenderOnce for ButtonIcon { let icon_color = if self.disabled { Color::Disabled + } else if self.selected_style.is_some() && self.selected { + self.selected_style.unwrap().into() } else if self.selected { Color::Selected } else { diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index f255104432..431286073f 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -4,6 +4,10 @@ use smallvec::SmallVec; use crate::prelude::*; +pub trait SelectableButton: Selectable { + fn selected_style(self, style: ButtonStyle) -> Self; +} + pub trait ButtonCommon: Clickable + Disableable { /// A unique element ID to identify the button. fn id(&self) -> &ElementId; @@ -36,17 +40,68 @@ pub enum IconPosition { End, } +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] +pub enum TintColor { + #[default] + Accent, + Negative, + Warning, +} + +impl TintColor { + fn button_like_style(self, cx: &mut WindowContext) -> ButtonLikeStyles { + match self { + TintColor::Accent => ButtonLikeStyles { + background: cx.theme().status().info_background, + border_color: cx.theme().status().info_border, + label_color: cx.theme().colors().text, + icon_color: cx.theme().colors().text, + }, + TintColor::Negative => ButtonLikeStyles { + background: cx.theme().status().error_background, + border_color: cx.theme().status().error_border, + label_color: cx.theme().colors().text, + icon_color: cx.theme().colors().text, + }, + TintColor::Warning => ButtonLikeStyles { + background: cx.theme().status().warning_background, + border_color: cx.theme().status().warning_border, + label_color: cx.theme().colors().text, + icon_color: cx.theme().colors().text, + }, + } + } +} + +impl From for Color { + fn from(tint: TintColor) -> Self { + match tint { + TintColor::Accent => Color::Accent, + TintColor::Negative => Color::Error, + TintColor::Warning => Color::Warning, + } + } +} + +// Used to go from ButtonStyle -> Color through tint colors. +impl From for Color { + fn from(style: ButtonStyle) -> Self { + match style { + ButtonStyle::Tinted(tint) => tint.into(), + _ => Color::Default, + } + } +} + #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] pub enum ButtonStyle { /// A filled button with a solid background color. Provides emphasis versus /// the more common subtle button. Filled, - /// 🚧 Under construction 🚧 - /// /// Used to emphasize a button in some way, like a selected state, or a semantic /// coloring like an error or success button. - Tinted, + Tinted(TintColor), /// The default button style, used for most buttons. Has a transparent background, /// but has a background color to indicate states like hover and active. @@ -86,12 +141,7 @@ impl ButtonStyle { label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, - ButtonStyle::Tinted => ButtonLikeStyles { - background: gpui::red(), - border_color: gpui::red(), - label_color: gpui::red(), - icon_color: gpui::red(), - }, + ButtonStyle::Tinted(tint) => tint.button_like_style(cx), ButtonStyle::Subtle => ButtonLikeStyles { background: cx.theme().colors().ghost_element_background, border_color: transparent_black(), @@ -115,12 +165,7 @@ impl ButtonStyle { label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, - ButtonStyle::Tinted => ButtonLikeStyles { - background: gpui::red(), - border_color: gpui::red(), - label_color: gpui::red(), - icon_color: gpui::red(), - }, + ButtonStyle::Tinted(tint) => tint.button_like_style(cx), ButtonStyle::Subtle => ButtonLikeStyles { background: cx.theme().colors().ghost_element_hover, border_color: transparent_black(), @@ -146,12 +191,7 @@ impl ButtonStyle { label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, - ButtonStyle::Tinted => ButtonLikeStyles { - background: gpui::red(), - border_color: gpui::red(), - label_color: gpui::red(), - icon_color: gpui::red(), - }, + ButtonStyle::Tinted(tint) => tint.button_like_style(cx), ButtonStyle::Subtle => ButtonLikeStyles { background: cx.theme().colors().ghost_element_active, border_color: transparent_black(), @@ -178,12 +218,7 @@ impl ButtonStyle { label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, - ButtonStyle::Tinted => ButtonLikeStyles { - background: gpui::red(), - border_color: gpui::red(), - label_color: gpui::red(), - icon_color: gpui::red(), - }, + ButtonStyle::Tinted(tint) => tint.button_like_style(cx), ButtonStyle::Subtle => ButtonLikeStyles { background: cx.theme().colors().ghost_element_background, border_color: cx.theme().colors().border_focused, @@ -208,12 +243,7 @@ impl ButtonStyle { label_color: Color::Disabled.color(cx), icon_color: Color::Disabled.color(cx), }, - ButtonStyle::Tinted => ButtonLikeStyles { - background: gpui::red(), - border_color: gpui::red(), - label_color: gpui::red(), - icon_color: gpui::red(), - }, + ButtonStyle::Tinted(tint) => tint.button_like_style(cx), ButtonStyle::Subtle => ButtonLikeStyles { background: cx.theme().colors().ghost_element_disabled, border_color: cx.theme().colors().border_disabled, @@ -264,6 +294,7 @@ pub struct ButtonLike { pub(super) style: ButtonStyle, pub(super) disabled: bool, pub(super) selected: bool, + pub(super) selected_style: Option, pub(super) width: Option, size: ButtonSize, rounding: Option, @@ -280,6 +311,7 @@ impl ButtonLike { style: ButtonStyle::default(), disabled: false, selected: false, + selected_style: None, width: None, size: ButtonSize::Default, rounding: Some(ButtonLikeRounding::All), @@ -309,6 +341,13 @@ impl Selectable for ButtonLike { } } +impl SelectableButton for ButtonLike { + fn selected_style(mut self, style: ButtonStyle) -> Self { + self.selected_style = Some(style); + self + } +} + impl Clickable for ButtonLike { fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self { self.on_click = Some(Box::new(handler)); @@ -364,6 +403,11 @@ impl ParentElement for ButtonLike { impl RenderOnce for ButtonLike { fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let style = self + .selected_style + .filter(|_| self.selected) + .unwrap_or(self.style); + self.base .h_flex() .id(self.id.clone()) @@ -382,12 +426,12 @@ impl RenderOnce for ButtonLike { ButtonSize::Default | ButtonSize::Compact => this.px_1(), ButtonSize::None => this, }) - .bg(self.style.enabled(cx).background) + .bg(style.enabled(cx).background) .when(self.disabled, |this| this.cursor_not_allowed()) .when(!self.disabled, |this| { this.cursor_pointer() - .hover(|hover| hover.bg(self.style.hovered(cx).background)) - .active(|active| active.bg(self.style.active(cx).background)) + .hover(|hover| hover.bg(style.hovered(cx).background)) + .active(|active| active.bg(style.active(cx).background)) }) .when_some( self.on_click.filter(|_| !self.disabled), diff --git a/crates/ui/src/components/button/icon_button.rs b/crates/ui/src/components/button/icon_button.rs index 0a75f361cf..d9ed6ccb5d 100644 --- a/crates/ui/src/components/button/icon_button.rs +++ b/crates/ui/src/components/button/icon_button.rs @@ -1,6 +1,6 @@ use gpui::{AnyView, DefiniteLength}; -use crate::prelude::*; +use crate::{prelude::*, SelectableButton}; use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize}; use super::button_icon::ButtonIcon; @@ -55,6 +55,13 @@ impl Selectable for IconButton { } } +impl SelectableButton for IconButton { + fn selected_style(mut self, style: ButtonStyle) -> Self { + self.base = self.base.selected_style(style); + self + } +} + impl Clickable for IconButton { fn on_click( mut self, @@ -109,12 +116,14 @@ impl RenderOnce for IconButton { fn render(self, _cx: &mut WindowContext) -> impl IntoElement { let is_disabled = self.base.disabled; let is_selected = self.base.selected; + let selected_style = self.base.selected_style; self.base.child( ButtonIcon::new(self.icon) .disabled(is_disabled) .selected(is_selected) .selected_icon(self.selected_icon) + .when_some(selected_style, |this, style| this.selected_style(style)) .size(self.icon_size) .color(self.icon_color), ) diff --git a/crates/ui/src/components/button/toggle_button.rs b/crates/ui/src/components/button/toggle_button.rs index f97498b0d8..e458c636ec 100644 --- a/crates/ui/src/components/button/toggle_button.rs +++ b/crates/ui/src/components/button/toggle_button.rs @@ -63,6 +63,13 @@ impl Selectable for ToggleButton { } } +impl SelectableButton for ToggleButton { + fn selected_style(mut self, style: ButtonStyle) -> Self { + self.base.selected_style = Some(style); + self + } +} + impl Disableable for ToggleButton { fn disabled(mut self, disabled: bool) -> Self { self.base = self.base.disabled(disabled); diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 9c0b1e58e3..4c6e48c0fc 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -29,6 +29,7 @@ pub enum Icon { ArrowRight, ArrowUp, ArrowUpRight, + ArrowCircle, AtSign, AudioOff, AudioOn, @@ -119,6 +120,7 @@ impl Icon { Icon::ArrowRight => "icons/arrow_right.svg", Icon::ArrowUp => "icons/arrow_up.svg", Icon::ArrowUpRight => "icons/arrow_up_right.svg", + Icon::ArrowCircle => "icons/arrow_circle.svg", Icon::AtSign => "icons/at_sign.svg", Icon::AudioOff => "icons/speaker_off.svg", Icon::AudioOn => "icons/speaker_loud.svg", diff --git a/crates/ui/src/components/tab.rs b/crates/ui/src/components/tab.rs index 9bafb0c0bb..351c851bb9 100644 --- a/crates/ui/src/components/tab.rs +++ b/crates/ui/src/components/tab.rs @@ -126,13 +126,14 @@ impl RenderOnce for Tab { if self.selected { this.border_l().border_r().pb_px() } else { - this.pr_px().pl_px().border_b() + this.pr_px().pl_px().border_b().border_r() } } TabPosition::Middle(Ordering::Equal) => this.border_l().border_r().pb_px(), TabPosition::Middle(Ordering::Less) => this.border_l().pr_px().border_b(), TabPosition::Middle(Ordering::Greater) => this.border_r().pl_px().border_b(), }) + .cursor_pointer() .child( h_stack() .group("") diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index dbf3c79b71..63d6c4b46a 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -12,7 +12,7 @@ pub use crate::selectable::*; pub use crate::styles::{vh, vw}; pub use crate::visible_on_hover::*; pub use crate::{h_stack, v_stack}; -pub use crate::{Button, ButtonSize, ButtonStyle, IconButton}; +pub use crate::{Button, ButtonSize, ButtonStyle, IconButton, SelectableButton}; pub use crate::{ButtonCommon, Color, StyledExt}; pub use crate::{Icon, IconElement, IconPosition, IconSize}; pub use crate::{Label, LabelCommon, LabelSize, LineHeightStyle}; diff --git a/crates/ui/src/ui.rs b/crates/ui/src/ui.rs index b34e66dbda..b074f10dad 100644 --- a/crates/ui/src/ui.rs +++ b/crates/ui/src/ui.rs @@ -2,15 +2,8 @@ //! //! This crate provides a set of UI primitives and components that are used to build all of the elements in Zed's UI. //! -//! ## Work in Progress -//! -//! This crate is still a work in progress. The initial primitives and components are built for getting all the UI on the screen, -//! much of the state and functionality is mocked or hard codeded, and performance has not been a focus. -//! -#![doc = include_str!("../docs/hello-world.md")] #![doc = include_str!("../docs/building-ui.md")] -#![doc = include_str!("../docs/todo.md")] mod clickable; mod components; diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 4839f9791a..d096248a28 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -59,153 +59,159 @@ pub struct WelcomePage { impl Render for WelcomePage { fn render(&mut self, cx: &mut gpui::ViewContext) -> impl IntoElement { - h_stack().full().track_focus(&self.focus_handle).child( - v_stack() - .w_96() - .gap_4() - .mx_auto() - .child( - svg() - .path("icons/logo_96.svg") - .text_color(gpui::white()) - .w(px(96.)) - .h(px(96.)) - .mx_auto(), - ) - .child( - h_stack() - .justify_center() - .child(Label::new("Code at the speed of thought")), - ) - .child( - v_stack() - .gap_2() - .child( - Button::new("choose-theme", "Choose a theme") - .full_width() - .on_click(cx.listener(|this, _, cx| { - this.workspace - .update(cx, |workspace, cx| { - theme_selector::toggle( - workspace, - &Default::default(), - cx, - ) - }) - .ok(); - })), - ) - .child( - Button::new("choose-keymap", "Choose a keymap") - .full_width() - .on_click(cx.listener(|this, _, cx| { - this.workspace - .update(cx, |workspace, cx| { - base_keymap_picker::toggle( - workspace, - &Default::default(), - cx, - ) - }) - .ok(); - })), - ) - .child( - Button::new("install-cli", "Install the CLI") - .full_width() - .on_click(cx.listener(|_, _, cx| { - cx.app_mut() - .spawn( - |cx| async move { install_cli::install_cli(&cx).await }, + h_stack() + .full() + .bg(cx.theme().colors().editor_background) + .track_focus(&self.focus_handle) + .child( + v_stack() + .w_96() + .gap_4() + .mx_auto() + .child( + svg() + .path("icons/logo_96.svg") + .text_color(gpui::white()) + .w(px(96.)) + .h(px(96.)) + .mx_auto(), + ) + .child( + h_stack() + .justify_center() + .child(Label::new("Code at the speed of thought")), + ) + .child( + v_stack() + .gap_2() + .child( + Button::new("choose-theme", "Choose a theme") + .full_width() + .on_click(cx.listener(|this, _, cx| { + this.workspace + .update(cx, |workspace, cx| { + theme_selector::toggle( + workspace, + &Default::default(), + cx, + ) + }) + .ok(); + })), + ) + .child( + Button::new("choose-keymap", "Choose a keymap") + .full_width() + .on_click(cx.listener(|this, _, cx| { + this.workspace + .update(cx, |workspace, cx| { + base_keymap_picker::toggle( + workspace, + &Default::default(), + cx, + ) + }) + .ok(); + })), + ) + .child( + Button::new("install-cli", "Install the CLI") + .full_width() + .on_click(cx.listener(|_, _, cx| { + cx.app_mut() + .spawn(|cx| async move { + install_cli::install_cli(&cx).await + }) + .detach_and_log_err(cx); + })), + ), + ) + .child( + v_stack() + .p_3() + .gap_2() + .bg(cx.theme().colors().elevated_surface_background) + .border_1() + .border_color(cx.theme().colors().border) + .rounded_md() + .child( + h_stack() + .gap_2() + .child( + Checkbox::new( + "enable-vim", + if VimModeSetting::get_global(cx).0 { + ui::Selection::Selected + } else { + ui::Selection::Unselected + }, ) - .detach_and_log_err(cx); - })), - ), - ) - .child( - v_stack() - .p_3() - .gap_2() - .bg(cx.theme().colors().elevated_surface_background) - .border_1() - .border_color(cx.theme().colors().border) - .rounded_md() - .child( - h_stack() - .gap_2() - .child( - Checkbox::new( - "enable-vim", - if VimModeSetting::get_global(cx).0 { - ui::Selection::Selected - } else { - ui::Selection::Unselected - }, + .on_click( + cx.listener(move |this, selection, cx| { + this.update_settings::( + selection, + cx, + |setting, value| *setting = Some(value), + ); + }), + ), ) - .on_click(cx.listener( - move |this, selection, cx| { - this.update_settings::( - selection, - cx, - |setting, value| *setting = Some(value), - ); - }, - )), - ) - .child(Label::new("Enable vim mode")), - ) - .child( - h_stack() - .gap_2() - .child( - Checkbox::new( - "enable-telemetry", - if TelemetrySettings::get_global(cx).metrics { - ui::Selection::Selected - } else { - ui::Selection::Unselected - }, + .child(Label::new("Enable vim mode")), + ) + .child( + h_stack() + .gap_2() + .child( + Checkbox::new( + "enable-telemetry", + if TelemetrySettings::get_global(cx).metrics { + ui::Selection::Selected + } else { + ui::Selection::Unselected + }, + ) + .on_click( + cx.listener(move |this, selection, cx| { + this.update_settings::( + selection, + cx, + |settings, value| { + settings.metrics = Some(value) + }, + ); + }), + ), ) - .on_click(cx.listener( - move |this, selection, cx| { - this.update_settings::( - selection, - cx, - |settings, value| settings.metrics = Some(value), - ); - }, - )), - ) - .child(Label::new("Send anonymous usage data")), - ) - .child( - h_stack() - .gap_2() - .child( - Checkbox::new( - "enable-crash", - if TelemetrySettings::get_global(cx).diagnostics { - ui::Selection::Selected - } else { - ui::Selection::Unselected - }, + .child(Label::new("Send anonymous usage data")), + ) + .child( + h_stack() + .gap_2() + .child( + Checkbox::new( + "enable-crash", + if TelemetrySettings::get_global(cx).diagnostics { + ui::Selection::Selected + } else { + ui::Selection::Unselected + }, + ) + .on_click( + cx.listener(move |this, selection, cx| { + this.update_settings::( + selection, + cx, + |settings, value| { + settings.diagnostics = Some(value) + }, + ); + }), + ), ) - .on_click(cx.listener( - move |this, selection, cx| { - this.update_settings::( - selection, - cx, - |settings, value| { - settings.diagnostics = Some(value) - }, - ); - }, - )), - ) - .child(Label::new("Send crash reports")), - ), - ), - ) + .child(Label::new("Send crash reports")), + ), + ), + ) } } diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index c6eaa71663..a7368f6136 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -246,7 +246,15 @@ impl Member { .size_full() .child(pane.clone()) .when_some(leader_border, |this, color| { - this.border_2().border_color(color) + this.child( + div() + .absolute() + .size_full() + .left_0() + .top_0() + .border_2() + .border_color(color), + ) }) .when_some(leader_status_box, |this, status_box| { this.child( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 7c2ca8a1f7..f46a1806e1 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -15,7 +15,7 @@ use anyhow::{anyhow, Context as _, Result}; use call::ActiveCall; use client::{ proto::{self, PeerId}, - Client, Status, TelemetrySettings, TypedEnvelope, UserStore, + Client, Status, TypedEnvelope, UserStore, }; use collections::{hash_map, HashMap, HashSet}; use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle}; @@ -107,6 +107,7 @@ actions!( NewCenterTerminal, ToggleTerminalFocus, NewSearch, + DeploySearch, Feedback, Restart, Welcome, @@ -1095,19 +1096,21 @@ impl Workspace { } pub fn close_global(_: &CloseWindow, cx: &mut AppContext) { - cx.windows().iter().find(|window| { - window - .update(cx, |_, window| { - if window.is_window_active() { - //This can only get called when the window's project connection has been lost - //so we don't need to prompt the user for anything and instead just close the window - window.remove_window(); - true - } else { - false - } - }) - .unwrap_or(false) + cx.defer(|cx| { + cx.windows().iter().find(|window| { + window + .update(cx, |_, window| { + if window.is_window_active() { + //This can only get called when the window's project connection has been lost + //so we don't need to prompt the user for anything and instead just close the window + window.remove_window(); + true + } else { + false + } + }) + .unwrap_or(false) + }); }); } @@ -1250,10 +1253,9 @@ impl Workspace { } pub fn open(&mut self, _: &Open, cx: &mut ViewContext) { - let telemetry_settings = TelemetrySettings::get_global(cx).clone(); self.client() .telemetry() - .report_app_event(telemetry_settings, "open project", false); + .report_app_event("open project", false, cx); let paths = cx.prompt_for_paths(PathPromptOptions { files: true, directories: true, @@ -3264,6 +3266,7 @@ impl Workspace { let user_store = project.read(cx).user_store(); let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx)); + cx.activate_window(); let app_state = Arc::new(AppState { languages: project.read(cx).languages().clone(), workspace_store, @@ -4762,8 +4765,7 @@ mod tests { }); // Deactivating the window saves the file. - cx.simulate_deactivation(); - cx.executor().run_until_parked(); + cx.deactivate_window(); item.update(cx, |item, _| assert_eq!(item.save_count, 1)); // Autosave on focus change. @@ -4783,14 +4785,13 @@ mod tests { item.update(cx, |item, _| assert_eq!(item.save_count, 2)); // Deactivating the window still saves the file. - cx.simulate_activation(); + cx.update(|cx| cx.activate_window()); item.update(cx, |item, cx| { cx.focus_self(); item.is_dirty = true; }); - cx.simulate_deactivation(); + cx.deactivate_window(); - cx.executor().run_until_parked(); item.update(cx, |item, _| assert_eq!(item.save_count, 3)); // Autosave after delay. diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index dba4f61793..39ab5e285b 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -11,7 +11,7 @@ path = "src/zed.rs" doctest = false [[bin]] -name = "zed" +name = "Zed" path = "src/main.rs" [dependencies] diff --git a/crates/zed/src/app_menus.rs b/crates/zed/src/app_menus.rs index a4b0e21d6e..2aff05d884 100644 --- a/crates/zed/src/app_menus.rs +++ b/crates/zed/src/app_menus.rs @@ -150,14 +150,6 @@ pub fn app_menus() -> Vec> { MenuItem::action("View Dependency Licenses", crate::OpenLicenses), MenuItem::action("Show Welcome", workspace::Welcome), MenuItem::separator(), - // todo!(): Needs `feedback` crate. - // MenuItem::action("Give us feedback", feedback::feedback_editor::GiveFeedback), - // MenuItem::action( - // "Copy System Specs Into Clipboard", - // feedback::CopySystemSpecsIntoClipboard, - // ), - // MenuItem::action("File Bug Report", feedback::FileBugReport), - // MenuItem::action("Request Feature", feedback::RequestFeature), MenuItem::separator(), MenuItem::action( "Documentation", diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 5ac850b1c6..e0da81edc4 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -144,6 +144,7 @@ fn main() { cx.set_global(client.clone()); + zed::init(cx); theme::init(theme::LoadThemes::All, cx); project::Project::init(&client, cx); client::init(&client, cx); @@ -158,7 +159,6 @@ fn main() { cx, ); assistant::init(cx); - // component_test::init(cx); cx.spawn(|_| watch_languages(fs.clone(), languages.clone())) .detach(); @@ -172,19 +172,16 @@ fn main() { .detach(); client.telemetry().start(installation_id, session_id, cx); - let telemetry_settings = *client::TelemetrySettings::get_global(cx); - client.telemetry().report_setting_event( - telemetry_settings, - "theme", - cx.theme().name.to_string(), - ); + client + .telemetry() + .report_setting_event("theme", cx.theme().name.to_string(), cx); let event_operation = match existing_installation_id_found { Some(false) => "first open", _ => "open", }; client .telemetry() - .report_app_event(telemetry_settings, event_operation, true); + .report_app_event(event_operation, true, cx); let app_state = Arc::new(AppState { languages: languages.clone(), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index bcfdb848ab..c7d30230ea 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -18,11 +18,11 @@ pub use only_instance::*; pub use open_listener::*; use anyhow::{anyhow, Context as _}; -use futures::{channel::mpsc, StreamExt}; +use futures::{channel::mpsc, select_biased, StreamExt}; use project_panel::ProjectPanel; use quick_action_bar::QuickActionBar; use search::project_search::ProjectSearchBar; -use settings::{initial_local_settings_content, load_default_keymap, KeymapFile, Settings}; +use settings::{initial_local_settings_content, KeymapFile, Settings, SettingsStore}; use std::{borrow::Cow, ops::Deref, sync::Arc}; use terminal_view::terminal_panel::TerminalPanel; use util::{ @@ -32,6 +32,7 @@ use util::{ ResultExt, }; use uuid::Uuid; +use welcome::BaseKeymap; use workspace::Pane; use workspace::{ create_and_open_local_file, notifications::simple_message_notification::MessageNotification, @@ -64,6 +65,13 @@ actions!( ] ); +pub fn init(cx: &mut AppContext) { + cx.on_action(|_: &Hide, cx| cx.hide()); + cx.on_action(|_: &HideOthers, cx| cx.hide_other_apps()); + cx.on_action(|_: &ShowAll, cx| cx.unhide_other_apps()); + cx.on_action(quit); +} + pub fn build_window_options( bounds: Option, display_uuid: Option, @@ -130,7 +138,6 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { status_bar.add_left_item(diagnostic_summary, cx); status_bar.add_left_item(activity_indicator, cx); status_bar.add_right_item(feedback_button, cx); - // status_bar.add_right_item(copilot, cx); status_bar.add_right_item(copilot, cx); status_bar.add_right_item(active_buffer_language, cx); status_bar.add_right_item(vim_mode_indicator, cx); @@ -207,15 +214,6 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { workspace .register_action(about) - .register_action(|_, _: &Hide, cx| { - cx.hide(); - }) - .register_action(|_, _: &HideOthers, cx| { - cx.hide_other_apps(); - }) - .register_action(|_, _: &ShowAll, cx| { - cx.unhide_other_apps(); - }) .register_action(|_, _: &Minimize, cx| { cx.minimize_window(); }) @@ -225,7 +223,6 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { .register_action(|_, _: &ToggleFullScreen, cx| { cx.toggle_full_screen(); }) - .register_action(quit) .register_action(|_, action: &OpenZedURL, cx| { cx.global::>() .open_urls(&[action.url.clone()]) @@ -403,8 +400,6 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { }); workspace.focus_handle(cx).focus(cx); - //todo!() - // load_default_keymap(cx); }) .detach(); } @@ -451,10 +446,10 @@ fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext) { .detach(); } -fn quit(_: &mut Workspace, _: &Quit, cx: &mut gpui::ViewContext) { +fn quit(_: &Quit, cx: &mut AppContext) { let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit; - cx.spawn(|_, mut cx| async move { - let mut workspace_windows = cx.update(|_, cx| { + cx.spawn(|mut cx| async move { + let mut workspace_windows = cx.update(|cx| { cx.windows() .into_iter() .filter_map(|window| window.downcast::()) @@ -463,14 +458,14 @@ fn quit(_: &mut Workspace, _: &Quit, cx: &mut gpui::ViewContext) { // If multiple windows have unsaved changes, and need a save prompt, // prompt in the active window before switching to a different window. - cx.update(|_, cx| { + cx.update(|cx| { workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false)); }) .log_err(); - if let (true, Some(_)) = (should_confirm, workspace_windows.first().copied()) { - let answer = cx - .update(|_, cx| { + if let (true, Some(workspace)) = (should_confirm, workspace_windows.first().copied()) { + let answer = workspace + .update(&mut cx, |_, cx| { cx.prompt( PromptLevel::Info, "Are you sure you want to quit?", @@ -500,9 +495,7 @@ fn quit(_: &mut Workspace, _: &Quit, cx: &mut gpui::ViewContext) { } } } - cx.update(|_, cx| { - cx.quit(); - })?; + cx.update(|cx| cx.quit())?; anyhow::Ok(()) }) .detach_and_log_err(cx); @@ -564,38 +557,60 @@ pub fn handle_keymap_file_changes( mut user_keymap_file_rx: mpsc::UnboundedReceiver, cx: &mut AppContext, ) { - cx.spawn(move |cx| async move { - // let mut settings_subscription = None; - while let Some(user_keymap_content) = user_keymap_file_rx.next().await { - if let Some(keymap_content) = KeymapFile::parse(&user_keymap_content).log_err() { - cx.update(|cx| reload_keymaps(cx, &keymap_content)).ok(); + BaseKeymap::register(cx); - // todo!() - // let mut old_base_keymap = cx.read(|cx| *settings::get::(cx)); - // drop(settings_subscription); - // settings_subscription = Some(cx.update(|cx| { - // cx.observe_global::(move |cx| { - // let new_base_keymap = *settings::get::(cx); - // if new_base_keymap != old_base_keymap { - // old_base_keymap = new_base_keymap.clone(); - // reload_keymaps(cx, &keymap_content); - // } - // }) - // })); + let (base_keymap_tx, mut base_keymap_rx) = mpsc::unbounded(); + let mut old_base_keymap = *BaseKeymap::get_global(cx); + cx.observe_global::(move |cx| { + let new_base_keymap = *BaseKeymap::get_global(cx); + if new_base_keymap != old_base_keymap { + old_base_keymap = new_base_keymap.clone(); + base_keymap_tx.unbounded_send(()).unwrap(); + } + }) + .detach(); + + load_default_keymap(cx); + + cx.spawn(move |cx| async move { + let mut user_keymap = KeymapFile::default(); + loop { + select_biased! { + _ = base_keymap_rx.next() => {} + user_keymap_content = user_keymap_file_rx.next() => { + if let Some(user_keymap_content) = user_keymap_content { + if let Some(keymap_content) = KeymapFile::parse(&user_keymap_content).log_err() { + user_keymap = keymap_content; + } else { + continue + } + } + } } + + cx.update(|cx| reload_keymaps(cx, &user_keymap)).ok(); } }) .detach(); } fn reload_keymaps(cx: &mut AppContext, keymap_content: &KeymapFile) { - // todo!() - // cx.clear_bindings(); + cx.clear_key_bindings(); load_default_keymap(cx); keymap_content.clone().add_to_cx(cx).log_err(); cx.set_menus(app_menus()); } +pub fn load_default_keymap(cx: &mut AppContext) { + for path in ["keymaps/default.json", "keymaps/vim.json"] { + KeymapFile::load_asset(path, cx).unwrap(); + } + + if let Some(asset_path) = BaseKeymap::get_global(cx).asset_path() { + KeymapFile::load_asset(asset_path, cx).unwrap(); + } +} + fn open_local_settings_file( workspace: &mut Workspace, _: &OpenLocalSettings,